[C++基础]046_拷贝构造函数与重载赋值运算符(精深版解释)
目录
1. <<Effective C++>>第三版第12条
2. 什么是copying函数?
3. 编译器自动生成时,复制的情况
4. 自己重载时,复制的情况
5. 最黑暗的遗漏复制
6. 解决基类成员的复制问题
7. 最佳解决办法
8. 扩展:构造函数调用构造函数
9. new 、operator new 和 placement new 区别
10. placement new的含义
11. Placement new 存在的理由
12.如何使用placement new
<<Effective C++>>第三版第12条
复制对象时勿忘其每一部分。
即:通常编译器为我们生成的copying函数会自动对每一个成员变量进行复制。而当我们自己重载copying函数时,如果遗漏了对某一成员变量的赋值,编译器不会有任何提醒。所以要我们在复制对象时不要忘记任何一部分。
什么是copying函数?
通常情况下,编译器会自动为我们的类创建"拷贝构造函数"和"赋值运算符"。这里的拷贝构造函数和赋值运算符就是所谓的copying函数。
编译器自动生成时,复制的情况
1 #include<iostream> 2 using namespace std; 3 4 class A{ 5 public: 6 A(){ this->aValue = 0; } 7 int getValue(){ return aValue; } 8 void setValue(int v){ this->aValue = v; } 9 private: 10 int aValue; 11 }; 12 13 class B{ 14 public: 15 B(int v){ this->bValue = v; } 16 void setA(int v){ this->a.setValue(v); } 17 void print(){ 18 cout<<"A:aValue = "<<a.getValue()<<endl; 19 cout<<"B:bValue = "<<this->bValue<<endl; 20 cout<<"-------------------------"<<endl; 21 } 22 private: 23 A a; 24 int bValue; 25 }; 26 27 int main(){ 28 29 B b1(1); 30 b1.setA(7); 31 b1.print(); 32 33 B b2(1); 34 b2 = b1; 35 b2.print(); 36 37 B b3(b1); 38 b3.print(); 39 40 system("pause"); 41 return 0; 42 }
输出结果:
A:aValue = 7 B:bValue = 1 ------------------------- A:aValue = 7 B:bValue = 1 ------------------------- A:aValue = 7 B:bValue = 1 ------------------------- 请按任意键继续. . .
结论:编译器自动生成拷贝构造函数和自动生成重载赋值运算符时,复制操作都是有效的。
自己重载时,复制的情况
1 #include<iostream> 2 using namespace std; 3 4 class A{ 5 public: 6 A(){ this->aValue = 0; } 7 int getValue(){ return aValue; } 8 void setValue(int v){ this->aValue = v; } 9 private: 10 int aValue; 11 }; 12 13 class B{ 14 public: 15 B(int v){ this->bValue = v; } 16 B(const B& rhs){ 17 this->bValue = rhs.bValue; 18 } 19 B& operator=(const B& rhs){ 20 this->bValue = rhs.bValue; 21 return *this; 22 } 23 void setA(int v){ this->a.setValue(v); } 24 void print(){ 25 cout<<"A:aValue = "<<a.getValue()<<endl; 26 cout<<"B:bValue = "<<this->bValue<<endl; 27 cout<<"-------------------------"<<endl; 28 } 29 private: 30 A a; 31 int bValue; 32 }; 33 34 int main(){ 35 36 B b1(1); 37 b1.setA(7); 38 b1.print(); 39 40 B b2(1); 41 b2 = b1; 42 b2.print(); 43 44 B b3(b1); 45 b3.print(); 46 47 system("pause"); 48 return 0; 49 }
输出结果:
A:aValue = 7 B:bValue = 1 ------------------------- A:aValue = 0 B:bValue = 1 ------------------------- A:aValue = 0 B:bValue = 1 ------------------------- 请按任意键继续. . .
结论:我们自己实现拷贝构造函数和重载赋值运算符时,因为遗漏了B类的成员变量a的赋值,复制操作变得无效了。
最黑暗的遗漏复制
1 class A{ 2 public: 3 A(){ this->aValue = 0; } 4 int getValue(){ return aValue; } 5 void setValue(int v){ this->aValue = v; } 6 private: 7 int aValue; 8 }; 9 10 class B : public A{ 11 public: 12 B(int v){ this->bValue = v; } 13 B(const B& rhs){ 14 this->bValue = rhs.bValue; 15 } 16 B& operator=(const B& rhs){ 17 this->bValue = rhs.bValue; 18 return *this; 19 } 20 private: 21 int bValue; 22 };
纠结:我怎么记得复制基类的成员啊?
解决基类成员的复制问题
1 #include<iostream> 2 using namespace std; 3 4 class A{ 5 public: 6 A(){ this->aValue = 0; } 7 A(const A& rhs){ this->aValue = rhs.aValue; } 8 int getValue(){ return aValue; } 9 void setA_Value(int v){ this->aValue = v; } 10 private: 11 int aValue; 12 }; 13 14 class B : public A{ 15 public: 16 B(int v){ this->bValue = v; } 17 B(const B& rhs){ 18 this->A::A(rhs); 19 this->bValue = rhs.bValue; 20 } 21 B& operator=(const B& rhs){ 22 this->A::A(rhs); 23 this->bValue = rhs.bValue; 24 return *this; 25 } 26 void print(){ 27 cout<<"A:aValue = "<<this->getValue()<<endl; 28 cout<<"B:bValue = "<<this->bValue<<endl; 29 } 30 private: 31 int bValue; 32 }; 33 34 int main(){ 35 36 B b1(1); 37 b1.setA_Value(3); 38 b1.print(); 39 40 B b2(b1); 41 b2.print(); 42 43 system("pause"); 44 return 0; 45 }
输出结果:
A:aValue = 3 B:bValue = 1 A:aValue = 3 B:bValue = 1 请按任意键继续. . .
可见成功地对基类的成员进行了复制,但是我们又看出一个问题。注意看A的拷贝构造函数和重载赋值运算符的函数体,如此相近。每一次追加成员变量如果都在两个地方重写相同的代码,势必会很痛苦,我们不自觉的可能会想到互相调用。要么拷贝构造函数调用赋值运算符函数,要么赋值运算符函数调用拷贝构造函数。但是,这样可以吗?我们把B类的赋值运算符函数改成下面这个样子:
1 B(const B& rhs){ 2 this->A::A(rhs); 3 this->bValue = rhs.bValue; 4 } 5 B& operator=(const B& rhs){ 6 this->B::B(rhs); 7 return *this; 8 }
输出结果:
A:aValue = 3 B:bValue = 1 A:aValue = 3 B:bValue = 1 请按任意键继续. . .
结论:在赋值运算符函数里调用拷贝构造函数可以得到我们要的结果。
上面已经实现了我们要的结果,但是这样好吗?不好。因为赋值运算符函数调用拷贝构造函数,这就好比是对一个已经存在的对象进行构造,尽管在功能上能实现。但是在语义上说不通。因为C++有规定,构造函数是用来初始化申请的空白内存的。我们这样做的意义是违反C++的规定的,这或许是C++不严谨的地方吧。
最佳解决办法
1 class A{ 2 public: 3 A(){ this->aValue = 0; } 4 A(const A& rhs){ this->aValue = rhs.aValue; } 5 int getValue(){ return aValue; } 6 void setA_Value(int v){ this->aValue = v; } 7 private: 8 int aValue; 9 }; 10 11 class B : public A{ 12 public: 13 B(int v){ this->bValue = v; } 14 B(const B& rhs){ 15 init(rhs); 16 } 17 B& operator=(const B& rhs){ 18 init(rhs); 19 return *this; 20 } 21 void print(){ 22 cout<<"A:aValue = "<<this->getValue()<<endl; 23 cout<<"B:bValue = "<<this->bValue<<endl; 24 } 25 private: 26 int bValue; 27 void init(const B& rhs){ 28 this->A::A(rhs); 29 this->bValue = rhs.bValue; 30 } 31 };
看起来很简单,就是将那些共通赋值部分的代码抽出来,放到一个私有的成员函数init里面。然后拷贝构造函数和赋值运算符函数只要调用这个函数。这样当我们追加新的成员变量时,只需要在init函数里追加一行函数即可。
扩展:构造函数调用构造函数
1 #include <iostream> 2 using namespace std; 3 4 class A 5 { 6 int m_i; 7 A( int i ) : m_i(i){} 8 A() 9 { 10 A(0); 11 } 12 }; 13 int main() 14 { 15 A obj; 16 cout << obj.m_i << endl; 17 18 system("PAUSE"); 19 return 0; 20 }
输出结果:
-858993460 请按任意键继续. . .
上述代码我们在默认构造函数里调用我们自己重载的有参数的构造函数,希望借此来初始化我们的成员变量。但是结果并不是我们所希望的那样,原因很简单,显然上面代码中,A obj;这里已经为obj分配了内存,然后调用默认构造函数,但是默认构造函数还未执行完,却调用了另一个构造函数,这样相当于产生了一个匿名的临时A对象,它调用A(int)构造函数,将这个匿名临时对象自己的数据成员m_i初始化为0;但是obj的数据成员并没有得到初始化。于是obj的m_i是未初始化的,因此其值也是不确定的。
因此,在构造函数里调用构造函数是需要千万小心的,但是这样我们就没有办法来实现我们的赋值计划了吗?不是的,我们还有一个神器:placement new。
修改后的代码如下:
1 class A 2 { 3 int m_i; 4 A( int i ) : m_i(i){} 5 A() 6 { 7 new (this)A(0); 8 } 9 };
下面就让我们来进入new的奇幻世界。
new 、operator new 和 placement new 区别
★new :不能被重载,其行为总是一致的。它先调用operator new分配内存,然后调用构造函数初始化那段内存,相当于全局的运算符+、-、*、/。
★operator new:要实现不同的内存分配行为,应该重载operator new,而不是new。delete和operator delete类似。delete首先调用对象的析构函数,然后调用operator delete释放掉所使用的内存。
★placement new:只是operator new重载的一个版本。它并不分配内存,只是返回指向已经分配好的某段内存的一个指针。因此不能删除它,但需要调用对象的析构函数。
placement new的含义
placement new 是重载operator new 的一个标准、全局的版本,它不能够被自定义的版本代替(不像普通版本的operator new 和 operator delete能够被替换)。
1 void *operator new( size_t, void *p ) throw() { return p; }
placement new的执行忽略了size_t参数,只返还第二个参数。其结果是允许用户把一个对象放到一个特定的地方,达到调用构造函数的效果。 和其他普通的new不同的是,它在括号里多了另外一个参数。比如:
1 Widget * p = new Widget; // ordinary new 2 pi = new (ptr) int; // placement new
括号里的参数ptr是一个指针,它指向一个内存缓冲器,placement new将在这个缓冲器上分配一个对象。Placement new的返回值是这个被构造对象的地址(比如括号中的传递参数)。
※placement new主要适用于:在对时间要求非常高的应用程序中,因为这些程序分配的时间是确定的;长时间运行而不被打断的程序;以及执行一个垃圾收集器 (garbage collector)。
Placement new 存在的理由
(1).用Placement new 解决buffer的问题
问题描述:用new分配的数组缓冲时,由于调用了默认构造函数,因此执行效率上不佳。若没有默认构造函数则会发生编译时错误。如果你想在预分配的内存上创建 对象,用缺省的new操作符是行不通的。要解决这个问题,你可以用placement new构造。它允许你构造一个新对象到预分配的内存上。
(2).增大时空效率的问题
使用new操作符分配内存需要在堆中查找足够大的剩余空间,显然这个操作速度是很慢的,而且有可能出现无法分配内存的异常(空间不够)。placement new 就可以解决这个问题。我们构造对象都是在一个预先准备好了的内存缓冲区中进行,不需要查找内存,内存分配的时间是常数;而且不会出现在程序运行中途出现内 存不足的异常。所以,placement new非常适合那些对时间要求比较高,长时间运行不希望被打断的应用程序。
如何使用placement new
下面是一个placement new使用的实例,注意这个实例在第一步中有三种实现方法,第二种就是传说中的栈空间申请内存。
1 #include <iostream> 2 using namespace std; 3 4 class A 5 { 6 public: 7 A(){} 8 A(int i){ 9 this->m_i = i; 10 } 11 void print(){ 12 cout<<"m_i = "<<this->m_i<<endl; 13 cout<<"-----------------"<<endl; 14 } 15 private: 16 int m_i; 17 }; 18 19 int main() 20 { 21 // 第一步 缓存提前分配 22 // 方法1:在堆上申请内存 23 char *buff1 = new char[sizeof(A)]; 24 25 // 方法2::在栈上申请内存 26 char buff2[2*sizeof(A)]; 27 28 // 方法3::直接通过地址 29 char* buff3 = reinterpret_cast<char*> (0x275315DE); 30 31 // 第二步:对象的分配 32 A *pA1 = new(buff1)A(1); 33 A *pA2 = new(buff2)A(2); 34 A *pA3 = new(buff3)A(3); 35 36 // 第三步:使用 37 pA1->print(); 38 pA2->print(); 39 pA3->print(); 40 41 // 第四步:释放内存 42 delete[] buff1; 43 delete[] buff2; 44 //delete[] buff3; 45 46 system("PAUSE"); 47 return 0; 48 }