“实际文件还留在原来的地方,而只修改记录。” —— 移动语义
目录
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 的转移复制构造函数,但这里却是复制构造函数。
参考博客:
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 后,运行结果符合预期。