C++构造函数、析构函数与拷贝控制深度解析

C++构造函数、析构函数与拷贝控制深度解析

一、核心知识点详解

1. 构造函数

  • 定义:对象创建时自动调用的特殊成员函数
  • 特点
    • 函数名与类名完全相同
    • 可以重载(提供多个构造函数版本)
    • 无返回类型,包括void
    • 可以有参数,支持默认参数
  • 分类
    • 默认构造函数:无参或所有参数都有默认值
    • 拷贝构造函数:用于对象初始化
    • 移动构造函数(C++11):高效资源转移
    • 委托构造函数(C++11):一个构造函数调用同类其他构造函数
    • 转换构造函数:单参数构造函数,可隐式转换

2. 析构函数

  • 定义:对象销毁时自动调用的特殊成员函数
  • 特点
    • 函数名为类名前加~
    • 不能重载(一个类只有一个析构函数)
    • 无返回类型,包括void
    • 无参数
  • 调用时机
    • 栈对象离开作用域
    • 堆对象被delete
    • 容器中的元素被清除
    • 临时对象生命周期结束

3. 深拷贝 vs 浅拷贝

  • 浅拷贝
    • 仅复制指针值,不复制指针指向的数据
    • 多个对象共享同一块内存
    • 可能导致:①双重释放 ②悬挂指针
  • 深拷贝
    • 复制指针指向的完整数据
    • 每个对象拥有独立的数据副本
    • 安全但可能效率较低

4. 移动语义(C++11)

  • 核心思想:"窃取"临时对象的资源,避免不必要拷贝
  • 右值引用T&& 表示临时对象的引用
  • 移动构造函数:从临时对象"移动"资源而非复制
  • 移动赋值运算符:类似移动构造的赋值版本

二、完整教学示例代码

cpp 复制代码
#include <iostream>
#include <cstring>
#include <utility>  // for std::move

class String {
private:
    char* m_data;
    size_t m_size;
    
public:
    // ==================== 构造函数系列 ====================
    
    // 1. 默认构造函数
    String() : m_data(nullptr), m_size(0) {
        std::cout << "默认构造函数" << std::endl;
    }
    
    // 2. 参数化构造函数
    String(const char* str) {
        std::cout << "参数化构造函数: " << (str ? str : "null") << std::endl;
        if (str) {
            m_size = strlen(str);
            m_data = new char[m_size + 1];
            strcpy(m_data, str);
        } else {
            m_size = 0;
            m_data = nullptr;
        }
    }
    
    // 3. 拷贝构造函数 - 深拷贝实现
    String(const String& other) {
        std::cout << "拷贝构造函数(深拷贝)" << std::endl;
        m_size = other.m_size;
        if (other.m_data) {
            m_data = new char[m_size + 1];
            strcpy(m_data, other.m_data);
        } else {
            m_data = nullptr;
        }
    }
    
    // 4. 拷贝构造函数 - 浅拷贝实现(危险!仅用于演示)
    String(const String& other, bool shallow) {
        std::cout << "拷贝构造函数(浅拷贝)" << std::endl;
        m_size = other.m_size;
        m_data = other.m_data;  // 危险:共享内存!
    }
    
    // 5. 移动构造函数(C++11)
    String(String&& other) noexcept {
        std::cout << "移动构造函数" << std::endl;
        // "窃取"资源
        m_data = other.m_data;
        m_size = other.m_size;
        // 置空原对象
        other.m_data = nullptr;
        other.m_size = 0;
    }
    
    // ==================== 赋值运算符系列 ====================
    
    // 6. 拷贝赋值运算符
    String& operator=(const String& other) {
        std::cout << "拷贝赋值运算符" << std::endl;
        if (this != &other) {  // 防止自赋值
            // 释放原有资源
            delete[] m_data;
            // 深拷贝
            m_size = other.m_size;
            if (other.m_data) {
                m_data = new char[m_size + 1];
                strcpy(m_data, other.m_data);
            } else {
                m_data = nullptr;
            }
        }
        return *this;
    }
    
