[C/C++] Language-related question types (1)

Article directory

    • 1. Type conversion in C/C++
      • 1.1 Type conversion in C
      • 1.2 Type Conversion in C++
      • 1.3 Examples
      • 1.4 Summary
    • 2. When does C++ generate a default constructor
      • 2.1 The case where the default constructor will not be generated
      • 2.2 The case where a default constructor will be generated
    • 3. When does C++ generate a default copy constructor
      • 3.1 Do not explicitly specify a copy constructor
      • 3.2 Situations where a copy constructor must be generated
    • 4. Shallow copy and deep copy
      • 4.1 C ++ will appear to copy the situation
      • 4.2 Shallow Copy
      • 4.3 Deep Copy
    • 5. The role of the const keyword
      • 5.1 Define variables
      • 5.2 Modified function parameters
      • 5.3 Modified function return value
      • 5.4 Constant member functions in classes
    • 6. Supplement

1. Type conversion in C/C++

1.1 Type conversion in C

In the C language, explicit type conversion can be performed in the following two ways:

  1. (T) exp: In this method, you will convert the expression (exp) to type T. This method of type conversion is called “C-style type conversion” or “traditional type conversion”.

  2. T(exp): In this way, you will also convert the expression (exp) to type T. This method of type conversion is more common in C++, and is called “function-style type conversion” or “constructor type conversion”.

However, these two methods have the same effect in the C language, that is, the type of the expression exp is converted to T. For example, if you have an integer int i = 10; and you want to convert it to a float, you can do this: (float) i or float (i).

But it should be noted that although both methods can be used in the C language, the function-style type conversion T(exp) is more common in C++. If you are writing a C program, it is recommended to use (T) exp, because this method is acceptable in all C compilers.

Finally, I want to emphasize that no matter what kind of type conversion method is used, it should be used with caution. Converting between different types may result in loss of data or loss of precision. For example, when converting a floating-point number to an integer, the decimal part will be discarded; and if a large integer is converted to a small integer type, it may cause data overflow. Therefore, when writing a program, make sure you understand why type conversion is required and what impact the conversion may have.

1.2 Type conversion in C++

In C++, four explicit type conversion operators are provided, namely static_cast, const_cast, dynamic_cast and reinterpret_cast . Each type conversion operator has its specific usage scenarios and rules.

  1. static_cast(exp):
    This is the most common type conversion method and can convert between any data types, including pointer types. However, it cannot convert a const object to a non-const object. In addition, when you perform static_cast on pointers, you must ensure that the type of conversion is safe, because there is no runtime type checking for this type of conversion.

    • Conversion between class hierarchies
      • Upconversion is safe
      • Downcasting is not safe, no dynamic type checking
    • Basic type conversion
    • A null pointer is converted to a null pointer of the target type
    • non-const converted to const
    • Limitations: properties such as const and volitale cannot be removed
  2. const_cast(exp):
    const_cast is mainly used to modify the const or volatile attribute of the type. For example, it can convert a const pointer to a non-const pointer, or convert a const object to a non-const object. It should be noted that const_cast does not change the underlying data, but only changes the access rights to the data.

    • Remove the object pointer or object reference const attribute
    • Purpose: Modify the permission of the pointer (reference), you can modify the value of a block of memory through the pointer or reference
  3. dynamic_cast(exp):
    dynamic_cast is mainly used for safe downcasting (casting from a base class to a derived class in an inheritance hierarchy). That is, it can perform type checking at runtime, and if the conversion is illegal, the result of the conversion will be nullptr. But this kind of type conversion can only be used for classes with virtual functions, because only such classes can perform runtime type checking.

    • For polymorphism, type conversion at runtime
    • Safely typecast in a class hierarchy, converting base class pointers (references) to derived class pointers (references)
    • Because the reference does not have a null reference, a bad_cast exception will be thrown if the conversion fails, and nullptr will be returned if the pointer conversion fails
  4. reinterpret_cast(exp):
    reinterpret_cast is the least safe type conversion method, it can convert between any types, including conversion between pointers and integers. However, using reinterpret_cast may produce invalid data, so it should be used only when absolutely necessary.

    • Change pointer (reference) type
    • Converts a pointer (reference) to an integer
    • Convert an integer to a pointer (reference)
    • T must be a pointer, reference, integer, function pointer, member pointer
    • Note that it is only a copy of bits, no security checks

1.3 Example

In the C language, common type conversions are mainly conversions between numeric types. For example:

int i = 10;
double d = (double)i; // Convert integer i to double type

In this example, we use (double)i for type conversion, converting the integer i to double type.

