在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
destructordestructor
理解了函数传参和函数返回都会进行拷贝的原理,上面的结果就很清晰了。但是我们并没有自己定义拷贝构造函数,怎么调用拷贝构造函数的呢?在没有定义拷贝构造函数时,编译器会自动为程序生成拷贝构造函数。自动合成的拷贝构造函数等价于下面自定义的拷贝构造函数:
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实例
- 那么拷贝构造函数与拷贝赋值运算符有什么区别呢,即什么情况下拷贝赋值运算符的定义才有意义?在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语句之后,该移动构造的临时对象也被销毁,因此调用了两次构造函数(一次普通构造,一次移动构造,对应两次析构,只释放一次内存)。
原文链接: https://www.cnblogs.com/yuanwebpage/p/13365038.html
欢迎关注
微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍
原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/201120
非原创文章文中已经注明原地址,如有侵权,联系删除
关注公众号【高性能架构探索】,第一时间获取最新文章
转载文章受原作者版权保护。转载请注明原作者出处!