Copy Constructor

Posted by jrmckee on Tue, 18 Jan 2022 02:30:54 +0100

copy constructor

1. What is a copy constructor

Objects of the same class have identical results in memory, and it is entirely possible to assign or copy them as a whole. This copy procedure** only needs to copy data members, while function members are public. ** When an object is created, it can be initialized with another object of the same class, and the constructor used becomes the copy constructor.

class Object
{
    int value;
public:
    Object ()   {}                  //General Constructor
    Object (int x = 0):value(x) {}  //Default constructor
    ~Object()   {}                  //Destructor
    Object(Object &obj):value(obj.value)             //Copy constructor, parameter type is a reference to class type
    {
        cout << "copy constructor" << endl;
    }
};

int main()
{
    Object obj1(10);
    Object obj2(obj1);
}

Line 8 is the copy constructor, the parameter type is the type name, and the parameter is the reference to the object.

The results are as follows:

Question 1: The parameter of the copy constructor is the reference type, what happens if the reference is removed

Answer: Dead Recursion.

Solution: Object (const Object &obj) //Add const and &, to prevent modifications to obj1 when building obj2.

#include<iostream>
using namespace std;

class Object
{
    int value;
public:
    Object() 
    {
        cout << "Object():" << this <<endl;
    }                  //General Constructor
    Object(int x = 0) :value(x) { cout << "Object(int x = 0):" << this << endl; }  //Default constructor
    ~Object() { cout << "~Object()" << this << endl; }                  //Destructor
    Object(Object obj) :value(obj.value)             //Here, the reference type of the copy construction is changed to the object type
    {
        cout << "Copy Object" << endl;
    }
};

int main()
{
    Object obj1(10);
    Object obj2(obj1);
}

VS2019 will prompt an error:

Problem Analysis:

Dead recursion occurs without references. We need to call copy construction to construct obj with obj1, but there is another obj inside the copy construction.

2. Use of copy constructors

First look at the following code:

class Object
{
    int value;
public:
    Object ()   
    {
        cout << "create:" << this << endl;
    }                  //General Constructor
    Object (int x = 0):value(x) {cout << "create:" << this << endl;}  //Default constructor
    ~Object()                       //Destructor
    {
        cout << "~Objcet() " << this << endl;
    }
    Object(Object &obj):value(obj.value)             //Copy constructor, parameter type is a reference to class type
    {
        cout << "Copy create:" << this << endl;
    }

    int SetValue(int x) { value = x ; return value;}          //The following 5 behaviors add code
    int GetValue() const 
    {
        return value;
    }
};

Object fun(Object obj)      //3
{
    int val = obj.SetValue(100);
    Object obj1(val);       //4
    return obj1;            //5//The copy passed here to obj1 to construct an object that will die. Move the copy constructor to build
}

int main()
{
    Object objx(0);         //1
    Object objy(0);         //2
    objy = fun(objx);       

    return 0;
}

Question 2: How many objects have been created and destructors have been called?

Answer: The results of VS2019 and g++ compilation run are different. g++ does not create an object with a return value and calls the copy constructor less than once.

We compiled and run using g++ and VS2019, respectively, with the following results:

VS2019 5 structures (counting copy structures), 5 destructions:

g++ 4 constructs (counting copy constructs), 4 destructions:

We found that VS2019 and g++ had different results. VS2019 makes one more call to the copy constructor than g++ and takes one more step of destructing. By looking at it, we find the difference of one copy constructor, so where is it?

We used VS2019 compilation results for analysis:

  1. First, we construct objx and objy objects with 0 arguments and call fun functions.
  2. Subsequently, after entering the function, a copy constructor is called to construct an object obj in the stack frame space of the funfunction. An object with a val ue parameter is constructed.
  3. Finally, obj1 is returned, and what is passed to the outside world is a copy of obj1, which is constructed as a dead value object, and the copy constructor is invoked to construct it.
  4. As the function completes, obj1 and obj are released, and when fun(objx) is passed to the objy object, a previous copy of the dead value is passed to the objy object. A copy of the object that will be destroyed is then released.

Note: Store the dead object only during the assignment statement.

For destructive sequence analysis:

  1. Release obj1, then obj, then the return object returned by the copy construction.
  2. Release objy and objx.

Two cases:

1. Instantiate an object with static before it. Calling it multiple times will only result in one object, and the entire memory space will be one.

2. Add static before fun function, which affects function name and has no meaning.

1. If static Object obja(val); An obja object is then generated when the fun function is called once. But the next time you call it, no obja object will be generated. There is only one in the entire memory space.

2. If static is added to a fun function, there is no static type for the return type of the function, but it only affects that the function name is static, and it does not affect the parameter and return value.

Question 3: How can you create objects to save space?

Answer: Add & to the fun function parameter and change to reference.

Object fun(Object &obj)      //Change to reference type.
{
    int val = obj.SetValue(100);
    Object obj1(val);       
    return obj1;            
}

After changing to a reference type, objx's address is passed to obj without dispatching the constructor at the parameter.

Question 4: When does the const of the formal parameter of the fun function be added and when is it not?

Answer: Related to the purpose of the function

