拷贝构造函数
1、什么是拷贝构造函数
同一个类的对象在内存中有完全相同的结果,如果作为一个整体进行赋值或者拷贝是完全可行的。这个拷贝过程**只需要拷贝数据成员,而函数成员是公用的。**在创建对象时可以用同一类的另一个对象来初始化该对象的存储空间,这时,所用的构造函数成为拷贝构造函数。
class Object
{
int value;
public:
Object () {} //普通构造函数
Object (int x = 0):value(x) {} //缺省构造函数
~Object() {} //析构函数
Object(Object &obj):value(obj.value) //拷贝构造函数,参数类型是类的类型的引用
{
cout << "拷贝构造函数" << endl;
}
};
int main()
{
Object obj1(10);
Object obj2(obj1);
}
第8行的就是拷贝构造函数,参数类型是类型名,参数是对象的引用。
运行结果如下:
#include<iostream>
using namespace std;
class Object
{
int value;
public:
Object()
{
cout << "Object():" << this <<endl;
} //普通构造函数
Object(int x = 0) :value(x) { cout << "Object(int x = 0):" << this << endl; } //缺省构造函数
~Object() { cout << "~Object()" << this << endl; } //析构函数
Object(Object obj) :value(obj.value) //此处,拷贝构造的引用类型改为对象类型
{
cout << "Copy Object" << endl;
}
};
int main()
{
Object obj1(10);
Object obj2(obj1);
}
VS2019会提示错误:
问题分析:
没有引用会出现死递归,我们用obj1构造obj需要调用拷贝构造,但是拷贝构造里面又有一个obj。
2、拷贝构造函数的用途
首先看下面的代码:
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 SetValue(int x) { value = x ; return value;} //下面5行为新增代码
int GetValue() const
{
return value;
}
};
Object fun(Object obj) //3
{
int val = obj.SetValue(100);
Object obj1(val); //4
return obj1; //5 //此处传递给obj1的副本,构建一个将亡值对象。调动拷贝构造函数来构建
}
int main()
{
Object objx(0); //1
Object objy(0); //2
objy = fun(objx);
return 0;
}
我们分别使用了g++和VS2019进行编译运行,结果如下:
VS2019 5次构造(算上拷贝构造),5次析构:
g++ 4次构造(算上拷贝构造),4次析构:
我们发现,VS2019和g++的结果不同。VS2019比g++多调用一次拷贝构造函数,并且多进行了一步析构。通过观察,发现差一次拷贝构造函数,那么具体在哪里呢?
我们使用VS2019的编译结果进行分析:
- 首先,我们构造参数为0的objx和objy对象,调用fun函数。
- 随后,进入函数后调用拷贝构造函数,在fun函数的栈帧空间中构造一个对象obj。再构造出一个参数为val的对象。
- 最后将obj1返回,这里传递给外界的是obj1的一个副本,构建一个将亡值对象,调动拷贝构造函数来构建。
- 随着函数的运行完毕,obj1和obj都被释放,将fun(objx)传递给objy对象时,会将之前的将亡值副本传递给objy对象。随后会释放将亡值对象的副本。
注意:将亡值对象只存储在赋值语句过程中。
对析构顺序分析:
- 先释放obj1,再释放obj, 后释放拷贝构造返回的return对象。
- 再释放objy和objx。
1、如果static Object obja(val);那么在一次调用fun函数时,会产生一个obja对象。但是在下次调用的时候,就不会产生obja对象。整个内存空间中只有一个。
2、如果给fun函数加static ,函数的返回类型不存在静态类型,只是影响到函数名是static,不会影响到形参和返回值。
Object fun(Object &obj) //改为引用类型。
{
int val = obj.SetValue(100);
Object obj1(val);
return obj1;
}
改为引用类型后,不用在形参处调度构造函数,将objx的地址传递给obj。
Object fun(Object &obj);
Object fun(const Object &obj); //防止通过形参改变实参对象。
如果想要通过形参修改实参的时候,不加const。
如果只是为了读取对象的值,那么可以不加const。
3、一个小技巧
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) //将返回值改为引用
{
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;
}
细看 Value写了两份,一份是普通的整形引用类型的函数。一份是添加const修饰整形引用类型的函数,并且限制了它的this指针也是const类型。为什么要这样呢?
常对象引用调用常方法。
普通对象引用调用普通方法。
这样想要修改的可以修改,常对象不想修改不可修改,这个小技巧要记住。
具体我们在下方详细分析。
4、是否添加引用
4.1 不添加引用
class Object
{
...
Object fun(const Object &obj) //不以引用返回
{
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;
}
分析流程图:
对应过程如下:
1、当函数执行后,会先为main函数开辟栈帧。预留出objx和objy所需要的空间。
注意:当两个对象调用构造函数后,才会将对象存放在空间当中。
2、调用fun函数,fun函数的形参是obj,32位系统占据4个字节,因为引用的底层是指针实现的,仅仅存放objx的地址。const 说明对象是个常指针,不可改变。
3、接着创建一个val变量,存储的是10。
4、**随后创建一个obja对象,会调用拷贝构造函数来创建,存放在main函数的栈帧中。**由于main函数栈帧顶部需要存放寄存器,它会在下面存放。创建完后,会将将亡值对象的地址传入eax寄存器。
5、fun函数结束,会将fun函数的栈帧全部销毁,销毁前调用obj1的析构函数。从fun函数回到主函数的时候,我们将我们的将网址对象给objy赋值,随后,将亡值对象也要被析构。
6、最后objy被析构,随后objx也被析构。
4.2 以引用返回
class Object
{
......
Object & fun(const Object &obj) //以引用返回
{
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;
}
分析流程图:
分析过程如下:
1、首先我们运行程序,会构造出main函数和fun函数的栈帧,程序预留出objx和objy对象的地址空间。当调用构造函数后,对象才会被实际的存放在内存空间中。
2、当我们调用fun函数后,由于fun函数的形参是引用类型,就不需要调用构造函数创建对象,而是直接在fun的栈帧中保存objx对象的地址。
3、随后会创建一个val变量,然后构造一个obj1对象,二者都保存在fun函数的栈帧空间中。
4、当执行到return obj1的时候,由于返回值是引用类型,它不会产生一个将亡值对象,而是将obj1对象的地址空间存放在eax寄存器中。
5、回到main函数中,会将eax存储的寄存器中obja的地址赋值给objy,但是由于fun函数结束,栈帧空间被销毁,obj1对象被析构。
1、首先我们运行程序,会构造出main函数和fun函数的栈帧,程序预留出objx和objy对象的地址空间。当调用构造函数后,对象才会被实际的存放在内存空间中。
2、当我们调用fun函数后,由于fun函数的形参是引用类型,就不需要调用构造函数创建对象,而是直接在fun的栈帧中保存objx对象的地址。
3、随后会创建一个val变量,然后构造一个obj1对象,二者都保存在fun函数的栈帧空间中。
4、当执行到return obj1的时候,由于返回值是引用类型,它不会产生一个将亡值对象,而是将obj1对象的地址空间存放在eax寄存器中。
5、回到main函数中,会将eax存储的寄存器中obja的地址赋值给objy,但是由于fun函数结束,栈帧空间被销毁,obj1对象被析构。
如果没有发生地址扰动,obja对象存储的值会不受影响;如果发生干扰,将会返回一个随机值。
总结:
以引用返回的时候,可能会返回一个随机值,不安全。
以对象返回的时候,返回的将亡值存储在main函数的栈帧中,再将地址传递给寄存器,数据正常,安全。