拷贝控制

1. 拷贝构造函数

  如果一个构造函数的第一个参数是自身类型的引用,且额外参数都有默认值,则此构造函数是拷贝构造函数。

  而合成拷贝构造函数是缺省的拷贝构造函数,即默认的拷贝构造函数,一般情况下,它的实际操作是对所有成员变量进行一次赋值操作如果成员的缺省的赋值操作不能满足需求(如是一个链表或含有指针),就必须要对成员类定义重载赋值操作符,才能够让合成拷贝构造函数起作用。

  通常在函数的调用时,使用非引用的对象类型作函数参数时,这个时候会调用该类的拷贝构造函数,如下形式:

#include <string>
#include <string.h>
#include <stdlib.h>
#include <iostream>
using namespace std ;
class Book
{
private:
    char* _bookISBN ;
    float _price ;
​
public:
    Book(char* ISBN,float price=0.0f): _price(price){
        _bookISBN = (char*)malloc(strlen(ISBN) + 1);
        strcpy(_bookISBN, ISBN);
        cout << "构造函数被调用." << endl;
    }
    Book(const Book& book): _price(book._price){
        _bookISBN = (char*)malloc(strlen(book._bookISBN) + 1);
        strcpy(_bookISBN, book._bookISBN);
        cout << "拷贝构造函数被调用" << endl;
    }
    ~Book(){
        if(_bookISBN != NULL){
            free(_bookISBN);
            _bookISBN = NULL;
        }
    }
    void print(){
        cout << _bookISBN << ": " << _price << endl;
    }
};
//此时形参是非引用,当有代码调用此处时,形参将调用拷贝构造函数去复制实参。
void printBook(Book book){
    book.print();
}
int main()
{
    Book A("A-A-A", 20);
    printBook(A);
    
    Book B = A; //赋值初始化也会调用拷贝构造函数
    return 0;
}
​
​
/* 
 *output 
 * 构造函数被调用.
 * 拷贝构造函数被调用
 * A-A-A: 20
 * 拷贝构造函数被调用
 */

  所以,拷贝构造函数的第一个参数必须是自身类型的引用,否则会陷入死循环——为了调用拷贝构造函数,因为参数不是引用,就需要调用拷贝构造函数去拷贝它的实参作为函数的参数,但是这个拷贝构造函数又是自身,如此就会无限循环。


2.  拷贝赋值运算

  拷贝赋值运算就是控制对象之间的赋值操作,通常是要重载该类的赋值运算。基于上面程序加上一个参数为空的构造函数和拷贝赋值运算的重载函数。

class Book
{
    //....
    Book(){}
    //赋值构造函数
    Book& operator=(const Book& book){
        _bookISBN = (char*)malloc(strlen(book._bookISBN) + 1);
        strcpy(_bookISBN, book._bookISBN);
        cout << "拷贝赋值运算符被调用。" <<endl;
    }
};
​
int main()
{
    Book A("A-A-A", 20);
    Book B;
    B = A;
    return 0;
}
​
/* 
 *output 
 * 构造函数被调用.
 * 拷贝赋值运算符被调用。
 * */  

合成函数的defaultdelete

  可以通过将拷贝控制成员定义为=default来让 编译器自动创建合成版本的拷贝函数

class Book{
public:
    Book() = default;
    Book(const Book &) = default;
    Book& operator=(const Book&) = default;
    ~Book() = default;
};
​

  在新标准中,可以通过定义delete来删除拷贝构造函数和拷贝赋值运算。形式如下:

