构造/析构/赋值运算理解

一、正常情况下

以下函数会自动生成:

cpp 复制代码
#include <iostream>

using namespace std;

class Empty {
   public:
    Empty() {}                  // 默认构造函数
    Empty(const Empty& rhs) {}  // 拷贝构造函数 rhs是右操作符缩写
    ~Empty() {}                 // 析构函数
    // 拷贝赋值运算符
    // 第一个&:返回类型是引用,第二个&:参数是一个常量对象的引用
    Empty& operator=(const Empty& rhs) {
        // this是一个隐含的指针参数,存在于每一个非静态成员函数中
        // 当你调用一个对象的成员函数的时,编译器会自动把该对象的地址传入this
        if(this!=&rhs){ 
            // 拷贝成员变量
        }
        return *this;
    }  

    int x;
    static int count; // 静态成员变量(属于类,全局唯一)
    // 非静态成员函数的本质,其实是一个普通函数,只是多了一个隐式的"this 指针"参数。
    // 非静态成员函数,属于对象

    void show(){ // 等于 void show(Empty* this)
        cout<<"x="<<x<<endl;
    }
    // 静态成员函数,属于类本身
    // 静态函数属于类,不属于对象,调用时没有this,因此不能访问对象成员
    // 静态成员函数可以访问静态变量
    
    static void info(){
        cout<<"count="<<count<<endl;
    }
};
 
int main(){
    Empty a; // 创建一个对象
    a.x =10;
    a.show();   // 编译器幕后会改写为 Empty::show(&a);而this==&a
    return 0;
}

二、若不想使用编译器自动生成的函数,就该明确拒绝(Effective C++ 06)

下面的例子是编译可以通过,但是可能报错的情况:

cpp 复制代码
#include <iostream>

using namespace std;

class File {
   public:
    File(const std::string& filename) {
        cout << "打开文件: " << filename << endl;
    }
    ~File() { cout << "关闭文件\n"; }
    // 没有禁拷贝!
};

int main() {
    File f1("data.txt");
    File f2 = f1;  // ✅ 编译能过,但逻辑错误!
}
// 可以编译通过
// 编译器会自动生成一个"浅拷贝"构造函数;
// f1 和 f2 都指向同一个文件;
// 当 main() 结束时,~File() 会执行两次;
// 💥 程序可能崩溃(重复关闭同一文件描述符)。

可以明确拒绝默认构造:

cpp 复制代码
#include <iostream>

using namespace std;

class File { // File类用来管理文件的打开与关闭
public:
    File(const std::string& filename) { open(filename); } // 构造函数:当创建FILE对象时,会自动打开
    ~File() { close(); } //析构函数

    File(const File&) = delete;             // ❌ 禁止拷贝构造
    File& operator=(const File&) = delete;  // ❌ 禁止赋值
private:
    void open(const std::string& filename);
    void close();
};
 
int main(){
    File f1("data.txt");
    // File f2 = f1;   // ❌ 编译器会自动调用拷贝构造函数!
    return 0;
}

明确拒绝的含义:

不要让编译器帮你做不安全的事情。

如果你知道这个类不应该被拷贝/赋值。

就要用=delete明确告诉编译器禁止生成。

三、为多态基类声明virtual析构函数(Effective C++ 07)

如果一个类打算被当作基类使用(尤其是通过基类指针删除派生类对象)

那它的析构函数就必须是 virtual 的。

问题示例:

cpp 复制代码
#include <iostream>

using namespace std;

class Base {
   public:
    ~Base() { cout << "Base::~Base()\n"; }
};

class Derived : public Base {
   public:
    ~Derived() { cout << "Derived::~Derived()\n"; }
};

int main() {
    Base* p = new Derived(); 
    // p的静态类型是Base*
    // 调用delete p;时,编译器只知道它是个Base*
    // 没有virtual,就不会做动态绑定
    //所以只调用Base::~Base
    delete p;  // ❌ 问题点!
}

// Derived 的析构函数 没有被调用!
// Derived 中的资源(比如动态内存、文件、锁等)不会被释放!
// 导致资源泄漏甚至未定义行为。

当析构函数不是 virtual 时,delete p; 只会调用指针静态类型的析构函数。

正确写法:

cpp 复制代码
#include <iostream>

using namespace std;

class Base {
   public:
    virtual ~Base() { cout << "Base::~Base()\n"; }
};

class Derived : public Base {
   public:
    ~Derived() { cout << "Derived::~Derived()\n"; }
};

int main() {
    Base* p = new Derived();
    delete p;  // ✅ 正确释放
}

如果你的类打算被继承,会通过基类指针或引用操作对象,尤其通过delete basePtr;删除对象,就必须加virtual.

四、别让异常逃离析构函数(Effective C++ 08)

因为在 C++ 中,如果析构函数在栈展开(stack unwinding)过程中抛出异常,

