C++ Big five(构造函数,析构函数,拷贝构造函数,复制赋值运算符,移动构造函数)

Big five

Cpp中的Big five指的是C++类中的5个重要的成员函数:

构造函数,析构函数,拷贝构造函数,复制赋值运算符,移动构造函数。

其中构造函数和析构函数通常是必须的,其余三个应根据需求实现。

1. 构造函数 (Constructor)

        构造函数很简单,就是初始化对象的默认状态,若没有定义任何构造函数,编辑器就会提供一个默认构造函数。

2. 析构函数 (Destructor)

        析构函数在对象生命周期结束时会自动调用,作用是释放资源。

3. 拷贝构造函数 (Copy Constructor)

        拷贝构造函数是一种特殊的构造函数,其通过现有对象来初始化新对象。即用于初始化一个新对象,以另一个同类型对象的内容作为初始值。拷贝构造函数分为深拷贝和浅拷贝。

浅拷贝:  仅拷贝对象的指针成员,不拷贝指针所指向的资源。

                原对象和拷贝对象共享同一块资源

                容易导致资源管理问题,如多次释放同一块内存。

                适用于不需要独立资源的场景,如只读数据或资源由外部管理。

class Shallow {
public:
    int* data;

    Shallow(int value) {
        data = new int(value);
    }

    // 默认的拷贝构造函数实现浅拷贝
    Shallow(const Shallow& other) : data(other.data) {}

    ~Shallow() {
        delete data;
    }
};

int main() {
    Shallow obj1(10);
    Shallow obj2 = obj1; // 浅拷贝

    return 0;
}

obj1obj2都指向同一个内存地址。因此,修改obj2的数据会影响到obj1的数据。

深拷贝:  拷贝对象的指针成员,还拷贝指针所指向的资源。

                原对象和拷贝对象拥有独立的资源。

                避免了资源管理问题,如内存泄漏和多次释放同一块内存。

                适用于需要独立资源的场景,如动态分配内存的类。

class Deep {
public:
    int* data;

    Deep(int value) {
        data = new int(value);
    }

    // 自定义拷贝构造函数实现深拷贝
    Deep(const Deep& other) {
        data = new int(*other.data);
    }

    ~Deep() {
        delete data;
    }
};

int main() {
    Deep obj1(10);
    Deep obj2 = obj1; // 深拷贝

    return 0;
}

obj1obj2指向不同的内存地址,因此修改obj2的数据不会影响obj1的数据。

4. 赋值操作符重载 (Assignment Operator Overloading) 或复制赋值运算符。

        在Cpp中,默认的复制操作符 ' = ' 执行的是浅拷贝(即只是简单地复制对象的数据成员。如果对象包含动态分配的资源(如指针),这种浅拷贝可能会导致资源管理问题,如双重释放(double delete)或内存泄漏。),这在处理内存或资源时有可能导致问题,因此我们需要通过重载赋值操作符,可以定制对象赋值时的行为,确保正确管理资源,让对象在赋值时不会共享同一块内存。

赋值操作符重载确保赋值时正确释放旧资源并分配新资源,实现深拷贝:

class Deep {
public:
    int* data;

    // 构造函数
    Deep(int value) {
        data = new int(value);
    std::cout << "构造函数调用" << std::endl;
    }

    // 拷贝构造函数实现深拷贝
    Deep(const Deep& other) {
        data = new int(*other.data);
    std::cout << "拷贝构造函数调用" << std::endl;
    }

    // 赋值操作符重载实现深拷贝
    Deep& operator=(const Deep& other) {
        std::cout << "赋值操作符调用" << std::endl;
        if (this != &other) { // 避免自我赋值
            delete data; // 释放已有资源
            data = new int(*other.data); // 分配新资源并复制值
        }
        return *this;
    }

    ~Deep() {
        delete data;
    std::cout << "析构函数调用" << std::endl;
    }
};

int main() {
    Deep obj1(10); // 调用构造函数
    Deep obj2(20); // 调用构造函数
    obj2 = obj1; // 调用赋值操作符

    std::cout << "obj1.data: " << *obj1.data << std::endl; //obj1.data: 10 
    std::cout << "obj2.data: " << *obj2.data << std::endl; //obj2.data: 10

    *obj2.data = 30; // 修改obj2的数据

    std::cout << "obj1.data: " << *obj1.data << std::endl; //obj1.data: 10
    std::cout << "obj2.data: " << *obj2.data << std::endl; //obj2.data: 30

    return 0;
}
结果如下:

