Lecture 4: Copy Constructor
一、拷贝构造函数基础概念
1. 拷贝构造函数的定义
拷贝构造函数是一个特殊的构造函数,它用于使用同一类的另一个对象来初始化新创建的对象。这种机制在C++中对于管理资源(如动态内存分配)特别重要。
-
作用:用已存在的对象初始化新对象。这意味着当一个对象被用作另一个对象的初始化时,会调用该对象的拷贝构造函数来完成新对象的初始化工作。这不仅涉及到成员变量值的复制,还可能涉及到深层复制以确保数据的独立性。
-
签名
T::T(const T&)
详解:-
T::
:这部分表示函数属于类T
。在C++中,成员函数(包括构造函数)通过在其名称前加上类名和作用域解析运算符::
来定义它们所属的类。 -
第二个
T
:这是指类的名字,即你正在为其定义拷贝构造函数的那个类。例如,如果你有一个名为Person
的类,那么拷贝构造函数的形式将是Person::Person(const Person&)
。 -
(const T&)
:这是拷贝构造函数的参数列表。它由三部分组成:-
const
:关键字const
意味着传递给函数的对象不能被该函数修改。这不仅保护了传入对象的数据不被意外更改,还允许编译器对传入的临时对象进行优化,因为编译器知道这些对象不会被改变。 -
T&
:这里的&
符号表示引用。使用引用而非值传递有几个优点:- 避免不必要的对象复制:如果直接传递对象而不是引用,则每次调用函数时都会创建对象的一个副本。对于大型对象或复杂的类,这种复制可能非常耗时和耗费资源。
- 更高效的内存使用:由于是引用,函数可以直接操作原始数据的地址,而不需要额外的空间来存储副本。
-
(const T&)
合并来看:表示这是一个对类型T
的常量引用。这意味着你可以将一个T
类型的对象按引用传递给函数,但不允许函数修改这个对象的内容。这对于拷贝构造函数特别重要,因为它通常只需要读取源对象的数据以初始化新对象,而不需要修改源对象。
-
-
例如,考虑下面的Person
类:
-
调用时机:
- 对象作为函数参数值传递时:当你将一个对象按值传递给函数,而不是通过指针或引用,就会触发拷贝构造函数,为函数内部创建该对象的一个副本。
- 对象作为函数返回值时:如果一个函数返回一个对象而不是引用或指针,则返回时会创建该对象的一个临时副本,这同样会调用拷贝构造函数。然而,现代编译器可能会优化掉这一过程(例如,NRVO —— Named Return Value Optimization)。
- 显式初始化对象时(如
Person baby_b = baby_a
):直接使用已有的对象来初始化新的对象实例也会触发拷贝构造函数。
拷贝构造函数对于正确管理资源至关重要,特别是在处理动态内存分配时,必须小心浅拷贝与深拷贝的区别。
2. 默认拷贝构造函数
- 编译器自动生成的条件:用户未显式定义
-
行为:对每个成员进行浅拷贝
- 基本类型:直接复制值
- 指针类型:复制指针地址(危险!会导致多个对象共享同一内存)
3. 深浅拷贝对比
类型 | 行为描述 | 适用场景 | 风险 |
---|---|---|---|
浅拷贝 | 仅复制指针地址 | 无动态内存管理的对象 | 双重释放内存风险 |
深拷贝 | 复制指针指向的完整数据 | 含动态内存管理的对象 | 无共享数据风险 |
二、关键代码示例分析
1. 含指针成员的类
class Person {
public:
Person(const char* s); // 普通构造函数
Person(const Person& w); // 拷贝构造函数
~Person(); // 析构函数
private:
char* name; // 动态分配的内存
};
2. 深拷贝实现
Person::Person(const Person& w) {
name = new char[strlen(w.name) + 1]; // 分配新内存
strcpy(name, w.name); // 复制内容
}
3. 错误示范(浅拷贝)
三、拷贝构造函数的调用场景
1. 函数参数传递
2. 对象初始化
3. 函数返回值(可能被优化)
四、特殊场景处理
1. 禁用拷贝构造
2. 委托构造函数(C++11)
class Person {
public:
Person() : Person("Anonymous") {} // 委托给另一个构造函数
explicit Person(const char* name);
};
五、静态成员与命名空间
1. 静态成员特性
类型 | 特点 | 示例 |
---|---|---|
静态成员变量 | 类所有实例共享,需单独初始化 | int Class::var = 0; |
静态成员函数 | 无this指针,只能访问静态成员 | static void func(); |
2. 命名空间使用
namespace MyLib {
void foo();
class Cat { void Meow(); };
}
// 使用方式:
MyLib::foo(); // 完全限定
using MyLib::Cat; Cat c; // using声明
using namespace MyLib; foo(); // using指令
六、最佳实践指南
- 三法则:如果需要定义析构函数,通常也需要拷贝构造函数和拷贝赋值运算符
- 移动语义:C++11后考虑添加移动构造函数(
T::T(T&&)
) -
成员类型处理:
- 基本类型:直接拷贝
- 对象成员:调用该成员的拷贝构造函数
- 指针成员:必须深拷贝
七、常见问题解答
Q: 为什么拷贝构造函数的参数必须是const引用?
A: 避免无限递归调用(值传递会再次调用拷贝构造),const保证不修改原对象
Q: 何时需要自定义拷贝构造函数?
A: 当类包含指针成员或需要管理其他资源(如文件句柄)时
Q: 如何避免对象被拷贝?
A: C++11可用= delete
标记,传统方法是将拷贝构造声明为private