会导致 程序直接终止(std::terminate()

栈展开:当异常被抛出后,程序未来找到合适的catch块,会依次退出当前函数调用栈中的函数,并在退出的过程中自动调用已构造对象的析构函数。

简单说,就是从抛出点开始,一层层弹出函数调用栈,清理已创建的局部对象。

错误示例:

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

class Test {
   public:
    ~Test() { throw runtime_error("析构函数异常!"); } // 3析构函数又抛出了第二个异常
};

void func() {
    Test t;  // 局部对象
    throw runtime_error("函数内部异常!");  // 2抛出异常,程序开始栈展开,准备析构局部对象t
}

int main() {
    try {
        func();  // 1
    } catch (const exception& e) {
        cout << "捕获异常:" << e.what() << endl;
    }
}

C++ 语言规定:

如果一个异常在栈展开期间再次抛出,程序必须调用 std::terminate()

原因:

  • 栈展开期间系统已经在处理一个异常;

  • 若又有另一个异常冒出,编译器无法同时处理两个异常;

  • 为保证系统稳定,只能直接终止程序。

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

class Test {
   public:
    ~Test() {
        try {
            danger();  // 可能抛出异常
        } catch (const exception& e) {
            cerr << "析构函数捕获异常" << e.what() << endl;
        }
    }
   private:
    void danger() { throw runtime_error("文件关闭失败"); }
    // 如果上层函数没有 catch,异常会继续向上层传播,这样可以不让它往上跑
};

void func() {
    Test t;  // 局部对象
    throw runtime_error("函数内部异常!");  // 2抛出异常,程序开始栈展开,准备析构局部对象t
}

int main() {
    try {
        func();  // 1
    } catch (const exception& e) {
        cout << "捕获异常:" << e.what() << endl;
    }
}

五、绝不在构造和析构过程中调用virtual函数(Effective C++ 09)

因为在构造和析构期间,对象的多态性还没"完全建立"或已经"失效"。

调用虚函数不会发生多态,而只会调用当前类版本的函数。

c++的对象在构造时是分阶段构造的:

1️⃣ 构造 Base 部分(调用 Base::Base())

2️⃣ 再构造 Derived 部分(调用 Derived::Derived())

也就是说,当执行 Base() 构造函数时,
整个对象的"有效类型"其实就是 Base。

错误示例:

正确示例:

如果非要在构造过程中自动调用怎么办?

用工厂函数或静态创建函数。

工厂函数:

把对象的创建过程封装起来,而不是直接在外部用 new 或构造函数创建对象。

六、令operator=返回一个reference to *this(Effective C++ 10)

"让赋值运算符函数返回当前对象本身的引用。"

因为我们希望赋值操作能连续使用

这样可以支持连锁赋值,就像内建类型(int,double)那样。

a = b = c;

编译器会当成:

a.operator=( b.operator=(c) );

要想这样写能成立,
b.operator=(c) 必须返回 b 自己(的引用)

这样外层的 a.operator=(...) 才能继续执行。

错误写法:

cpp 复制代码
class A {
public:
    A operator=(const A& rhs) {   // 返回一个"副本",注意:返回类型是 A(值),不是 A&
        // 拷贝数据...
        return *this;             // 返回一个临时对象
    }
};

这样会发生:

  • (b = c) 返回了一个临时副本;

  • 外层 a = (b = c) 就变成了 a = 临时对象

  • 临时对象马上销毁;

  • 效率差,行为不符合期望。

解释:多一次拷贝,效率变差

正确写法:

cpp 复制代码
class A {
public:
    A& operator=(const A& rhs) {  // 返回引用
        if (this != &rhs) {
            // 拷贝数据
        }
        return *this;             // 返回自己
    }
};

上面这份代码就不会产生一个临时对象。

七、在operator=中处理"自我赋值"(Effective C++ 11)

a=a ;

a = f(); // f() 恰好返回 a 的引用

这个就是自我赋值,如果没有处理好,就可能在赋值过程中把自己给毁掉了。

加一条:检查自我赋值就可以了。

八、复制对象时勿忘其每一个成分(Effective C++ 12)

当你为一个类写拷贝构造函数或者拷贝赋值运算符时,一定要记得复制对象的所有成员和所有基类部分。

错误示范:

正确示范:

一旦定义了自己的拷贝构造函数或operator=,编译器就不会再自动生成默认版本,而默认版本里才会包含"基类+成员"的完整拷贝逻辑。

所以,当你手动定义了,就必须接住这个责任。

相关推荐
大佬,救命!!!4 小时前
C++多线程运行整理
开发语言·c++·算法·学习笔记·多线程·新手练习
合作小小程序员小小店4 小时前
web网页开发,旧版在线%考试,判题%系统demo,基于python+flask+随机分配考试题目,基于开发语言python,数据库mysql
开发语言·后端·python·mysql·flask·html5
蜗牛沐雨4 小时前
C++ 输出流(Output Stream)全解析
开发语言·c++
余道各努力,千里自同风4 小时前
如何使用 Promise.all() 处理异步并发操作?
开发语言·前端·javascript
小白讲编程5 小时前
C++ 基础学习总结:从入门到构建核心认知
c++·学习·青少年编程
国服第二切图仔5 小时前
Rust开发之使用 Trait 定义通用行为——实现形状面积计算系统
开发语言·网络·rust
前端小咸鱼一条5 小时前
14. setState是异步更新
开发语言·前端·javascript
L_09075 小时前
【Algorithm】Day-10
c++·算法·leetcode
15Moonlight5 小时前
09-MySQL内外连接
数据库·c++·mysql