18.2 移动语义和右值引用

“实际文件还留在原来的地方,而只修改记录。”        —— 移动语义

目录

18.2.2 一个移动示例 

18.2.3 转移复制构造函数的进一步解析 

18.2.5 强制移动(没有照着书来!)


 

18.2.2 一个移动示例 

  • main.cpp
  • move.cpp
  • move.h
  • Makefile 

move.h

#pragma once

#include <iostream>
#include <cstring>
using namespace std;

class Aespa {
private:
    // 实例对象
    int elem_num;       // 元素个数
    char * p;           // 字符数组
    // 静态对象
    static int obj_num; // 对象个数
    void ShowObject() const;

public:
    Aespa();
    Aespa(int n, char ch);  // 含参构造函数
    Aespa(const Aespa & l); // 复制构造函数
    Aespa(Aespa && l);      // 转移复制构造函数
    ~Aespa();
    Aespa operator+(const Aespa & l); // 运算符函数
    void ShowData() const;
};

move.cpp

.h 里面声明,.cpp 里面实现。 

#include "move.h"

int Aespa::obj_num = 0;

Aespa::Aespa() {
    cout << "in Aespa()" << endl;
    cout << ++obj_num << endl;
    elem_num = 0;
    p = nullptr;
    ShowObject();
    cout << "-------------------" << endl;
}

Aespa::Aespa(int n, char ch) : elem_num(n) {
    cout << "in Aespa(int n, char ch)" << endl;
    cout << ++obj_num << endl;
    p = new char[elem_num];
    for (int i = 0; i < elem_num; ++i)
        p[i] = ch;
    ShowObject();
    cout << "-------------------" << endl;
}

Aespa::Aespa(const Aespa & l) : elem_num(l.elem_num) {
    cout << "in copy Aespa()" << endl;
    cout << ++obj_num << endl;
    p = new char[elem_num];
    for (int i = 0; i < elem_num; ++i)
        p[i] = l.p[i];
    ShowObject();
    cout << "-------------------" << endl;
}

Aespa::Aespa(Aespa && l) : elem_num(l.elem_num) {
    cout << "in move Aespa()" << endl;
    cout << ++obj_num << endl;
    p = l.p;
    l.p = nullptr;
    l.elem_num = 0;
    ShowObject();
    cout << "-------------------" << endl;
}

Aespa::~Aespa() {
    cout << "in ~Aespa()" << endl;
    cout << --obj_num << endl;
    ShowObject();
    delete [] p;
    cout << "-------------------" << endl;
}

Aespa Aespa::operator+(const Aespa & l) {
    cout << "in +()" << endl;
    Aespa temp;
    temp.elem_num = elem_num + l.elem_num;
    temp.p = new char[temp.elem_num];
    for (int i = 0; i < elem_num; ++i)
        temp.p[i] = p[i];
    for (int i = elem_num; i < temp.elem_num; ++i)
        temp.p[i] = l.p[i-elem_num];
    temp.ShowObject();
    cout << "-------------------" << endl;
    return temp;
}

void Aespa::ShowObject() const {
    cout << "Number of elements: " << elem_num << endl;
    cout << "Data address: " << (void *)p << endl;
}

void Aespa::ShowData() const {
    if (elem_num == 0)
        cout << "(this object is empty)";
    else
        for (int i = 0; i < elem_num; ++i)
            cout << p[i];
    cout << endl;
    cout << "-------------------" << endl;
}

main.cpp

#include <iostream>
#include "move.h"

int main() {
    Aespa karina(22, 'k');  // 构造时复制
    Aespa god = karina;  // 构造时复制
    Aespa winter(21, 'w');
    Aespa karwin(karina + winter);
    cout << "object karina: ";
    karina.ShowData();
    cout << "object god: ";
    god.ShowData();
    cout << "object karwin: ";
    karwin.ShowData();

    return 0;
}

Makefile 

source = *.cpp
target = aespa

CXX = g++
CXXFLAG = -Wall -g -std=c++23 
ASAN = -fsanitize=address

LIB =

all:
	$(CXX) $(source) $(CXXFLAG) $(ASAN) -o $(target) $(LIB)

clean:
	rm $(target)

运行结果:

in Aespa(int n, char ch)
1
Number of elements: 22
Data address: 0x603000000040
-------------------
in copy Aespa()
2
Number of elements: 22
Data address: 0x603000000070
-------------------
in Aespa(int n, char ch)
3
Number of elements: 21
Data address: 0x6030000000a0
-------------------
in +()
in Aespa()
4
Number of elements: 0
Data address: 0
-------------------
Number of elements: 43
Data address: 0x604000000010
-------------------
object karina: kkkkkkkkkkkkkkkkkkkkkk
-------------------
object god: kkkkkkkkkkkkkkkkkkkkkk
-------------------
object karwin: kkkkkkkkkkkkkkkkkkkkkkwwwwwwwwwwwwwwwwwwwww
-------------------
in ~Aespa()
3
Number of elements: 43
Data address: 0x604000000010
-------------------
in ~Aespa()
2
Number of elements: 21
Data address: 0x6030000000a0
-------------------
in ~Aespa()
1
Number of elements: 22
Data address: 0x603000000070
-------------------
in ~Aespa()
0
Number of elements: 22
Data address: 0x603000000040
-------------------

