89.移动语义 in C++
当我们知道左值和右值,左值引用和右值引用后,我们可以看看它们最大的一个用处:移动语义
🍅移动语义概述
移动语义让事情变得简单,移动语义本质上允许我们移动对象。这在C++11前是不可能的,因为C++11引入了右值引用,这是移动语义所必须的
其基本思想是:当写C++代码时,很多情况下,我们不需要或者不想把一个对象从一个地方复制到另一个地方,但又不得不复制。
- 例如,把一个对象传给一个函数,那么它就要获得那个对象的所有权,我别无选择,只能拷贝。而当想从一个函数返回一个对象时也是如此,我仍然需要在函数中创建那个对象,然后返回它,也就是说我又要复制数据了
当然如果对象仅仅是由一对整数或者类似的东西组成,复制似乎无妨。但是如果里面有堆分配这种东西,那么就会大幅降低速度了。而这便是移动语义的用武之处了。如果我们只是移动对象而不是复制他,那么性能会更高
来点例子
注意下面的String
假设是自己弄出来的字符串类
class Entity {
public:
Entity(const String& name)
: m_Name(name) {}
void PrintName() {
std::cout << m_Name << "\n";
}
private:
String m_Name;
};
int main() {
Entity a("Cherno"); //本质上是Entity a(String("Cherno")); 这里将"Cherno"视为左值
a.PrintName();
}
就上面这么简单的构建一个对象的操作,便分配了两次内存
- 首先在main函数的作用域中创建了一个"Cherno"的临时字符串,然后将其传入
Entity
的构造函数 - 然后在构造函数中,将这个临时字符串复制给了
m_Name
那么为什么不能在main函数中分配,然后把它移动到m_Name
里呢?这就用到了移动语义了
🍅移动语义的使用
如上个例子,我们需要写一个move
构造函数,这和复制构造函数很类似。
move
构造函数接受的是一个右值,即临时值
即在Entity a("Cherno");
中,将"Cherno"
不再视为左值,它只是作为Entity
构造函数的一个参数
noexcept 指定符
含义:指定函数是否抛出异常。
举例:
void f() noexcept {};
// 函数 f() 不抛出异常
String(String &&other) noexcept {
}
通过指定这个构造函数,希望最终执行那个”复制“时,不会复制,而变成移动
💡而为了让他能工作,还要确保Entity
有一个构造函数能够接受一个临时值!
- 这里还要用
std::move
来显示转换name
成临时值,才可以调用move构造函数!才能让具体构造出来的对象是临时值右值!!!(怎么字符串这么多事的)
Entity(String &&name)
: m_Name(std::move(name)) {}
正式书写move构造函数,与原来复制构造函数不同的是:只是简单的给指针赋值
(注意这里的m_Data是一个指针!)
//下面是String类的复制构造函数
String(const String &other) {
m_Size = other.m_Size; //记录字符串大小,不用理会
m_Data = new char[m_Size];
memcpy(m_Data, other.m_Data, m_Size);
}
//下面是String类的move构造函数
String(String &&other) noexcept { //注意这里是右值引用!!!
m_Size = other.m_Size;
m_Data = other.m_Data; //不同之处
//这里便完成了数据的转移,将other里的数据偷走了
other.m_Size = 0;
other.m_Data = nullptr;
}
💡两步操作:让新对象的指针指向指定内存,然后将旧对象的指针移开
所以这里做的其实是接管了原来旧的内存,而不是将这片内存复制粘贴!