Object fun(Object &obj);
Object fun(const Object &obj);		//Prevents changing the argument object by a parameter.

If you want to modify an argument through a parameter, do not const it.

If you want to read only the value of the object, you can do so without const.

3. A Tip

class Object
{
    int value;
public:
    Object ()   
    {
        cout << "create:" << this << endl;
    }     
    
    Object (int x = 0):value(x) {cout << "create:" << this << endl;}
    
    ~Object()                      
    {
        cout << "~Objcet() " << this << endl;
    }
    
    Object(Object &obj):value(obj.value)            
    {
        cout << "Copy create:" << this << endl;
    }

    int & Value()
    {
        return value;
    }

    const int &Value() const 
    {
        return value;
    }  
};

Object &fun(const Object &obj)      //Change Return Value to Reference
{                                   
    int val = obj.Value() + 10;
    Object obj1(val);       
    return obj1;   
}

int main()
{
    Object objx(0);
    Object objy(0);
    objy = fun(objx);
    cout << objy.Value() << endl;
    return 0;
}

Look closely at Value, who wrote two copies, one of which is a function of the common integer reference type. One is a function that adds a const modifier to the reference type and limits its this pointer to the const type. Why?

A constant object references a call to a common method.

A common object reference calls a common method.

This way you can modify what you want, and what you don't want to modify is not modifiable. Remember this little trick.

Question 5: What is the difference between a function fun with and without a reference?

Answer: Functions need to be unreferenced.

Specifically, we will do a detailed analysis below.

4. Add References

4.1 Do not add references

class Object
{
   ...
Object fun(const Object &obj)      //Do not return by reference
{                                   
    int val = obj.Value() + 10;
    Object obj1(val);       
    return obj1;   
}
    
int main()
{
    Object objx(0);
    Object objy(0);
    objy = fun(objx);
    cout << objy.Value() << endl;
    return 0;
}

Analysis Flowchart:

The corresponding process is as follows:

1. When the function is executed, a stack frame is opened for the main function first. Reserve the space required for objx and objy.

Note: Objects are not stored in space until two objects call a constructor.

2. Call fun function, the parameter of fun function is obj, 32-bit system occupies 4 bytes, because the bottom level of reference is pointer implementation, only store the address of objx. const indicates that the object is a constant pointer and cannot be changed.

3. Next, create a val ue variable that stores 10.

4. ** An obja object is then created, which is created by calling a copy constructor and stored in the stack frame of the main function. ** Since the register needs to be stored at the top of the main function stack frame, it will be stored below. When created, the address of the dead object is passed into the eax register.

5. When the fun function ends, it destroys all the stack frames of the fun function and calls obj1's destructor before destroying. When we return from the fun function to the main function, we assign objy a web address object and then destruct the dead object.

6. Finally, objy is destructed, and then objx is destructed.

Question: Why are dead objects built in the stack frame space of the main function?

Answer: Because the main function is the caller of the fun function, if it is built in the address space of the fun function, there will be no more stack frames destroyed.

4.2 Return by Reference

class Object
{
    ......
        
Object & fun(const Object &obj)      //Return by reference
{                                   
    int val = obj.Value() + 10;
    Object obj1(val);       
    return obj1;   
}

int main()
{
    Object objx(0);
    Object objy(0);
    objy = fun(objx);
    cout << objy.Value() << endl;
    return 0;
}

Analysis Flowchart:

The analysis process is as follows:

1. First, we run the program, which constructs stack frames of main and fun functions. The program reserves address space for objx and objy objects. When the constructor is called, the object is actually stored in memory space.

2. When we call a fun function, since the parameter of the fun function is a reference type, we do not need to call the constructor to create the object, but store the address of the objx object directly in the stack frame of the fun.

3. A val ue variable is then created, and an obj1 object is constructed, both of which are stored in the stack frame space of the fun function.

4. When return obj1 is executed, since the return value is a reference type, it does not produce a dead value object, but stores the address space of the obj1 object in the eax register.

5. Back in the main function, the address of obja in the register stored by eax is assigned to objy, but because the fun function ends, the stack frame space is destroyed and the obj1 object is destructed.

1. First, we run the program, which constructs stack frames of main and fun functions. The program reserves address space for objx and objy objects. When the constructor is called, the object is actually stored in memory space.

2. When we call a fun function, since the parameter of the fun function is a reference type, we do not need to call the constructor to create the object, but store the address of the objx object directly in the stack frame of the fun.

3. A val ue variable is then created, and an obj1 object is constructed, both of which are stored in the stack frame space of the fun function.

4. When return obj1 is executed, since the return value is a reference type, it does not produce a dead value object, but stores the address space of the obj1 object in the eax register.

5. Back in the main function, the address of obja in the register stored by eax is assigned to objy, but because the fun function ends, the stack frame space is destroyed and the obj1 object is destructed.

If no address perturbation occurs, the values stored in the obja object are unaffected. If interference occurs, a random value is returned.

Summary:
When returned by reference, a random value may be returned, which is unsafe.
When returned as an object, the returned dead value is stored in the stack frame of the main function, and then the address is passed to the register. The data is normal and safe.

Topics: C++ Visual Studio