在C++Primer中(P440)中阐述了一个类中的拷贝控制操作有以下4类:
(1)拷贝构造函数
(2)拷贝赋值运算符
(3)析构函数
(4)移动构造函数
(5)移动赋值运算符
其中,(4)和(5)是新的C++标准特性。
这里先讨论拷贝构造函数和赋值运算符的区别,什么时候会调用这两个构造函数。
然后讨论浅拷贝和深拷贝的区别。
class 是类名
拷贝构造函数:class(const class &);
赋值运算符:class& operator=(const class&);
如果你认为两者调用是按函数声明调用的,那就错了。
用下面程序简单说明:
#include "stdafx.h"
#include
// 拷贝和赋值的区别
class Test{
public:
Test(){};
Test& operator=(const Test&){ std::cout << "执行了赋值操作" << std::endl; return *this; };
Test(const Test&){ std::cout << "执行了拷贝操作" << std::endl; }
~Test(){};
};
int main(){
Test t1;
// 拷贝
Test t2(t1);
Test t3 = t1;
// 赋值
Test t4, t5;
t5 = t4 = t1;
return 0;
}
// 结果显示:
执行了拷贝操作
执行了拷贝操作
执行了赋值操作
执行了赋值操作
请按任意键继续. . .
分析:
(1) 是不是觉得t3 = t1是执行了拷贝构造函数很奇怪?
那我们假设t3 = t1执行了赋值运算符会发什么什么。重载赋值运算符函数里,会把t1里的成员数据复制到自身(即t3),但问题来了,t3这个时候都没有被实例化(即没有构造),压根没有this指针,如何复制呢。
(2)Test t2(t1) 和 Test t3 = t1 其实还是有区别的
Test t2(t1); 属于直接初始化:要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数。
Test t3 = t1; 属于拷贝初始化:要求编译器将右侧运算对象拷贝到正在创建的对象中,如果需要还要进行类型转换。
总结:
(1)拷贝构造是对象还没有创建,而赋值时对象已经创建了。
(2)拷贝构造函数什么时候会被调用:
- 用 = 定义变量时
- 将一个对象作为实参传递给一个非引用类型的形参
- 从一个返回类型为非引用类型的函数返回一个对象
- 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
- 某些类类型还会对它们所分配的对象使用拷贝初始化。例如,当我们初始化标准库容器或是调用insert或push成员,容器会对其元素进行拷贝初始化。与之相反,用emplace成员创建的元素都进行直接初始化。
为什么拷贝构造函数的参数要为引用?
那我们假设拷贝构造函数里的参数不为引用会发什么什么?执行 Test t3 = t1 时,将t1传递给 Test(Test),因为不是引用,所以是值传递,值传递要拷贝,t1执行拷贝构造函数,如此就会陷入无限循环中。
为什么赋值运算符要返回自身的引用(也就是返回一个指向左侧运算符对象的引用)?
链式操作,t5 = t4 = t1;
看以下两段代码
class Test{
public:
Test() = default;
Test(int x){ std::cout << "执行构造操作" << std::endl;};
Test(const Test&){ std::cout << "执行了拷贝操作" << std::endl; }
Test& operator=(const Test&){ std::cout << "执行了赋值操作" << std::endl; return *this; };
~Test(){};
};
int main(){
Test t = 5;
return 0;
}
结果显示:
执行构造操作
请按任意键继续. . .
class Test{
public:
Test() = default;
explicit Test(int x){ std::cout << "执行构造操作" << std::endl;};
Test(const Test&){ std::cout << "执行了拷贝操作" << std::endl; }
Test& operator=(const Test&){ std::cout << "执行了赋值操作" << std::endl; return *this; };
~Test(){};
};
int main(){
Test t = Test(5);
return 0;
}
// 结果显示:
执行构造操作
请按任意键继续. . .
第一段代码,Test t = 5,由于构造函数中有Test(int x),这个构造函数第一个函数为int(后面可以有默认值参数),所以隐式转换,这种叫转移构造函数。我一直以为,当执行了构造函数后得到一个临时对象然后在通过拷贝构造函数将这个临时对象传给t,但是根据上面的代码,此转移构造函数相当于 Test t(5)。
第二段代码,加了explicit关键字,说明上面的隐式转换不会发生,所以就要显示使用。所以只能 Test t(5)。
已知,如果一个类没有显示构造函数、拷贝构造函数、赋值构造函数时,编译器会默认提供一个构造函数、拷贝构造函数、赋值构造函数。
至于,如果一个类没有显示构造函数,编译器会提供一个合成的默认构造函数这句话是有问题的,只有当类需要编译器提供默认构造函数才会提供。这里就不具体展开了。。
浅拷贝和深拷贝的区别:前者就是使用编译器提供的默认拷贝构造函数或者默认赋值构造函数。后者是自己显示实现的拷贝/赋值构造函数。
大多时候,使用默认的拷贝/赋值构造函数就行了,但是当类中有动态分配的数据,或者类中有别的类成员时且该类中也有动态分配内存,然后也没有显示拷贝/赋值函数时....层次递推,可能这个包含类中也包含了别的类。。。
这个时候如果用编译器默认提供的拷贝和赋值构造函数 就会出现问题。
简单用下面两段代码说明:
(1)使用默认的拷贝/赋值构造函数
class Test{
public:
Test(int x, int y);
~Test();
int a;
int b;
int *num;
};
Test::Test(int x, int y) : a(x), b(y){
num = new int[a * b]();
}
Test::~Test(){
delete[] num;
num = nullptr;
}
int main(){
Test t1(5, 5);
Test t2 = t1;
std::cout << "t1.a = " << t1.a << " " << "t1.b = " << t1.b << std::endl;
std::cout << "t2.a = " << t2.a << " " << "t2.b = " << t2.b << std::endl;
return 0;
}
// 结果报错
(2)自定义拷贝构造函数
class Test{
public:
Test(int x, int y);
Test(const Test&);
~Test();
int a;
int b;
int *num;
};
Test::Test(int x, int y) : a(x), b(y){
num = new int[a * b]();
}
Test::~Test(){
delete[] num;
num = nullptr;
}
Test::Test(const Test& other){
this->a = other.a;
this->b = other.b;
int len = this->a * this->b;
this->num = new int[this->a * this->b];
for (int i = 0; i < len; i++){
this->num[i] = other.num[i];
}
}
int main(){
Test t1(5, 5);
Test t2 = t1;
std::cout << "t1.a = " << t1.a << " " << "t1.b = " << t1.b << std::endl;
std::cout << "t2.a = " << t2.a << " " << "t2.b = " << t2.b << std::endl;
return 0;
}
// 结果显示:
t1.a = 5 t1.b = 5
t2.a = 5 t2.b = 5
请按任意键继续. . .
分析:代码(1)为什么会报错,因为代码1中的默认构造函数实现如下:
Test::Test(const Test& other){
this->a = other.a;
this->b = other.b;
this->num = other.num;
}
默认构造函数,只是将t1中的num指针赋值给t2,也就是t1和t2对象中的num共同指向相同的内存,但是main()函数结束时,要执行析构函数,t1执行一次,num释放内存了,而t2又释放已经不存在的内存,当然会报错!
所以,自己定义类的时候一定要自己显示拷贝/赋值构造函数!!!。。什么时候执行拷贝构造函数,上面已经说了。