    // 7. 移动赋值运算符(C++11)
    String& operator=(String&& other) noexcept {
        std::cout << "移动赋值运算符" << std::endl;
        if (this != &other) {
            // 释放原有资源
            delete[] m_data;
            // 移动资源
            m_data = other.m_data;
            m_size = other.m_size;
            // 置空原对象
            other.m_data = nullptr;
            other.m_size = 0;
        }
        return *this;
    }
    
    // ==================== 析构函数 ====================
    ~String() {
        std::cout << "析构函数";
        if (m_data) {
            std::cout << ": 释放 " << m_data;
        }
        std::cout << std::endl;
        delete[] m_data;  // delete nullptr 是安全的
    }
    
    // ==================== 工具函数 ====================
    const char* c_str() const { return m_data ? m_data : ""; }
    size_t size() const { return m_size; }
    
    void print() const {
        std::cout << "String(\"" << (m_data ? m_data : "nullptr") 
                  << "\", size=" << m_size << ")" << std::endl;
    }
};

// ==================== 演示函数 ====================

void demonstrate_constructors() {
    std::cout << "\n=== 构造函数演示 ===" << std::endl;
    
    // 1. 默认构造
    String s1;
    s1.print();
    
    // 2. 参数化构造
    String s2("Hello");
    s2.print();
    
    // 3. 拷贝构造(深拷贝)
    String s3 = s2;  // 等价于 String s3(s2)
    s3.print();
    
    std::cout << "s2地址: " << (void*)s2.c_str() << std::endl;
    std::cout << "s3地址: " << (void*)s3.c_str() << std::endl;
    std::cout << "注意:深拷贝时地址不同" << std::endl;
}

void demonstrate_shallow_copy_problem() {
    std::cout << "\n=== 浅拷贝问题演示 ===" << std::endl;
    
    String* original = new String("Test");
    
    // 危险:创建浅拷贝
    {
        String shallowCopy(*original, true);  // 使用浅拷贝构造函数
        std::cout << "原始: ";
        original->print();
        std::cout << "浅拷贝: ";
        shallowCopy.print();
    }  // shallowCopy离开作用域,调用析构函数,释放了共享的内存!
    
    // 此时original的m_data已经是悬空指针!
    // 下一行代码会导致未定义行为(可能崩溃)
    // std::cout << "原始对象(已无效): " << original->c_str() << std::endl;
    
    delete original;  // 双重释放!运行时错误
}

void demonstrate_deep_copy_safety() {
    std::cout << "\n=== 深拷贝安全性演示 ===" << std::endl;
    
    String original("Safe String");
    String deepCopy = original;  // 使用深拷贝构造函数
    
    // 修改拷贝,不影响原始对象
    std::cout << "修改前:" << std::endl;
    std::cout << "原始: ";
    original.print();
    std::cout << "拷贝: ";
    deepCopy.print();
    
    // 注意:这里不能直接修改,因为String没有提供修改接口
    // 实际中应该提供修改方法,这里仅演示拷贝的独立性
    
    std::cout << "原始地址: " << (void*)original.c_str() << std::endl;
    std::cout << "拷贝地址: " << (void*)deepCopy.c_str() << std::endl;
    std::cout << "地址不同,内存独立" << std::endl;
}

void demonstrate_move_semantics() {
    std::cout << "\n=== 移动语义演示 ===" << std::endl;
    
    // 创建临时对象
    String temp("Temporary String");
    
    std::cout << "\n1. 移动构造演示:" << std::endl;
    String moved1(std::move(temp));  // 移动构造
    std::cout << "移动后temp: ";
    temp.print();  // temp现在为空
    std::cout << "移动后moved1: ";
    moved1.print();
    
    std::cout << "\n2. 移动赋值演示:" << std::endl;
    String moved2;
    moved2 = std::move(moved1);  // 移动赋值
    std::cout << "移动后moved1: ";
    moved1.print();  // moved1现在为空
    std::cout << "移动后moved2: ";
    moved2.print();
}

