C++默认构造,拷贝构造,赋值运算符构造,移动构造
在C++的一个类中,有几种常见的构造函数,默认构造函数,拷贝构造函数,赋值运算构造函数以及移动构造函数。单独讲解每一个构造函数的概念都比较清晰,结合函数引用和赋值,理解起来就存在许多问题。本文重点不在于概念讲解,侧重于对各种函数不同特性的理解。
1. 函数参数和返回值
对于一个函数,如下定义:
int func(int a) { return a; }
传入参数时,实际执行的是一个赋值操作,相当于临时变量a=实参a。函数返回时,执行的也是一个赋值操作,把形参a赋值给另一个变量返回,然后销毁形参a(整个函数执行了两次拷贝,在函数完成时会销毁两个临时变量,一个形参a,一个返回时赋值的返回参数)。
2. 拷贝构造函数
对于函数参数和返回值有了一定的理解,我们再来看拷贝构造函数。先看下面的类定义:
class A { public: A() { cout << "default constructor" << endl; //默认构造函数 } A(int num):m_num(num){ cout << "constructor" << endl; //普通构造函数 } ~A() { cout << "destructor" << endl; } };
PS:默认构造函数不含任何参数,在没有定义其他构造函数的情况下,编译器会自动生成默认构造函数(一旦定义了其他构造函数,不会生成默认构造函数)。
定义一个函数和main函数:
A func(A a) { A a1 = a; return a1; } int main() { A a1; //调用默认构造函数 A a2(3); //普通构造函数 func(a2); //函数调用,会调用两次拷贝构造函数 return 0; }
执行结果如下:
default constructor constructor destructor destructor destructor destructor
destructor
理解了函数传参和函数返回都会进行拷贝的原理,上面的结果就很清晰了。但是我们并没有自己定义拷贝构造函数,怎么调用拷贝构造函数的呢?在没有定义拷贝构造函数时,编译器会自动为程序生成拷贝构造函数。自动合成的拷贝构造函数等价于下面自定义的拷贝构造函数:
class A { public: A() { cout << "default constructor" << endl; } A(int num):m_num(num){ cout << "constructor" << endl; } A(A& a) { m_num = a.m_num; cout << "copy constructor" << endl; //拷贝构造函数 } ~A() { cout << "destructor" << endl; } public: int m_num=0; };
自定义拷贝构造之后,再运行上面的函数和main函数,得到结果如下:
default constructor
constructor
copy constructor //形参初始化调用一次copy constructor
copy constructor //函数内类赋值一次copy constructor
copy constructor //返回时拷贝一次copy constructor
destructor
destructor
destructor
destructor
destructor
可以看到函数执行时确实调用了两次拷贝构造函数。
PS:
1. 通常情况下,编译器合成的拷贝构造函数没有什么问题,但是当类中存在指针时,就会出现深拷贝和浅拷贝的问题,此时必须自定义拷贝构造函数实现深拷贝。
2. 拷贝构造函数第一个参数必须是该类的一个引用(不能是普通参数)。
3. 赋值拷贝运算符
对2中定义的类再添加赋值拷贝运算符定义:
class A { public: A() { //默认构造函数 cout << "default constructor" << endl; } A(int num):m_num(num){ cout << "constructor" << endl; } A(A& a) { //拷贝构造函数 m_num = a.m_num; cout << "copy constructor" << endl; } A& operator=(const A& a) { //拷贝赋值运算符 this->m_num = a.m_num; cout << "= constructor" << endl; return *this; } ~A() { cout << "destructor" << endl; } public: int m_num=0; };
定义下面的函数和main函数:
A func(A a)
{
A a1;
a1 = a;
return a1;
}
int main() { A a1; A a2(3); func(a2); return 0; }
执行之后的结果为:
default constructor //main函数第一句A a1; 执行默认构造函数 constructor //main函数第二句A a2(3);执行普通构造函数 copy constructor //函数形参拷贝,执行拷贝构造函数 default constructor //函数func第一句A a1;执行默认构造函数 = constructor //函数func第二句a1=a;执行等号赋值运算符,注意此时由于a1已经调用默认构造函数初始化,所以赋值运算符不会实例化一个对象,此句不对应析构函数 copy constructor //return a1返回时,调用一次拷贝构造函数 destructor destructor destructor destructor destructor
PS:
1. 拷贝赋值运算符永远不会实例化一个对象,因此也就不对应一个析构函数,即使像下面的语句此时也是调用拷贝构造函数进行初始化。
A a3=a2; //a2是一个A实例
2. 那么拷贝构造函数与拷贝赋值运算符有什么区别呢,即什么情况下拷贝赋值运算符的定义才有意义?在shared_ptr的实现上,可以看出两者的一个区别:
class A { public: A():m_num(NULL),count(NULL){ //默认构造函数 } A(int* p) :m_num(p) { *count=1; } A(A& a) { //拷贝构造函数,之前count,m_num一定没有指向其他值 if (a.m_num) { m_num = a.m_num; count = a.count; *count++; } } A& operator=(const A& a) { //拷贝赋值运算符 if (a.m_num) { (*(a.count))++; } if (count && (--(*count))) //此时此类实例指向其他对象,不为空,那么其他对象的引用计数要减1 { delete count; delete m_num; } m_num = a.m_num; //改变指向 count = a.count; } ~A() { if (count && !(--(*count))) { delete count; count = nullptr; delete m_num; m_num = nullptr; } } private: int* m_num; atomic<int>* count; };
4. 移动构造
4.1 左值、右值、左值引用、右值引用
左值,可以简单理解为能用取地址运算符&取其地址的,在内存中可访问的( primer C++第五版说当对象是左值时,用的是对象的身份,即在内存中的位置)。右值即临时变量,即将要被销毁的,不能获取地址(primer C++第五版说当对象是右值时,用的是对象的值)。左值引用就是对左值的引用,右值引用就是对右值的引用(右值引用只能绑定到一个即将被销毁的对象上)。下面看几个例子:
int i=42; //i是左值 int &r=i; //r是左值引用 int &&ri; //错误,不能将右值引用绑定到左值 int &r2=i*42; //错误,i*42是一个临时对象,为右值,不能将左值引用绑定到右值上 const int &r3=i*42; //正确,const引用可以绑定到右值上 int &&rr2=i*42; //正确 int getZero() { int zero=0; return zero; } int a=getZero(); //a是左值,getZero是右值,之前提过函数返回值的原理
PS:const引用既可以绑定到左值,也可以绑定到右值。
4.2 移动构造函数
下面的图很好的说明了移动构造的原理。
为了说明移动构造,我们改造一下之前的类A。
class A { public: A() { //默认构造函数 cout << "default constructor" << endl; } A(int num) :m_ptr(new int(num)) { cout << "constructor" << endl; } A(A& a) { //拷贝构造函数,此时要写成深拷贝 m_ptr = new int(*a.m_ptr); cout << "copy constructor" << endl; } //拷贝赋值运算符在此意义不大 A& operator=(A& a) { //拷贝赋值运算符 if (m_ptr) delete m_ptr; //删除原对象 m_ptr = a.m_ptr; //此时两个指针指向同一对象 cout << "= constructor" << endl; return *this; } //移动构造函数,传进来的一定是右值引用,这样保证a.ptr不会再被使用 A(A&& a) :m_ptr(a.m_ptr) { a.m_ptr = NULL; cout << "move constructor" << endl; } ~A() { if(m_ptr) delete m_ptr; cout << "destructor" << endl; } public: int* m_ptr; }; int main() { A a1(3); //调用普通构造函数 A a2(a1); //调用拷贝构造函数 A a3(move(a1)); //move函数保证传进去的是右值,移动构造 cout << *a1.m_ptr << endl; //错误,调用移动构造后,a1.m_ptr的内存被a3接管,a1指针为空 //这也是为什么要求传进移动构造函数的对象为右值,右值保证后续不会再被访问 return 0; }
下面再看一个例子(注:此例子来自C++移动构造):
#include<iostream> using namespace std; class IntNum { public: IntNum(int x = 0) : xptr(new int(x)) { //构造函数 cout << "Calling constructor..." << endl; } IntNum(const IntNum & n) : xptr(new int(*n.xptr)) {//复制构造函数 cout << "Calling copy constructor..." << endl; } IntNum(IntNum && n) : xptr(n.xptr) { //移动构造函数 n.xptr = nullptr; cout << "Calling move constructor..." << endl; } ~IntNum() { //析构函数 delete xptr; cout << "Destructing..." << endl; } int getInt() { return *xptr; } private: int *xptr; }; //返回值为IntNum类对象 IntNum getNum() { IntNum a; return a; } int main() { cout << getNum().getInt() << endl; return 0; }
该函数的例子运行结果如下:
Calling constructor... Calling move constructor... Destructing... 0 Destructing...
解释:调用getNum()函数,首先生成局部变量a(调用构造函数),在getNum return返回时,返回值是一个临时变量(右值),因此采用移动构造返回,返回之后a销毁,然后获取移动构造的值打印出来,cout语句之后,该移动构造的临时对象也被销毁,因此调用了两次构造函数(一次普通构造,一次移动构造,对应两次析构,只释放一次内存)。