In C++, type conversion is more complicated. The following are examples of using static_cast, const_cast, dynamic_cast and reinterpret_cast:

  1. static_cast:
int i = 10;
double d = static_cast<double>(i); // convert integer i to double type

In this example, we use static_cast(i) to convert the integer i to double.

  1. const_cast:
const int i = 10;
int* pi = const_cast<int*>( & amp;i); // Convert const int pointer to non-const int pointer

In this example, we use const_cast( & amp;i) for type conversion, converting the const int pointer to a non-const int pointer.

  1. dynamic_cast:
class Base {<!-- -->
    virtual void foo() {<!-- -->}
};
class Derived : public Base {<!-- -->
    void foo() override {<!-- -->}
};

Base* b = new Derived();
Derived* d = dynamic_cast<Derived*>(b); // Safely cast Base* to Derived*

In this example, we use dynamic_cast(b) for type conversion, safely converting Base* to Derived*.

  1. reinterpret_cast:
int i = 10;
int* pi = &i;
char* pc = reinterpret_cast<char*>(pi); // convert int pointer to char pointer

In this example, we use reinterpret_cast(pi) for type conversion, converting int* to char*.

1.4 Summary

  • Remove the const attribute and use const_cast
  • Basic type conversion with static_cast
  • Dynamic_cast is used for type conversion between polymorphic classes
  • Different types of pointer type conversion with reinterpret_cast

2. When does C++ generate a default constructor

2.1 The case where the default constructor will not be generated

An empty class definition will not generate a constructor, which is meaningless. Even if it contains member variables, the member variables are of the basic type, and the default constructor will not be generated, and the compiler does not know what value to initialize

2.2 will generate a default constructor

  1. The data member in class A is object B, and class B provides a default constructor
    • In order for the constructor of B to be called, a default constructor has to be generated for A
  2. The base class of the class provides a default constructor
    • The subclass constructor must first initialize the parent class, and then initialize its own member variables
    • If the parent class does not provide a default constructor, the subclass does not need to provide a default constructor
    • If the parent class provides a default constructor, the subclass has to generate a default constructor
  3. A virtual function is defined in the class
    • In order to implement the polymorphic mechanism, it is necessary to maintain a virtual function table for the class
    • All objects of the class need to save a pointer to the virtual function table
    • The object needs to initialize the pointer to the virtual function table
    • Had to provide a default constructor to initialize the vtable pointer
  4. The class uses virtual inheritance
    • The virtual base class table records the offset positions of all virtual base class subobjects inherited by the class within the object defined by this class
    • In order to achieve virtual inheritance, the object needs to maintain a pointer to the virtual base class table during the initialization phase
    • Had to provide a default constructor to initialize the virtual base class pointer

3. When does C++ generate a default copy constructor

3.1 Do not explicitly specify a copy constructor

In C++, the compiler will generate a default copy constructor (that is, an implicit copy constructor) for a class as long as you don’t explicitly define a copy constructor for the class. This default copy constructor will perform a copy of each member, which usually means executing the member’s copy constructor (for members of class types), or performing a simple bit copy strong> (for members of built-in types).

This behavior is defined by the C++ standard. The default copy constructor is usually sufficient unless your class needs to manage its own resources, such as dynamically allocated memory. In this case, you need to define your own copy constructor to properly perform deep copy, otherwise problems may occur (for example, multiple deletion of resources caused by shallow copy).

It should be noted that if you define any other constructor (such as a move constructor or a parameterized constructor), but do not define a copy constructor, the compiler will still generate a default copy constructor for you. Only when you explicitly define a copy constructor will the compiler not generate a default copy constructor.

3.2 Situations where a copy constructor must be generated

Same as 2.2

4. Shallow copy and deep copy

4.1 C ++ will be copied

  1. Initialization: A copy operation occurs when an object is initialized with another object. For example, if you declare a new object with another object as its initial value, a copy operation will occur:

    MyClass obj1;
    MyClass obj2 = obj1; // copy operation
    
  2. Function parameter passing: When an object is used as a function parameter, if the parameter is passed by value, then a copy operation will occur when the function is called:

    void foo(MyClass obj) {<!-- --> /* ... */ }
    
    MyClass obj;
    foo(obj); // when calling foo, obj will be copied
    
  3. Function returns: When a function returns an object, if the object is returned by value, then a copy operation occurs when the function returns:

    MyClass foo() {<!-- -->
        MyClass obj;
        return obj; // when returning, obj will be copied
    }
    
  4. Assignment: A copy operation occurs when an object is assigned the value of another object:

    MyClass obj1;
    MyClass obj2;
    obj2 = obj1; // copy operation
    
  5. As a container element: When an object is inserted into a container (eg, std::vector, std::list, etc.), a copy operation occurs:

    std::vector<MyClass> vec;
    MyClass obj;
    vec.push_back(obj); // obj is copied into vec
    

