C++ 的拷贝和移动 —— 个人理解

在 C++ 中,拷贝的能力分为 初始化拷贝赋值拷贝 两种。默认情况下,就算你不声明对象是否存在这两种能力,编译器也会帮你自动生成一个默认版本。

class T {};

此时的 T 已经具有默认拷贝的功能, 但没有移动能力。

T a;
T b(a);  // 初始化拷贝
b = a;   // 赋值拷贝

当你需要定义拷贝里的一些操作是,就可以显示声明并定义内容。

class T {
public:
    /* 初始化拷贝 */
    T(const T &) ;

    /* 赋值拷贝 */
    T & operator= (const T &);
};

注意,上面的声明不一定要全写完。但最好是成对出现,这样使用语义比较完整。

当你的类不希望有 copy 的情况时,可以这样声明。

class T {
public:
    T(const T&)                        = delete;
    T & operator=(const T &)   = delete;
};

=================================================================

拷贝的语义比较好理解,但移动的可能在理解上会有一定的误区。

我开始看移动时,一直以为,C++ 的移动和 Rust 里的一样,也是移动内存变量的使用权。后来,我才明白,其只是语义上的移动,内存所有权基本不变。

不过,这样理解也不对,因为要视使用场景而言。

C++ 的移动和右值有关,而右值又引申出右值引用的概念 T &&。请注意,右值和右值引用是两个不同的概念,右值通常是指临时变量、字面量等,而右值引用是能直接引用右值的变量,其本身是左值变量。

例如,函数 int func(); 在被调用后的返回值就是一个临时变量,其不能被左值引用直接引用,但可以被右值引用引用(但要注意,这可能会出现很多问题)。

int func();

int &r1 = func();  // 错误

int v = func();  // 先把临时返回值放至本函数栈内
int &r1 = v;    // 再使用左值引用去引用

int &&r2 = func();  // 正确,右值引用可以直接引用临时变量

注意,上述的右值引用虽然能通过编译,但这是存在很大隐患的。

int && func() {
    int i = 13;
    return move(i);
}
int &&r = func();

明显,右值引用 r 指向的内存是 func 函数栈里的 i。当 r 完成初始化后,func > > 的栈空间就会被销毁,故此时 r 指向的是一块无效内存,并引发 运行时的错误
编译可以通过,但一般会报警告。

C++ 的移动语义是与右值引用有关。其 std::move() 原理就是把一个你要移动的值,转变为右值。move() 的简化版本如下,

template <typename T>
T && move(T &obj) {
    return static_case<T &&>(obj);    // static_case 强转类型
}

接下来,说怎样定义一个类型具有移动的能力。

首先,类型在默认情况下,是不具备移动能力的。这和拷贝不一样。

同理,移动也分为初始化移动能力和赋值移动能力两种。

class T {
public:
    /* 初始化移动 */
    T(T &&);    

    /* 赋值移动 */
    T & operator=(T &&);
};

使用场景是,

T a;
T b = move(a);

a.xxx();  // 正常使用
b.xxx();

此时,若你以为把 amove 后,就不能使用,那你就大错特错了。因为 move 的本质只是把 a 从左值变成右值而已。

所以,一定要注意的是,C++ 的移动,是语义上的移动,而不是内存所有权的移动(后面会说一个例外)。

但,你可能说,如果是这样,那移动是有什么用呢?

其实移动的一个很有用的场景是在智能指针上,

移动的智能指针
int main() {
    SmartPoint p1, p2(13);

    rt_kprintf("...before move...\n");
    rt_kprintf("p1: ");
    p1.print();
    rt_kprintf("p2: ");
    p2.print();

    p1 = move(p2);

    rt_kprintf("...move...\n");
    rt_kprintf("p1: ");
    p1.print();
    rt_kprintf("p2: ");
    p2.print();

    return 0;
}

输出结果为,

输出结果

==================================================================

当一个类型,既定义了 拷贝,又定义了 移动。那到底是调用谁呢?

接下来,我们做几个实验。实验 1

class A {
public:
    A() = default;

    A(const A &) {rt_kprintf("A copy...\n");}
    A(A&&)       {rt_kprintf("A move...\n");}

    A & operator=(const A &)  {rt_kprintf("A copy=...\n"); return *this;}
    A & operator=(const A &&) {rt_kprintf("A move=...\n"); return *this;}
};

上述的 A 既具能 copy 又能 move。

    A a;

    A b1(a);
    A b2(move(a));

    A b3, b4;

    b3 = a;
    b4 = move(a);

结果是,


image.png

实验 2 ,是类型 A 只能 copy 不能 move

class A {
public:
    A() = default;

    A(const A &) {rt_kprintf("A copy...\n");}
//    A(A&&)       {rt_kprintf("A move...\n");}

    A & operator=(const A &)  {rt_kprintf("A copy=...\n"); return *this;}
//    A & operator=(const A &&) {rt_kprintf("A move=...\n"); return *this;}
};

结果是,移动的被转移到 copy 中了。

image.png

实验 3 ,是只能 move 不能 copy 时,结果发现,编译不通过。

==================================================================

最后,说一种,在 C++ 上真正是内存转移的情况。不对,应该说是逻辑上的。

开始时说过,如果一个函数返回的是引用时,一定要注意其指向内存的生命周期是否大于引用变量的,不然该引用变量就很容易变成悬垂指针,引发运行异常。

所以,在函数体内,想直接移动体内的局部变量是不行的。即,

T && func() {
    T a;
    return move(a);
}

那只能是通过拷贝,即

T func() {
    T a;
    return a;
}

T t;
t = func();

其调用情况是,调用 func() 时,编译器会为其生成专用栈空间,而 a 就是存在这个栈空间内。当调用 return 后,就会调用 t.operator= (a);。最后,系统回收 func 的栈空间,而 a 原本的内存无效。

此时,你可能会疑惑,如果 t 既有移动又有拷贝,那它到底是调用哪个呢?这里,就要从 t = func(); 这个语句分析起了。对于 t 而言,我不管 func() 里做了什么,创建了多少变量,我只关心你返回了什么。显然,对于函数返回值而言,其属于临时变量,也就是右值。故 其调用的是 move 语义的函数

回到之前的问题,我想知道 C++ 里在哪存在内存所有权的转移。答案就在初始化时。

class A {
public:
    A() = default;
    A(const A &obj) {rt_kprintf("copy...\n");}

    void print() {rt_kprintf("self addr is %d\n", this);}
};

A func() {
    A a;
    rt_kprintf("func a addr is %d\n", &a);
    return a;
}

int main() {

    A a = func();
    a.print();

    return 0;
}

结果是


image.png

此时,func() 的函数形式,是不是很熟悉,和调用初始化函数很像。这就是编译器优化后的结果。逻辑上,是把局部变量从内部转移到外部了。

其实,是编译器对此形式进行了优化。其分析内部的局部变量最后一定会流向外部的值时,就没必要为他再开辟内存,直接使用就行了。

但这不一定时时能实现的,因为只是优化产生的一个特例。

class A {
public:
    A() = default;
    A(const A &obj) {rt_kprintf("copy...\n");}

    void print() {rt_kprintf("self addr is %d\n", this);}
};

A func(int n) {
    A a, b;
    rt_kprintf("func a addr is %d\n", &a);
    rt_kprintf("func b addr is %d\n", &b);

    if (n > 3)
        return a;
    return b;
}

int main() {

    A a = func(1);
    a.print();

    return 0;
}

此时就不行了,因为编译器没法判断你是想把哪个返回。

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

推荐阅读更多精彩内容