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语句之后,该移动构造的临时对象也被销毁,因此调用了两次构造函数(一次普通构造,一次移动构造,对应两次析构,只释放一次内存)。

posted @ 2020-08-04 16:12  晨枫1  阅读(1091)  评论(0编辑  收藏  举报