4.2 Shallow Copy

A shallow copy means copying only the object’s member values, regardless of whether those values represent references to other resources. If an object has a pointer to dynamically allocated memory or other resources, shallow copy will only copy the value of the pointer (that is, copy the memory address), but not the resource pointed to by the pointer.

This can cause problems because when multiple objects share the same resource, once one of the objects frees the resource on destruction, the other objects hold an invalid pointer. This is known as the dangling pointer problem.

Here is an example of a simple shallow copy:

class ShallowCopyExample {<!-- -->
    int* data;
public:
    ShallowCopyExample(int value) {<!-- -->
        data = new int;
        *data = value;
    }

    // shallow copy copy constructor
    ShallowCopyExample(const ShallowCopyExample & amp; source) {<!-- -->
        data = source. data;
    }

    ~ShallowCopyExample() {<!-- -->
        delete data;
    }
};

4.3 Deep Copy

In contrast, a deep copy not only copies the object’s member values, but also creates new copies of any dynamically allocated resources. That is, if an object has a pointer to dynamically allocated memory, deep copy will create a new copy of this memory and point the new object’s pointer to this new memory.

Doing this avoids the dangling pointer problem, since each object has its own resources that are not shared with other objects.

The following is an example of a simple deep copy:

class DeepCopyExample {<!-- -->
    int* data;
public:
    DeepCopyExample(int value) {<!-- -->
        data = new int;
        *data = value;
    }

    // deep copy copy constructor
    DeepCopyExample(const DeepCopyExample & amp; source) {<!-- -->
        data = new int; // allocate memory for new object
        *data = *source.data; // copy resource
    }

    ~DeepCopyExample() {<!-- -->
        delete data; // Release the resources of the object
    }
};

It should be noted that C++’s default copy constructor and assignment operator both perform shallow copies. If your class manages dynamically allocated resources, you need to rewrite your own copy constructor and assignment operator to implement deep copy.

5. The role of the const keyword

5.1 Define variables

  1. local const

    Local const refers to const variables declared inside a function. This kind of variable is only visible inside the body of the function that declares it, and its value cannot be changed within its scope.

    For example:

    void func() {<!-- -->
        const int x = 10; // x is local const
    }
    
  2. global const

    Global const refers to const variables declared outside a function. This variable is visible throughout the program and its value cannot be changed.

    For example:

    const int y = 20; // y is global const
    
    void func() {<!-- -->
        // can use y here
    }
    
  3. Symbol table

    The symbol table is information used by the compiler to store identifiers for all variables, functions, etc. used in the program. For const variables, their value and type are stored in the symbol table. Has the following effects:

    1. Type checking: The compiler performs type checking by consulting the type information in the symbol table. The compiler can issue a warning or error if a const variable is used in an expression that expects a different type.

    2. Value substitution and optimization: Since the values of const variables are known at compile time, the compiler can use these values directly during compilation and optimization. For example, if you have an expression const int x = 5; int y = x * 2;, the compiler may directly optimize it to int y = 10;. This can improve runtime performance.

    3. Protect the value of a const variable: Since the value of a const variable is in the symbol table, the compiler can ensure that this value will not be changed anywhere in the program. The compiler will issue an error if you try to modify the value of a const variable.

  4. constant pointer

    A constant pointer is a pointer to a const object, that is, you cannot modify the value it points to through this pointer, but you can change the address pointed to by this pointer.

    For example:

    const int x = 10;
    const int* p = &x;
    

    In this example, p is a pointer to const int, so you cannot change the value of x through p.

  5. pointer constant

    A pointer constant is a pointer that you cannot change the address it points to, but you can modify the value it points to through this pointer.

    For example:

    int x = 10;
    int* const p = &x;
    

    In this example, p is a const pointer, so you cannot change the value of p (that is, you cannot make p point to other address), but you can change the value of x through p.
    6. Function

    • avoid modification
    • Avoid multiple memory allocations
    • type checking, scope checking

5.2 Modified function parameters

