乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • C++ 学习路径
  • 第1章 C++概述与开发环境

    • C++ 语言概述与编译模型
    • 第一个 C++ 程序与规范
    • 开发环境配置
    • 预处理指令详解
  • 第2章 基本语法与类型系统

    • 变量与基本类型
    • 类型转换
    • 枚举类型
    • 引用与指针
    • 数组与 stdarray
    • 字符串与原始字符串字面量
    • const 与 constexpr
    • nullptr 与空指针
    • 类型推导 auto 与 decltype
    • 基于范围的 for 循环
    • 值类别全面解析
    • char16_t 与 char32_t
    • static_assert 编译期断言
  • 第3章 函数与重载

    • 函数声明与定义
    • 函数重载
    • 默认参数与内联函数
    • Lambda 表达式
    • 函数对象与 stdfunction
    • 后置返回类型与 noexcept
  • 第4章 类与对象

    • 类的基本定义
    • 构造函数与析构函数
    • 拷贝控制
    • 移动构造函数与移动赋值
    • 列表初始化与类内初始化器
    • 静态成员与嵌套类
    • 友元
    • =default 与 =delete
  • 第5章 继承与多态

    • 继承基础
    • 虚函数与多态
    • 虚函数表与动态绑定原理
    • 虚析构函数
    • 抽象类与纯虚函数
    • 多重继承与虚继承
    • 继承构造函数
  • 第6章 运算符重载

    • 运算符重载基础
    • 算术与关系运算符重载
    • 赋值与移动运算符重载
    • 特殊运算符重载
  • 第7章 模板与泛型编程

    • 函数模板
    • 类模板
    • 模板特化与偏特化
    • 可变参数模板
    • 别名模板与模板模板参数
    • SFINAE 与类型萃取
    • 依赖名与 typename/template 关键字
  • 第8章 异常处理

    • 异常处理机制
    • noexcept 与异常安全
    • RAII 与异常安全实践
  • 第9章 内存管理与智能指针

    • 动态内存与内存分区
    • RAII 惯用法
    • unique_ptr
    • shared_ptr 与 weak_ptr
    • 内存管理最佳实践
  • 第10章 右值引用与移动语义

    • 右值引用与值类别深度解析
    • std::move 原理与使用
    • 完美转发与 std::forward
    • 移动语义性能对比与最佳实践
  • 第11章 STL容器

    • vector 深度剖析
    • deque 内部机制
    • list 与 forward_list
    • map 与 set 深度解析
    • unordered 容器与哈希原理
    • array 与 tuple
    • 容器适配器
    • 容器选择全景指南
  • 第12章 STL算法与迭代器

    • 迭代器体系全解
    • 非变异算法
    • 变异算法
    • 排序与二分算法
    • Lambda 与算法组合
    • std::random 随机数库
    • 自定义迭代器开发
  • 第13章 IO流与文件

    • 标准 IO 流
    • 格式化输出控制
    • 文件流操作
    • 字符串流
    • std::regex 正则表达式
  • 第14章 并发与多线程

    • thread 基础与线程管理
    • mutex 与 lock_guard
    • unique_lock 与 condition_variable
    • thread_local 线程局部存储
    • atomic 与内存序
    • future 与 async 异步编程
    • std::chrono 时间库
  • 第15章 现代C++新特性

    • 从 C++11 到 C++20 演进路线
    • C++14 关键新特性
    • C++17 关键新特性
    • C++20 核心特性速览
  • 第16章 面试考点与最佳实践

    • C++ 综合最佳实践清单
    • 高频面试题精讲
    • 多线程面试题与实战
    • 内存管理常见陷阱与排查
  • 附录

    • C++ 核心知识点
    • C++ 专业术语

拷贝控制

定义与作用

拷贝控制(Copy Control)指控制对象被拷贝时的行为,包括两个核心操作:

操作语法何时调用
拷贝构造函数T(const T&)用已有对象初始化新对象
拷贝赋值运算符T& operator=(const T&)将已有对象赋值给另一个已有对象

