拷贝赋值操作
-
基本概念
①.拷贝构造函数:是一种构造函数,用同类型的对象初始化本对象的操作,即将新对象初始化为同类型另一个对象的副本。
②.拷贝赋值运算符:接收一个本类型对象的赋值运算符版本,返回本对象的引用。
-
类的默认函数
①.默认合成函数
当我们定义了一个空类后,C++ 会为我们默认生成一个构造函数、一个拷贝构造函数、一个拷贝赋值运算符、一个析构函数,并且默认都是 public 的;一旦我们定义了带参数的构造函数,那么编译器就不会再生成默认的无参构造函数了。
class Empty { };//定义了一个空类,无任何成员 //等同于以下定义 class Empty { public: Empty();//默认构造函数 Empty( const Empty& emp);//拷贝构造函数 Empty& operator= (const Empty& emp);//拷贝赋值运算符 ~Empty();//默认析构函数 };
②.显示使用默认函数
我们可用通过将拷贝构造函数定义为 =default 来显示的要求编译器生成默认拷贝构造函数;只能对具有合成版本的成员函数使用 =default 。
class Example { public: Example() = default;//默认构造函数,显示生成 ~Example() = default;//默认析构函数 Example( Example & emp) = default ;//拷贝构造函数 Example& operator= (const Example& emp) = delete;//拷贝赋值运算符 private: };
③.拒绝默认函数
若不想编译器为我们生成默认的拷贝构造函数,可以将拷贝构造函数声明为 private ,并且不定义实现即可。
class Example { public: Example();//默认构造函数 ~Example();//默认析构函数 private: Example( const Example& emp);//拷贝构造函数,声明但不定义 Example& operator= (const Example& emp);//拷贝赋值运算符 };
在新标准下,也可以通过将拷贝构造函数定义为删除的函数来阻止拷贝;可以对任何函数使用 =delete,包含成员函数和非成员函数。
class Example { public: Example();//默认构造函数 ~Example();//默认析构函数 Example( Example & emp) = delete;//拷贝构造函数,定义为删除的函数 Example& operator= (const Example& emp) = delete;//拷贝赋值运算符 private: };
-
拷贝构造函数
①.定义
如果一个构造函数的第一个参数是自身类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数;默认的拷贝构造函数会将其参数的成员逐个拷贝到正在创建的对象中。
class Example { public: Example() ;//构造函数 ~Example() ;//析构函数 Example( Example & emp) ;//拷贝构造函数 Example& operator= (const Example& emp) = delete;//拷贝赋值运算符 private: int a; string b; }; Example::Example( Example Empty& emp): //等价默认拷贝构造函数 a(emp.a),//内置类型直接拷贝 b(emp.b)//使用 string 的拷贝构造函数 { //空函数体 }
②.直接初始化
在定义对象时,实际上是要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数:可以是普通的构造函数,也可以是拷贝构造函数;直接初始化最基本的特征就是使用 ( ) 来定义对象。
class Example { public: Example() ;//构造函数 Example( string s) :b(s){};//构造函数 ~Example() ;//析构函数 Example( Example & emp) ;//拷贝构造函数 Example& operator= (const Example& emp) = delete;//拷贝赋值运算符 private: int a; string b; }; Example exp1;//直接初始化,调用默认构造函数 Example exp2("hello");//也是直接初始化,调用带一个参数的构造函数 Example exp3(exp2);也是直接初始化,调用拷贝构造函数
③.拷贝初始化
在定义对象时,拷贝初始化实际上是要求编译器将右侧运算对象拷贝到正在创建的对象中,通常用拷贝构造函数来完成。
拷贝初始化首先使用指定构造函数创建一个临时对象,然后用拷贝构造函数将那个临时对象拷贝到正在创建的对象,一般发生在用 = 来定义对象。
class Example { public: Example() ;//构造函数 Example( string s) :b(s){};//构造函数 ~Example() ;//析构函数 Example( Example & emp) ;//拷贝构造函数 Example& operator= (const Example& emp) = delete;//拷贝赋值运算符 private: int a; string b; }; Example exp4 = exp3;//拷贝初始化,由于 exp3 已经存在,直接调用拷贝构造函数 Example exp5 = “hello”;//拷贝初始化,先调用构造函数将 string 转成 临时对象,然后调用拷贝构造函数 Example exp6 = Example("hello");//包括初始化,先调用构造函数创建临时对象,然后调用拷贝构造函数
以下情况也会发生拷贝初始化:
将一个对象作为实参传递给非引用类型的形参时
class Example {}; void f1( Example exp) { //函数体 }; Example exp1; f1(exp1);//会调用拷贝构造函数创建临时对象,临时对象用于拷贝实参,并传入函数
将一个返回类型为非引用类型的函数返回一个对象时
class Example {}; Example f2( ) { Example exp1; return exp1; }; f2();//会调用拷贝构造函数创建临时对象,临时对象用于存储返回值
用花括号列表初始化一个数组或者一个聚合类中的成员时
class Example {}; vector<Example> vec = {exp1,exp2,exp3};//拷贝初始化没个元素 vec.push_back("hello");//拷贝初始化 vec.emplace("hello");//直接初始化
-
拷贝赋值运算符
①.定义
重载运算符本质上是函数,其名字由 operator 关键字后表示要定义的运算符的符号组成,即赋值运算符就是一个名为 operator= 的函数;赋值运算符通常应该返回一个指向左侧运算对象的引用。
class Example { public: Example() ;//构造函数 ~Example() ;//析构函数 Example( Example & emp) ;//拷贝构造函数 Example& operator= (const Example& emp) ;//拷贝赋值运算符 private: int a; string b; }; Example& //返回对象的引用 Example::operator=( Example & emp) //等价默认拷贝赋值运算符 { a = emp.a;//内置类型,直接拷贝 b = emp.b;// 调用 string 的拷贝赋值运算符 return *this;// 返回指向左侧对象的引用 }
②.必须返回左侧对象的引用
返回值类型:函数返回的是一个对象的副本;返回引用类型:函数返回的是一个真实对象的别名。
operator= 函数返回引用,可用提高效率:
class Example { public: Example() ;//构造函数 ~Example() ;//析构函数 Example( Example & emp) ;//拷贝构造函数 Example operator= (const Example& emp) ;//拷贝赋值运算符 private: int a; string b; }; Example //返回对象副本 Example::operator=( Example & emp) { a = emp.a;//内置类型,直接拷贝 b = emp.b;// 调用 string 的拷贝赋值运算符 return *this;// 调用拷贝构造函数生成临时对象 } Example exp1,exp2,exp3; exp1 = exp2 = exp3;//会比返回引用多调用两次拷贝构造函数
为了支持另一种形式的联锁赋值:
class Example ; Example exp1,exp2,exp3; // 若非返回引用,将无法支持以下操作 (exp1 = exp2 ) = exp3;//exp3 将作用于 exp1 的副本
③.自我赋值及异常安全
赋值运算符通常组合了析构函数和构造函数的操作:类似析构函数,赋值操作会销毁左侧运算对象、类型拷贝构造函数,赋值操作会从右侧运算对象拷贝数据。
我们需要确保赋值操作已正确的顺序执行,即使将一个对象赋值给自身也确保正确,如果有异常发生也要确保左侧对象的状态没有影响。
一般通过先拷贝右侧运算对象再销毁左侧对象的顺序,可用实现自我赋值以及异常安全。
class Example { public: Example(string s ):b(new string(s)) {} ;//构造函数 ~Example() ;//析构函数 Example( Example & emp) :b( new string(*emp.b)) {};//拷贝构造函数 Example& operator= (const Example& emp) ;//拷贝赋值运算符 private: int a; string *b; }; Example& //返回对象的引用 Example::operator=( Example & emp) { if( *this == emp ) return;//判断自复制 string * temp = new string(*emp.b);//拷贝底层 string 数据 delete b;//释放旧对象,因为拷贝操作已经安全的执行,释放是安全的 b = temp;//从右侧对象拷贝数据 a = emp.a; return *this;// 返回指向左侧对象的引用 }
-
深拷贝和浅拷贝
①.浅拷贝
如果用默认的赋值运算符函数去赋值有指针成员变量的对象,就会使得两个对象的指针地址也是一样的,也就是两个对象的指针成员变量指向的地址是同一个地方,这种方式就是浅拷贝。
浅拷贝副本和原对象共享底层数据,即仅仅复制了指针本身的值,这时当一个对象释放了指针成员变量时,那么另外一个对象的指针成员变量指向的地址就是空的了,再次使用这个对象时,程序就会奔溃。
class Example { public: Example(string s ):b(new string(s)) {} ;//构造函数 ~Example() ;//析构函数 Example( Example & emp) :b( new string(*emp.b)) {};//拷贝构造函数 Example& operator= (const Example& emp) = default;//默认拷贝赋值运算符 private: int a; string *b; }; Example emp1("hello"); Example emp2("world"); emp1 = emp2;// emp1.b 和 emp2.b 都指向了 world
②.深拷贝
深拷贝是指拷贝对象的具体内容,而内存地址是自主分配的,拷贝结束之后,两个对象虽然存的值是相同的,但是内存地址不一样,两个对象也互不影响,互不干涉。
class Example { public: Example(string s ):b(new string(s)) {} ;//构造函数 ~Example() ;//析构函数 Example( Example & emp) :b( new string(*emp.b)) {};//拷贝构造函数 Example& operator= (const Example& emp) ;//拷贝赋值运算符 private: int a; string *b; }; Example& //返回对象的引用 Example::operator=( Example & emp) //拷贝底层数据 { if( *this == emp ) return;//判断自复制 string * temp = new string(*emp.b);//拷贝底层 string 数据 delete b;//释放旧对象,因为拷贝操作已经安全的执行,释放是安全的 b = temp;//从右侧对象拷贝数据 a = emp.a; return *this;// 返回指向左侧对象的引用 } Example emp1("hello"); Example emp2("world"); emp1 = emp2;// emp1.b 和 emp2.b 都指向的内存地址不同
-
三/五法则
①.类内动态分配内存,必须实现析构函数
如果一个成员变量是别的对象的指针,而且这个指针不是传进来的地址,而是这个指针指向的对象是在本类中在堆中开辟的空间创建的,则必须实现析构函数,在析构函数中进行动态内存释放
②.如果一个类需要析构函数,那么也需要实现拷贝、赋值操作
③.如果一个类需要拷贝操作,那么也需要实现赋值操作