C++构造、析构、拷贝构造、拷贝赋值、移动构造、移动赋值之二

一、概述

拷贝和移动是操作类对象的另外两种重要的方式。在类中的定义方式如下:

class T{
public:
    T(const T&);            //拷贝构造
    T(T&&);                 //移动构造
    T& operator=(const T&); //拷贝赋值
    T& operator=(T&&);      //移动赋值
}

在程序中发生,拷贝和移动在如下情况下默认发生:

1.赋值操作 发生拷贝赋值:source是lvalue的时候 或者移动赋值:source是rvalue的时候

2.拷贝初始化(使用已有对象进行初始化) 发生拷贝构造:允许隐式类型转换的发生。

3.函数参数传递 发生拷贝构造:相当于对函数的形式参数进行拷贝初始化,因此,允许隐式类型转换的发生。

4.函数返回值 发生移动构造:将函数栈空间上的临时变量直接通过右值引用到caller的接收变量上,不需要发生拷贝动作。

5.抛出异常 发生拷贝构造

<原则>:默认情况下,程序应当优先进行拷贝操作,拷贝虽然牺牲性能,但是保证正确,而移动操作只在少数情况下使用。

二、拷贝

1. 拷贝的含义

将x拷贝给y,要保持如下两个特性:a.相同(拷贝前后,x和y在值上完全相等);b.独立(拷贝后,x和y相互独立)

浅拷贝和深拷贝(shallow copy and deep copy): 浅拷贝是按成员进行拷贝(member-wise copy),仅仅将成员拷贝一份。对于resource handler的成员,不拷贝其所占有的资源对象。

深拷贝是完整的拷贝,拷贝所有成员,以及resource handler成员所占有的资源对象。

拷贝构造和拷贝赋值: 拷贝构造是从无到有,拷贝目标变量是空的,需要分配空间,并进行初始化。拷贝赋值需要处理拷贝目标变量可能已经占有的资源,需要将原来的资源释放。例如:

class CoMo{
public:
    CoMo(){}//Default Constructor
    CoMo(int);//Single-para Constructor
    CoMo(int, string);//Double-para Constructor
    CoMo(std::initializer_list<char>, string);//Initializer List Constructor
    ~CoMo();//Destructor
    //Copy Construction
    CoMo(const CoMo &);
    //Copy Assignment
    CoMo&operator=(const CoMo &);
    int size;
    char * ch;
    string s;
};
CoMo::CoMo(int x):size{x},s{""}{
    ch=new char[size];
    cout << "Single-Para Constructor...." << endl;
}

CoMo::CoMo(int x, string str):size{x},s{str}
{
    ch=new char[size];
    cout << "Double-Para Constructor...." << endl;
}

CoMo::CoMo(std::initializer_list<char> list, string str)
    :s{str}
{
    size=list.size();
    ch=new char[size];
    for (int i=0;i!=size;i++){
        ch[i]=list.begin()[i];
    }
    cout << "Initializer List Constructor..." << endl;
}

CoMo::~CoMo()
{
    if(ch){
        delete [] ch;
        cout << "Destructing the char vector..." << endl;
    }
    cout << "Destruction Completed!!" << endl;
}

CoMo::CoMo(const CoMo & source)
    :size{source.size},s{source.s}
{
    ch=new char[size];
    for (int i=0;i!=size;i++){
        ch[i]=source.ch[i];
    }
    cout << "Deep Copy Construction..." << endl;
}

CoMo& CoMo::operator= (const CoMo & source)
{
    size=source.size;
    s=source.s;
    if(!ch){delete [] ch; cout << "Deleting Target Variable Resource..." << endl;}
    ch=new char[size];
    for (int i=0;i!=size;i++){
        ch[i]=source.ch[i];
    }
    cout << "Deep Copy Assignment..." << endl;
    return *this;
}

void myprint(CoMo SC){
    cout << "Printing Elements..." << endl;
    cout << "Char Vector Size: " << SC.size << endl;
    cout << "Char Value: " << SC.ch[0] << " at the address: "
         << static_cast<void*>(SC.ch) << endl;
    cout << "String in SC is: " << SC.s << endl;
}

void myprint2(CoMo & SC)
{
    cout << "Printing Elements..." << endl;
    cout << "Char Vector Size: " << SC.size << endl;
    cout << "Char Value: " << SC.ch[0] << " at the address: "
         << static_cast<void*>(SC.ch) << endl;
    cout << "String in SC is: " << SC.s << endl;
}
int main(){
    CoMo A{
            {'f','b','c','d','e'},"Hello"};
    CoMo B{
            {'g','b'},"World"};
    CoMo C{A};      //Copy Construction
    A=B;            //Copy Assignment
    myprint2(A);    //Copy Construction in function argument passing
    myprint(A);
    return0;
}

