首页 > 代码库 > C++ Primer 笔记——拷贝控制

C++ Primer 笔记——拷贝控制

1.如果构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。拷贝构造函数的第一个参数必须是引用类型(否则会无限循环的调用拷贝构造函数)。

 

2.如果没有为一个类定义拷贝构造函数,编译器会为我们定义一个合成拷贝构造函数。与合成默认构造函数不同,即使我们定义了其他的构造函数,编译器也会为我们合成一个拷贝构造函数。

 

3.合成的拷贝构造函数会将其参数的成员逐个拷贝到正在创建的对象中(除了static成员)。对于类类型的成员,会使用其拷贝构造函数来拷贝,虽然我们不能拷贝一个数组,但会逐元素的拷贝一个数组类型的成员。

 

4.当使用直接初始化时,我们实际上是要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数,当我们使用拷贝初始化时,我们要求编译器将右侧运算对象拷贝到正在创建的对象中,如果需要的话还要进行类型转换。

 

5.在拷贝初始化过程中,编译器可以(但不是必须)跳过拷贝/移动构造函数,直接创建对象。但是拷贝/移动构造函数必须是存在且可访问的(例如,不能是private)。

std::string str("test");    // 编译器略过了拷贝构造函数

 

6.与拷贝构造函数一样,如果类未定义自己的拷贝赋值运算符,编译器会为它合成一个。或者我们可以自己重载。

class test{public:    test& operator=(const test& t);private:    int m_id;};test& test::operator=(const test& t){    m_id = t.m_id;    return *this;    // 返回一个此对象的引用}

 

7.析构函数释放对象使用的资源,并销毁对象的非static数据成员。由于析构函数不接受参数,所以不能被重载。在一个析构函数中,首先执行函数体,然后销毁成员。成员按初始化顺序的逆序销毁。内置类型没有析构函数,因此销毁内置类型成员什么也不需要做。

 

8.与普通指针不同,智能指针是类类型,所以具有析构函数,因此与普通指针不同,智能指针成员在析构阶段会被自动销毁。

 

9.我们只能对编译器可以合成的默认构造函数或拷贝控制成员使用=default,在类内用=default修饰成员的声明时,合成的函数将隐式地声明为内联的,如果不希望合成的成员是内联的,应该在类外定义使用=default。

 

10.与=defalut不同的是,我们可以对任何函数指定=delete。而且=dalete必须出现在函数第一次声明的时候。对于析构函数已删除的类型,不能定义该类型的变量或释放指向该类型动态分配对象的指针。

test t;    // 错误test *p = new test();    // 正确delete p;    // 错误


11.如果一个类有数据成员不能默认构造,拷贝,复制或销毁,则对应的成员函数将被定义为删除的,例如成员有引用类型或者无法默认构造的const成员等。

 

12.当你编写一个赋值运算符时,一个好的模式是先将右侧运算对象拷贝到一个局部临时对象中(因为可能将一个对象赋予它自身而且可能会出现异常)。当拷贝完成后,销毁左侧运算对象的现有成员就是安全的了。

 

13.对于分配了资源的类,自定义swap可能是一种很重要的优化手段,因为我们可以只交换指针而不是交换整个动态分配的内存。而标准库的swap会进行一次拷贝两次赋值。

class test{public:    test(int i) :m_p(new int(i)){}    ~test() { if (m_p) delete m_p; m_p = nullptr; }    int *m_p;};inline void swap(test& t1, test& t2){    std::swap(t1.m_p, t2.m_p);    // 交换指针,而不是动态分配的int}test t1(1);test t2(2);std::cout << *t1.m_p << std::endl;    // 输出1std::cout << *t2.m_p << std::endl;    // 输出2using std::swap;swap(t1, t2);    // 如果存在类型特定的swap版本,则不会调用std::swapstd::cout << *t1.m_p << std::endl; // 输出2std::cout << *t2.m_p << std::endl; // 输出1

 

14.一般而言,一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值。所谓右值引用就是必须绑定到右值得引用,我们不能将一个右值引用绑定到一个左值上。

int i = 42;int &r = i;    // 正确int &&rr = i;    // 错误,右值引用不能绑定到一个左值上int &r1 = i * 10; // 错误,i*10是一个右值const int &r2 = i * 10;    // 正确,我们可以将一个const引用绑定到一个右值上int &&rr1 = i * 10;        // 正确


15.由于右值引用只能绑定到临时对象,我们得知:所引用的对象将要被销毁;该对象没有其他用户。这两个特性意味着使用右值引用的代码可以自由地接管所引用的对象的资源。

 

16.变量是左值,因此我们不能将一个右值引用直接绑定到一个变量上,即使这个变量是右值引用类型也不行。但是我们可以显示的将一个左值转换为对应的右值引用类型。我们还可以通过调用一个名为move的新标准库函数来获得绑定到左值上的右值引用。

int &&rr = 10;    int &&rr1 = rr;    // 错误,rr是左值int &&rr2 = std::move(rr);    // 正确

move调用告诉编译器,我们有一个左值,但是我们希望像一个右值一样处理它。我们必须认识到,调用move就意味着承诺:除了对rr赋值或销毁它外,我们将不再使用它。在调用move之后,我们不能对移后源对象的值做任何假设。

 

17.类似拷贝构造函数。移动构造函数的第一个参数是该类类型的引用,但是是一个右值引用,任何额外的参数都必须有默认实参。除了完成资源移动,移动构造函数还必须确保移后源对象被销毁是无害的。特别的是,一旦资源完成移动,源对象必须不再指向被移动的资源。

class test{public:    test(int i) :m_p(new int(i)){}    test(test &&t)     {         if (m_p)             delete m_p;        // 先释放自己的资源        m_p = t.m_p;        // 指向t的资源        t.m_p = nullptr;    // 保证t可以被正常析构    }    int *m_p;};test t1(1);test t2(std::move(t1));


