Skip to content

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类:

class Person {
public:
    // 拷贝构造函数
    Person(const Person& person);
private:
    char* name;
};
  • 调用时机

    • 对象作为函数参数值传递时:当你将一个对象按值传递给函数,而不是通过指针或引用,就会触发拷贝构造函数,为函数内部创建该对象的一个副本。
    void roster(Person player);  // 值传递触发拷贝构造
    Person child("Ruby");
    roster(child);  // 调用拷贝构造函数
    
    • 对象作为函数返回值时:如果一个函数返回一个对象而不是引用或指针,则返回时会创建该对象的一个临时副本,这同样会调用拷贝构造函数。然而,现代编译器可能会优化掉这一过程(例如,NRVO —— Named Return Value Optimization)。
    Person captain() {
        Person player("George");
        return player;  // 可能调用拷贝构造(NRVO可能优化)
    }
    
    • 显式初始化对象时(如 Person baby_b = baby_a):直接使用已有的对象来初始化新的对象实例也会触发拷贝构造函数。
    Person baby_a("Fred");
    Person baby_b = baby_a;   // 调用拷贝构造(非赋值!)
    Person baby_c(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. 错误示范(浅拷贝)

// 编译器生成的默认拷贝构造函数等价于:
Person::Person(const Person& w) {
    name = w.name;  // 仅复制指针地址!
}

三、拷贝构造函数的调用场景

1. 函数参数传递

void roster(Person player);  // 值传递触发拷贝构造
Person child("Ruby");
roster(child);  // 调用拷贝构造函数

2. 对象初始化

Person baby_a("Fred");
Person baby_b = baby_a;   // 调用拷贝构造(非赋值!)
Person baby_c(baby_a);    // 显式调用拷贝构造

3. 函数返回值(可能被优化)

Person captain() {
    Person player("George");
    return player;  // 可能调用拷贝构造(NRVO可能优化)
}

四、特殊场景处理

1. 禁用拷贝构造

class NoCopy {
private:
    NoCopy(const NoCopy&);  // 声明为private且不实现
};

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指令

六、最佳实践指南

  1. 三法则:如果需要定义析构函数,通常也需要拷贝构造函数和拷贝赋值运算符
  2. 移动语义:C++11后考虑添加移动构造函数(T::T(T&&)
  3. 成员类型处理

    • 基本类型:直接拷贝
    • 对象成员:调用该成员的拷贝构造函数
    • 指针成员:必须深拷贝

七、常见问题解答

Q: 为什么拷贝构造函数的参数必须是const引用?
A: 避免无限递归调用(值传递会再次调用拷贝构造),const保证不修改原对象

Q: 何时需要自定义拷贝构造函数?
A: 当类包含指针成员或需要管理其他资源(如文件句柄)时

Q: 如何避免对象被拷贝?
A: C++11可用= delete标记,传统方法是将拷贝构造声明为private