当前位置: 首页 > news >正文

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

一、正常情况下

以下函数会自动生成:

#include <iostream>using namespace std;class Empty {public:Empty() {}                  // 默认构造函数Empty(const Empty& rhs) {}  // 拷贝构造函数 rhs是右操作符缩写~Empty() {}                 // 析构函数// 拷贝赋值运算符// 第一个&:返回类型是引用,第二个&:参数是一个常量对象的引用Empty& operator=(const Empty& rhs) {// this是一个隐含的指针参数,存在于每一个非静态成员函数中// 当你调用一个对象的成员函数的时,编译器会自动把该对象的地址传入thisif(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==&areturn 0;
}

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

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

#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() 会执行两次;
// 💥 程序可能崩溃(重复关闭同一文件描述符)。


可以明确拒绝默认构造:

#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 的。

问题示例:

#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::~Basedelete p;  // ❌ 问题点!
}// Derived 的析构函数 没有被调用!
// Derived 中的资源(比如动态内存、文件、锁等)不会被释放!
// 导致资源泄漏甚至未定义行为。

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

正确写法:

#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块,会依次退出当前函数调用栈中的函数,并在退出的过程中自动调用已构造对象的析构函数。

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

错误示例:

#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()

原因:

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

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

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

#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=(...) 才能继续执行。

错误写法:

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

这样会发生:

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

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

  • 临时对象马上销毁;

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

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

正确写法:

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=,编译器就不会再自动生成默认版本,而默认版本里才会包含”基类+成员“的完整拷贝逻辑。

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

http://www.dtcms.com/a/553056.html

相关文章:

  • 给予虚拟成像台尝鲜版九,支持 HTML 原型模式
  • 区块链技术在生产数据管理中的应用:Hyperledger Fabric与蚂蚁链智能合约设计
  • 可以用手机建设网站吗wordpress程序
  • deepin Ubuntu/Debian系统 环境下安装nginx,php,mysql,手动安装,配置自己的项目
  • SDC命令详解:使用set_dont_touch_network命令进行约束
  • CI/CD(三)—— 【保姆级实操】Jenkins+Docker GitLab+Tomcat 实现微服务CI/CD全流程部署
  • 20. Portals和Fragment
  • 企业网站管理wordpress必备插件
  • 数据结构之栈和队列-栈
  • 操作系统-线程
  • sward零基础学习,创建第一个知识库
  • 门户网站建设方式网站制作叫什么
  • step-ca 证书生成完整步骤指南
  • 从字节码生成看 Lua VM 前端与后端协同:编译器与执行器衔接逻辑
  • SQLite3语句以及FMDB数据存储初步学习
  • 抽奖网站怎么制作长沙人力资源招聘网
  • IntelliJ IDEA 远程断点调试完全指南
  • uniapp使用sqlite模块
  • Lua--数据文件和持久性
  • Spark SQL 解锁电商数据密码:窗口函数大显身手
  • 用R语言生成指定品种与对照的一元回归直线(含置信区间)
  • NVR(网络视频录像机)和视频网关的工作方式
  • 如何架设网站服务器网络搭建百度百科
  • opencv 学习: 04 通过ROI处理图片局部数据,以添加水印为例
  • 中小企业网站模板诚信网站平台建设方案
  • chatgpt崩溃了,gpt怎么了
  • [MySQL] 页与索引
  • 2025年AI生成PPT工具评测:以“全链路一体化服务能力”为尺,ChatPPT再登顶
  • 天津特定网站建设推广搜素引擎优化
  • 直播美颜sdk特效功能架构全解析:从图像处理到AI渲染的技术演进