 18.只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会为它合成移动构造函数或移动赋值运算符。与拷贝操作不同,移动操作永远不会隐式的定义为删除的函数,但是,如果我们显示的要求编译器生成=default的移动操作,且编译器不能移动所有成员,则编译器会将移动操作定义为删除的函数。

 

19.定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作。否则,这些成员默认地被定义为删除的。

 

20.如果一个类既有移动构造函数,也有拷贝构造函数,编译器使用普通的函数匹配规则来确定使用哪个构造函数:移动右值,拷贝左值。如果没有移动构造函数,则都调用拷贝构造函数,赋值预算符也一样。

 

21.如果我们为一个类添加一个移动构造函数,它实际上也会获得一个移动赋值运算符。

class test{public:    test(int i):m_p(new int(i)) {}    test(test &t) { m_p = t.m_p; }    // 因为定义了移动构造函数,这里必须显示定义    test(test &&t)     {         if (m_p)            delete m_p;        std::swap(m_p, t.m_p);    }    test& operator=(test t)     {        std::swap(m_p, t.m_p);         return *this;     }    int* m_p;};test t1(1);test t2 = t1;    // 调用了拷贝构造函数test t3 = std::move(t1);    // 调用了移动构造函数

 

22.移动迭代器解引用运算符生成一个右值引用。我们通过调用make_move_iterator将一个普通迭代器转换为一个移动迭代器。

std::vector<std::string> vec = { "1","2","3" };std::allocator<std::string> alloc;auto first = alloc.allocate(5);auto last = std::uninitialized_copy(std::make_move_iterator(vec.begin()), std::make_move_iterator(vec.end()),first);    // 移动构造,vec里现在存的已经是空字串

 

23.我们可以在参数列表后放置一个引用限定符来阻止向右值赋值。

  • 类似const限定符,引用限定符只能用于(非static)成员函数,且必须同时出现在声明和定义中
  • 类似const,引用限定符也可以区分重载版本
  • 引用限定符必须跟随在const限定符之后
  • 如果我们定义两个或两个以上具有相同名字和相同参数列表的成员函数,就必须对所有函数都加上引用限定符,或者所有都不加
std::string str1, str2;(str1 + str2) = "test";    // 可以向右值赋值,但没有意义class test{public:    test(int i) { m_id = i; }    test& operator=(const test& t) &    // 只能向可修改的左值赋值    {         m_id = t.m_id;         return *this;     }    test add() const &     // 如果和const一起用,则const必须在前面    {        test t(*this);    // 不能改变this        t.m_id++;        return t;    }        test add() &&        // 本对象为右值    {        m_id++;        return *this;    }public:    int m_id;};test t1(1);t1.add();        // t1是左值,调用test add() const &test(2).add();    // 右值,调用test add() &&
class test{public:    void add() && ;    void add() const;    // 错误,必须加上引用限定符    void sub();    void sub() const;    // 正确,两个版本都没有引用限定符};

 


 

C++ Primer 笔记——拷贝控制