C++ 副本构造器
对象赋值中的指针变量成员
逐位复制(bitwise copy) :
众所周知,我们可以把 一个对象实例赋值给一个类型与之相同的对象(例如 MyClass a=MyClass b),在赋值过程中编译器将生成必要的代码把"源"对象各属性的值分别赋值给"目标"对象的对应成员,这种赋值行为被称为逐位复制(bitwise copy)。
逐位复制下的普通变量与指针变量 :
逐位赋值在绝大多数场合都是没有问题的,但如果某些成员变量是指针的话,问题就来了:对象成员进行逐位复制的结果是你将拥有两个一模一样的实例,这两个实例的普通变量虽然名字一样,但是都有不同的地址。而这两个副本实例的同名指针变量却会指向相同的一个地址…
漏洞:
如果两个实例的同名指针变量指向着同一个地址,当删除其中一个对象,它包含的指针也将被删除,但万一此时另一个副本(对象)还在引用这个指针,便会出现问题!
例如:
建立一个Myclass类,类中有一个普通成员变量test和指针成员变量testp:
class Myclass
{
public:
int test; //普通成员变量test
int* testp; //指针成员变量testp
};
在主函数中建立同属于Myclass类的两个对象实例a和b,并将b对象赋值给对象a,然后分别打印a和b对象内的非指针变量成员和指针变量成员的地址:
int main()
{
Myclass a(new int(1));//1.进入主构造器 开始构造对象a | 2.构造完毕离开主构造器
Myclass b(new int(2));//1.进入主构造器 开始构造对象b | 2.构造完毕离开主构造器
a = b;//1.进入赋值语句重载函数 | 2.离开赋值语句重载函数
int* aTestpointer = &a.test;
int* bTestpointer = &b.test;
int* aTestPpointer = a.testp;
int* bTestPpointer = b.testp;
//为了便于阅读,省略了打印地址的代码
***
打印指针地址...
}
运行结果:
从运行结果我们可以看出,两个同类副本对象中的指针变量testp指向的确实是同一个地址,这导致如果我们删除其中一个对象,另一个对象便不能引用这个Testp指针,为了解决这个隐患,能不能在调用另一个副本对象的同时删除这个被第一个对象删除的指针,避免第二个副本对象引用一个不存在的指针呢?
虽然这么做在逻辑上没有问题,但从实际情况上来看是不可能的,因为CPU本身就是逐条指令执行的,逐条执行有先后顺序,当试图第二次释放同一块内存,程序肯定会崩溃。
解决方案:
那么怎么才能解决这个问题, 让程序员能在进行对象“复制”的时候能够精确的表明应该复制什么内容和如何进行赋值,接下来我们暂时先利用重载赋值操作符解决这个问题。
重载操作符
现在我们有两个Myclass类的副本对象a和b,我们再将a对象的值赋值给对象b:
MyClass a;
MyClass b;
a=b;
第三行对象赋值操作语句中,将a对象的值赋值给对象b,这里就可能因为赋值时对象中有指针变量成员而埋下祸根,那么,怎样才能在发生错误前截获这个赋值操作并告诉它应该如何处理那些埋下祸根的指针变量呢。
重载赋值操作符:
几乎所有C++的操作符都可以重载,而赋值操作符 " = " 恰好是其中的一个,我们将重载这个 " = "操作符,在其中对对象赋值时的指针变量进行处理:
Myclass &operator=(const Myclass &rhs);
该函数返回一个引用,该引用指向一个Myclass类的对象
上述语句告诉我们这个方法所预期的输入参数应该是一个Myclass类型的,不可被改变的引用
因为这里使用的输入参数是一个引用,所以编译器在传递输入参数的时候就不会再为它创建另一个副本(否则会导致无限递归),用引用做参数也方便我们把一组赋值语句串联起来,如a = b = c;
又因为这里只需要读取这个输入参数,而不用改变它的值,所以我们用const把那个引用声明为一个常量(const),确保万无一失。
只对赋值操作符 " = " 进行重载,解决指针变量在对象赋值对象时的问题
class Myclass
{
public:
Myclass(int* p);
~Myclass();
Myclass& operator=(const Myclass& rhs);//todo重载赋值号函数
void print();
private:
int *ptr;
};
Myclass::Myclass(int* p)
{
ptr = p;
}
Myclass::~Myclass()
{
delete ptr;
}
Myclass& Myclass::operator=(const Myclass& rhs)
{
//条件一 : 对象赋值 赋值号两边为不同的对象 a=b; (a为本对象,b为传入的对象)
if (this != &rhs)//判断重载函数右操作数传入的对象的引用是否和本类对象的地址一致
{
//todo如果地址不一致(不是同一个对象)
delete ptr; //释放删除 a (本对象)的指针 先把a对象的指针空间清理出来
ptr = new int; //动态分配内存给ptr这个指针
*ptr = *rhs.ptr;//将ptr这个指针值赋值为传入的对象的指针的值。
🔥以上操作在两个同类对象赋值时(a=b):
1.先进行判断,判断两个对象是否为同一个对象(a和b不是同一个对象)
2.若不是同一个对象,将逐位赋值时的a对象的名为ptr的指针(逐位赋值时直接被赋值为对象b的ptr指针地址)释放,再给a对象的名为
ptr的指针动态分配一个与对象b的ptr指针完全不同的地址(此时我们便保证了副本对象a和b中的同名指针变量ptr地址是不同的)
3.让a和b的ptr指针指向不同地址后,此时a的ptr指针虽然有不同的地址,但这个地址不指向任何一个值(0000000),但是b对象的指针
变量ptr指向了一个整型数值,所以我们一定要将b对象的ptr指向的值赋值给a对象的ptr指针
}
//条件二:对象赋值 赋值号两边为同一个对象 a=a;
else
{
cout << "赋值号两边为同个对象,不做处理!\n";
}
return *this;//🔥返回一个引用
}
void Myclass::print()//打印两个副本对象同名指针ptr的值
{
cout << *ptr << endl;
}
int main()
{
Myclass a(new int(1));
Myclass b(new int(2));
a.print();
b.print();
a = b;
cout << endl;
a.print();
b.print();
}
运行结果:
虽然对赋值操作符进行重载完成了程序的要求,但是还不够完美,所以我们还需要运用副本构造器对代码进行改良:
赋值操作符重载和副本构造器这两种解决方案的区别:
重载赋值操作符: 先创建两个同类的实例a和b,再把b实例赋值给实例a。
Myclass a;
Myclass b;
b=a;
副本构造器: 先创建一个实例a,然后在创建另一个同类实例b的同时用实例a的值对实例b进行初始化。
Myclass a;
Myclass b=a;//此时编译器将在Myclass这个类中寻找一个副本构造器(copy constructor),如果找不到这个副本构造器,它会自行创建一个副本构造器
虽然这两种方案看起来区别不大,但编译器却会生成完全不同的程序代码 :这是因为如果编译器在对实例对象声明的同时进行初始化(方案二)的时候,编译器将在Myclass这个类中寻找一个副本构造器(copy constructor),如果找不到这个副本构造器,它会自行创建一个副本构造器。
为什么需要副本构造器:
赋值操作符重载的这个方案的缺陷在于:即使我们对赋值操作符进行了重载,但是由编译器自行创建的副本构造器仍会以"逐位复制"的方法把 对象b复制给对象a,这仍是一个隐患。想要避免这个隐患,我们需要 亲自定义一个副本构造器,而不是让编译器帮我们自动生成。
副本构造器(防止指针重复释放)
副本构造器的作用: 让程序员能在进行对象“复制”的时候能够精确的表明应该复制什么内容和如何进行赋值
副本构造器的声明:
Myclass(const Myclass &rhs);
这个构造器需要一个固定不变(const)的Myclass类的引用作为输入参数,就像赋值操作符那样,因为它是一个构造器,所以不需要返回类型,只需要相应的类名和参数列表。
我们需要使用副本构造器继续改善代码…
#include <iostream>
#include <string>
using namespace std;
class Myclass
{
public:
Myclass(int* p);//todo声明主构造器
Myclass(const Myclass& rhs);🔥 声明副本构造器
~Myclass();//todo声明主析构器
int test;
int* testp;
Myclass& operator=(const Myclass& rhs);//todo重载赋值号函数
void print();
private:
int *ptr;
};
Myclass::Myclass(int* p)
{
cout << "进入主构造器" << endl;
ptr = p;
cout << "离开主构造器" << endl;
}
Myclass::Myclass(const Myclass& rhs)🔥 定义副本构造器
{
cout << "进入副本构造器" << endl;
*this = rhs;//🔥调用赋值符重载函数
cout << "离开副本构造器" << endl;
}
Myclass::~Myclass()
{
cout << "进入主析构器" << endl;
delete ptr;
cout << "离开主析构器" << endl;
}
Myclass& Myclass::operator=(const Myclass& rhs)
{
//条件一 : 对象赋值 赋值号两边为不同的对象 a=b; (a为本对象,b为传入的对象)
if (this != &rhs)//判断重载函数右操作数传入的对象的引用是否和本类对象的地址一致
{
//todo如果地址不一致(不是同一个对象)
delete ptr; //释放删除 a (本对象)的指针 先把a对象的指针空间清理出来
ptr = new int; //动态分配内存给ptr这个指针
*ptr = *rhs.ptr;//将ptr这个指针值赋值为传入的对象的指针的值。
🔥以上操作在两个同类对象赋值时(a=b):
1.先进行判断,判断两个对象是否为同一个对象(a和b不是同一个对象)
2.若不是同一个对象,将逐位赋值时的a对象的名为ptr的指针(逐位赋值时直接被赋值为对象b的ptr指针地址)释放,再给a对象的名为
ptr的指针动态分配一个与对象b的ptr指针完全不同的地址(此时我们便保证了副本对象a和b中的同名指针变量ptr地址是不同的)
3.让a和b的ptr指针指向不同地址后,此时a的ptr指针虽然有不同的地址,但这个地址不指向任何一个值(0000000),但是b对象的指针
变量ptr指向了一个整型数值,所以我们一定要将b对象的ptr指向的值赋值给a对象的ptr指针
}
//条件二:对象赋值 赋值号两边为同一个对象 a=a;
else
{
cout << "赋值号两边为同个对象,不做处理!\n";
}
return *this;//🔥返回一个引用
}
void Myclass::print()
{
cout << *ptr << endl;
}
int main()
{
cout << "\n-------不同的两个对象在进行对象赋值操作时\n(声明完对象后再对此对象进行赋值操作 :不调用副本构造器 仅进行赋值语句重载函数):\n" << endl;
//todonew int(1)声明整型的空间 里面存放整型量
Myclass a(new int(1));//1.进入主构造器 开始构造对象a | 2.构造完毕离开主构造器
Myclass b(new int(2));//1.进入主构造器 开始构造对象b | 2.构造完毕离开主构造器
a = b;//1.进入赋值语句重载函数 | 2.离开赋值语句重载函数
cout << "对象赋值后 未启用副本构造器的情况下: \n"
a.print();//对象a调用类中的打印函数
b.print();//对象b调用类中的打印函数
//todo运行结果为 2 2
cout << "\n-------不同的两个对象在进行对象赋值操作时\n(声明对象的时候同时进行对此对象进行赋值操作 :调用副本构造器中的赋值语句重载函数):\n" << endl;
Myclass c(new int(3));//1.进入主构造器 开始构造对象c | 2.构造完毕离开主构造器
Myclass d = c;//1.进入副本构造器 开始构造对象d | 2.进入赋值语句重载函数 | 3.离开赋值语句重载函数 | 4.构造完毕离开副本构造器
c.print();
d.print();
//todo运行结果为 3 3
cout << "\n-------相同的两个对象在进行对象赋值操作时:\n" << endl;
Myclass e(new int(4));//1.进入主构造器 开始构造对象e | 2.构造完毕离开主构造器
e = e;//1.进入赋值语句重载函数 | “赋值号两边为同个对象,不做处理!” | 2.离开赋值语句重载函数
e.print();
//todo运行结果为4
return 0;*/
}
运行结果:
来自:
http://www.bubuko.com/infodetail-262935.html
https://blog.csdn.net/joan11_3/article/details/51628095