第 13 章 拷贝控制

  • 当定义一个类时,我们显式地或隐式地指定在此类型的对象拷贝,移动,赋值和销毁时做什么
  • 一个类通过五种特殊的成员函数来控制这些操作,包括:拷贝构造函数拷贝赋值运算符移动构造函数移动赋值运算符析构函数
  • 拷贝和移动构造函数定义了当用同类型的另一个对象初始化本对象时做什么。拷贝和移动赋值运算符定义了将一个对象赋予同类型的另一个对象时做什么。析构函数定义了当此类型对象销毁时做什么
  • 我们称这些操作为拷贝控制操作

13.1 拷贝、赋值与销毁

  • 以最基本操作 拷贝构造函数拷贝赋值运算符析构函数 作为开始。移动操作在 13.6 节讲述

13.1.1 拷贝构造函数

  • 如果一个构造函数第一个参数是自身类类型的引用,且任何额外参数都有默认值,则称此构造函数是拷贝构造函数
拷贝初始化
  • 当使用直接初始化时,我们实际上时要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数
  • 当我们使用拷贝初始化时,我们要求编译器将右侧运算对象拷贝到正在创建的对象中,如果需要的话还要进行类型转换
string dots(10, '.');    // 直接初始化
string s(dots);         // 直接初始化
string s2 = dots;       // 拷贝初始化
string null_book = "9-999-99999-9";     // 拷贝初始化
string nines = string(100, '9');        // 拷贝初始化
参数和返回值
  • 在函数调用过程中,具有非引用类型的参数要进行拷贝初始化
  • 当一个函数具有非引用的返回类型时,返回值会被用来初始化调用方的结果

13.1.2 拷贝赋值运算符

  • 与类控制其对象如何初始化一样,类也可以控制其对象如何赋值
Sales_data trans, accum;
trans = accum;   // 使用 Sales_data 的拷贝赋值运算符
  • 与拷贝构造函数一样,如果类未定义自己的拷贝赋值运算符,编译器会为它合成一个
重载赋值运算符
  • 重载运算符本质上是函数,其名字由 operator 关键字后接表示要定义的运算符的符号组成。因此,赋值运算符就是一个名为 operator= 的函数。类似于任何其他函数,运算符函数也有一个返回类型和一个参数列表
  • 赋值运算符通常应该返回一个指向其左侧运算对象的引用
class Foo{
    public:
        Foo& operator=(const Foo&);  // 赋值运算符
        // ....
};
A& operator= (const A& a){ //拷贝赋值运算符
    val = a.val;
    return *this;
}
  • 与处理拷贝构造函数一样,如果一个类未定义自己的拷贝赋值运算符,编译器会为它生成一个合成拷贝赋值运算符

13.1.3 析构函数

  • 析构函数执行与构造函数相反的操作:构造函数初始化对象的非 static 数据成员,还可能做一些其他工作;析构函数释放对象使用的资源,并销毁对象的非 static 数据成员
  • 析构函数是类的一个成员函数,名字由波浪号接类名构成。它没有返回值也不接受参数
class Foo{
    public:
        ~Foo();  // 析构函数
        // ...
};
  • 由于析构函数不接受参数,因此它不能被重载。对于一个给定类,只会有唯一一个析构函数
  • 无论何时一个对象被销毁,就会自动调用其析构函数
    1, 变量在离开其作用域时被销毁
    2, 当一个对象被销毁时,其成员被销毁
    3, 容器被销毁时,其元素被销毁
    4, 对于动态分配的对象,当对指向它的指针应用 delete 运算符时被销毁
    5, 对于临时对象,当创建它的完整表达式结束时被销毁
  • 析构函数体自身并不直接销毁成员。成员是在析构函数体之后隐含的析构阶段中被销毁的

13.1.4 三/五法则

  • 如果一个类需要一个析构函数,我们几乎可以肯定它也需要一个拷贝构造函数和一个拷贝赋值运算符
  • 如果一个类需要一个拷贝构造函数,几乎可以肯定它也需要一个拷贝赋值运算符。反之亦然 - 如果一个类需要一个拷贝赋值运算符,几乎可以肯定它也需要一个拷贝构造函数。然而,无论是需要拷贝构造函数还是需要拷贝赋值运算符都不必然意味着也需要析构函数

13.1.5 使用 =default

  • 我们可以通过将拷贝控制成员定义为 =default 来显式地要求编译器生成合成的版本。合成的版本就是默认版本

13.1.5 阻止拷贝

  • 大多数类应该定义默认构造函数、拷贝构造函数和拷贝赋值运算符,无论是隐式地还是显式地
  • 虽然大多数类应该定义拷贝构造函数和拷贝赋值运算符,但对某些类来说,这些操作没有合理的意义。在此情况下,定义类时必须采用某种机制阻止拷贝或赋值
定义删除的函数
  • 在新标准下,我们可以将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝。
  • 删除的函数是这样一种函数: 我们虽然声明了它们,但不能以任何方式使用它们。
  • 在函数的参数列表后面加上 =delete 来指出我们希望将它定义为删除的。=delete 通知编译器(以及我们代码的读者),我们不希望定义这些成员
struct NoCopy{
    NoCopy () = default;    // 使用合成的默认构造函数
    NoCopy (const NoCopy&) = delete;    // 阻止拷贝
    NoCopy &operator=(const NoCopy&) = delete;   // 阻止赋值
    ~NoCopy() = default;   // 使用合成的析构函数 
};
析构函数不能是删除的函数
  • 对于析构函数已删除的类型,不能定义该类型的变量或释放指向该类型动态分配对象的指针
