C++复制控制
一、复制控制
类能控制复制,赋值,撤销该类的对象时的动作,分别通过下面的成员函数:
-
复制构造函数:具有单个形参,该形参是对该类类型的引用(通常用 const 修饰);
-
赋值操作符
-
析构函数:不管有没显示定义,编译器都自动执行类中非 static 数据成员的析构函数
这三个函数就成为复制控制。
二.为什么要研究复制控制
如果没有显示地定义复制构造函数和赋值操作符,编译器会为我们定义。
但是编译器合成的复制控制函数只做必需的工作,某些类如果依赖于默认的定义会导致错误,例如类具有指针成员。
难点: 识别何时需要覆盖默认版本,定义自己的复制构造函数
所以有时候要自定义复制构造函数,定义复制构造函数跟构造函数是一样的,难点是能认识到需要定义复制构造函数。
(具体请看第五点:” 什么时候要自己的复制构造函数”)
三.复制构造函数会几时用到?
首先:区分构造函数和复制构造函数
1. 根据另一个同类型的对象显式或者隐式地初始化一个对象
区别下面:
(1)下面的构造函数和复制构造函数仅在低级别优化上存在差异
String str3(10,“a”); // 调用构造函数
String str4 ; // 调用构造函数
String str1= “hello”; // 调用复制构造函数
String str2 = string(); // 调用复制构造函数
(2)对于不支持复制的类型,或者使用非 explicit 构造函数的时候,它们有本质区别:
ifstream file1(“filename”); //OK, 调用构造函数
ifstream file2 = “filename” ; //ERROR, 不能复制 IO 类型的对象
//(复制构造函数是 private 的)
Sales_item item = string(“hello”); // 取决于构造函数是不是 explicit,如果构造函数 是显式的,则初始化失败,如果构造函数是隐式的,则初始化成功。
2. 一个对象作为实参传给一个函数,或者作为函数的返回值
例如: string fun(const stirng& A, const string B);
返回值和 第二个参数 B 隐式地调用复制构造函数;第一个参数 A, 是 const 引用,不能复制。
3. 初始化顺序容器中的元素
当用表示容量的单个形参来初始化容器的时候,用到了默认构造函数和复制构造函数。
vector svec(5);
先用 string 默认构造函数创建一个临时值来初始化 svec, 然后是使用复制构造函数将临时值复制到 svec 的每一个元素。
4. 根据 “元素初始化列表 " 初始化数组元素
(1)如果没有为类类型数组提供元素初始化式,是调用默认构造函数初始化每个元素,
sale_item ClassArray[];
(2)如果用花括号扩展的数组初始化列表来提供显式元素初始化,则是用复制构造函数.
根据指定值创建适当类型的元素,然后用复制构造函数将该值复制到相应的元素。
Sale_item ClassArray2[] = { string(“ab”), // 直接指定一个值,调用单实参构造函数
string(“cd”),
Sale_item(), // 使用完整的构造函数语法(0 或者多个实参)
};
备注:可以直接指定一个值,调用元素类型的单实参构造函数,如前两个元素的初始化;
如果不指定实参或者指定多个实参,就需要使用完整的构造函数语法,如最后一个元素的初始化
四.编译器合成的复制构造函数
前面提到,如果我们没有定义,编译器会默认合成一个复制构造函数,该函数会对该对象的每一个非 static 成员依次复制到正创建的对象。
其中,不同类型的复制如下:
1. 直接复制内置类型成员的值(不是指针)
2. 类型成员使用该类的复制构造函数
3. 数组复制数组的每一个元素
等价于: 一个构造函数,每个数据成员在构造函数初始化列表中进行初始化;
例如 有三个数据成员的类的合成构造函数:
Sales_item :: Sales_item(const Sales_item &orig) :A(orig.A),B(orig.B),C(orig.C){}
五.什么时候要自己定义复制构造函数(重点)
有些类必须对复制对象时发生的事情进行控制,例如
1、类中有数据成员是指针或者有成员在构造函数中分配其他资源
2、在创建对象时必须做一些特定工作
以上的情况都必须定义复制构造函数。
六、怎么定义复制构造函数(同构造函数一样)
同类同名,没有返回值,可以使用构造函数初始化列表初始化新创建对象的成员,可以在函数体中任何其他必要的工作。
七、如何禁止复制
例如:iostream 类就不允许复制
-
如果不自定义复制构造函数,编译器也会自动合成一个,无法禁止复制
-
为了防止复制,类必须显式地声明其复制构造函数为 private(其友元和成员依然可以复制)
-
如果要连友元和成员的复制也禁止,就可以声明一个 private 的复制构造函数但不对它进行定义。(如果复制类对象会提示编译错误,如果成员和友元尝试复制就会导致链接错误)
八、赋值操作符
1. 如果没有定义自己的赋值操作符,编译器也会自动合成一个(类似于合成的复制构造函数,进行各个成员的赋值)
2. 如果类自定义自己的复制操作符,那么一般类也需要自定义赋值操作符(具体的请看后面的例子)
九、析构函数
1、 析构函数通常用于释放在构造函数或者在对象生命期内获取的资源。析构函数可以执行任意操作,一般是在类对象使用完毕之后要执行的动作。
三法则: 如果类需要析构函数,则它也需要复制操作符和复制构造函数。
2、合成的析构函数按创建的逆序撤销每个非 static 成员。(并不删除指针成员所指向的对象),无论有没创建自定义函数,编译器都会合成析构函数
3、注意:即便自定义了自己的析构函数,编译器在运行自定义析构函数之后,还会运行合成析构函数。