C++ Primer学习笔记 – 第13章 拷贝控制

本章主要内容,类定义构造函数,用来控制在创建此类型对象时做什么。学习类如何控制该类型对象拷贝、赋值、移动或销毁时做什么。
主要函数:拷贝构造函数、移动构造函数、拷贝赋值运算、移动赋值运算符以及析构函数。

拷贝控制操作 --
拷贝和移动构造函数,定义了当用同类型的另一个对象初始化本对象时做什么。
拷贝和移动赋值运算符,定义了将一个对象赋予同类型的另一个对象时做什么。
析构函数,定义了当此类型对象销毁时做什么。

13.1 拷贝、赋值与销毁

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

class Foo {
public:
    Foo(); // 默认构造函数
    Foo(const Foo&); // 拷贝构造函数,通常不应该是explicit的,即允许隐式转化
    // ...
};

合成拷贝构造函数
如果没有为一个类定义拷贝构造函数,编译器会为我们定义一个。与合成默认构造函数不同,即使定义了其他构造函数,编译器也会为我们合成一个拷贝构造函数。
对某些类来说,合成拷贝构造函数用来阻止拷贝该类类型的对象。
一般情况,合成拷贝构造函数会将其参数的成员逐个拷贝到正在创建的对象中。编译器从给定对象中依次将每个非static成员拷贝到正在创建的对象中。
每个成员的类型决定了它如何拷贝:对类类型的成员,使用其拷贝构造函数来拷贝;内置类型的成员,直接拷贝。

问题:如果类的成员是数组,那么如何拷贝?
不能直接拷贝一个数组,但合成的拷贝构造函数会逐元素地拷贝一个数组类型的成员。如果数组是类类型(如array,vector),则用元素的拷贝构造函数来进行拷贝。

例子,Sales_data类的合成拷贝构造函数等价于

class Sales_data{
    Sales_data(const Sales_data&);

private:
    std::string bookNo;
    int units_sold = 0;
    double revene = 0.0;
};

// 与Sales_data的合成的拷贝构造函数等价
Sales_data::Sales_data(const Sales_data& orig) : 
bookNo(orig.bookNo),  // 使用string的拷贝构造函数
units_sold(orig.units_sold),  // 直接值拷贝
revenue(orig.revene)  // 直接值拷贝
{ // 空函数体
}

拷贝初始化
直接初始化 vs 拷贝初始化
使用自己初始化时,实际上要求编译器使用普通的函数匹配,来选择与我们提供的参数最匹配的构造函数。 -- 相当于从一组重载函数中,选择一个参数最匹配的版本(不一定是拷贝构造函数)
使用拷贝初始化时,要求编译器将右侧运算对象拷贝到正在创建的对象中,如果需要的话,还要进行类型转换。 -- 调用拷贝构造函数进行构造,有时是移动构造函数

拷贝初始化应用场景:

  1. 用= 定义变量;
  2. 将一个对象作为实参传递给一个非引用类型的形参;
  3. 用花括号列表初始化一个数组中的元素或一个聚合类中的成员;
  4. 初始化标准库容器,或调用其insert/push函数时,容器会对其元素进行拷贝初始化;

参数和返回值
函数调用过程中,具有非引用类型的参数要进行拷贝初始化。返回非引用类型时,返回值会被用来初始化调用方的结果。

拷贝初始化的限制
拷贝构造函数一般不能是explicit类型的。如果要求使用的初始化值用一个explicit的构造函数来进行类型转换,那么要小心使用直接初始化和拷贝初始化。

// vector接受单一大小参数的构造函数是explicit的

vector<int> v1(10); // 正确:直接初始化
vector<int> v2 = 10;  // 错误:接受容器大小参数的构造函数是explicit的

// 对于函数f,接收类型为vector<int>的参数
void f(vector<int>);
f(10); // 调用错误:不能用一个explicit的构造函数拷贝一个实参
f(vector<int>(10)); // 正确:从一个int直接构造一个临时vector

假设vector构造函数不是explicit,那么下面语句是正确的

vector<int> v2 = 10; 

// 等价于
vector<int> tmp = vecotr<int>(10);
vector<int> v2 = tmp;

编译器可以绕过拷贝构造函数
拷贝、移动构造函数必须是存在且可访问的(非private)

string null_book = "9-999-99999-9"; // 拷贝初始化

string null_book("9-999-99999-9"); // 编译器绕过拷贝构造函数,直接创建对象

13.1.2 拷贝赋值运算符

类可以用 “=” 控制其对象如何赋值。类似拷贝构造函数,如果类未定义自己的拷贝赋值运算符,编译器会合成一个。

Sales_data trans, accum;
trans = accum; // 使用Sales_data的拷贝赋值运算符

拷贝赋值运算符和用“=”定义变量(拷贝初始化),最大的区别在于:使用拷贝赋值运算符的时候,前提条件是对象必须已经存在,即初始化完毕。而如果还是在构建对象阶段,就需要使用拷贝构造函数。

重载赋值运算符
重载运算符本质是函数,赋值运算是名为operator=的函数。
= 左侧运算对象绑定到隐式this参数,右侧运算对象作为显式参数传递。赋值运算符返回一个指向左侧运算对象的引用。

class Foo {
public:
    Foo& operator=(const Foo&); // 赋值运算符
    ...
};