分析:

注意到没有调用转移复制构造函数,且只创建了 4 个对象。创建 对象karwin 时,该编译器没有调用任何构造函数;相反,它推断出 karwin 是 operator+() 所做工作的受益人,因此将 operator+() 创建的对象转到 karwin 名下。一般而言,编译器完全可以进行优化,只要结果与为优化时相同。即使我们省略掉该程序中的转移复制构造函数,并使用 g++ 进行编译,结果也将相同。


 

复制构造函数

我们用的是深复制。

  

转移复制构造函数

Aespa::Aespa(Aespa && l) : elem_num(l.elem_num) {
    cout << "in move Aespa()" << endl;
    cout << ++obj_num << endl;
    p = l.p;
    l.p = nullptr;
    l.elem_num = 0;
    ShowObject();
    cout << "-------------------" << endl;
}

p 和 l.p 指向相同的数据,调用析构函数时这将带来麻烦,因为程序不能对同一个地址调用 delete 两次。为了避免这种问题,该构造函数随后将原来的指针置为空指针,因为对空指针执行 delete 是没有问题的。这种夺取所有权的方式常被称为 窃取

注意,由于修改了 l对象,这要求不能在参数声明中使用 const! 

 

18.2.3 转移复制构造函数的进一步解析 

让移动语义发生的两个条件:

  • 右值引用让编译器知道何时可以使用移动语义
  • 编写转移复制构造函数,使其提供所需的行为

1. 右值引用让编译器知道何时可以使用移动语义

Aespa god = karina;
Aespa karwin(karina + winter);

对象karina 是左值,与左值引用匹配;而表达式 karina + winter 是右值,与右值引用匹配。因此,右值引用让编译器使用转移复制构造函数来初始化 对象karwin。 

2. 编写转移复制构造函数,使其提供所需的行为 

传说如果没有转移复制构造函数、编译器没有优化,那么 Aespa karwin(karina + winter); 将会调用复制构造函数。

对于复制构造函数,如果传入的实参为右值,其 const引用形参 将指向一个临时对象。

 

18.2.5 强制移动(没有照着书来!)

之前遇到的问题:

dlist f(dlist k) {
    k.push_back(7);
    return k;
}

int main() {
    dlist l{1, 2, 3, 4, 5, 6};
 
    f(std::move(l)); // 把l伪装成一个右值对象
 
    return 0;
}

运行结果:

in dlist()
1
in dlist()
2
in copy dlist()
in move copy dlist()
3
in ~dlist()
2
in ~dlist()
1
in ~dlist()
0

按理说,这里把 l 伪装成了一个右值对象,那么实参l 与形参k 结合的时候调用的应该是形参k 的转移复制构造函数,但这里却是复制构造函数。 


参考博客:

std::move()的学习总结_qls315的博客-CSDN博客_std::move()目录1. 移动语义2. std::move的实现及使用3. 总结1. 移动语义移动语义可以使得编译器使用不那么昂贵的移动操作,来替换昂过的复制操作。同拷贝构造函数、拷贝赋值运算符赋予人们复制意义的能力一样,移动构造函数、移动赋值运算符也赋予人们移动语义的能力。更通俗的说:移动语义是通过移动构造函数或者移动赋值运算符实现的举个例子,对于C++标准库提供的std::shared_ptr智能指针,当对其进行复制操作时,需要增加其引用计数,而引用计数是个原子类型,因此增加引用计数是个耗时操作https://blog.csdn.net/qls315/article/details/106868799?ops_request_misc=&request_id=&biz_id=102&utm_term=move%E5%BC%BA%E5%88%B6%E7%A7%BB%E5%8A%A8%20const%E5%AF%B9%E8%B1%A1&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-3-106868799.142^v51^new_blog_pos_by_title,201^v3^control_2&spm=1018.2226.3001.4187

Q:std::move() 一定会导致移动发生么?

A:std::move() 不一定会导致移动操作发生。

std::move() 的参数是个万能引用,其在C++11中的一个实现示例如下:

template <typename T>
typename std::remove_reference<T>::type&& move(T&& param) {
     using return_type = typename std::remove_reference<T>::type&&;
     return static_cast<return_type>(param);
 }

当我们传入一个 const对象的左值 时,其 const属性 依然会成为参数的一部分。因此,转移复制构造函数无法成为候选者,而复制构造函数将会成为候选者。故此时会调用复制构造函数,产生上述结果。
 


去除 const 后,运行结果符合预期。 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值