Book& operator=(const Book& book) = delete;

  通过=delete告知编译器,该类不想定义这种赋值运算符,但要注意的是析构函数是不允许被删除的函数。


 3. 右值引用

  右值转移的一个最大作用是移动语义——在知乎上看到一个很形象的例子,主要讲解的是对象的资源所有权转移的问题,可以取消对一些即将失效对象的资源销毁操作,同时将资源的拥有权转向一个新的需要用到的对象。具体可以用这个比喻来形象的解释:

  比如现在我们要搬家(假设现在所有的物品都是新的),从A住所搬到B住所,我们通常的做法是将原住所A的物品搬到B住所去,因为是新的,所以我们可以继续使用,只不过这些家具的所属地从A换到了B而已。还有另外一种做法就是,将A的物品全部销毁,然后再去商场上购买一样的家具放到B住所。显然第二种做法既费时又废财,但在C++11之前,C++都是以第二种方法来实现的,类似无法将存在的对象资源合法的转移到另一个新对象,这种现象就被称为移动语义的缺失,但在C++11中,新的标准库的实现在多种场景下消除了不必要的额外开销,可以实现对象资源所有权的转移,通常用在按值传入参数(比如移动构造函数),还有按值返回的函数等等。

  右值引用即必须绑定到右值的引用,通过&&表示右值引用,右值引用的一个重要特性是只能绑定到一个即将销毁的对象。

  左值和右值的区别(左值持久;右值短暂——左值对应变量的存储位置,而右值对应变量的值本身

    • 左值是有持久的状态。
    • 右值要么是字面常量,要么就是在表达式求值过程中创建的临时变量。

  通常左值引用不能将其绑定到要求转换的表达式、字面常量或是返回右值的表达式。右值引用有着完全相反的特性,可以将右值绑定到这类表达式上。

int &&rr1 = 42;     //正确,字面常量是右值
int &&rr2 = rr1;    //错误,表达式rr1是左值
int &&rr3 = i * 2;  //正确,表达式是右值

  虽然右值引用不能直接绑定到一个左值上,但可以通过move函数显示的实现对左值的绑定,如

int &&rr2 = std::move(rr1); //正确,告诉编译器像一个右值一样处理左值

  调用move后就意味着:除了对rr1进行赋值操作或者销毁它,程序将不会在使用到该变量。

#include <string>
#include <string.h>
#include <stdlib.h>
#include <iostream>
#include <vector>
​
using namespace std ;
class Book
{
private:
    char* _bookISBN ;
    float _price ;
public:
    Book(){
        _bookISBN = NULL;
    }
    Book(char* ISBN,float price=0.0f): _price(price){
        _bookISBN = (char*)malloc(strlen(ISBN) + 1);
        strcpy(_bookISBN, ISBN);
        cout << "构造函数被调用." << endl;
    }
    ~Book(){
        if(_bookISBN != NULL){
            free(_bookISBN);
            _bookISBN = NULL;
        }
         cout << "析构函数被调用" << endl;
    }
};
​
int main()
{
    vector<Book> v;;
    Book b("aaa", 10);
    cout << &b << endl;
    Book &&bb = std::move(b);
    cout << &bb << endl;
    return 0;
}
​
/* output
​
        构造函数被调用.
        0x7ffcbeb6d7b0
        0x7ffcbeb6d7b0
        析构函数被调用
*/

移动构造函数

  如果一个指针所指向非常大的内存数据的话,则调用拷贝构造的代价就非常昂贵,会极大地影响性能。C++11提供一种简洁解决方法:移动构造函数,它将新对象的成员指针指向原来对象成员指针指向的内存地址,然后将原对象成员指针置为空指针,从而保证在析构的时候不会产生产生double free。这样既不用分配新内存,也不会产生内存泄漏,从而很好地解决了上述问题。示例代码如下,假设Book类中的char数组_bookISBN占用的空间比较大,可以通过调用移动构造函数来实现对这块内存的转移:

#include <string>
#include <string.h>
#include <stdlib.h>
#include <iostream>
using namespace std ;
class Book
{
private:
    char* _bookISBN ;
    int _price ;
public:
    Book(){
        _bookISBN = NULL;
    }
    Book(char* ISBN,float price=0.0f): _price(price){
        _bookISBN = (char*)malloc(strlen(ISBN) + 1);
        strcpy(_bookISBN, ISBN);
        cout << "构造函数被调用." << endl;
    }
    ~Book(){
        if(_bookISBN != NULL){
            free(_bookISBN);
            _bookISBN = NULL;
        }
        cout << "析构函数被调用" << endl;
    }
    Book(Book &&book) noexcept : _price(book._price){
        //令book的成员变量为空,,防止对这块地址的double free
        _bookISBN = NULL;
        _bookISBN = std::move(book._bookISBN);
        book._bookISBN = NULL;
        cout << "移动构造函数被调用" << endl;
    }
};
int main()
{
    vector<Book> v;;
    Book b("aaasdgddsfsfdsgsgdgfsdgsgsdarhdwthjdftj", 10);
    Book bb = std::move(b);
    return 0;
}

 

参考资料

  1. C++ primer

  2. 知乎:如何评价 C++11 的右值引用(Rvalue reference)特性?

 

posted @ 2019-08-29 22:05  晓乎  阅读(449)  评论(0编辑  收藏  举报
总访问: counter for blog 次