struct NoDtor{
    NoDtor () = default;   // 使用默认构造函数
    ~NoDtor() = delete;   // 我们不能销毁 NoDtor 类型的对象
}
NoDtor nd;  // 错误:NoDtor 的析构函数是删除的
NoDtor *p = new NoDtor();    // 正确: 但我们不能 delete p
delete p;    // 错误: NoDtor 的析构函数是删除的
合成的拷贝控制成员可能是删除的
  • 本质上,当不可能拷贝、赋值或销毁类的成员时,类的合成拷贝控制成员就被定义为删除的
private 拷贝控制
  • 在新标准发布之前,类是通过将其拷贝构造函数和拷贝赋值运算符声明为 private 来阻止拷贝
class PrivateCopy{
    // 无访问说明符;接下来的成员默认是 private 
    // 拷贝控制成员是 private 的,因此普通用户代码无法访问
    PrivateCopy(const PrivateCopy&);
    PrivateCopy &operator=(const PrivateCopy&);
    // 其他成员
public:
    PrivateCopy() = default;   // 使用合成的默认构造函数
    ~PrivateCopy();    // 用户可以定义此类型的对象,但无法使用它们
};
  • 由于析构函数是 public 的,用户可以定义 PrivateCopy 类型的对象。但是,由于拷贝构造函数和拷贝赋值运算符是 private 的,用户代码将不能拷贝这个类型的对象
建议:希望阻止拷贝的类应该使用 =delete 来定义它们自己的拷贝构造函数和拷贝赋值运算符,而不应该将它们声明为 private 的

13.2 拷贝控制和资源管理

  • 类的行为像一个值,意味着它应该也有自己的状态。当我们拷贝一个像值的对象时,副本和原对象是完全独立的。改变副本不会对原对象有任何影响,反之亦然
  • 行为像指针的类则共享状态。当我们拷贝一个这种类的对象时,副本和原对象使用相同的底层数据。改变副本也会改变原对象,反之亦然

13.2.1 行为像值的类 重点

  • C++ Primer 453页,这一节太完美了,直接去看书吧,都是重点
  • 讲的是深拷贝

13.2.2 定义行为像指针的类 重点

  • C++ Primer 455页,这一节太完美了,直接去看书吧,都是重点
  • 可以理解为 shared_ptr 的底层实现。(腾讯音乐面试就问到了,可惜当时太菜)

13.3 交换操作

  • C++ Primer 457页,这一节太完美了,直接去看书吧,都是重点
  • 简而言之,直接交换两个对象的话,涉及到深拷贝 (重新分配一块空间,将将新内容放到这块空间,原空间内容释放) 这一系列的操作,很是没必要。这节讲的是直接交换指向两块空间的指针
#include<iostream>
using namespace std;

class AA{
    // 友元, 以便访问 AA 的 private 数据成员
    friend void swap(AA &l, AA &r);

    public:
        // 构造函数
        AA(int bb):aa(bb) {
            std::cout << aa << " 构造函数" << endl;
        }

        // 拷贝构造函数
        AA(const AA &temp){
            std::cout << temp.aa << " 拷贝构造函数" << endl;
            this->aa = temp.aa;
        }

        // 拷贝赋值运算符
        AA& operator=(const AA &temp){
            std::cout << " 拷贝赋值运算符" << endl;
            this->aa = temp.aa;
            return *this;
        }

        ~AA(){
            std::cout << aa << " 析构函数" << endl;
        }

    private:
        int aa;
};

// 内联函数
inline void swap(AA &l, AA &r){
    std::cout << "交换之前, l.aa = " << l.aa << ", r.aa = " << r.aa << endl;
    std::swap(l.aa, r.aa);
    std::cout << "交换完成, l.aa = " << l.aa << ", r.aa = " << r.aa << endl;
}

int main(){
    AA ff(11);   // 调用构造函数
    AA gg = ff;  // 调用拷贝构造函数
    AA hh(ff);   // 调用拷贝构造函数

    AA kk(77);   // 调用构造函数
    kk = ff;     // 调用拷贝赋值运算符

    AA a(11111);
    AA b(22222);
    swap(a, b);
}
11 构造函数
11 拷贝构造函数
11 拷贝构造函数
77 构造函数
 拷贝赋值运算符
11111 构造函数
22222 构造函数
交换之前, l.aa = 11111, r.aa = 22222
交换完成, l.aa = 22222, r.aa = 11111
11111 析构函数
22222 析构函数
11 析构函数
11 析构函数
11 析构函数
11 析构函数

13.4 拷贝控制示例

  • 讲了一个拷贝控制的小例子,挺好的,建议看看

13.5 动态内存管理类

  • 实现标准库 vector 的一个简化版本。功能是不使用模板来实现
  • 类在运行时分配可变大小的内存空间

13.6 对象移动

  • 新标准的一个最主要特性是可以移动而非拷贝对象的能力。很多情况下都会发生对象的拷贝。在其中某些情况下,对象拷贝后就立即被销毁了。在这种情况下,移动而非拷贝对象会大幅度提升性能
  • 在上一节中看到,我们的 StrVec 类是这种不必要的拷贝的一个很好的例子。在重新分配内存的过程中,从旧内存将元素拷贝到新内存是不必要的,更好的方式是移动元素

13.6.1 右值引用

  • 去看书吧

13.6.2 移动构造函数和移动赋值运算符

  • 去看书吧

13.6.3 对象移动

  • 去看书吧
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,511评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,495评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,595评论 0 225
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,558评论 0 190
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,715评论 3 270
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,672评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,112评论 2 291
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,837评论 0 181
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,417评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,928评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,316评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,773评论 2 234
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,253评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,827评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,440评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,523评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,583评论 2 249

推荐阅读更多精彩内容