各位编程爱好者,大家好!欢迎来到今天的专题讲座——“构造函数全攻略:默认构造、拷贝构造与移动构造的执行时机”。在C++的世界里,构造函数是创建对象的基石,它们负责对象的初始化,确保对象在被使用之前处于有效状态。深入理解各种构造函数的行为、生成规则以及它们在不同场景下的执行时机,是编写高效、健壮C++代码的关键。
今天的讲座,我们将一同揭开C++构造函数的神秘面纱,从最基本的默认构造函数,到处理对象复制的拷贝构造函数,再到C++11引入的、旨在提升性能的移动构造函数。我们将通过丰富的代码示例,详细剖析它们的工作原理和执行时机,帮助大家构建一个清晰而准确的认知框架。
第一章:基石——默认构造函数
默认构造函数是C++中最基础的构造函数之一,它在没有提供任何初始化参数的情况下,负责创建并初始化一个对象。理解它的生成规则和行为对于正确使用类至关重要。
1.1 什么是默认构造函数?
默认构造函数是一个不带任何参数的构造函数。它的主要职责是在对象被创建时,确保其成员变量被赋予一个初始状态。
函数签名示例:
ClassName();
1.2 默认构造函数的隐式生成规则
C++编译器在特定条件下会自动为类生成一个公共的、内联的默认构造函数。这个隐式生成的默认构造函数会执行以下操作:
- 调用基类的默认构造函数: 如果类有基类,它会先调用基类的默认构造函数。
- 调用成员对象的默认构造函数: 如果类有成员对象(非静态),它会调用这些成员对象的默认构造函数。
- 不初始化内置类型(POD类型)成员: 对于内置类型(如
int,double, 指针等)和POD(Plain Old Data)类型的成员,隐式生成的默认构造函数不会对它们进行初始化。这意味着这些成员将包含不确定的“垃圾”值。
隐式生成条件:
只有当类中没有声明任何其他构造函数(包括拷贝构造函数、移动构造函数以及任何带参数的构造函数)时,编译器才会隐式生成默认构造函数。
示例1.2.1:隐式生成的默认构造函数
#include <iostream>
#include <string>
class MyClassImplicit {
public:
int id; // 内置类型,不会被默认初始化
std::string name; // std::string有自己的默认构造函数
// 没有声明任何构造函数,编译器会隐式生成一个默认构造函数
// MyClassImplicit() { /* 编译器生成 */ }
};
class BaseClass {
public:
BaseClass() { std::cout << "BaseClass default constructor called." << std::endl; }
};
class DerivedClassImplicit : public BaseClass {
public:
int value;
// 没有声明任何构造函数,编译器会隐式生成一个默认构造函数
// 它会先调用BaseClass的默认构造函数
};
int main() {
std::cout << "--- MyClassImplicit ---" << std::endl;
MyClassImplicit obj1; // 调用隐式生成的默认构造函数
std::cout << "obj1.id: " << obj1.id << std::endl; // 可能输出垃圾值
std::cout << "obj1.name: '" << obj1.name << "'" << std::endl; // std::string被默认初始化为空字符串
std::cout << "n--- DerivedClassImplicit ---" << std::endl;
DerivedClassImplicit obj2; // 调用隐式生成的默认构造函数,它会链式调用BaseClass的默认构造函数
std::cout << "obj2.value: " << obj2.value << std::endl; // 可能输出垃圾值
return 0;
}
输出分析: obj1.id 和 obj2.value 的值是不确定的,而 obj1.name 则被 std::string 的默认构造函数初始化为空字符串。DerivedClassImplicit 的创建会触发 BaseClass 的默认构造函数。
1.3 默认构造函数的显式定义
如果需要对内置类型成员进行初始化,或者执行其他自定义的初始化逻辑,就必须显式定义默认构造函数。
示例1.3.1:显式定义的默认构造函数
#include <iostream>
#include <string>
class MyClassExplicit {
public:
int id;
std::string name;
// 显式定义默认构造函数
MyClassExplicit() : id(0), name("default_name") {
std::cout << "MyClassExplicit default constructor called." << std::endl;
}
};
int main() {
std::cout << "--- MyClassExplicit ---" << std::endl;
MyClassExplicit obj; // 调用显式定义的默认构造函数
std::cout << "obj.id: " << obj.id << std::endl; // 输出0
std::cout << "obj.name: '" << obj.name << "'" << std::endl; // 输出'default_name'
return 0;
}
输出分析: obj.id 和 obj.name 都被按照预期进行了初始化。
1.4 默认构造函数不会被隐式生成的情况
以下情况之一发生时,编译器将不会为类隐式生成默认构造函数:
- 用户已声明了任何其他构造函数: 如果你声明了任何一个带参数的构造函数、拷贝构造函数或移动构造函数,编译器就不会再自动生成默认构造函数。
- 基类没有可访问的默认构造函数: 如果类有基类,且基类没有默认构造函数,或者其默认构造函数不可访问(如私有或保护),则派生类不会隐式生成默认构造函数。
- 非静态成员对象没有可访问的默认构造函数: 如果类包含非静态成员对象,且这些成员的类型没有可访问的默认构造函数,则该类不会隐式生成默认构造函数。
- 虚基类或带有虚函数的类: 这些情况通常不直接阻止默认构造函数的生成,但它们会影响构造函数的实现细节。
在这些情况下,如果你仍需要无参数构造对象,必须显式定义一个默认构造函数。否则,尝试无参数创建对象将导致编译错误。
1.5 = default 和 = delete
C++11引入了 = default 和 = delete 语法,用于明确指示编译器生成或禁止特定特殊成员函数。
= default: 用于要求编译器生成默认实现。即使存在其他构造函数,也可以强制编译器生成默认构造函数。= delete: 用于明确禁止编译器生成或使用某个特殊成员函数。
示例1.5.1:= default 和 = delete
#include <iostream>
#include <string>
class MyClassControl {
public:
int id;
std::string name;
// 显式定义一个带参数的构造函数
MyClassControl(int i) : id(i), name("custom") {
std::cout << "MyClassControl(int) constructor called. id=" << id << std::endl;
}
// 此时,编译器不会隐式生成默认构造函数。
// 如果我们仍需要一个默认构造函数,必须显式定义或使用= default。
MyClassControl() = default; // 强制编译器生成默认构造函数
// 禁止拷贝构造函数
MyClassControl(const MyClassControl&) = delete;
void print() const {
std::cout << "ID: " << id << ", Name: " << name << std::endl;
}
};
int main() {
std::cout << "--- MyClassControl ---" << std::endl;
MyClassControl obj1; // 调用=default的默认构造函数 (id, name将是未初始化的或默认构造的)
std::cout << "After default construction (obj1): ";
obj1.print(); // id可能是垃圾值,name是空字符串
MyClassControl obj2(100); // 调用带参数的构造函数
std::cout << "After parameterized construction (obj2): ";
obj2.print();
// MyClassControl obj3 = obj1; // 编译错误:拷贝构造函数被删除了
return 0;
}
输出分析: obj1 调用了 = default 的默认构造函数,其行为与隐式生成时类似,内置类型 id 未初始化。obj2 则正常调用了带参数的构造函数。尝试拷贝 obj1 会导致编译错误,因为拷贝构造函数被 = delete 了。
1.6 默认构造函数的执行时机
默认构造函数在以下情况下执行:
- 无参数创建对象: 当你使用
ClassName obj;或ClassName obj();(C++11前,现在推荐ClassName obj{};用于值初始化) 语法创建对象时。 - 动态分配内存并无参数构造对象:
new ClassName();或new ClassName;。 - 作为数组元素被创建:
ClassName arr[10];(如果 ClassName 有默认构造函数)。 - 作为另一个类的数据成员,且宿主类被默认构造时: 如果成员是对象,其默认构造函数会被调用。
- 作为基类子对象,且派生类被默认构造时。
std::vector等容器在resize或emplace_back时,如果无参数填充。- 使用
{}进行值初始化时:ClassName obj{};。
示例1.6.1:执行时机示例
#include <iostream>
#include <string>
#include <vector>
class MyClassDefaultCtor {
public:
int id;
MyClassDefaultCtor() : id(0) { // 显式默认构造函数,初始化id
std::cout << "MyClassDefaultCtor default constructor called. id=" << id << std::endl;
}
};
int main() {
std::cout << "--- Default Constructor Execution Timings ---" << std::endl;
// 1. 无参数创建栈对象
std::cout << "nScenario 1: Stack object creation" << std::endl;
MyClassDefaultCtor obj1;
// 2. 动态分配内存并无参数构造
std::cout << "nScenario 2: Dynamic object creation" << std::endl;
MyClassDefaultCtor* ptr = new MyClassDefaultCtor(); // new ClassName() 保证值初始化
delete ptr;
// 3. 作为数组元素被创建
std::cout << "nScenario 3: Array elements creation" << std::endl;
MyClassDefaultCtor arr[2]; // 每个元素都会调用默认构造函数
// 4. 作为另一个类的数据成员
std::cout << "nScenario 4: Member object creation" << std::endl;
class Container {
public:
MyClassDefaultCtor memberObj;
Container() {
std::cout << "Container default constructor called." << std::endl;
}
};
Container containerObj;
// 5. 作为基类子对象
std::cout << "nScenario 5: Base class subobject creation" << std::endl;
class Base {
public:
Base() { std::cout << "Base default constructor called." << std::endl; }
};
class Derived : public Base {
public:
Derived() { std::cout << "Derived default constructor called." << std::endl; }
};
Derived derivedObj;
// 6. std::vector 容器填充
std::cout << "nScenario 6: std::vector resize/emplace_back" << std::endl;
std::vector<MyClassDefaultCtor> vec;
vec.resize(1); // 插入一个元素,调用默认构造函数
vec.emplace_back(); // 再插入一个元素,调用默认构造函数
// 7. 使用 {} 进行值初始化
std::cout << "nScenario 7: Value initialization with {}" << std::endl;
MyClassDefaultCtor obj2{};
return 0;
}
输出分析: 每个场景都清晰地展示了 MyClassDefaultCtor 或其关联类(如 Base, Container)的默认构造函数被调用的时机。特别注意 new MyClassDefaultCtor(); 和 new MyClassDefaultCtor; 在内置类型上的区别,前者会进行值初始化(零初始化),后者不会。但对于拥有用户定义默认构造函数的类,两者都会调用该构造函数。
第二章:复制的艺术——拷贝构造函数
拷贝构造函数是C++中用于创建对象副本的特殊成员函数。它在对象通过另一个同类型对象进行初始化时被调用。理解拷贝构造函数对于避免浅拷贝问题和正确管理资源至关重要。
2.1 什么是拷贝构造函数?
拷贝构造函数是一种构造函数,它接受一个同类型对象的 const 引用作为参数,并使用该对象来初始化新创建的对象。
函数签名示例:
ClassName(const ClassName& other);
这里的 const 确保了源对象不会被修改,而引用 & 避免了在传递参数时进行额外的拷贝。
2.2 拷贝构造函数的隐式生成规则
与默认构造函数类似,C++编译器也会在特定条件下为类隐式生成一个公共的、内联的拷贝构造函数。这个隐式生成的拷贝构造函数执行的是成员逐一拷贝(member-wise copy),即简单地将其成员变量从源对象复制到新对象。
隐式生成条件:
只有当类中没有声明任何拷贝构造函数时,编译器才会隐式生成一个。此外,如果类声明了移动构造函数或移动赋值运算符,但没有声明拷贝构造函数、拷贝赋值运算符或析构函数,则隐式生成的拷贝构造函数将被 delete。
示例2.2.1:隐式生成的拷贝构造函数
#include <iostream>
#include <string>
class MyClassImplicitCopy {
public:
int id;
std::string name;
MyClassImplicitCopy(int i = 0, const std::string& n = "default") : id(i), name(n) {
std::cout << "MyClassImplicitCopy constructor called. id=" << id << ", name='" << name << "'" << std::endl;
}
// 没有声明拷贝构造函数,编译器会隐式生成一个
~MyClassImplicitCopy() {
std::cout << "MyClassImplicitCopy destructor called. id=" << id << ", name='" << name << "'" << std::endl;
}
};
int main() {
std::cout << "--- MyClassImplicitCopy ---" << std::endl;
MyClassImplicitCopy obj1(1, "Original"); // 调用普通构造函数
MyClassImplicitCopy obj2 = obj1; // 调用隐式生成的拷贝构造函数 (拷贝初始化)
std::cout << "obj2.id: " << obj2.id << ", obj2.name: '" << obj2.name << "'" << std::endl;
MyClassImplicitCopy obj3(obj1); // 调用隐式生成的拷贝构造函数 (直接初始化)
std::cout << "obj3.id: " << obj3.id << ", obj3.name: '" << obj3.name << "'" << std::endl;
return 0;
}
输出分析: obj2 = obj1; 和 obj3(obj1); 都触发了隐式生成的拷贝构造函数。由于 std::string 自身有定义良好的拷贝构造函数,所以 name 成员会进行深拷贝。id 成员则直接按值拷贝。
2.3 浅拷贝与深拷贝问题
隐式生成的拷贝构造函数执行的是成员逐一拷贝,这对于内置类型或拥有良好拷贝语义的类成员(如 std::string, std::vector)是没问题的。然而,如果类管理着动态分配的资源(如堆内存、文件句柄等),简单的成员逐一拷贝会导致浅拷贝(Shallow Copy)问题。
浅拷贝问题: 两个对象会共享同一份资源。当其中一个对象修改资源或销毁时,另一个对象将受到影响,甚至导致双重释放(double free)或悬挂指针(dangling pointer)等运行时错误。
为了解决浅拷贝问题,当类管理动态资源时,必须显式定义一个深拷贝(Deep Copy)的拷贝构造函数。
示例2.3.1:浅拷贝问题与深拷贝解决方案
#include <iostream>
#include <cstring> // For strlen, strcpy
// 示例:浅拷贝问题
class ShallowCopyExample {
public:
int* data;
ShallowCopyExample(int val) {
data = new int(val);
std::cout << "ShallowCopyExample constructor called. data=" << *data << " (addr: " << data << ")" << std::endl;
}
// 隐式生成的拷贝构造函数将只拷贝指针data的值
// ShallowCopyExample(const ShallowCopyExample& other) { data = other.data; } // 隐式行为类似此行
~ShallowCopyExample() {
if (data) { // 避免重复删除已释放的指针
std::cout << "ShallowCopyExample destructor called. Deleting data " << *data << " (addr: " << data << ")" << std::endl;
delete data;
data = nullptr; // 防止悬挂指针
}
}
};
// 示例:深拷贝解决方案
class DeepCopyExample {
public:
int* data;
DeepCopyExample(int val) {
data = new int(val);
std::cout << "DeepCopyExample constructor called. data=" << *data << " (addr: " << data << ")" << std::endl;
}
// 显式定义深拷贝构造函数
DeepCopyExample(const DeepCopyExample& other) {
data = new int(*other.data); // 分配新的内存并复制内容
std::cout << "DeepCopyExample COPY constructor called. data=" << *data << " (addr: " << data << ") from " << *other.data << " (addr: " << other.data << ")" << std::endl;
}
// 显式定义拷贝赋值运算符(通常与拷贝构造函数一同出现,Rule of Three)
DeepCopyExample& operator=(const DeepCopyExample& other) {
if (this != &other) { // 防止自我赋值
delete data; // 释放原有资源
data = new int(*other.data); // 分配新资源并复制内容
std::cout << "DeepCopyExample COPY ASSIGNMENT called. data=" << *data << " (addr: " << data << ") from " << *other.data << " (addr: " << other.data << ")" << std::endl;
}
return *this;
}
~DeepCopyExample() {
if (data) {
std::cout << "DeepCopyExample destructor called. Deleting data " << *data << " (addr: " << data << ")" << std::endl;
delete data;
data = nullptr;
}
}
};
void testShallowCopy() {
std::cout << "n--- Testing Shallow Copy ---" << std::endl;
ShallowCopyExample s1(10);
ShallowCopyExample s2 = s1; // 隐式拷贝构造,s1.data 和 s2.data 指向同一块内存
std::cout << "s1.data: " << *s1.data << ", s2.data: " << *s2.data << std::endl;
*s2.data = 20; // 修改s2会影响s1
std::cout << "After modifying s2.data:" << std::endl;
std::cout << "s1.data: " << *s1.data << ", s2.data: " << *s2.data << std::endl;
// 当s2和s1销毁时,会尝试两次删除同一块内存,导致运行时错误
std::cout << "Exiting testShallowCopy scope. Destructors will be called." << std::endl;
} // s2 析构,s1 析构,导致 double free
void testDeepCopy() {
std::cout << "n--- Testing Deep Copy ---" << std::endl;
DeepCopyExample d1(30);
DeepCopyExample d2 = d1; // 显式深拷贝构造,d1.data 和 d2.data 指向不同内存
std::cout << "d1.data: " << *d1.data << ", d2.data: " << *d2.data << std::endl;
*d2.data = 40; // 修改d2不会影响d1
std::cout << "After modifying d2.data:" << std::endl;
std::cout << "d1.data: " << *d1.data << ", d2.data: " << *d2.data << std::endl;
std::cout << "Exiting testDeepCopy scope. Destructors will be called." << std::endl;
} // d2 析构,d1 析构,各自删除自己的内存
int main() {
// testShallowCopy(); // 运行此函数可能会崩溃或报错,请谨慎测试
testDeepCopy();
DeepCopyExample d3(50);
DeepCopyExample d4(60);
d4 = d3; // 调用拷贝赋值运算符
std::cout << "d3.data: " << *d3.data << ", d4.data: " << *d4.data << std::endl;
return 0;
}
输出分析: testShallowCopy 演示了浅拷贝的危险性,两个对象共享同一块内存,导致修改一个影响另一个,并且在析构时会尝试双重释放。testDeepCopy 通过显式定义拷贝构造函数和拷贝赋值运算符,确保每个对象拥有独立的资源副本,从而避免了这些问题。
2.4 = default 和 = delete 用于拷贝构造函数
与默认构造函数类似,= default 和 = delete 也可以应用于拷贝构造函数:
ClassName(const ClassName&) = default;: 强制编译器生成默认的成员逐一拷贝构造函数。当用户定义了移动构造函数或移动赋值运算符,但仍希望有默认拷贝构造函数时,这很有用。ClassName(const ClassName&) = delete;: 明确禁止对象的拷贝。这适用于那些设计上不应被复制的类(例如,管理唯一资源的类)。
2.5 拷贝构造函数的执行时机
拷贝构造函数在以下几种情况中被调用:
-
使用一个已存在的对象来初始化一个新对象时:
- 直接初始化:
ClassName obj2(obj1); - 拷贝初始化:
ClassName obj2 = obj1; - 列表初始化:
ClassName obj2{obj1};(C++11起)
- 直接初始化:
-
将对象作为参数按值传递给函数时:
函数的形式参数是ClassName param,当调用func(obj)时,obj会被拷贝到param。 -
函数返回对象时:
当函数返回一个局部对象或匿名临时对象时,该对象会被拷贝到接收它的变量中。注意: 现代C++编译器通常会进行返回值优化(RVO/NRVO),直接在调用方的栈帧上构造对象,从而省略拷贝构造函数的调用。C++17甚至在某些情况下强制进行拷贝省略。 -
当一个对象被抛出(throw)或捕获(catch)时:
throw obj;时,obj会被拷贝到一个内部异常对象中。catch (ClassName ex)时,异常对象会再次被拷贝到ex中。 -
将对象放入标准库容器时:
例如,std::vector::push_back(obj)会将obj拷贝到容器内部。
示例2.5.1:拷贝构造函数的执行时机
#include <iostream>
#include <string>
#include <vector>
class MyClassCopyCtor {
public:
int id;
std::string name;
MyClassCopyCtor(int i = 0, const std::string& n = "Default") : id(i), name(n) {
std::cout << "MyClassCopyCtor constructor called. id=" << id << ", name='" << name << "'" << std::endl;
}
// 显式定义拷贝构造函数,以便观察调用时机
MyClassCopyCtor(const MyClassCopyCtor& other) : id(other.id), name(other.name) {
std::cout << "MyClassCopyCtor COPY constructor called. id=" << id << " (from " << other.id << ")" << std::endl;
}
~MyClassCopyCtor() {
std::cout << "MyClassCopyCtor destructor called. id=" << id << std::endl;
}
void print() const {
std::cout << "MyClassCopyCtor object: ID=" << id << ", Name='" << name << "'" << std::endl;
}
};
// 场景2: 对象作为参数按值传递给函数
void funcByValue(MyClassCopyCtor obj) {
std::cout << " Inside funcByValue. ";
obj.print();
} // obj 销毁
// 场景3: 函数返回对象
MyClassCopyCtor funcReturnByValue(int val) {
std::cout << " Inside funcReturnByValue. Creating temporary object." << std::endl;
MyClassCopyCtor temp(val, "Returned");
std::cout << " Returning from funcReturnByValue." << std::endl;
return temp; // RVO/NRVO可能会省略拷贝
} // temp 销毁
int main() {
std::cout << "--- Copy Constructor Execution Timings ---" << std::endl;
// Scenario 1: 使用一个已存在的对象初始化新对象
std::cout << "nScenario 1: Object initialization" << std::endl;
MyClassCopyCtor original(10, "Original"); // 普通构造
MyClassCopyCtor copy1 = original; // 拷贝初始化
MyClassCopyCtor copy2(original); // 直接初始化
MyClassCopyCtor copy3{original}; // 列表初始化 (C++11)
// Scenario 2: 对象作为参数按值传递给函数
std::cout << "nScenario 2: Passing object by value" << std::endl;
funcByValue(original); // 拷贝构造函数的调用
// Scenario 3: 函数返回对象 (受RVO/NRVO影响)
std::cout << "nScenario 3: Returning object by value (RVO/NRVO)" << std::endl;
MyClassCopyCtor returnedObj = funcReturnByValue(20); // 期望被优化,不触发拷贝构造
std::cout << " After funcReturnByValue. ";
returnedObj.print();
// Scenario 4: 抛出和捕获异常
std::cout << "nScenario 4: Throwing and catching objects by value" << std::endl;
try {
MyClassCopyCtor exceptionObj(30, "Exception");
std::cout << " Throwing exception object." << std::endl;
throw exceptionObj; // 拷贝构造函数的调用
} catch (MyClassCopyCtor ex) { // 拷贝构造函数的调用
std::cout << " Caught exception. ";
ex.print();
} // ex 销毁
// exceptionObj 销毁
// Scenario 5: 将对象放入标准库容器
std::cout << "nScenario 5: Putting object into std::vector" << std::endl;
std::vector<MyClassCopyCtor> vec;
MyClassCopyCtor vec_elem(40, "VectorElement");
vec.push_back(vec_elem); // 拷贝构造函数的调用
std::cout << " Vector size: " << vec.size() << std::endl;
vec.push_back(MyClassCopyCtor(50, "AnotherElement")); // 临时对象,可能移动构造,也可能拷贝构造(取决于C++版本和编译器)
std::cout << "n--- End of main ---" << std::endl;
return 0;
}
输出分析:
- 场景1 清晰地展示了各种初始化语法触发拷贝构造函数。
- 场景2
funcByValue的参数obj在进入函数时通过拷贝构造original而创建。 - 场景3
funcReturnByValue的返回值temp在返回时,如果编译器进行了RVO/NRVO优化,则不会调用拷贝构造函数。它会直接在returnedObj的内存位置构造temp。如果未优化,则会发生拷贝。现代编译器普遍会进行这种优化。 - 场景4 异常抛出和捕获时,对象会被拷贝。
- 场景5
vec.push_back(vec_elem)会将vec_elem拷贝到vector内部,可能伴随vector扩容时的多次拷贝。vec.push_back(MyClassCopyCtor(50, "AnotherElement"))会使用临时对象,如果类有移动构造函数,则会优先调用移动构造函数,否则调用拷贝构造函数。
第三章:效率的驱动——移动构造函数
C++11引入了右值引用(rvalue reference)和移动语义(move semantics),从而催生了移动构造函数。移动构造函数旨在通过“窃取”临时对象(右值)的资源而非复制它们,来提高程序的性能,特别是在处理大型对象或动态资源时。
3.1 什么是移动构造函数?
移动构造函数是一种构造函数,它接受一个同类型对象的右值引用(&&)作为参数,并“移动”源对象的资源(如指针、文件句柄等)到新对象中,同时将源对象置于一个有效但未指定的状态(通常是将其资源指针置空),以防止双重释放。
函数签名示例:
ClassName(ClassName&& other);
3.2 移动构造函数的隐式生成规则
C++编译器在特定条件下也会为类隐式生成一个移动构造函数。隐式生成的移动构造函数执行的是成员逐一移动(member-wise move),即对每个成员调用其移动构造函数(如果存在),否则调用其拷贝构造函数。对于内置类型,移动语义与拷贝语义相同。
隐式生成条件:
只有当类中没有声明任何拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符或析构函数时,编译器才会隐式生成一个移动构造函数。如果类显式声明了析构函数或拷贝操作,但没有声明移动操作,则不会隐式生成移动构造函数,并且会隐式删除移动构造函数。
3.3 移动构造函数的显式定义
当类管理着动态分配的资源时,为了实现真正的资源转移,必须显式定义移动构造函数。
示例3.3.1:显式定义移动构造函数
#include <iostream>
#include <string>
#include <utility> // For std::move
class MyClassMoveCtor {
public:
int* data;
std::string name;
MyClassMoveCtor(int val = 0, const std::string& n = "Default") : name(n) {
data = new int(val);
std::cout << "MyClassMoveCtor constructor called. data=" << *data << ", name='" << name << "' (addr: " << data << ")" << std::endl;
}
// 拷贝构造函数 (深拷贝)
MyClassMoveCtor(const MyClassMoveCtor& other) : name(other.name) {
data = new int(*other.data);
std::cout << "MyClassMoveCtor COPY constructor called. data=" << *data << " (from " << *other.data << ")" << std::endl;
}
// 移动构造函数 (资源转移)
MyClassMoveCtor(MyClassMoveCtor&& other) noexcept : data(other.data), name(std::move(other.name)) {
other.data = nullptr; // 将源对象的资源指针置空
std::cout << "MyClassMoveCtor MOVE constructor called. data=" << (data ? *data : -1) << " (stole from " << (other.data ? *other.data : -1) << ")" << std::endl;
}
// 拷贝赋值运算符 (深拷贝)
MyClassMoveCtor& operator=(const MyClassMoveCtor& other) {
if (this != &other) {
delete data;
data = new int(*other.data);
name = other.name;
std::cout << "MyClassMoveCtor COPY ASSIGNMENT called. data=" << *data << std::endl;
}
return *this;
}
// 移动赋值运算符 (资源转移)
MyClassMoveCtor& operator=(MyClassMoveCtor&& other) noexcept {
if (this != &other) {
delete data; // 释放自己的资源
data = other.data; // 窃取源对象的资源
name = std::move(other.name); // 移动std::string
other.data = nullptr; // 将源对象资源指针置空
std::cout << "MyClassMoveCtor MOVE ASSIGNMENT called. data=" << (data ? *data : -1) << std::endl;
}
return *this;
}
~MyClassMoveCtor() {
if (data) {
std::cout << "MyClassMoveCtor destructor called. Deleting data " << *data << " (addr: " << data << ")" << std::endl;
delete data;
} else {
std::cout << "MyClassMoveCtor destructor called. No data to delete (was moved or nullptr)." << std::endl;
}
}
void print() const {
if (data) {
std::cout << "MyClassMoveCtor object: ID=" << *data << ", Name='" << name << "'" << std::endl;
} else {
std::cout << "MyClassMoveCtor object: No data (was moved), Name='" << name << "'" << std::endl;
}
}
};
int main() {
std::cout << "--- MyClassMoveCtor ---" << std::endl;
MyClassMoveCtor obj1(10, "Source"); // 普通构造
std::cout << "obj1 state: "; obj1.print();
// 场景1: 从右值初始化 (临时对象)
std::cout << "nScenario 1: Initializing from an rvalue (temporary object)" << std::endl;
MyClassMoveCtor obj2 = MyClassMoveCtor(20, "Temporary"); // 移动构造
std::cout << "obj2 state: "; obj2.print();
// obj2 已经拥有了临时对象的资源,临时对象被置空后销毁
// 场景2: 从右值初始化 (std::move 转换的左值)
std::cout << "nScenario 2: Initializing from an rvalue (std::move'd lvalue)" << std::endl;
MyClassMoveCtor obj3 = std::move(obj1); // 移动构造
std::cout << "obj3 state: "; obj3.print();
std::cout << "obj1 state after move: "; obj1.print(); // obj1 的资源已被窃取,data为nullptr
// 场景3: 作为函数返回值 (RVO/NRVO 或移动构造)
std::cout << "nScenario 3: Returning an rvalue from a function" << std::endl;
auto createObject = [](int val) {
MyClassMoveCtor localObj(val, "Local");
std::cout << " Inside createObject before return. "; localObj.print();
return localObj; // 可能会发生移动构造(或RVO)
};
MyClassMoveCtor obj4 = createObject(30); // 移动构造(或RVO)
std::cout << "obj4 state: "; obj4.print();
// 场景4: std::vector::push_back 或 emplace_back 与右值
std::cout << "nScenario 4: std::vector::push_back with rvalues" << std::endl;
std::vector<MyClassMoveCtor> vec;
vec.reserve(2); // 预留空间避免扩容时的额外移动/拷贝
MyClassMoveCtor obj_for_vec(40, "VectorElem");
vec.push_back(std::move(obj_for_vec)); // 移动构造
std::cout << "obj_for_vec state after move to vector: "; obj_for_vec.print();
vec.emplace_back(50, "AnotherVectorElem"); // 直接构造在vector内部,通常是最优
std::cout << "n--- End of main ---" << std::endl;
return 0;
}
输出分析:
- 场景1
MyClassMoveCtor(20, "Temporary")创建了一个临时对象(右值)。obj2直接通过移动构造函数从这个临时对象“窃取”了资源,避免了深度拷贝。 - 场景2
std::move(obj1)将左值obj1强制转换为右值引用。然后obj3通过移动构造函数从obj1窃取资源。此时obj1.data被置为nullptr,其资源已被转移,但obj1本身仍然是有效对象。 - 场景3 匿名lambda函数
createObject返回localObj。这里会发生移动构造(如果编译器没有进行RVO)。obj4通过移动构造从localObj接收资源。 - 场景4
vec.push_back(std::move(obj_for_vec))将obj_for_vec的资源移动到vector内部新创建的元素中。obj_for_vec同样处于被移动后的状态。emplace_back通常直接在容器内部构造,避免了额外的构造和移动。
3.4 std::move 的作用
std::move 并不是真正执行“移动”操作,它是一个函数模板,其作用仅仅是将一个左值表达式无条件地转换为对应的右值引用类型。这个转换使得该左值表达式能够匹配移动构造函数或移动赋值运算符的右值引用参数。实际的“移动”行为,即资源转移,是由被调用的移动构造函数或移动赋值运算符内部实现的。
重要提示: 一旦对一个对象使用了 std::move,就意味着你承诺不再使用该对象的资源,它处于一个有效但未指定的状态。除非你重新初始化它,否则不应该再依赖它的内容。
3.5 noexcept 关键字
在移动构造函数和移动赋值运算符的声明中,通常会使用 noexcept 关键字。这表示这些操作不会抛出异常。noexcept 对于标准库容器的性能至关重要,因为如果移动操作是 noexcept 的,容器在扩容时会优先使用移动而不是拷贝,从而提升效率。如果移动操作可能抛出异常,容器为了保证强异常安全,可能会退化为使用拷贝操作。
3.6 移动构造函数的执行时机
移动构造函数在以下几种情况中被调用:
-
使用一个右值(临时对象或经
std::move转换的左值)来初始化一个新对象时:ClassName obj2 = ClassName(val);(临时对象)ClassName obj2(ClassName(val));(临时对象)ClassName obj2 = std::move(obj1);(经std::move转换的左值)ClassName obj2(std::move(obj1));(经std::move转换的左值)
-
函数返回右值时:
当函数返回一个局部对象时,如果RVO/NRVO不适用或未发生,则会发生移动构造。C++11规定,从函数返回一个局部变量时,如果该局部变量是可移动的,编译器会优先尝试移动构造而不是拷贝构造。 -
将右值放入标准库容器时:
例如,std::vector::push_back(std::move(obj))或std::vector::emplace_back(...)。emplace_back尤其高效,因为它通常直接在容器内部构造对象,避免了任何拷贝或移动。
表格总结:构造函数类型、签名与用途
| 构造函数类型 | 典型签名 | 用途 | 隐式生成条件 The ability to effectively manage object construction and lifecycle is a hallmark of an expert C++ programmer. Today, we’s embark on a comprehensive journey through the fundamental yet intricate world of C++ constructors: the default, copy, and move constructors. We will dissect their underlying mechanisms, the conditions under which they are implicitly generated or deleted, and most importantly, pinpoint the precise moments of their execution.
By the end of this lecture, you will not only understand what these constructors are but also when and why they are invoked, empowering you to write more efficient, robust, and predictable C++ applications.
第一章:基石——默认构造函数 (The Foundation: Default Constructor)
默认构造函数是C++类生命周期中最初的入口点,负责在没有外部参数的情况下初始化对象。它是所有其他构造函数的基础。
1.1 什么是默认构造函数?
默认构造函数是一个不接受任何参数的构造函数。它可以在对象声明时被调用,而无需提供显式初始化值。
典型签名:
ClassName();
1.2 默认构造函数的隐式生成与行为
C++编译器会在特定条件下为类自动生成一个公共的、内联的默认构造函数。这个隐式生成的默认构造函数会执行以下初始化行为:
- 对于基类子对象: 它会递归地调用所有直接基类的默认构造函数。
- 对于非静态成员对象: 它会递归地调用所有非静态成员对象的默认构造函数。
- 对于内置类型(如
int,double, 指针等)和POD(Plain Old Data)类型成员: 隐式生成的默认构造函数不会对它们进行初始化。这意味着这些成员将包含不确定的“垃圾”值,除非它们是全局、静态或数组元素,它们会被零初始化。
隐式生成条件:
编译器只会在类中没有声明任何其他构造函数(包括任何带参数的构造函数、拷贝构造函数或移动构造函数)时,才会隐式生成一个默认构造函数。
示例1.2.1:隐式生成的默认构造函数及其行为
#include <iostream>
#include <string>
#include <vector>
// 基类,有显式默认构造函数
class Base {
public:
int base_id;
Base() : base_id(100) {
std::cout << " Base::Base() called. base_id = " << base_id << std::endl;
}
};
// 成员类,有显式默认构造函数
class Member {
public:
std::string member_name;
Member() : member_name("DefaultMember") {
std::cout << " Member::Member() called. member_name = '" << member_name << "'" << std::endl;
}
};
// 示例类:MyClassImplicitDefault
// 未声明任何构造函数,编译器将隐式生成一个默认构造函数
class MyClassImplicitDefault : public Base {
public:
int value; // 内置类型,不会被隐式默认构造函数初始化 (垃圾值)
Member member_obj; // 成员对象,其默认构造函数会被调用
std::string str_member; // std::string 有自己的默认构造函数 (空字符串)
};
int main() {
std::cout << "--- MyClassImplicitDefault ---" << std::endl;
MyClassImplicitDefault obj; // 调用隐式生成的默认构造函数
std::cout << "obj.value: " << obj.value << std::endl; // 不确定值
std::cout << "obj.base_id: " << obj.base_id << std::endl; // 100 (由Base的默认构造函数初始化)
std::cout << "obj.member_obj.member_name: '" << obj.member_obj.member_name << "'" << std::endl; // "DefaultMember"
std::cout << "obj.str_member: '" << obj.str_member << "'" << std::endl; // "" (空字符串)
return 0;
}
输出分析:
在创建 obj 时:
- 首先调用
Base的默认构造函数,初始化base_id为100。 - 接着调用
Member成员对象member_obj的默认构造函数,初始化member_name为"DefaultMember"。 std::string成员str_member的默认构造函数被调用,将其初始化为空字符串。- 内置类型
value未被初始化,因此输出不确定值。
1.3 默认构造函数的显式定义
为了确保所有成员(包括内置类型)都被初始化,或者执行特定的初始化逻辑,我们通常会显式定义默认构造函数。
示例1.3.1:显式定义的默认构造函数
#include <iostream>
#include <string>
class MyClassExplicitDefault {
public:
int id;
std::string name;
// 显式定义默认构造函数,使用成员初始化列表
MyClassExplicitDefault() : id(0), name("ExplicitDefault") {
std::cout << "MyClassExplicitDefault::MyClassExplicitDefault() called." << std::endl;
}
};
int main() {
std::cout << "--- MyClassExplicitDefault ---" << std::endl;
MyClassExplicitDefault obj; // 调用显式定义的默认构造函数
std::cout << "obj.id: " << obj.id << std::endl; // 输出 0
std::cout << "obj.name: '" << obj.name << "'" << std::endl; // 输出 "ExplicitDefault"
return 0;
}
输出分析: obj.id 和 obj.name 都被按照预期进行了初始化。
1.4 默认构造函数不会被隐式生成的情况
如果出现以下任何一种情况,编译器将不会为类隐式生成默认构造函数:
- 用户已声明了任何其他构造函数: 如果类中声明了任何用户定义的构造函数(包括带参数的构造函数、拷贝构造函数、移动构造函数),编译器就不会再自动生成默认构造函数。如果你仍然需要一个无参数构造函数,必须显式定义它(或使用
= default)。 - 基类没有可访问的默认构造函数: 如果类有基类,且基类没有默认构造函数,或者其默认构造函数不可访问(如
private或protected),则派生类不会隐式生成默认构造函数。 - 非静态成员对象没有可访问的默认构造函数: 如果类包含非静态成员对象,且这些成员的类型没有可访问的默认构造函数,则该类也不会隐式生成默认构造函数。
- 类中包含引用类型成员: 引用成员必须在构造函数中通过成员初始化列表初始化。
- 类中包含
const成员:const成员必须在构造函数中通过成员初始化列表初始化。
在这些情况下,如果尝试无参数创建对象,将导致编译错误。
1.5 = default 和 = delete 语法
C++11引入了 = default 和 = delete,以更精细地控制特殊成员函数的生成。
= default: 用于请求编译器生成默认的实现。即使存在其他用户定义的构造函数,你也可以强制编译器生成默认构造函数。这对于保持类是POD类型或希望编译器提供默认行为非常有用。= delete: 用于明确禁止编译器生成或使用某个特殊成员函数。这适用于那些设计上不应被默认构造的类,例如,所有对象都必须通过特定参数初始化。
示例1.5.1:= default 和 = delete 应用于默认构造函数
#include <iostream>
#include <string>
class MyClassControlDefaultCtor {
public:
int value;
// 用户定义了带参数的构造函数
MyClassControlDefaultCtor(int v) : value(v) {
std::cout << "MyClassControlDefaultCtor(int) called. value=" << value << std::endl;
}
// 此时编译器不会隐式生成默认构造函数。
// 如果我们想强制它生成,可以使用 = default
MyClassControlDefaultCtor() = default; // 强制生成默认构造函数
// 另一个类,禁止其默认构造函数
class NoDefaultConstruction {
int id;
public:
NoDefaultConstruction(int i) : id(i) {
std::cout << "NoDefaultConstruction(int) called. id=" << id << std::endl;
}
NoDefaultConstruction() = delete; // 明确禁止默认构造
};
};
int main() {
std::cout << "--- MyClassControlDefaultCtor ---" << std::endl;
MyClassControlDefaultCtor obj1; // OK: 调用 '= default' 的默认构造函数
std::cout << "obj1.value (after default construction): " << obj1.value << std::endl; // 可能是垃圾值
MyClassControlDefaultCtor obj2(100); // OK: 调用带参数的构造函数
std::cout << "n--- NoDefaultConstruction ---" << std::endl;
// MyClassControlDefaultCtor::NoDefaultConstruction obj3; // 编译错误: 默认构造函数被删除了
MyClassControlDefaultCtor::NoDefaultConstruction obj4(200); // OK: 调用带参数的构造函数
return 0;
}
输出分析:
obj1成功调用了= default的默认构造函数。由于是= default,其行为与隐式生成相同,内置类型value未被初始化。obj2正常调用了带参数的构造函数。- 尝试默认构造
NoDefaultConstruction类型的obj3会导致编译错误,因为其默认构造函数被明确删除了。
1.6 默认构造函数的执行时机
默认构造函数在以下几种情况中执行:
-
无参数创建对象:
- 栈对象:
ClassName obj;或ClassName obj{};(C++11起,obj{}执行值初始化,会调用默认构造函数或零初始化) - 堆对象:
new ClassName();(执行值初始化,会调用默认构造函数) 或new ClassName;(如果用户提供了默认构造函数则调用,否则内置类型不初始化)。
- 栈对象:
-
作为数组元素被创建:
ClassName arr[10];(数组中的每个元素都会调用默认构造函数)。 -
作为另一个类的数据成员,且宿主类被默认构造时: 如果成员是一个对象,其默认构造函数会被调用。
-
作为基类子对象,且派生类被默认构造时。
-
std::vector等标准库容器在resize或emplace_back时,如果无参数填充: 例如std::vector<MyClass>::resize(N);会对新增的N个元素调用默认构造函数。std::vector<MyClass>::emplace_back();也会调用默认构造函数。
示例1.6.1:默认构造函数执行时机综合示例
#include <iostream>
#include <string>
#include <vector>
class MyClassDefaultCtorTiming {
public:
int id;
MyClassDefaultCtorTiming(int i = 0) : id(i) { // 显式默认构造函数
std::cout << "MyClassDefaultCtorTiming constructor called. ID=" << id << std::endl;
}
~MyClassDefaultCtorTiming() {
std::cout << "MyClassDefaultCtorTiming destructor called. ID=" << id << std::endl;
}
};
// 包含MyClassDefaultCtorTiming成员的类
class ContainerClass {
public:
MyClassDefaultCtorTiming member1; // 成员对象,其默认构造函数会被调用
MyClassDefaultCtorTiming member2;
ContainerClass() : member1(10), member2(20) { // 显式初始化成员,否则会调用默认构造
std::cout << "ContainerClass default constructor called." << std::endl;
}
~ContainerClass() {
std::cout << "ContainerClass destructor called." << std::endl;
}
};
int main() {
std::cout << "--- Default Constructor Execution Timings ---" << std::endl;
// 1. 无参数创建栈对象
std::cout << "nScenario 1: Stack object (obj1)" << std::endl;
MyClassDefaultCtorTiming obj1; // id=0
// 2. 动态分配内存并无参数构造
std::cout << "nScenario 2: Dynamic object (ptr_obj)" << std::endl;
MyClassDefaultCtorTiming* ptr_obj = new MyClassDefaultCtorTiming(30); // 参数构造
delete ptr_obj; // 销毁
// 3. 作为数组元素被创建
std::cout << "nScenario 3: Array elements (arr)" << std::endl;
MyClassDefaultCtorTiming arr[2]; // arr[0] 和 arr[1] 都会调用默认构造函数 (id=0)
// 4. 作为另一个类的数据成员
std::cout << "nScenario 4: Member object (container)" << std::endl;
ContainerClass container; // container的成员会被构造
// 5. std::vector 容器填充
std::cout << "nScenario 5: std::vector operations" << std::endl;
std::vector<MyClassDefaultCtorTiming> vec;
vec.reserve(3); // 预留空间,避免不必要的拷贝/移动
std::cout << " vec.emplace_back():" << std::endl;
vec.emplace_back(); // 调用默认构造函数 (id=0)
std::cout << " vec.resize(2):" << std::endl;
vec.resize(2); // 如果size < 2,会添加新元素,调用默认构造函数 (id=0)
std::cout << "n--- End of main ---" << std::endl;
return 0; // 所有栈对象和数组元素在此处销毁
}
输出分析: 每个场景都明确展示了 MyClassDefaultCtorTiming 及其相关类的默认构造函数的调用时机。特别是 arr 数组的创建和 vec 的 emplace_back 及 resize 操作,都导致了默认构造函数的调用。
第二章:复制的艺术——拷贝构造函数 (The Art of Duplication: Copy Constructor)
拷贝构造函数是C++中用于创建对象副本的特殊成员函数。它在对象通过另一个同类型对象进行初始化时被调用,是实现对象值语义的关键。
2.1 什么是拷贝构造函数?
拷贝构造函数是一个构造函数,它接受一个同类型对象的 const 引用作为参数,并使用该对象来初始化新创建的对象。
典型签名:
ClassName(const ClassName& other);
这里的 const 确保了源对象在拷贝过程中不会被修改,而引用 & 避免了在传递参数时进行不必要的拷贝。
2.2 拷贝构造函数的隐式生成与行为
C++编译器会在特定条件下为类隐式生成一个公共的、内联的拷贝构造函数。这个隐式生成的拷贝构造函数执行的是成员逐一拷贝(member-wise copy):
- 对于基类子对象: 调用基类的拷贝构造函数。
- 对于非静态成员对象: 调用成员对象的拷贝构造函数。
- 对于内置类型(如
int)和POD类型成员: 直接按位复制其值。
隐式生成条件:
编译器只会在类中没有声明任何拷贝构造函数时,才会隐式生成一个。然而,如果类声明了移动构造函数或移动赋值运算符,但没有声明拷贝构造函数、拷贝赋值运算符或析构函数,则隐式生成的拷贝构造函数将被隐式删除(= delete)。
示例2.2.1:隐式生成的拷贝构造函数
#include <iostream>
#include <string>
class MyClassImplicitCopy {
public:
int id;
std::string name;
MyClassImplicitCopy(int i = 0, const std::string& n = "Default") : id(i), name(n) {
std::cout << "MyClassImplicitCopy constructor called. ID=" << id << ", Name='" << name << "'" << std::endl;
}
// 没有声明拷贝构造函数,编译器会隐式生成一个
~MyClassImplicitCopy() {
std::cout << "MyClassImplicitCopy destructor called. ID=" << id << std::endl;
}
};
int main() {
std::cout << "--- MyClassImplicitCopy ---" << std::endl;
MyClassImplicitCopy obj1(10, "Original"); // 普通构造
// 拷贝初始化:调用隐式生成的拷贝构造函数
MyClassImplicitCopy obj2 = obj1;
std::cout << "obj2.id: " << obj2.id << ", obj2.name: '" << obj2.name << "'" << std::endl;
// 直接初始化:调用隐式生成的拷贝构造函数
MyClassImplicitCopy obj3(obj1);
std::cout << "obj3.id: " << obj3.id << ", obj3.name: '" << obj3.name << "'" << std::endl;
// 列表初始化 (C++11起):如果类没有用户定义的构造函数,会尝试聚合初始化;
// 如果有用户定义的构造函数,则会尝试匹配构造函数,这里会匹配拷贝构造
MyClassImplicitCopy obj4{obj1};
std::cout << "obj4.id: " << obj4.id << ", obj4.name: '" << obj4.name << "'" << std::endl;
return 0;
}
输出分析: obj2, obj3, obj4 的创建都触发了隐式生成的拷贝构造函数。id 成员按值拷贝,std::string 成员 name 会调用其自身的拷贝构造函数进行深拷贝。
2.3 浅拷贝与深拷贝问题:何时需要显式定义拷贝构造函数
隐式生成的拷贝构造函数执行的是成员逐一拷贝。对于不包含动态分配资源的类或仅包含具有良好拷贝语义(即它们自己执行深拷贝)的成员的类,这种行为是安全的。
然而,如果类管理着动态分配的资源(如堆内存、文件句柄、网络连接等),简单的成员逐一拷贝会导致浅拷贝(Shallow Copy)问题。
浅拷贝的危害:
- 资源共享: 多个对象会持有指向同一块资源的指针。
- 双重释放: 当对象销毁时,所有共享该资源的对象都会尝试释放同一块内存,导致运行时错误。
- 悬挂指针: 一个对象释放资源后,其他对象持有的指针变为无效。
- 数据损坏: 一个对象通过其指针修改资源,会影响所有共享该资源的其他对象。
为避免这些问题,对于管理动态资源的类,必须显式定义一个执行深拷贝(Deep Copy)的拷贝构造函数。深拷贝意味着为新对象分配独立的资源,并将源对象资源的内容复制到新分配的资源中。
示例2.3.1:浅拷贝问题与深拷贝解决方案
#include <iostream>
#include <cstring> // For strlen, strcpy
// ----------------------------------------------------
// 浅拷贝问题示例:不定义拷贝构造函数,让编译器生成默认的成员逐一拷贝
class ShallowCopyProblem {
public:
char* data;
int size;
ShallowCopyProblem(const char* str) {
size = std::strlen(str) + 1;
data = new char[size];
std::strcpy(data, str);
std::cout << "ShallowCopyProblem constructor. Data: '" << data << "' (addr: " << (void*)data << ")" << std::endl;
}
// 隐式生成的拷贝构造函数将只拷贝 'data' 指针的值,而不是其指向的内容
// ShallowCopyProblem(const ShallowCopyProblem& other) { data = other.data; size = other.size; }
~ShallowCopyProblem() {
if (data) {
std::cout << "ShallowCopyProblem destructor. Deleting data: '" << data << "' (addr: " << (void*)data << ")" << std::endl;
delete[] data;
data = nullptr; // 防止悬挂指针
} else {
std::cout << "ShallowCopyProblem destructor. No data to delete (already freed or nullptr)." << std::endl;
}
}
void print() const {
std::cout << "Object data: '" << (data ? data : "nullptr") << "'" << std::endl;
}
};
// ----------------------------------------------------
// 深拷贝解决方案示例:显式定义拷贝构造函数
class DeepCopySolution {
public:
char* data;
int size;
DeepCopySolution(const char* str) {
size = std::strlen(str) + 1;
data = new char[size];
std::strcpy(data, str);
std::cout << "DeepCopySolution constructor. Data: '" << data << "' (addr: " << (void*)data << ")" << std::endl;
}
// 显式定义深拷贝构造函数
DeepCopySolution(const DeepCopySolution& other) : size(other.size) {
data = new char[size]; // 分配新的内存
std::strcpy(data, other.data); // 复制内容
std::cout << "DeepCopySolution COPY constructor. Data: '" << data << "' (addr: " << (void*)data << ") from '" << other.data << "'" << std::endl;
}
// 显式定义拷贝赋值运算符(通常与拷贝构造函数一同出现,Rule of Three/Five)
DeepCopySolution& operator=(const DeepCopySolution& other) {
if (this != &other) { // 防止自我赋值
delete[] data; // 释放原有资源
size = other.size;
data = new char[size]; // 分配新资源
std::strcpy(data, other.data); // 复制内容
std::cout << "DeepCopySolution COPY ASSIGNMENT. Data: '" << data << "' (addr: " << (void*)data << ") from '" << other.data << "'" << std::endl;
}
return *this;
}
~DeepCopySolution() {
if (data) {
std::cout << "DeepCopySolution destructor. Deleting data: '" << data << "' (addr: " << (void*)data << ")" << std::endl;
delete[] data;
data = nullptr;
} else {
std::cout << "DeepCopySolution destructor. No data to delete (already freed or nullptr)." << std::endl;
}
}
void print() const {
std::cout << "Object data: '" << (data ? data : "nullptr") << "'" << std::endl;
}
};
void testShallow() {
std::cout << "n--- Testing ShallowCopyProblem ---" << std::endl;
ShallowCopyProblem s1("Hello");
ShallowCopyProblem s2 = s1; // 隐式拷贝,s1.data 和 s2.data 指向同一块内存
std::cout << "s1 and s2 after copy:" << std::endl;
s1.print();
s2.print();
// 修改 s2 会影响 s1
if (s2.data) s2.data[0] = 'J';
std::cout << "s1 and s2 after modifying s2:" << std::endl;
s1.print();
s2.print();
// 退出作用域时,s2 先析构,释放了data。s1 再析构,尝试释放已释放的data,导致崩溃。
std::cout << "Exiting testShallow scope. Expecting double free or crash." << std::endl;
}
void testDeep() {
std::cout << "n--- Testing DeepCopySolution ---" << std::endl;
DeepCopySolution d1("World");
DeepCopySolution d2 = d1; // 显式深拷贝,d1.data 和 d2.data 指向不同内存
std::cout << "d1 and d2 after copy:" << std::endl;
d1.print();
d2.print();
// 修改 d2 不会影响 d1
if (d2.data) d2.data[0] = 'K