上述程序输出:

Initializer List Constructor...
Initializer List Constructor...
Deep Copy Construction...
Deep Copy Assignment...
Printing Elements...
Char Vector Size: 2
Char Value: g at the address: 0x7f91f5402720
String in SC is: World
Deep Copy Construction...
Printing Elements...
Char Vector Size: 2
Char Value: g at the address: 0x7f91f5402730
String in SC is: World
Destructing the char vector...
Destruction Completed!!
Destructing the char vector...
Destruction Completed!!
Destructing the char vector...
Destruction Completed!!
Destructing the char vector...
Destruction Completed!!

可以看到,函数参数by-value传递时,调用了拷贝构造函数,而传递引用时,没有对对象进行拷贝,变量的地址值并不相同。而且,在by-value传递中,从函数返回的时候,局部对象的析构函数被调用。

2.默认的拷贝

如果不提供拷贝构造和拷贝赋值函数,那么,在进行拷贝构造和拷贝赋值的时候,将调用对应的默认的函数,这些函数的仅仅执行浅拷贝。

3.父类的拷贝

不严格的说,父类可以视为子类的一个成员。因此,如同子类构造函数对父类对象的构造仅需要在成员初始化列表中写入一样,对父类的拷贝,同样只需要进行同样的操作即可(拷贝构造函数也可以带有成员初始化列表,用子类对象给父类拷贝初始化即可,这样会调用父类的拷贝构造函数)。例如:

class Sub_CoMo::public CoMo{
public:
    Sub_CoMo()
        :CoMo{
                {'a','w'},"HelloWorld"},
                sub_a{2},
                sub_s{"SUB"}
        {}
    Sub_CoMo(const Sub_CoMo & source)
        :CoMo{source},sub_a{source.sub_a},sub_s{source.sub_s}
        {}
    int sub_a;
    string sub_s;
};
int main(){
    Sub_CoMo SubA{};
    Sub_CoMo SubB{SubA};
    return 0;
}

输出:

Initializer List Constructor...
Deep Copy Construction...

可以看到,SubA在构造时调用了父类的构造函数。在SubA对SubB拷贝构造时,直接使用Sub_CoMo的对象对父类进行拷贝构造,发生隐式slicing,调用父类的拷贝构造函数。

4.Slicing

在上述第三条中,将子类对象通过拷贝的方式赋值给父类,可能发生slicing的现象。指向父类型的指针可以指向子类型对象,更糟糕的是,这种类型转换是隐式的。子类中很可能有父类所不具有的成员和函数,这样将子类对象拷贝给父类变量时,子类中有父类中没有的成员会自动丢失。

三、移动

1.移动的含义

传统的程序中,总是使用拷贝,虽然影响性能,但是总是安全的。引入移动的目的是为了减少不必要的拷贝操作。其想法是:如果一个已经构造好的对象在其所处的作用域内不再被使用(马上要离开scope,delete掉或者本身就是个临时的对象),但是在其他作用域中仍需要使用这个对象,那么与其把这个对象拷贝到其他作用域并析构本作用域中的这个对象,不如用其他作用域的指针或者引用,指向这个对象。 宏观上,看起来像是把这个对象从一处移动到另一处了,本质上,就是拷贝了这个对象的地址。对指针或者引用的拷贝开销很小,可以忽略不计(况且,built-in类型的移动就是拷贝)。

移动操作一定要保证原处的对象的状态是可以被正确析构的(因为马上就不用了,要被析构掉,在析构前进行移动,如果移动操作产生了留下了不确定的状态,致使析构出了异常,是不允许的)。

2.移动构造函数

类似拷贝构造函数,移动构造函数就是用源对象的指针(或者引用)拷贝初始化目标对象的指针(或者引用),同时将源对象的指针(或引用)的状态正确处理,保证析构正常。

3.移动赋值函数

类似拷贝赋值函数,移动赋值函数是将源对象的指针(或引用)拷贝赋值给目标对象的指针(或引用),同时将源对象的指针(或引用)的状态正确处理,保证析构正常。拷贝赋值中要正确处理源对象已经占用的资源,一般要先进行释放。

既然目标对象和源对象都要进行析构(早晚),与其在移动赋值函数中通过裸露的delete进行删除,不如遵循RAII原则,将资源的释放交给析构函数进行,在移动赋值中,仅仅交换指针(或引用)即可。这种交换当然是拷贝操作,但是开销可以忽略。