当类管理动态资源(堆内存、文件句柄、网络连接)时,默认的逐成员拷贝会引发双重释放或资源泄漏,必须自定义拷贝语义。

核心原理

浅拷贝 vs 深拷贝

Rule of Three

如果类需要自定义析构函数、拷贝构造函数或拷贝赋值运算符中的任意一个,那么通常三者都需要自定义。C++11 扩展为 Rule of Five(加上移动构造和移动赋值)。

完整示例

示例一:动态数组类的深拷贝实现

场景说明:实现一个简化的动态数组类,展示深拷贝的完整实现和必要性。

#include <iostream>
#include <algorithm>
#include <stdexcept>

class IntArray {
public:
    // 构造函数
    explicit IntArray(size_t size = 0)
        : size_(size), data_(size ? new int[size]() : nullptr) {
        std::cout << "[构造] 分配 " << size_ << " 个元素" << std::endl;
    }

    // 拷贝构造函数(深拷贝)
    IntArray(const IntArray& other)
        : size_(other.size_), data_(other.size_ ? new int[other.size_] : nullptr) {
        std::copy(other.data_, other.data_ + size_, data_);
        std::cout << "[拷贝构造] 深拷贝 " << size_ << " 个元素" << std::endl;
    }

    // 拷贝赋值运算符(深拷贝 + 自赋值安全)
    IntArray& operator=(const IntArray& other) {
        std::cout << "[拷贝赋值] 从 " << other.size_ << " 元素拷贝" << std::endl;
        if (this != &other) {  // 自赋值检查
            // copy-and-swap 惯用法
            IntArray temp(other);  // 拷贝构造临时对象
            std::swap(size_, temp.size_);
            std::swap(data_, temp.data_);
        }  // temp 析构,释放旧资源
        return *this;
    }

    // 析构函数
    ~IntArray() {
        std::cout << "[析构] 释放 " << size_ << " 个元素" << std::endl;
        delete[] data_;
    }

    // 访问元素
    int& operator[](size_t i) {
        if (i >= size_) throw std::out_of_range("索引越界");
        return data_[i];
    }

    const int& operator[](size_t i) const {
        if (i >= size_) throw std::out_of_range("索引越界");
        return data_[i];
    }

    size_t size() const { return size_; }

    void print() const {
        std::cout << "[";
        for (size_t i = 0; i < size_; ++i) {
            if (i > 0) std::cout << ", ";
            std::cout << data_[i];
        }
        std::cout << "]" << std::endl;
    }

private:
    size_t size_;
    int* data_;
};

int main() {
    std::cout << "===== 拷贝构造 =====" << std::endl;
    IntArray a(3);
    a[0] = 10; a[1] = 20; a[2] = 30;
    std::cout << "a = "; a.print();

    // 拷贝构造:独立的内存空间
    IntArray b = a;  // 等价 IntArray b(a)
    std::cout << "b = "; b.print();

    // 验证独立性:修改 b 不影响 a
    b[0] = 999;
    std::cout << "\n修改 b[0] = 999 后:" << std::endl;
    std::cout << "a = "; a.print();
    std::cout << "b = "; b.print();

    std::cout << "\n===== 拷贝赋值 =====" << std::endl;
    IntArray c(2);
    c[0] = 1; c[1] = 2;
    std::cout << "赋值前 c = "; c.print();
    c = a;  // 拷贝赋值
    std::cout << "赋值后 c = "; c.print();

    std::cout << "\n===== 自赋值安全 =====" << std::endl;
    a = a;  // 自赋值,应该安全
    std::cout << "自赋值后 a = "; a.print();

    return 0;
}

预期输出:

===== 拷贝构造 =====
[构造] 分配 3 个元素
a = [10, 20, 30]
[拷贝构造] 深拷贝 3 个元素
b = [10, 20, 30]

修改 b[0] = 999 后:
a = [10, 20, 30]
b = [999, 20, 30]

===== 拷贝赋值 =====
[构造] 分配 2 个元素
赋值前 c = [1, 2]
[拷贝赋值] 从 3 元素拷贝
[拷贝构造] 深拷贝 3 个元素
[析构] 释放 2 个元素
赋值后 c = [10, 20, 30]