void demonstrate_anonymous_objects() {
    std::cout << "\n=== 匿名对象演示 ===" << std::endl;
    
    // 匿名对象:没有名字的临时对象
    std::cout << "创建匿名对象: ";
    String("Anonymous").print();  // 立即析构
    
    // 匿名对象在表达式结束后立即析构
    std::cout << "匿名对象已销毁" << std::endl;
    
    // 匿名对象作为函数参数
    std::cout << "\n匿名对象作为拷贝构造参数:" << std::endl;
    String s = String("Parameter");  // 可能被编译器优化(RVO/NRVO)
    s.print();
}

void demonstrate_rule_of_five() {
    std::cout << "\n=== 三五法则演示 ===" << std::endl;
    
    // 三五法则:如果需要定义以下任何一个,通常需要定义所有五个
    // 1. 析构函数
    // 2. 拷贝构造函数
    // 3. 拷贝赋值运算符
    // 4. 移动构造函数(C++11)
    // 5. 移动赋值运算符(C++11)
    
    std::cout << "String类遵循三五法则,正确定义了所有五个特殊成员函数" << std::endl;
}

int main() {
    std::cout << "C++构造函数、析构函数与拷贝控制深度教学" << std::endl;
    std::cout << "========================================" << std::endl;
    
    demonstrate_constructors();
    
    // 警告:浅拷贝演示会崩溃,注释掉以供学习
    // demonstrate_shallow_copy_problem();
    
    demonstrate_deep_copy_safety();
    demonstrate_move_semantics();
    demonstrate_anonymous_objects();
    demonstrate_rule_of_five();
    
    std::cout << "\n程序结束,所有栈对象将按创建相反顺序析构" << std::endl;
    
    return 0;
}

三、关键要点总结

1. 构造函数要点

  • 构造函数可以重载,提供多种初始化方式
  • 使用初始化列表初始化成员变量(更高效)
  • 委托构造函数可减少代码重复

2. 拷贝控制要点

  • 浅拷贝问题
    • 双重释放:两个析构函数释放同一内存
    • 悬挂指针:一个对象删除数据,另一个对象指针失效
  • 深拷贝实现
    • 在拷贝构造函数和拷贝赋值运算符中分配新内存
    • 注意处理自赋值情况(if (this != &other)

3. 移动语义要点

  • 移动操作"窃取"资源,不分配新内存
  • 被移动的对象应处于有效但未指定的状态
  • 使用std::move将左值转换为右值引用
  • 标记移动操作为noexcept有助于标准库优化

4. 三五法则

如果一个类需要显式定义以下任何一个,则通常需要定义所有五个:

  1. 析构函数
  2. 拷贝构造函数
  3. 拷贝赋值运算符
  4. 移动构造函数
  5. 移动赋值运算符

5. 最佳实践

  1. 使用= default让编译器生成默认版本
  2. 使用= delete禁用不需要的操作
  3. 优先使用移动语义提高性能
  4. 对于资源管理类,总是实现深拷贝或禁用拷贝
  5. 考虑使用智能指针避免手动内存管理
相关推荐
Larry_Yanan2 小时前
Qt+OpenCV(一)环境搭建
开发语言·c++·qt·opencv·学习
独特的螺狮粉2 小时前
开源鸿蒙跨平台Flutter开发:微波射频阻抗匹配系统-极坐标史密斯圆图与天线信号渲染架构
开发语言·flutter·华为·架构·开源·harmonyos
冰暮流星2 小时前
javascript之dom方法访问内容
开发语言·前端·javascript
我命由我123452 小时前
在 React 项目中,配置了 setupProxy.js 文件,无法正常访问 http://localhost:3000
开发语言·前端·javascript·react.js·前端框架·ecmascript·js
草莓熊Lotso2 小时前
MySQL 事务管理全解:从 ACID 特性、隔离级别到 MVCC 底层原理
linux·运维·服务器·c语言·数据库·c++·mysql
浅时光_c2 小时前
9 循环语句
c语言·开发语言
stevenzqzq2 小时前
Kotlin 协程:withContext 与 async 核心区别与使用场景
android·开发语言·kotlin
CDN3602 小时前
弱网下游戏盾掉线重连失败?链路保活与超时参数优化
开发语言·游戏·php
im_AMBER2 小时前
Leetcode 153 课程表 | 腐烂的橘子
开发语言·算法·leetcode·深度优先·图搜索