那么,什么时候调用移动赋值函数,什么时候调用拷贝赋值函数?除了极少数情形,自动调用移动赋值函数,比如函数返回时(函数返回时调用的是移动构造函数)。一般来说,必须在=右侧使用rvalue来显式调用移动赋值函数。如果=右侧是lvalue,那么调用拷贝赋值函数。

4.std::move函数

为了显式的调用移动赋值函数,有时需要将lvalue转换成rvalue,std::move函数就是起到这个作用。

使用移动函数的例子: 1)定义如下的类:

class CoMo{
public:
    CoMo(){}//Default Constructor
    CoMo(int);//Single-para Constructor
    CoMo(int, string);//Double-para Constructor
    CoMo(std::initializer_list<char>, string);//Initializer List Constructor
    ~CoMo();//Destructor
    //Copy Construction
    CoMo(const CoMo &);
    //Copy Assignment
    CoMo&operator=(const CoMo &);
    //Move Construction
    CoMo(CoMo && source);
    //Move Assignment
    CoMo& operator= (CoMo &&);
    int size;
    char * ch;
    string s;
};

2)定义移动构造函数和移动赋值函数:

CoMo::CoMo(CoMo && source)
    :size{source.size},ch{source.ch},s{source.s} //Initialize target object
{
    source.size=0;
    source.ch=nullptr;
    source.s="";
    cout << "Move Constructor..." << endl;
}

CoMo& CoMo:: operator= (CoMo && source){
    if (ch) {delete [] ch; cout << "Cleanup Target Resource..." << endl;} 
    //Cleanup target resource
    //Ordinary Copy Assignment
    size=source.size;
    ch=source.ch;
    s=source.s;
    //Cleanup source object
    source.size=0;
    source.ch=nullptr;
    source.s="";
    cout << "Move Assignment..." << endl;
    return *this;
}

类的构造和析构函数的定义同第一部分。 3)定义测试移动构造函数的函数:

CoMo Basics::MoveTest(CoMo source) {return source;}//Test Return Method

4)主函数部分

int main(){
CoMo A{
        {'f','b','c','d','e'}, "Hello"};
CoMo B{
        {'g','b'},"World"};
auto C=MoveTest(A);
A=B;        //Copy Assignment
B=move(C);  //Move Assignment
return 0;}

输出结果:

Initializer List Constructor...
Initializer List Constructor...
Deep Copy Construction...       (函数参数按照值传递)
Move Constructor...             (return语句)
Destruction Completed!!         (清空局部变量)
Deep Copy Assignment...         (A=B)
Cleanup Target Resource...      (释放A占用的资源)
Move Assignment...              (B=move(C))
Destruction Completed!!         (析构C)
Destructing the char vector...  (析构B)
Destruction Completed!!
Destructing the char vector...
Destruction Completed!!

首先,构造A和B两个对象,然后调用MoveTest时,进行by-value的拷贝构造。从MoveTest返回时调用移动构造函数,紧接着析构函数内的局部变量(已经被架空的A对象的局部拷贝,因为已经被架空,所以析构的时没有释放char的资源)。随后是拷贝赋值函数,将B拷贝给A。最后显式调用移动赋值函数(move函数强制将lvalue的C转成rvalue),C被架空。最后退出main的作用域,析构C,B,A。由于C已经被架空,所以析构C时,可以看到并没有释放char的资源,而析构B和A时,释放了相应的资源。

如果没有定义移动类的函数,函数按值返回时,将会调用对应类的拷贝构造函数。输出如下:

Initializer List Constructor...
Initializer List Constructor...
Deep Copy Construction...
Deep Copy Construction...   (return语句)
Destructing the char vector...
Destruction Completed!!
Deep Copy Assignment...
Deep Copy Assignment...     (B=move(C),由于没有移动赋值函数,即使用右值调用=,也是调用的拷贝赋值函数)
Destructing the char vector...
Destruction Completed!!
Destructing the char vector...
Destruction Completed!!
Destructing the char vector...
Destruction Completed!!

可以看到,在A=B这个拷贝构造之前,调用了一次拷贝构造,就是来自MoveTest的return语句。这也导致没有对象被架空,所有的析构函数都调用了释放char*资源的部分。

通过这个简单的对比,可以看出移动函数的在提升效率方面的重要意义。有了移动构造函数,再占空间的局部类对象,也可以以极小的代价从函数中by-value的返回,而不用再返回引用和指针了(这很容易出错,返回局部对象的引用或者指针是很危险的,可以视为是禁止使用的,通常是在函数外先开辟内存,将这块内存的指针或引用传递给函数)。

补充 函数返回过程: 这里详细说明一下函数的调用和返回的过程,可以通过这个例子辨析清楚函数调用和返回中的资源管理过程。