In C++, the const keyword to modify function parameters has the following main functions:

  1. Prevent parameter values from being modified: When you use the const keyword to modify a function parameter, you cannot modify the value of the parameter in the function body. This prevents erroneous modifications in functions.

    void foo(const int x) {<!-- -->
        x = 42; // compile error because x is const
    }
    
  2. Fulfilling interface promises: const parameters can convey important information to the programmer using the function. It clearly states that the function does not modify the value of the parameter. This is a great form of self-documentation and helps prevent errors from people using the function.

  3. Improve generality of functions: const reference or pointer parameters allow functions to accept const and non-const arguments instead of const code>References or pointers can only accept non-const arguments. This allows functions to be used in more contexts.

    void bar(const std::string & amp; str) {<!-- -->
        // str is a const reference and cannot be modified in the function body
    }
    
    const std::string str1 = "Hello";
    std::string str2 = "World";
    
    bar(str1); // works because str1 is const
    bar(str2); // also works, since non-const arguments can be converted to const references
    
  4. Improve performance: For large objects, passing parameters by const reference can avoid copying operations, and at the same time ensure that the function will not modify the parameters.

    It should be noted that for basic data types (such as int, float, etc.), passing parameters by value and using const decoration usually does not Brings substantial performance benefits, since duplicating these types is cheap. However, for large classes and structures, passing parameters by const reference can yield significant performance benefits.

5.3 Modified function return value

In C++, the const keyword to modify the return value of a function mainly has the following functions:

  1. Return the read-only version of the object: If the function returns a reference or pointer to an object, and you don’t want the caller to modify the object through this reference or pointer, you can use the const keyword to modify the return type. This means that the reference or pointer returned by the function is read-only and cannot be used to modify the object.

    const int & amp; foo() {<!-- -->
        static int x = 0;
        return x;
    }
    

    In this example, the foo function returns a const reference to int, which means you cannot modify x through this reference .

  2. Prevent accidental object modification: The const keyword prevents you from inadvertently modifying objects that should not be modified. This is a safety mechanism that can help you avoid some behaviors that may lead to errors.

  3. Enhanced interface semantics: In some cases, the const keyword can provide clearer semantics. For example, if you have a member function that returns a reference to a member variable, and the function should not change the state of the object, you might choose to return a const reference.

It should be noted that for a function that returns a local object, the return type should generally not be a const reference or pointer, because when the function returns, the local object does not exist. In this case you should return a value or a pointer to dynamically allocated memory.

5.4 Constant member functions in classes

  1. be careful

    • You cannot modify any non-static member variables of the class in a constant member function
    • Read-only objects can only call constant member functions
    • Any non-member function and non-member variable in the class cannot be called in the constant member function, because the non-member function may modify the member variable of the class, and the non-member variable will also be modified
  2. effect

    • Avoid unintentional modification of member variables, which is more secure
    • It can be used for function overloading. Constant member functions and non-member functions are two different functions, so different versions of functions are called according to whether the object is a constant
    • Allow constant objects to call their member functions. If there is a member function that needs to be called on a constant object, then this member function should be a constant member function

6. Supplement

  1. Difference between wild pointer and dangling pointer
    A Wild Pointer usually refers to any pointer that is not initialized or is not set correctly, while a Dangling Pointer usually refers to a pointer to an object that has been deallocated or has exceeded its lifetime.

  2. operator overloading
    In C++, operator overloading is a language feature that enables you to change the behavior of operators on a type, especially user-defined types. You can overload operators by defining special member functions or global functions.

    The following is an example of overloading the + operator. We define a Complex class, which represents complex numbers, and then overload the + operator to implement complex addition.

    class Complex {<!-- -->
    public:
        Complex(double real, double imag) : real_(real), imag_(imag) {<!-- -->}
        double real() const {<!-- --> return real_; }
        double imag() const {<!-- --> return imag_; }
        Complex operator + (const Complex & amp; other) const {<!-- -->
            return Complex(real_ + other. real_, imag_ + other. imag_);
        }
    
    private:
        double real_;
        double imag_;
    };
    
    int main() {<!-- -->
        Complex a(1.0, 2.0);
        Complex b(3.0, 4.0);
        Complex c = a + b; // use our overloaded + operator
        // c.real() has a value of 4.0 and c.imag() has a value of 6.0
        return 0;
    }
    

    Note the following points:

    • Operator overloading does not change operator precedence. For example, even if you overload the + and * operators, the * operator still has more higher priority.
    • Not all operators can be overloaded. For example, the . (member access) operator, :: (scope resolution) operator, sizeof operator, and ?: The (conditional) operator cannot be overloaded.
    • You can’t change the “basic semantics” of an operator. For example, you cannot define an overload that causes the & amp; & amp; operator to perform multiplication.
    • You cannot create new operators. You can only overload operators that already exist.
    • For binary operators such as + or - , you can choose to overload it as a member function or as a global function. If you choose to overload it as a member function, the first operand is the object on which the function is called. If you choose to overload it as a global function, then you need to provide arguments for both operands.