1. 从Student类看拷贝与移动的本质区别
让我们从一个简单的Student类开始,这个类包含姓名和学号两个成员变量:
cpp复制class Student {
public:
std::string name;
int id;
// 默认构造函数
Student(const std::string& n = "", int i = 0) : name(n), id(i) {
std::cout << "默认构造:" << name << std::endl;
}
// 拷贝构造函数
Student(const Student& other) : name(other.name), id(other.id) {
std::cout << "拷贝构造:" << name << std::endl;
}
// 移动构造函数
Student(Student&& other) noexcept
: name(std::move(other.name)), id(other.id) {
std::cout << "移动构造:" << name << std::endl;
}
// 析构函数
~Student() {
std::cout << "析构:" << name << std::endl;
}
};
这个简单的类已经足够展示拷贝和移动的核心差异。当我们在main函数中执行以下代码时:
cpp复制Student s1("Alice", 1001); // 默认构造
Student s2 = s1; // 拷贝构造
Student s3 = std::move(s1); // 移动构造
输出结果会清晰地展示三种构造方式的调用时机:
code复制默认构造:Alice
拷贝构造:Alice
移动构造:Alice
析构:Alice
析构:Alice
析构:Alice
关键观察:移动构造后,原对象s1的name变为空字符串,这就是移动语义的"转移"特性——资源所有权被转移,原对象进入有效但未定义状态。
1.1 拷贝构造的深层次实现
拷贝构造函数的标准签名是T(const T&),它接收一个const左值引用。在我们的Student示例中:
cpp复制Student(const Student& other) : name(other.name), id(other.id) {
std::cout << "拷贝构造:" << name << std::endl;
}
这里发生了以下关键操作:
- 对string类型的name成员执行了拷贝构造
- 对int类型的id执行了值拷贝
- 原对象other保持不变
这种"全量复制"的方式在以下场景非常必要:
- 需要独立副本时(如多线程环境)
- 对象生命周期需要完全独立时
- 对象包含不可共享的资源(如文件句柄)
但它的效率问题也很明显——当对象包含大量数据或持有昂贵资源时,深拷贝可能成为性能瓶颈。
1.2 移动构造的革命性突破
移动构造函数的标准签名是T(T&&),它接收一个非const的右值引用。在我们的示例中:
cpp复制Student(Student&& other) noexcept
: name(std::move(other.name)), id(other.id) {
std::cout << "移动构造:" << name << std::endl;
}
这里的关键点在于:
- 对name使用了std::move,触发string的移动构造
- 基础类型id仍然执行值拷贝(因为移动int没有意义)
- 原对象other的name被置空(但仍是合法状态)
移动构造的精髓在于资源所有权的转移而非复制,这使得它特别适合以下场景:
- 临时对象的构造(如函数返回值)
- 显式标记为可移动的对象(通过std::move)
- 容器扩容时的元素迁移
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 左值引用与右值引用的本质解析
2.1 左值引用的特性与限制
左值引用(Lvalue reference)使用&声明,它本质上是一个持久对象的别名。在我们的Student类中,拷贝构造函数使用的就是const左值引用:
cpp复制Student(const Student& other);
左值引用的关键特性包括:
- 必须绑定到确切的
