一、移动语言是如何解决深拷贝的性能问题?
-
深拷贝会复制整个底层数据,让源对象和目标对象都拥有自己独立的堆内存空间
-
移动语义是转换底层资源的所有权,将源对象的指针指向目标对象,而不赋值底层数据,时间复杂都为1
-
移动语义将源对象的资源指针移动到目标对象,并将源对象指针设置为nullptr,这样源对象的析构操作就不会释放已经释放的资源了。
二、代码示例
- 移动构造函数,与移动赋值操作符函数实现
cpp
#include <iostream>
#include <cstring>
class MyString {
private:
char* m_data;
size_t m_len;
public:
// ---------- 构造函数 ----------
MyString(const char* str = "")
: m_len(strlen(str)), m_data(new char[m_len + 1]) {
strcpy(m_data, str);
}
// ---------- 析构函数 ----------
~MyString() {
delete[] m_data; // 如果 m_data 是 nullptr,安全无操作
}
// ==========================================
// 1. 深拷贝构造(O(n),昂贵)
// ==========================================
MyString(const MyString& other)
: m_len(other.m_len), m_data(new char[m_len + 1]) {
strcpy(m_data, other.m_data); // 申请内存 + 复制数据
std::cout << "深拷贝构造(耗时 O(n))" << std::endl;
}
// ==========================================
// 2. 移动构造(O(1),零开销)
// ==========================================
MyString(MyString&& other) noexcept
: m_data(other.m_data) // ① 窃取指针(只复制地址)
, m_len(other.m_len) { // ② 窃取长度
other.m_data = nullptr; // ③ 关键!置空源对象,切断所有权
other.m_len = 0;
std::cout << "移动构造(O(1) 指针交换,未复制任何数据)" << std::endl;
}
// ==========================================
// 3. 移动赋值(O(1))对比深拷贝赋值
// ==========================================
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] m_data; // 释放自己的旧资源(如果有)
m_data = other.m_data; // 窃取指针
m_len = other.m_len;
other.m_data = nullptr; // 置空源对象
other.m_len = 0;
std::cout << "移动赋值(O(1))" << std::endl;
}
return *this;
}
};
- 外部调用代码
cpp
int main() {
std::cout << "========== 场景1:拷贝构造 vs 移动构造 ==========" << std::endl;
{
MyString s1("HelloWorld"); // 1. 普通构造(分配堆内存)
MyString s2(s1); // 2. 深拷贝构造:申请新内存 + 复制10个字符
MyString s3(std::move(s1)); // 3. 移动构造:只交换指针,O(1)!
// 注意:s1 现在是一个"空壳"(m_data = nullptr),不能再读取其内容!
// 如果取消下面这行注释,程序会崩溃(访问空指针)。
// std::cout << "s1 内容: " << s1.c_str() << std::endl;
std::cout << "s2 内容: " << s2.c_str() << " (独立内存)" << std::endl;
std::cout << "s3 内容: " << s3.c_str() << " (窃取了 s1 的资源)" << std::endl;
} // 作用域结束:s1(空)、s2、s3 各自析构,不会双重释放!
std::cout << std::endl;
三、移动语义操作中置空源对象的原因?
1、防止双重释放:
- 如果源对象的析构函数被调用时,m_data还指向原来的地址,他就会delete\[\]这块内存,而目标对象也指向这块地址,两个析构先后释放同一块内存,进程立即崩溃。
2、保证析构安全:
- 置空后,源对象的析构函数执行delete\[\] m_data,由于m_data是nullp,delete\[\]是无害的空操作。
四、移动构造函数使用noexcept的原因?
-
C++标准库承诺vector扩容必须提供强异常安全保证,也就是扩容过程中如果抛出异常,程序的状态必须安全地恢复到扩容前的状态
-
但是移动构造函数会将源对象置为空了,无法恢复
-
所以加noexcept,相当于向编译器承诺此操作不会抛异常,让stl容器放心地使用移动,从而达到最高性能。