obj1 构造函数调用
obj2 构造函数调用
赋值操作符调用,obj2 = obj1
析构函数调用,释放 obj2 的原始内存

obj1.data: 10 // 显示 obj1 的数据
obj2.data: 10 // 显示 obj2 的数据,应该与 obj1 的数据相同

After modifying obj2.data:

obj1.data: 10 // 修改 obj2.data 后,obj1 的数据不变
obj2.data: 30 // 修改 obj2.data 后,显示修改后的值

析构函数调用,释放 obj1 的内存
析构函数调用,释放 obj2 的内存

若没有赋值操作符重载:

结果如下:

obj1 构造函数调用
obj2 构造函数调用
浅拷贝obj2 = obj1
析构函数调用,释放 obj2 的原始内存

obj1.data: 10 // 显示 obj1 的数据
obj2.data: 10 // 显示 obj2 的数据,应该与 obj1 的数据相同

After modifying obj2.data:

obj1.data: 30 // 修改 obj2.data 后,obj1 也被修改
obj2.data: 30 // 修改 obj2.data 后,显示修改后的值

析构函数调用,释放 obj1 的内存
析构函数调用,释放 obj2 的内存,试图释放已释放的内存

***注意:拷贝构造函数 Deep obj2 = obj1;

而赋值操作 obj2 = obj1;如果没有重载赋值操作符,Cpp会使用默认的浅拷贝。

不要混淆。

5.移动构造函数 (Move Constructor)

         移动构造函数数是一种特殊的构造函数,用于“移动”资源,而不是“复制”资源。

         使用一个临时对象 (右值) 来初始化/赋值 (而非拷贝) 对象,这避免了不必要的深拷贝操作。可以大大提高性能 (其不用像拷贝,需要new)。

        右值引用(Rvalue Reference)是用 '&&' 表示的新引用类型,用于绑定到右值(临时对象)。这使得移动语义成为可能。

class MyClass {
public:
    int a;
    int b;

    // 默认构造函数
    MyClass(int value1 = 0, int value2 = 0) 
        : a(value1), b(value2) {
        cout << "构造函数: 初始化值" << endl;
    }

    // 拷贝构造函数
    MyClass(const MyClass& other) 
        : a(other.a), b(other.b) {
        cout << "拷贝构造函数: 复制值" << endl;
    }

    // 移动构造函数
    MyClass(MyClass&& other) noexcept 
        : a(other.a), b(other.b) {
        other.a = 0;
        other.b = 0;
        cout << "移动构造函数: 移动值" << endl;
    }

    // 拷贝赋值运算符
    MyClass& operator=(const MyClass& other) {
        if (this != &other) {
            a = other.a;
            b = other.b;
            cout << "拷贝赋值运算符: 复制值" << endl;
        }
        return *this;
    }

    // 移动赋值运算符
    MyClass& operator=(MyClass&& other) noexcept {
        if (this != &other) {
            a = other.a;
            b = other.b;
            other.a = 0;
            other.b = 0;
            cout << "移动赋值运算符: 移动值" << endl;
        }
        return *this;
    }

    // 打印函数
    void print() const {
        cout << "a: " << a << ", b: " << b << endl;
    }
};

int main() {
    MyClass obj1(10, 20);           // 调用构造函数
    MyClass obj2 = std::move(obj1); // 调用移动构造函数

    obj2.print();

    // 检查 obj1 的状态
    if (obj1.a == 0 && obj1.b == 0) {
        cout << "obj1 已被移动,值已重置" << endl;
    }

    return 0;
}

std::move 函数用于将 obj1 转换为右值引用,从而触发移动构造函数。

构造函数: 初始化值
移动构造函数: 移动值
a: 10, b: 20
obj1 已被移动,值已重置

对于noexpect:

1. 如果编译器知道一个函数不会抛出异常,它可以进行一些优化,比如省略为处理异常所需的代码。这可以减少代码大小和运行时的开销。

2. 某些标准库容器(如 std::vector)在内部使用移动构造函数和移动赋值运算符来管理元素。如果这些函数被标记为 noexcept,容器可以更安全地执行内存管理操作,而不需要担心异常导致的资源泄漏或未定义行为。

3. 明确标记一个函数不会抛出异常,可以提高代码的安全性,帮助开发者更好地理解和维护代码。

  • 15
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值