===== 自赋值安全 =====
[拷贝赋值] 从 3 元素拷贝
[拷贝构造] 深拷贝 3 个元素
[析构] 释放 3 个元素
自赋值后 a = [10, 20, 30]
...

逐段分析:

  • 拷贝构造分配新内存并复制数据——b 和 a 的数据完全独立
  • 深拷贝验证:修改 b[0] = 999 后 a[0] 仍是 10——两对象内存独立
  • 拷贝赋值使用 copy-and-swap 惯用法:先构造临时对象(可能抛异常但不影响原对象),再交换指针(永远成功),临时对象析构释放旧资源
  • 自赋值安全:if (this != &other) 检查自赋值,但 copy-and-swap 本身也能正确处理自赋值
  • 如果没有自定义拷贝控制,默认的逐成员拷贝会导致两个 IntArray 共享同一块内存,析构时双重释放 → 崩溃

示例二:禁用拷贝的类

#include <iostream>

class DatabaseConnection {
public:
    DatabaseConnection(const std::string& conn_str) {
        std::cout << "连接数据库: " << conn_str << std::endl;
    }

    // 禁用拷贝:数据库连接不应被随意复制
    DatabaseConnection(const DatabaseConnection&) = delete;
    DatabaseConnection& operator=(const DatabaseConnection&) = delete;

    ~DatabaseConnection() {
        std::cout << "断开数据库连接" << std::endl;
    }
};

int main() {
    DatabaseConnection db1("mysql://localhost:3306/mydb");

    // 以下两行都会编译错误:
    // DatabaseConnection db2 = db1;
    // DatabaseConnection db3(""); db3 = db1;

    return 0;
}

易错场景与面试考点

易错场景

场景错误表现正确做法
默认拷贝导致双重释放程序崩溃管理资源的类自定义拷贝控制
拷贝赋值忘记自赋值检查先释放再使用自身数据使用 copy-and-swap 惯用法
拷贝构造中浅拷贝指针两对象共享同一内存分配新内存并复制数据
派生类拷贝构造忘记调用基类拷贝构造基类部分未正确拷贝初始化列表中调用基类拷贝构造
拷贝赋值未处理异常安全赋值中途抛异常,对象处于中间状态copy-and-swap 保证强异常安全

常见面试问题

  1. 什么是浅拷贝和深拷贝?各自的适用场景?——浅拷贝只复制指针值,两对象共享资源;深拷贝复制指针指向的内容。默认编译器生成的是浅拷贝。管理堆资源的类必须深拷贝。

  2. Rule of Three 是什么?为什么需要?——如果自定义了析构函数、拷贝构造函数或拷贝赋值运算符中的一个,通常三者都需要。因为它们都涉及资源管理,编译器默认生成的行为通常不正确。

  3. copy-and-swap 惯用法的原理和优势?——先用拷贝构造创建临时对象,再 swap 资源,临时对象析构释放旧资源。优势:自赋值安全、强异常安全、代码复用。

  4. 如何禁止拷贝?C++11 前后有什么不同?——C++11 前将拷贝构造和拷贝赋值声明为 private 且不实现;C++11 起用 = delete 明确禁止。

  5. 编译器何时自动生成拷贝构造函数和拷贝赋值运算符?——当用户没有自定义时自动生成(逐成员拷贝)。C++11 中如果自定义了移动操作,拷贝操作被隐式删除。

小结

  • 管理动态资源的类必须自定义拷贝控制,实现深拷贝
  • Rule of Three:自定义析构/拷贝构造/拷贝赋值中的任一个,通常都需要全部自定义
  • copy-and-swap 是拷贝赋值的标准惯用法:自赋值安全 + 强异常安全
  • 不打算支持拷贝的类使用 = delete 明确禁用
  • C++11 起 Rule of Five 替代 Rule of Three(增加移动构造和移动赋值)
上一页
构造函数与析构函数
下一页
移动构造函数与移动赋值