首先通过函数的入口地址进入一个函数,这个地址指向函数所在的程序区(代码区)。随后,给这个函数分配数据区,数据区分为栈和堆,栈用于存储函数执行过程中的状态变化,堆用于存储局部变量(可以随机访问)。在堆上,首先参数传递被进来,无论以何种形式传递参数(引用,指针,by-value,还是临时变量),如果传递的是lvalue,这个过程都是拷贝构造过程,只不过拷贝的是一个地址还是一个真实的对象;如果传递的是rvalue,那么在堆上直接调用构造函数生成这个临时变量。随着函数的执行,局部对象在堆上被依次构造起来,并被相应的处理,直到来到return语句。

如果return语句中有表达式或者函数调用,先进行计算,计算结果存在堆上的一个临时变量中。如果return语句返回一个已经存在的变量(无所谓局部还是全局还是静态),则不产生新的变量。然后执行return语句,在函数的栈顶产生一个返回值的地址,可能指向一个临时变量或者局部变量。最后,开始返回过程,将这个地址所指向的对象通过拷贝构造或者移动构造转移给caller的接收变量。如果没有接收的变量,那么就转移给堆上的一个临时变量(注意,拷贝或者移动依然发生)。一般来说,有return语句,一定会进行至少一次拷贝或者移动。由于return的开销有时很大,编译器大多会进行优化,存在多次冗余的拷贝和移动的情形时,往往会优化成一次拷贝或者移动。

离开函数作用域之后,开始依次析构堆上引用计数为0的对象。如果函数的返回值没有接收变量,首先就会析构return转移给的临时变量。

考虑如下代码:

int main(){
    CoMo A{
            {'f','b','c','d','e'}, "Hello"};
    CoMo B{
            {'g','b'},"World"};
    cout << "--------------------" << endl;
    auto C=MoveTest({
                        {'d'},"Hi~~"});
    cout << "--------------------" << endl;
    auto D=MoveTest(A);
    cout << "--------------------" << endl;
    MoveTest(A);
    cout << "--------------------" << endl;
    return 0;
}

输出:

Initializer List Constructor...
Initializer List Constructor...
--------------------
Initializer List Constructor...     (在堆上构造出临时对象)
Move Constructor...                 (返回临时对象,移动给C)
Destruction Completed!!             (销毁临时对象)
--------------------
Deep Copy Construction...
Move Constructor...
Destruction Completed!!
--------------------
Deep Copy Construction...           (将A参数拷贝构造传递)
Move Constructor...                 (return结果给一个临时变量)
Destructing the char vector...      (释放上一步产生的返回值临时变量)
Destruction Completed!!
Destruction Completed!!             (释放MoveTest中存在的被架空的局部变量)
--------------------

如果定义一个新的MoveTest2函数,在return语句中,进行更加复杂的返回操作:

CoMo MoveTest2(CoMo source) {
    auto tmp=MoveTest(source);
    return tmp;}

在main中执行:

CoMo E=MoveTest2(A);
cout << "--------------------" << endl;
MoveTest2(A);
cout << "--------------------" << endl;

输出:

Deep Copy Construction...
Deep Copy Construction...
Move Constructor...
Destruction Completed!!
Destructing the char vector...
Destruction Completed!!
--------------------
Deep Copy Construction...
Deep Copy Construction...
Move Constructor...
Destruction Completed!!
Destructing the char vector...
Destruction Completed!!
Destructing the char vector...
Destruction Completed!!
--------------------

按道理,在MoveTest2中调用了MoveTest,返回时,应该发生一次移动操作,然后从MoveTest2中返回,再发生一次移动操作。但是我们看到,从MoveTest2中返回时,并没有发生多次移动或拷贝操作,仅仅一次移动操作就正确的返回了我们要的结果。所以,这说明编译器对函数的返回过程进行了优化,真实的返回过程并非机械的按照上述的步骤进行。

最后,为了提高效率,可以使用swap函数来定义移动赋值函数,如下:

CoMo& CoMo::operator=(CoMo && source) {
    swap(size,source.size);
    swap(ch,source.ch);
    swap(s,source.s);
    cout << "Move Assignment by Swap..." << endl;
    return *this;
}

这样的输出结果如下:

Initializer List Constructor...
Initializer List Constructor...
Deep Copy Construction...
Move Constructor...
Destruction Completed!!
Deep Copy Assignment...
Move Assignment by Swap...
Destructing the char vector...
Destruction Completed!!
Destructing the char vector...
Destruction Completed!!
Destructing the char vector...
Destruction Completed!!

由于Swap没有架空C,析构C的时候要释放char*的资源。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值