合成拷贝赋值运算符
如果一个类未定义直接的拷贝赋值运算符,编译器会为它生成一个合成拷贝赋值运算符。会将右侧运算符对象的每个非static成员赋予左侧运算对象的对应成员。对于数组成员逐个赋值数组元素。

// 等价于合成拷贝赋值运算符
Sales_data&
Sales_data::operator=(const Sales_data& rhs) {
    bookNo = rhs.bookNo; // 调用string::operator=
    units_sold = rhs.units_sold; // 使用内置的int
    revenue = rhs.revenue; // 使用内置的double赋值
    return *this; // 返回此对象引用
}

// 这样定义了合成拷贝赋值运算符后,可以对Sales_data对象进行拷贝赋值
Sales_data d1, d2;
d1 = d2; // 拷贝赋值运算

// 注意下面是调用拷贝构造函数
Sales_data d3(d1);
Sales_data d4 = d2;

13.1.3 析构函数

析构函数执行与构造函数相反操作:构造函数初始化对象的非static数据成员,析构函数是否对象使用的资源,销毁对象的非static数据成员。
析构函数没有返回值,也不接受参数。析构函数不能被重载,而且对应一个类仅有一个。

class Foo {
public:
    ~Foo(); // 析构函数
    // ..
}

析构函数完成什么工作?
析构函数释放对象在生存期分配的所有资源。
构造函数初始化成员,是按它们在类中出现的顺序进行初始化。析构函数中,先执行函数体,然后按初始化顺序的逆序销毁成员。

注意:内置指针类型的成员不会delete所指向的对象
智能指针与普通指针不同,智能指针是类类型,具有析构函数,在析构阶段自动销毁。

什么时候调用析构函数
无论何时一个对象被销毁,会自动调用其析构函数,具体体现在:

  • 变量值离开其作用域时被销毁;
  • 当一个对象被销毁时,其成员被销毁;
  • 容器(标准库容器或数组)被销毁时,其元素被销毁;
  • 对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁;
  • 对应临时对象,当创建它的完整表达式结束时被销毁;

合成析构函数
当一个类未定义自己的析构函数时,编译器会为它定义一个合成析构函数。
需要注意的是:

  • 析构函数本身并不直接销毁成员,成员是在析构函数体之后隐含的析构阶段中被销毁的;
  • 合成析构函数函数不会delete一个指针数据成员,需要定义一个析构函数来释放函数分配的内存;

13.1.4 三/五法则

三个基本操作控制类的拷贝:拷贝构造函数、拷贝赋值运算符、析构函数;
两个新增的操作:移动构造函数、移动赋值运算符。

需要析构函数的类也需要拷贝和赋值操作
因为自定义析构函数往往用来释放合成析构函数无法释放的内存,比如指针数据成员指向的内存。此时,也需要自定义拷贝和赋值操作,因为合成的拷贝和赋值操作,通常无法处理指针指向的内存。

需要拷贝操作的类也需要赋值操作,反之亦然
某些类需要完成的工作,只需要拷贝或赋值,不需要析构。比如,类为每个对象分配一个独有的、唯一的序号。该类需要自定义一个拷贝构造函数为每个新建对象生成该序号。赋值操作也同样需要自定义。

13.1.5 使用=default

将拷贝控制成员定义为=default显示地要求编译器生成合成的版本 -- 合成默认构造函数,合成拷贝构造函数,合成赋值运算符,合成析构函数。
注意:只能对具有合成版本的成员函数使用 =default (默认构造函数,拷贝控制成员)

// 类内用=default修饰声明,合成的函数将隐式声明为内联的;类外,就不是内联的。

class Sales_data {
public:
    Sales_data() = default;
    Sales_data(const Sales_data&) = default;
    Sales_data& operator=(const Sales_data &);
    ~Sales_data() = default;
    // 其他成员的定义,略
}

Sales_data& operator=(const Sales_data &) = default;

13.1.6 阻止拷贝

大多数类应该定义默认构造函数、拷贝构造函数和拷贝赋值运算符。少数类可能会需要阻止拷贝,如iostream类阻止拷贝,避免多个对象写入或读取相同的IO缓冲。

定义删除的函数
可以通过将拷贝构造函数和拷贝赋值运算符,定义为删除的函数来阻止拷贝。
删除的函数:虽然声明了该函数,但不能以任何方式使用它们。

struct NoCopy {
    NoCopy() = default; // 使用合成的默认构造函数
    NoCopy(const NoCopy&) = delete; // 阻止拷贝
    NoCopy& operator=(const NoCopy&) = delete; // 阻止赋值
    ~NoCopy() = default; // 使用合成的析构函数
}

析构函数不能是删除的成员
不能删除析构函数,如果删除析构函数,就无法销毁此类型对象。

合成的拷贝控制成员可能是删除的
如果一个类有一个数据成员不能默认构造、拷贝、复制或销毁,则对应成员函数将被定义为删除的。
一个成员有删除的,或不可访问的析构函数,会导致合成的默认和拷贝构造函数被定义为删除的。

原文链接: https://www.cnblogs.com/fortunely/p/14519242.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    C++ Primer学习笔记 - 第13章 拷贝控制

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/402880

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年4月21日 上午11:15
下一篇 2023年4月21日 上午11:15

相关推荐