【C++类和数据抽象】复制构造函数

目录

一、对象复制的本质需求

[1.1 为什么需要对象复制?](#1.1 为什么需要对象复制?)

[1.2 默认复制行为的问题](#1.2 默认复制行为的问题)

二、复制构造函数的基本概念

[2.1 定义与作用](#2.1 定义与作用)

[2.2 与拷贝赋值运算符的区别](#2.2 与拷贝赋值运算符的区别)

三、复制构造函数的触发场景

[3.1 对象初始化](#3.1 对象初始化)

[3.2 函数参数传递](#3.2 函数参数传递)

[3.3 函数返回对象](#3.3 函数返回对象)

[3.4 容器元素操作](#3.4 容器元素操作)

四、默认复制构造函数:浅拷贝的风险

[4.1 编译器自动生成的默认行为](#4.1 编译器自动生成的默认行为)

[4.2 浅拷贝引发的典型问题](#4.2 浅拷贝引发的典型问题)

五、自定义复制构造函数:实现深拷贝

[5.1 深拷贝的实现原则](#5.1 深拷贝的实现原则)

[5.2 深拷贝代码示例](#5.2 深拷贝代码示例)

六、复制构造函数的高级特性

[6.1 防止对象复制:删除复制构造函数](#6.1 防止对象复制:删除复制构造函数)

[6.2 移动语义与复制构造函数](#6.2 移动语义与复制构造函数)

[6.3 复制构造函数与 const 关键字](#6.3 复制构造函数与 const 关键字)

七、常见陷阱与最佳实践

[7.1 陷阱一:遗漏复制构造函数导致资源泄漏](#7.1 陷阱一:遗漏复制构造函数导致资源泄漏)

[7.2 陷阱二:复制构造函数中未正确初始化成员](#7.2 陷阱二:复制构造函数中未正确初始化成员)

[7.3 最佳实践:遵循 Rule of Three/Five](#7.3 最佳实践:遵循 Rule of Three/Five)

八、总结

九、参考资料


在 C++ 的面向对象编程中,对象的复制是一个核心操作。复制构造函数作为类的特殊成员函数,负责从一个已存在的对象创建新对象,确保资源管理的正确性和数据的一致性。

一、对象复制的本质需求

1.1 为什么需要对象复制?

在C++程序设计中,对象复制是面向对象编程的核心操作之一。典型应用场景包括:

  • 函数参数的值传递

  • 返回局部对象

  • 容器元素操作

  • 对象初始化

cpp 复制代码
void processObject(MyClass obj);  // 值传递触发拷贝
MyClass createObject();          // 返回对象触发拷贝

int main() {
    MyClass a;
    MyClass b = a;              // 初始化拷贝
    vector<MyClass> vec;
    vec.push_back(a);           // 容器存储拷贝
}

1.2 默认复制行为的问题

编译器生成的默认拷贝构造函数执行浅拷贝(Shallow Copy),对于包含指针成员的类会引发严重问题:

cpp 复制代码
class ShallowArray {
    int* data;
    size_t size;
public:
    ShallowArray(size_t n) : size(n), data(new int[n]) {}
    ~ShallowArray() { delete[] data; }
};

int main() {
    ShallowArray arr1(5);
    ShallowArray arr2 = arr1;  // 双重释放崩溃!
}

二、复制构造函数的基本概念

2.1 定义与作用

复制构造函数 是一种特殊的构造函数,其函数名与类名相同,没有返回类型,参数为当前类的常量引用const ClassName&)。它的作用是:

  • 从已存在的对象创建新对象,实现对象的深拷贝(Deep Copy)
  • 确保在对象复制过程中,资源(如动态内存、文件句柄等)被正确复制,避免浅拷贝(Shallow Copy)导致的悬挂指针或资源泄漏。

语法格式

cpp 复制代码
class ClassName {
public:
    ClassName(const ClassName& obj); // 复制构造函数声明
};

// 定义示例
ClassName::ClassName(const ClassName& obj) {
    // 实现对象复制逻辑
}

2.2 与拷贝赋值运算符的区别

特性 复制构造函数 拷贝赋值运算符(operator=)
触发时机 对象创建时(初始化阶段) 对象已存在时(赋值操作阶段)
参数 const ClassName& const ClassName&
返回值 无(构造函数无返回值) ClassName&(当前对象引用)
核心功能 从无到有创建对象的复制版本 从已有对象向当前对象赋值

示例对比

cpp 复制代码
ClassName obj1;          // 调用默认构造函数
ClassName obj2(obj1);    // 调用复制构造函数(对象创建)
obj2 = obj1;             // 调用拷贝赋值运算符(对象已存在)

三、复制构造函数的触发场景

3.1 对象初始化

当使用一个已存在的对象初始化新对象时,复制构造函数被触发:

cpp 复制代码
#include <iostream> // 包含输入输出流头文件
using namespace std; // 使用标准命名空间

class Point {
public:
    int x, y;
    Point(int a, int b) : x(a), y(b) {}
    Point(const Point& p) : x(p.x), y(p.y) { // 复制构造函数
        cout << "Copy constructor called" << endl;
    }
};

int main() {
    Point p1(1, 2);
    Point p2 = p1; // 等价于Point p2(p1),触发复制构造函数
    return 0;
}

3.2 函数参数传递

当对象以值传递方式传入函数时,会触发复制构造函数(形参是实参的副本):

cpp 复制代码
#include <iostream> 
using namespace std;

// 定义Point类
class Point {
public:
    int x, y; // 公有数据成员
    // 构造函数
    Point(int a, int b) : x(a), y(b) {}
    // 复制构造函数
    Point(const Point& p) : x(p.x), y(p.y) {
        cout << "Copy constructor called: (" << x << "," << y << ")" << endl;
    }
};

// 值传递函数,触发复制构造函数
void printPoint(Point p) {
    cout << "Point value: " << p.x << "," << p.y << endl;
}

int main() {
    Point p(3, 4); // 创建Point对象p
    printPoint(p); // 调用printPoint,触发复制构造函数
    return 0;
}

3.3 函数返回对象

当函数返回一个对象时,会通过复制构造函数创建临时对象作为返回值:

cpp 复制代码
#include <iostream>
using namespace std;

class Point {
public:
    int x, y;
    // 构造函数
    Point(int a = 0, int b = 0) : x(a), y(b) {
        cout << "Constructor called: (" << x << "," << y << ")" << endl;
    }
    // 复制构造函数
    Point(const Point& p) : x(p.x), y(p.y) {
        cout << "Copy constructor called: (" << x << "," << y << ")" << endl;
    }
    // 移动构造函数(C++11,用于验证RVO与移动语义)
    Point(Point&& p) noexcept : x(p.x), y(p.y) {
        cout << "Move constructor called: (" << x << "," << y << ")" << endl;
        p.x = p.y = 0; // 标记原对象已移动
    }
};

Point getPoint() {
    Point temp(5, 6); // 调用构造函数创建temp
    cout << "Returning temp from getPoint" << endl;
    return temp; // 返回对象,触发复制/移动构造或RVO
}

int main() {
    cout << "Creating point p..." << endl;
    Point p = getPoint(); // 接收返回值,观察构造函数调用
    cout << "p.x = " << p.x << ", p.y = " << p.y << endl;
    return 0;
}

3.4 容器元素操作

在容器(如vectorlist)中插入对象时,可能触发复制构造函数:

cpp 复制代码
#include <iostream>
#include <vector> // 引入vector容器头文件
using namespace std;

// 定义Point类
class Point {
public:
    int x, y;
    // 构造函数
    Point(int a = 0, int b = 0) : x(a), y(b) {
        cout << "Constructor called: (" << x << "," << y << ")" << endl;
    }
    // 复制构造函数(关键:确保容器复制元素时正确调用)
    Point(const Point& p) : x(p.x), y(p.y) {
        cout << "Copy constructor called: (" << x << "," << y << ")" << endl;
    }
};

int main() {
    vector<Point> vec; // 创建空vector
    Point p(1, 2); // 调用构造函数创建对象p
    cout << "Pushing p into vector..." << endl;
    vec.push_back(p); // 向vector添加元素,触发复制构造函数
    cout << "Vector size: " << vec.size() << endl;
    return 0;
}

四、默认复制构造函数:浅拷贝的风险

4.1 编译器自动生成的默认行为

如果类中未显式定义复制构造函数,编译器会自动生成一个默认复制构造函数 ,执行浅拷贝(逐字节复制):

  • 对于基本数据类型(如intdouble),浅拷贝是安全的。
  • 对于指针成员,浅拷贝会导致多个对象指向同一内存地址,引发悬挂指针双重释放问题。

4.2 浅拷贝引发的典型问题

场景 :类中包含动态分配的内存(如int* data),使用默认复制构造函数:

cpp 复制代码
class BadExample {
public:
    int* data;
    BadExample(int size) {
        data = new int[size]; // 分配内存
    }
    // 未定义复制构造函数,使用默认浅拷贝
};

int main() {
    BadExample obj1(5);
    BadExample obj2 = obj1; // 默认复制构造函数,浅拷贝data指针
    obj1.data[0] = 10;      // obj2.data[0]也会变为10(共享内存)
    delete[] obj1.data;     // 释放内存后,obj2.data成为悬挂指针
    delete[] obj2.data;     // 二次释放,程序崩溃
    return 0;
}

问题根源浅拷贝导致多个对象的指针成员指向同一内存块,释放后引发未定义行为。

五、自定义复制构造函数:实现深拷贝

5.1 深拷贝的实现原则

当类中包含资源管理成员 (如指针、文件句柄等)时,必须显式定义复制构造函数,实现深拷贝

  • 为新对象分配独立的资源。
  • 将原对象的资源内容复制到新资源中。
  • 确保原对象与新对象的资源相互独立,互不影响。

5.2 深拷贝代码示例

cpp 复制代码
#include <iostream>       
#include <algorithm>      // 包含copy算法
using namespace std;      

class GoodExample {
public:
    int* data;
    int size;
    GoodExample(int s) : size(s) {
        data = new int[size]; // 分配动态内存
        fill(data, data + size, 0); // 使用fill初始化数组为0(替代手动循环)
    }
    // 自定义复制构造函数(深拷贝)
    GoodExample(const GoodExample& obj) : size(obj.size) {
        data = new int[size]; // 为新对象分配独立内存
        copy(obj.data, obj.data + size, data); // 复制数据
        cout << "Deep copy constructor called" << endl;
    }
    ~GoodExample() {
        delete[] data; // 释放动态内存
    }
};

int main() {
    GoodExample obj1(3); // 创建包含3个元素的对象
    obj1.data[0] = 1;    // 修改obj1的第一个元素为1
    GoodExample obj2 = obj1; // 调用深拷贝构造函数,创建obj2
    obj1.data[0] = 100;  // 修改obj1的第一个元素为100
    // 输出obj2的第一个元素(深拷贝后应保持原值0,因fill初始化时设为0,且未被obj2修改)
    cout << "obj2.data[0] = " << obj2.data[0] << endl; 
    return 0;
}

六、复制构造函数的高级特性

6.1 防止对象复制:删除复制构造函数

通过将复制构造函数声明为delete,可以禁止对象的复制(C++11 及以后):

cpp 复制代码
class NonCopyable {
public:
    NonCopyable() = default;
    NonCopyable(const NonCopyable&) = delete; // 禁止复制构造
    NonCopyable& operator=(const NonCopyable&) = delete; // 禁止拷贝赋值
};

int main() {
    NonCopyable obj1;
    NonCopyable obj2 = obj1; // 编译错误:复制构造函数被删除
    return 0;
}

6.2 移动语义与复制构造函数

在 C++11 中,移动构造函数(Move Constructor)与复制构造函数共同构成对象的复制 / 移动语义:

  • 复制构造函数 :处理左值引用(已存在的对象),执行深拷贝。
  • 移动构造函数 :处理右值引用(临时对象),转移资源所有权,避免深拷贝的开销。

示例:移动构造函数优化性能

cpp 复制代码
#include <iostream>

// 定义 Resource 类
class Resource {
public:
    int* data;
    // 构造函数,分配指定大小的内存
    Resource(int size) : data(new int[size]) {
        std::cout << "Constructor: " << data << std::endl;
    }
    // 复制构造函数(深拷贝)
    Resource(const Resource& rhs) : data(new int[rhs.data[0]]) {
        std::cout << "Copy Constructor: " << data << " from " << rhs.data << std::endl;
    }
    // 移动构造函数(资源转移)
    Resource(Resource&& rhs) noexcept : data(rhs.data) {
        rhs.data = nullptr; // 原对象置空,避免双重释放
        std::cout << "Move Constructor: " << data << " from " << rhs.data << std::endl;
    }
    // 析构函数,释放动态分配的内存
    ~Resource() {
        delete[] data;
        std::cout << "Destructor: " << data << std::endl;
    }
};

// 返回一个 Resource 对象
Resource getResource() {
    return Resource(10); // 返回右值,触发移动构造函数
}

int main() {
    Resource obj = getResource(); // 优先调用移动构造函数
    return 0;
}    

6.3 复制构造函数与 const 关键字

复制构造函数的参数必须为常量引用const ClassName&),原因如下:

  • 允许接收临时对象 :临时对象是右值,只能绑定到const引用。
  • 避免递归调用 :若参数为非const引用,复制构造函数内部创建临时对象时会再次触发复制构造,导致无限递归。
cpp 复制代码
ClassName::ClassName(ClassName& obj) { // 错误:非const引用无法接收临时对象
    // 编译错误:无法从const ClassName转换为ClassName&
}

七、常见陷阱与最佳实践

7.1 陷阱一:遗漏复制构造函数导致资源泄漏

场景:类中包含动态资源(如文件句柄、网络连接),未定义复制构造函数:

cpp 复制代码
class FileHandler {
public:
    FILE* file;
    FileHandler(const char* path) {
        file = fopen(path, "r"); // 打开文件
    }
    ~FileHandler() {
        fclose(file); // 关闭文件
    }
};

int main() {
    FileHandler f1("data.txt");
    FileHandler f2 = f1; // 默认浅拷贝,f1和f2的file指针相同
    // f1和f2析构时会两次关闭同一文件,导致程序崩溃
    return 0;
}

解决方案:定义复制构造函数,实现资源的深拷贝或禁止复制。

7.2 陷阱二:复制构造函数中未正确初始化成员

错误示例

cpp 复制代码
class Vector {
public:
    int* elements;
    int length;
    Vector(int len) : length(len) {
        elements = new int[len];
    }
    Vector(const Vector& vec) { // 错误:未初始化length
        copy(vec.elements, vec.elements + vec.length, elements);
    }
};

正确实现

cpp 复制代码
Vector(const Vector& vec) : length(vec.length) { // 先初始化length
    elements = new int[length];
    copy(vec.elements, vec.elements + length, elements);
}

7.3 最佳实践:遵循 Rule of Three/Five

  • Rule of Three(C++98):若类需要自定义析构函数、复制构造函数或拷贝赋值运算符中的任意一个,通常需要同时定义这三个函数。
  • Rule of Five(C++11):新增移动构造函数和移动赋值运算符,若需要其中一个,通常需定义五个函数(析构 + 复制 + 移动)。

示例:完整资源管理类

cpp 复制代码
#include <iostream>
#include <algorithm>

class ResourceManager {
public:
    // 构造函数
    ResourceManager(int size) : data(new int[size]), length(size) {
        std::cout << "Constructor called with size: " << length << std::endl;
    }

    // 析构函数
    ~ResourceManager() {
        std::cout << "Destructor called for size: " << length << std::endl;
        delete[] data;
    }

    // 复制构造函数
    ResourceManager(const ResourceManager& rhs) : 
        data(new int[rhs.length]), length(rhs.length) {
        std::cout << "Copy constructor called for size: " << length << std::endl;
        std::copy(rhs.data, rhs.data + length, data);
    }

    // 拷贝赋值运算符
    ResourceManager& operator=(const ResourceManager& rhs) {
        std::cout << "Copy assignment operator called for size: " << length << std::endl;
        if (this != &rhs) {
            delete[] data;
            data = new int[rhs.length];
            length = rhs.length;
            std::copy(rhs.data, rhs.data + length, data);
        }
        return *this;
    }

    // 移动构造函数
    ResourceManager(ResourceManager&& rhs) noexcept :
        data(rhs.data), length(rhs.length) {
        std::cout << "Move constructor called for size: " << length << std::endl;
        rhs.data = nullptr;
        rhs.length = 0;
    }

    // 移动赋值运算符
    ResourceManager& operator=(ResourceManager&& rhs) noexcept {
        std::cout << "Move assignment operator called for size: " << length << std::endl;
        if (this != &rhs) {
            delete[] data;
            data = rhs.data;
            length = rhs.length;
            rhs.data = nullptr;
            rhs.length = 0;
        }
        return *this;
    }

private:
    int* data;
    int length;
};

int main() {
    // 测试构造函数
    ResourceManager rm1(5);

    // 测试复制构造函数
    ResourceManager rm2(rm1);

    // 测试拷贝赋值运算符
    ResourceManager rm3(3);
    rm3 = rm1;

    // 测试移动构造函数
    ResourceManager rm4(std::move(ResourceManager(7)));

    // 测试移动赋值运算符
    ResourceManager rm5(2);
    rm5 = std::move(ResourceManager(9));

    return 0;
}    

八、总结

复制构造函数是 C++ 中对象复制的核心机制,其设计直接影响程序的正确性和性能。关键点总结:

  • 何时需要自定义:当类中包含资源管理成员(如指针、文件句柄)时,必须定义复制构造函数实现深拷贝。
  • 避免浅拷贝陷阱:默认复制构造函数的浅拷贝会导致资源共享,引发悬挂指针和双重释放。
  • 结合现代 C++ 特性 :利用移动语义(std::move)和const引用提升性能与安全性。
  • 遵循资源管理规则:严格遵守 Rule of Three/Five,确保析构函数、复制 / 移动函数的一致性。

九、参考资料

  • **《C++ Primer(第 5 版)》**这本书是 C++ 领域的经典之作,对 C++ 的基础语法和高级特性都有深入讲解。

  • **《Effective C++(第 3 版)》**书中包含了很多 C++ 编程的实用建议和最佳实践。

  • 《C++ Templates: The Complete Guide(第 2 版)》 该书聚焦于 C++ 模板编程,而using声明在模板编程中有着重要应用,如定义模板类型别名等。

  • C++ 官方标准文档:C++ 标准文档是最权威的参考资料,可以查阅最新的 C++ 标准(如 C++11、C++14、C++17、C++20 等)文档。例如,ISO/IEC 14882:2020 是 C++20 标准的文档,可从相关渠道获取其详细内容。

  • :这是一个非常全面的 C++ 在线参考网站,提供了详细的 C++ 语言和标准库文档。

  • :该网站提供了系统的 C++ 教程,配有丰富的示例代码和清晰的解释,适合初学者学习和理解相关知识。

  • 《C++标准库(第2版)》Nicolai M. Josuttis 著

  • Effective STL Scott Meyers 著

  • C++ Core Guidelines:C++ Core Guidelines

  • C++ Reference:https://en.cppreference.com/w/


相关推荐
jerry6097 分钟前
c++流对象
开发语言·c++·算法
fmdpenny7 分钟前
用python写一个相机选型的简易程序
开发语言·python·数码相机
虾球xz12 分钟前
游戏引擎学习第247天:简化DEBUG_VALUE
c++·学习·游戏引擎
海盗强1 小时前
Babel、core-js、Loader之间的关系和作用全解析
开发语言·前端·javascript
猿榜编程1 小时前
python基础-requests结合AI实现自动化数据抓取
开发语言·python·自动化
我最厉害。,。1 小时前
PHP 反序列化&原生类 TIPS&字符串逃逸&CVE 绕过漏洞&属性类型特征
android·开发语言·php
爱编程的鱼1 小时前
C# 类(Class)教程
开发语言·c#
2301_817031651 小时前
C语言-- 深入理解指针(4)
c语言·开发语言·算法
superior tigre1 小时前
C++学习:六个月从基础到就业——模板编程:模板特化
开发语言·c++·学习
码农新猿类1 小时前
信号量函数
linux·c++·visual studio