类的拷贝
第一章 类的拷贝基础
1.1 默认构造函数
默认构造函数是当你不提供任何构造函数时,编译器自动生成的构造函数。它分为以下几种情况:
1.1.1 隐式生成的默认构造 🏭
class MyClass {
public:
int value;
double data;
};
MyClass obj; // 调用隐式默认构造,value 和 data 未初始化(垃圾值)1.1.2 编译器生成的条件
只有当你没有声明任何构造函数时,编译器才会生成隐式默认构造。如果你声明了带参数的构造函数但没有默认构造函数,则 MyClass obj; 会编译失败。
class MyClass {
public:
int value;
MyClass(int v) : value(v) {} // 只声明了有参构造
};
MyClass obj; // ❌ 编译错误:没有默认构造函数
MyClass obj(10); // ✅ 正确1.1.3 默认成员初始化器 (C++11)
class MyClass {
public:
int value = 0; // 默认值为 0
double data{1.0}; // 默认值为 1.0
std::string name = "unnamed";
};
MyClass obj; // ✅ value=0, data=1.0, name="unnamed"1.2 拷贝构造函数 🔄
拷贝构造函数用于创建一个新对象,作为已有对象的副本。
1.2.1 拷贝构造的形式
class MyClass {
public:
int value;
std::string name;
// 参数必须是 const 引用,避免无限递归
MyClass(const MyClass& other) : value(other.value), name(other.name) {}
};1.2.2 拷贝构造的调用时机
MyClass obj1(10, "Alice");
MyClass obj2(obj1); // ① 直接初始化
MyClass obj3 = obj1; // ② 拷贝初始化(可能触发隐式拷贝)
MyClass obj4(obj1); // ③ 作为函数参数(按值传递)注意
按值传递参数时,会调用拷贝构造函数。在某些性能敏感的场景下,可以考虑使用 const& 引用传递来避免不必要的拷贝。
void func(MyClass obj) {} // 会调用拷贝构造
void func(const MyClass& obj) {} // 不会调用拷贝构造,更推荐1.2.3 隐式拷贝构造
如果你没有声明拷贝构造,编译器会生成一个浅拷贝的隐式拷贝构造。
class MyClass {
public:
int value;
double data;
};
MyClass obj1;
obj1.value = 10;
MyClass obj2 = obj1; // 调用隐式浅拷贝构造,obj2.value == 101.3 浅拷贝 vs 深拷贝 ⚖️
这是理解类拷贝的关键概念。
1.3.1 浅拷贝 (Shallow Copy)
浅拷贝只拷贝成员变量的值(对于指针,只拷贝指针的地址,不拷贝指针指向的数据)。
#include <cstring>
#include <iostream>
class MyString {
public:
char* data;
int length;
MyString(const char* str) {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
}
// 编译器生成的隐式拷贝构造是浅拷贝!
// 危险:obj2 的 data 指针指向 obj1 的同一块内存
~MyString() {
delete[] data;
}
};
int main() {
MyString s1("Hello");
MyString s2 = s1; // 浅拷贝:s2.data 和 s1.data 指向同一块内存
std::cout << s1.data << std::endl; // Hello
std::cout << s2.data << std::endl; // Hello(同一块内存!)
// ❌ 双重删除!s1 和 s2 析构时都会 delete[] data
return 0;
}1.3.2 深拷贝 (Deep Copy)
深拷贝不仅拷贝指针,还拷贝指针指向的实际数据。
#include <cstring>
#include <iostream>
class MyString {
public:
char* data;
int length;
MyString(const char* str = "") {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
}
// 自定义拷贝构造:深拷贝
MyString(const MyString& other) : length(other.length) {
data = new char[length + 1];
strcpy(data, other.data);
}
~MyString() {
delete[] data;
}
};
int main() {
MyString s1("Hello");
MyString s2 = s1; // 深拷贝:s2.data 是独立的副本
std::cout << s1.data << std::endl; // Hello
std::cout << s2.data << std::endl; // Hello
// ✅ 安全!两个对象各自拥有独立的内存
return 0;
}1.3.3 对比表格
| 特性 | 浅拷贝 | 深拷贝 |
|---|---|---|
| 拷贝内容 | 只拷贝指针值(地址) | 拷贝指针指向的数据 |
| 内存情况 | 多个指针指向同一块内存 | 每个对象有独立内存 |
| 析构风险 | ⚠️ 双重删除风险 | ✅ 安全 |
| 性能 | 快(仅拷贝指针) | 较慢(需分配新内存) |
1.4 拷贝赋值运算符 📋
拷贝赋值运算符 operator= 用于将一个已存在对象的值赋给另一个对象。
1.4.1 默认拷贝赋值运算符
类似于拷贝构造,如果没有自定义,编译器会生成浅拷贝版本的拷贝赋值运算符。
class MyString {
public:
char* data;
int length;
MyString(const char* str = "") {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
}
// 拷贝赋值运算符
MyString& operator=(const MyString& other) {
if (this != &other) { // 防止自赋值
delete[] data; // 释放原有内存
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
}
return *this;
}
~MyString() {
delete[] data;
}
};1.4.2 自赋值检查
编写拷贝赋值运算符时,必须检查自赋值情况:
MyString s("Hello");
s = s; // 自赋值!如果没有检查,会先 delete[] data,导致问题1.5 移动语义 (C++11) 🚀
移动语义是 C++11 引入的重要特性,用于避免不必要的拷贝。
1.5.1 移动构造函数
class MyString {
public:
char* data;
int length;
MyString(const char* str = "") {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
}
// 移动构造函数
MyString(MyString&& other) noexcept : data(other.data), length(other.length) {
other.data = nullptr; // 防止析构时 delete[]
other.length = 0;
}
~MyString() {
delete[] data;
}
};1.5.2 移动赋值运算符
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
length = other.length;
other.data = nullptr;
other.length = 0;
}
return *this;
}1.5.3 std::move
使用 std::move 可以将左值转换为右值引用,触发移动语义:
MyString s1("Hello");
MyString s2 = std::move(s1); // 调用移动构造,s2 获得资源,s1 变为空1.6 五大构造/赋值函数的优先级 ⚡
这是非常重要的知识点,理解优先级能避免潜在的 bug。
1.6.1 编译器生成的条件
| 函数 | 编译器自动生成的条件 |
|---|---|
| 默认构造 | 未声明任何构造函数 |
| 拷贝构造 | 未声明拷贝构造 |
| 拷贝赋值 | 未声明拷贝赋值 |
| 移动构造 | 未声明拷贝构造且未声明移动操作 |
| 移动赋值 | 未声明拷贝赋值且未声明移动操作 |
重要规则
一旦你声明了任何拷贝构造(包括 = default),编译器就不会自动生成移动构造。反之亦然:声明了移动构造,编译器也不会生成拷贝构造。
这意味着:
class MyClass {
public:
MyClass(const MyClass&) = default; // 显式声明拷贝构造
// 编译器不会生成隐式移动构造!
};1.6.2 调用时的优先级规则
当同时存在多个构造函数可供调用时:
| 拷贝构造 | 移动构造 | 传递方式 | 调用 |
|---|---|---|---|
| ✅ | ✅ | MyClass obj(other) | 拷贝构造(左值优先拷贝) |
| ✅ | ✅ | MyClass obj(std::move(other)) | 移动构造(右值优先移动) |
| ✅ | ❌ | MyClass obj(other) | 拷贝构造(退化为拷贝) |
| ❌ | ✅ | MyClass obj(other) | 移动构造(退化) |
class MyClass {
public:
int value;
MyClass() : value(0) {}
MyClass(const MyClass&) : value(0) { std::cout << "copy\n"; }
MyClass(MyClass&&) noexcept : value(0) { std::cout << "move\n"; }
};
MyClass obj1;
MyClass obj2 = obj1; // 输出 "copy"(左值调用拷贝构造)
MyClass obj3 = std::move(obj1); // 输出 "move"(右值调用移动构造)1.6.3 ⚠️ 常见陷阱:声明默认拷贝导致移动失效
你遇到的 bug 正是这个:
class MyClass {
public:
int* ptr;
int newValue; // 后来新增的字段
MyClass() : ptr(nullptr), newValue(0) {}
// 你以为这样声明没事
MyClass(const MyClass&) = default; // ❌ 危险!
// 但新增的字段不会被移动!
};
int main() {
MyClass obj1;
obj1.newValue = 100;
MyClass obj2 = std::move(obj1); // 调用移动构造!
// newValue 没有被移动,只是一个未定义的垃圾值!
}1.6.4 正确做法
方案一:如果不需要移动,确保拷贝完整
class MyClass {
public:
int* ptr;
int newValue;
MyClass() : ptr(nullptr), newValue(0) {}
// ✅ 手动实现,确保每个字段都被拷贝
MyClass(const MyClass& other)
: ptr(other.ptr ? new int(*other.ptr) : nullptr),
newValue(other.newValue) {}
};方案二:如果需要移动,一并实现
class MyClass {
public:
int* ptr;
int newValue;
MyClass() : ptr(nullptr), newValue(0) {}
MyClass(const MyClass& other)
: ptr(other.ptr ? new int(*other.ptr) : nullptr),
newValue(other.newValue) {}
// ✅ 同时实现移动,确保新增字段也被正确移动
MyClass(MyClass&& other) noexcept
: ptr(other.ptr), newValue(other.newValue) {
other.ptr = nullptr;
// other.newValue 不需要处理,因为是值类型
}
};方案三:使用 = default 前确认没有遗漏
class MyClass {
public:
int value; // ✅ 只有简单成员,用 = default 是安全的
double data;
std::string name;
MyClass(const MyClass&) = default; // ✅ 编译器会正确处理
};1.6.5 推荐使用场景
| 场景 | 推荐做法 |
|---|---|
| 简单 POD 类型(int、double、string 等) | = default 让编译器生成 |
| 有原始指针,需要深拷贝 | 自定义拷贝构造和拷贝赋值 |
| 有资源(文件、socket、锁) | 自定义析构 + 五大函数全部显式定义 |
| 性能敏感,避免不必要的拷贝 | 同时实现移动构造和移动赋值 |
| 使用智能指针管理资源 | = default 拷贝/移动,或直接用 = default |
推荐
现代 C++ 中,优先使用智能指针(std::unique_ptr、std::shared_ptr)和 STL 容器管理资源。它们已经实现了正确的拷贝/移动语义,使用 = default 即可。
1.7 总结:第一类知识点回顾 📝
class MyClass {
int value;
std::string name;
int* ptr;
public:
MyClass() = default; // 默认构造
MyClass(int v, const std::string& n); // 带参构造
// 方式一:简单类型用 = default
MyClass(const MyClass&) = default;
MyClass& operator=(const MyClass&) = default;
// 方式二:需要深拷贝时手动实现
MyClass(const MyClass& other) : value(other.value), name(other.name) {
ptr = new int(*other.ptr); // 深拷贝
}
MyClass& operator=(const MyClass& other) {
if (this != &other) {
delete ptr;
value = other.value;
name = other.name;
ptr = new int(*other.ptr); // 深拷贝
}
return *this;
}
// 如果需要移动语义,也要一并实现!
MyClass(MyClass&& other) noexcept
: value(other.value), name(std::move(other.name)), ptr(other.ptr) {
other.ptr = nullptr;
}
MyClass& operator=(MyClass&& other) noexcept {
if (this != &other) {
delete ptr;
value = other.value;
name = std::move(other.name);
ptr = other.ptr;
other.ptr = nullptr;
}
return *this;
}
~MyClass() { delete ptr; }
};第二章 基类和派生类的拷贝
2.1 基类与派生类的关系 🔗
派生类继承自基类,派生类对象包含基类部分。这意味着拷贝派生类对象时,需要考虑基类部分的拷贝。
class Base {
public:
int baseValue;
};
class Derived : public Base {
public:
int derivedValue;
};2.2 派生类的拷贝构造 🏗️
2.2.1 默认行为
如果派生类没有自定义拷贝构造,编译器会:
- 调用基类的默认构造(如果存在)
- 对派生类的成员进行浅拷贝
class Base {
public:
int baseValue;
Base() : baseValue(0) {}
};
class Derived : public Base {
public:
int derivedValue;
};
Derived d1;
d1.baseValue = 10;
d1.derivedValue = 20;
Derived d2 = d1; // 调用基类默认构造 + 派生类浅拷贝
// d2.baseValue = 10, d2.derivedValue = 202.2.2 自定义派生类拷贝构造
关键点:派生类的拷贝构造必须显式调用基类的拷贝构造!
class Base {
public:
int baseValue;
Base() : baseValue(0) {}
Base(const Base& other) : baseValue(other.baseValue) {}
};
class Derived : public Base {
public:
int derivedValue;
// 自定义拷贝构造:必须显式调用基类拷贝构造
Derived(const Derived& other)
: Base(other), // ✅ 显式调用基类拷贝构造
derivedValue(other.derivedValue) {}
};常见错误
如果不显式调用基类拷贝构造,编译器会调用基类的默认构造,可能导致基类部分未被正确拷贝:
Derived(const Derived& other)
: derivedValue(other.derivedValue) // ❌ 基类部分调用默认构造!
{
// baseValue 未被正确拷贝
}2.3 派生类的拷贝赋值运算符 📋
2.3.1 基本写法
派生类的拷贝赋值运算符同样需要考虑基类部分:
class Base {
public:
int baseValue;
Base& operator=(const Base& other) {
baseValue = other.baseValue;
return *this;
}
};
class Derived : public Base {
public:
int derivedValue;
Derived& operator=(const Derived& other) {
if (this != &other) {
Base::operator=(other); // ✅ 调用基类拷贝赋值
derivedValue = other.derivedValue;
}
return *this;
}
};2.3.2 注意事项
- 不要重复调用基类的拷贝赋值:确保基类部分只被赋值一次
- 自赋值检查:虽然基类可能有自赋值检查,但派生类也需要
- 异常安全:如果可能,遵循异常安全的原则
2.4 基类与派生类拷贝的差异 ⚡
2.4.1 向上转型与拷贝
将派生类拷贝给基类时会发生对象切片(Object Slicing):
class Base {
public:
int baseValue;
Base(int v = 0) : baseValue(v) {}
};
class Derived : public Base {
public:
int derivedValue;
Derived(int b = 0, int d = 0) : Base(b), derivedValue(d) {}
};
Derived d(10, 20);
Base b = d; // ⚠️ 对象切片!只拷贝了 Base 部分,derivedValue 被切掉2.4.2 防止对象切片
如果需要保留派生类的完整信息,使用指针或引用:
Derived d(10, 20);
Base* pb = &d; // ✅ 指向派生类对象
Base& rb = d; // ✅ 引用派生类对象
// 通过基类指针调用虚函数,可以保留派生类行为2.5 多继承情况下的拷贝 🔱
多继承时,派生类的拷贝构造需要调用所有直接基类的拷贝构造:
class Base1 {
public:
int value1;
Base1(const Base1& other) : value1(other.value1) {}
};
class Base2 {
public:
int value2;
Base2(const Base2& other) : value2(other.value2) {}
};
class Derived : public Base1, public Base2 {
public:
int derivedValue;
Derived(const Derived& other)
: Base1(other), // ✅ 调用 Base1 拷贝构造
Base2(other), // ✅ 调用 Base2 拷贝构造
derivedValue(other.derivedValue) {}
};2.6 虚基类情况下的拷贝 (菱形继承) 💎
菱形继承结构中,虚基类只会有一个实例,需要特别注意:
class Base {
public:
int value;
Base(const Base& other) : value(other.value) {}
};
class Derived1 : virtual public Base {
public:
int d1;
Derived1(const Derived1& other) : Base(other), d1(other.d1) {}
};
class Derived2 : virtual public Base {
public:
int d2;
Derived2(const Derived2& other) : Base(other), d2(other.d2) {}
};
class FinalClass : public Derived1, public Derived2 {
public:
int finalValue;
FinalClass(const FinalClass& other)
: Base(other), // ✅ 虚基类,只调用一次
Derived1(other),
Derived2(other),
finalValue(other.finalValue) {}
};提示
虚基类在初始化列表中只出现一次,且由最终派生类负责初始化。
2.7 完整示例 📦
#include <iostream>
#include <cstring>
class BaseString {
protected:
char* data;
int length;
public:
BaseString(const char* str = "") {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
}
// 基类拷贝构造(深拷贝)
BaseString(const BaseString& other) : length(other.length) {
data = new char[length + 1];
strcpy(data, other.data);
}
// 基类拷贝赋值
BaseString& operator=(const BaseString& other) {
if (this != &other) {
delete[] data;
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
}
return *this;
}
virtual ~BaseString() {
delete[] data;
}
void print() const {
std::cout << data << std::endl;
}
};
class DerivedString : public BaseString {
private:
int refCount; // 引用计数
public:
DerivedString(const char* str = "") : BaseString(str), refCount(1) {}
// 派生类拷贝构造
DerivedString(const DerivedString& other)
: BaseString(other), // ✅ 显式调用基类拷贝构造
refCount(other.refCount) {}
// 派生类拷贝赋值
DerivedString& operator=(const DerivedString& other) {
if (this != &other) {
BaseString::operator=(other); // ✅ 调用基类拷贝赋值
refCount = other.refCount;
}
return *this;
}
void addRef() { ++refCount; }
void release() { --refCount; }
};
int main() {
DerivedString s1("Hello");
DerivedString s2 = s1; // 调用派生类拷贝构造
s1.print(); // Hello
s2.print(); // Hello
return 0;
}2.8 第二章要点总结 🎯
| 要点 | 说明 |
|---|---|
| 派生类拷贝构造 | 必须显式调用 Base(other) |
| 派生类拷贝赋值 | 必须调用 Base::operator=(other) |
| 对象切片 | 派生类赋给基类时会切片,使用指针/引用避免 |
| 多继承 | 每个直接基类都要在初始化列表中调用拷贝构造 |
| 虚基类 | 只初始化一次,由最终派生类负责 |
| 深拷贝考虑 | 基类有深拷贝资源时,派生类也要正确处理 |
2.9 最佳实践 💡
// ✅ 推荐:使用 = default 让编译器自动生成简单拷贝
class Simple {
int value;
std::string name;
public:
Simple() = default;
Simple(const Simple&) = default;
Simple& operator=(const Simple&) = default;
};
// ✅ 推荐:需要深拷贝时,显式定义并正确调用基类
class Complex : public Base {
int* ptr;
public:
Complex() : ptr(new int(0)) {}
Complex(const Complex& other) : Base(other), ptr(new int(*other.ptr)) {}
Complex& operator=(const Complex& other) {
if (this != &other) {
Base::operator=(other);
delete ptr;
ptr = new int(*other.ptr);
}
return *this;
}
};
// ✅ 推荐:考虑使用智能指针管理资源
class Smart : public Base {
std::unique_ptr<int> ptr;
public:
Smart() : ptr(std::make_unique<int>(0)) {}
Smart(const Smart& other) : Base(other), ptr(std::make_unique<int>(*other.ptr)) {}
Smart& operator=(const Smart& other) {
if (this != &other) {
Base::operator=(other);
ptr = std::make_unique<int>(*other.ptr);
}
return *this;
}
};提示
现代 C++ 中,优先使用智能指针 (std::unique_ptr、std::shared_ptr) 来管理资源,可以大大简化拷贝/移动语义的实现。