Effective CPP(三):类的构造/析构/赋值运算

文章目录

  • 一、C++类中编译器默认创建的函数
  • 二、为多态基类声明一个虚析构函数
  • 三、在析构函数中 "捕获" 异常
  • 四、不在构造函数和析构函数的过程中调用虚函数
  • 五. 重载赋值类运算符号的时候返回 this 指针
  • 六. 在 operator= 中处理“自我赋值”
  • 七. 拷贝复制对象的时候应该考虑全面


一、C++类中编译器默认创建的函数

当你在C++中创建一个空类的时候,编译器会默认为它创建下列内容:

class Empty {
public:Empty() { ... }                   // 默认构造函数Empty(const Empty&) { ... }       // 拷贝构造函数Empty(Empty&&) { ... }            // 移动构造函数(since cpp11)~Empty() { ... }                  // 析构函数Empty& operator=(const Empty&) { ... }    // 拷贝运算符 Empty& operator=(Empty&&) { ... }         // 移动运算符
}; 当触发下列情景的时候,他们才会真正被编译器创建出来
Empty e1; //默认构造函数
Empty e2(e1); //拷贝构造函数
Empty e3 = std::move(e2); //移动构造函数
e2 = e1; //拷贝赋值运算符
e3 = std::move(e1); //移动赋值运算符

当然,如果条件不允许创建某一个构造函数或者运算符,编译器也不会创建的,比如对于一个只包含一个引用对象的类,由于引用无法指向不同的对象,所以编译器不会为他创建一个默认的拷贝赋值运算符和拷贝构造函数。 与此同时,如果类中包含有 const 的成员,或者基类中包含有 private 的拷贝赋值运算符,拷贝赋值运算符和拷贝构造函数也不会被创建。

当然,如果你不想创建的话,直接像单例模式那样声明为=delete 即可(since cpp11)

二、为多态基类声明一个虚析构函数

当派生类对象由一个基类指针被删除的时候,而该基类指针带着一个非虚析构函数的类对象,其结果就是未定义的。当通过基类指针删除派生类的对象的时候,如果基类析构函数不是虚的,那么只有基类的析构函数会被调用。 这意味着只有基类部分的资源会被释放。而派生类的析构函数就不会被调用,因此不会被释放。如果在基类中定义了虚析构函数,在删除对象释放资源的时候,会首先调用派生类的析构函数,再调用基类的析构函数。

一个好玩的用法是如果你想将基类当做一个抽象类,但是手头上没有其他的虚函数,那么将他的析构函数设置为纯虚函数也是一个不错的想法,但是请注意这种用法需要你写出析构函数的函数体:

class Base {
public:virtual ~Base() = 0 {} 
}; 

三、在析构函数中 “捕获” 异常

在 RAII 思想下,我们通常将释放资源的操作封装在析构函数中。例子如下:


class DBConn {
public: ~DBConn() {db.close(); //这一操作有可能会抛出异常}
private:DBConnection db;
};为了在析构函数中完成对异常的处理,以下是几种常见的做法:
第一种,杀死程序:
DBConn::~DBConn() {try {db.close();} catch(...) {std::abort();}
}第二种(推荐做法), 将有可能引起异常的操作暴露在普通函数中:
class DBConn {
public:void close() {db.close();closed = True;}~DBConn() {if(!closed) {try {db.close();} catch(...) { //捕获异常}}}
}在新的设计中,我们提供了 close 函数供客户手动调用,这样可会可以根据自己的意愿处理异常;如果客户忘记手动调用,析构函数才会自动调用 close 函数。 当一个操作可能会抛出需要客户处理的异常的时候,将其暴露在普通函数而非析构函数中是一个更好的选择。

四、不在构造函数和析构函数的过程中调用虚函数

在创建派生类对象的时候,基类的构造函数会早于派生类的构造函数调用, 基类的析构函数会晚于派生类的析构函数被调用。
假如第四点条款没有被遵守,在调用基类的构造函数以期望调用派生类中已经重写的虚函数的时候(比如使用基类的指针构造派生类),会意外执行到还没有被重写的虚函数。 当然一般不会有人傻到直接在构造函数中调用虚函数,但是很多时候我们可能不小心间接调用了虚函数:

class Transaction {
public:Transaction() { Init(); }virtual void LogTransaction() const = 0;private:void Init(){...LogTransaction();      // 此处间接调用了虚函数!}
};

如果想要基类在构造的时候就得知派生类的构造信息,推荐的做法是在派生类的构造函数中将必要的信息向上传递给基类的构造函数。

class Transaction {
public:explicit Transaction(const std::string& logInfo);void LogTransaction(const std::string& logInfo) const;...
};Transaction::Transaction(const std::string& logInfo) {LogTransaction(logInfo);                           // 更改为了非虚函数调用
}class BuyTransaction : public Transaction {
public:BuyTransaction(...): Transaction(CreateLogString(...)) { ... }    // 将信息传递给基类构造函数...private:static std::string CreateLogString(...);
}

请注意,在这里静态成员函数是必须的,因为 其在构造函数中被调用了,只有使用静态成员函数才不会使用还没有完成初始化的成员变量。

五. 重载赋值类运算符号的时候返回 this 指针

一个理想的重载 += 和 = 操作符的模板应该是这样的:

class Widget {
public:Widget& operator+=(const Widget& rhs) {    // 这个条款适用于...                                    // +=, -=, *= 等等运算符return *this;}Widget& operator=(int rhs) {               // 即使参数类型不是 Widget& 也适用...return *this;}
};

如果我们不使用*this而是返回一个临时创建的Widget对象,比如:

class Widget {
public:int value; Widget(int val = 0) : value(val) {} Widget& operator+=(const Widget& rhs) {// ...return someOtherWidget; // 返回另一个 Widget 的引用}// ...
};Widget w1, w2 ,w3;
w1 += w2 += w3; 

六. 在 operator= 中处理“自我赋值”

自我赋值是合法的操作,但在一些情况下可能会导致意外的错误,例如在复制堆上的资源时:

请注意,在这里删除原来的 Resource 对象是必须的。否则会:

  1. 内存泄露 : *this 指向的原来的 Resource 对象没有被删除,导致内存泄露。
  2. 悬垂指针:如果 rhs 被销毁了或者更改其指向,则*this的 pRes 将会变成悬垂指针,指向无效的内存。
  3. 牵一发而动全身: 由于*this 和 rhs 共享同一个 Resource 对象。如果其中一个进行了修改这个对象,另外一个对象也会收到牵连。

Widget& operator+=(const Widget& rhs) {
delete pRes; // 删除当前持有的资源
pRes = new Resource(*rhs.pRes); // 复制传入的资源
return this;
}
但若rhs和
this指向的是相同的对象,就会导致访问到已删除的数据。

最简单的解决方法是在执行后续语句前先进行证同测试(Identity test):

Widget& operator=(const Widget& rhs) {
if (this == &rhs) return *this; // 若是自我赋值,则不做任何事

delete pRes;
pRes = new Resource(*rhs.pRes);
return *this;

}
另一个常见的做法是只关注异常安全性,而不关注是否自我赋值:

Widget& operator=(const Widget& rhs) {
Resource* pOrigin = pRes; // 先记住原来的pRes指针
pRes = new Resource(*rhs.pRes); // 复制传入的资源
delete pOrigin; // 删除原来的资源
return *this;
}
仅仅是适当安排语句的顺序,就可以做到使整个过程具有异常安全性。

还有一种取巧的做法是使用 copy and swap 技术,这种技术聪明地利用了栈空间会自动释放的特性,这样就可以通过析构函数来实现资源的释放:

Widget& operator=(const Widget& rhs) {
Widget temp(rhs);
std::swap(*this, temp);
return *this;
}
上述做法还可以写得更加巧妙,就是利用按值传参,自动调用构造函数:

Widget& operator=(Widget rhs) {
std::swap(*this, rhs);
return *this;
}
这种写法也被叫做拷贝交换技术。

七. 拷贝复制对象的时候应该考虑全面

当手动实现拷贝构造函数或者拷贝赋值运算符的时候,忘记赋值任何一个成员都可能会导致意外的错误.
当使用继承的时候,继承自基类的成员往往容易忘记在派生类中完成赋值,如果你的基类拥有拷贝构造函数和拷贝赋值运算符,应该记得及时调用他们.

class PriorityCustomer : public Customer { 
public:PriorityCustomer(const PriorityCustomer& rhs);PriorityCustomer& operator=(const PriorityCustomer& rhs);...
private:int priority;
};PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs) : Customer(rhs), priority(rhs.priority) {...
}PriorityCustomer::PriorityCustomer& operator=(const PriorityCustomer& rhs) { Customer::operator=(rhs);priority = rhs.priority;return *this;
}

请注意,根据CPP 类型安全原则,将派生类赋值给基类是允许的,因为派生类包含比基类更多的信息,而反过来却不行。请注意在这里调用了Customer::operator=(rhs); 在这个函数内部修改了this指针,所以尽管没有用this来接收它,Customer也已经改变了。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/192253.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

菜鸟内推java岗一面

菜鸟内推,java岗,电话一面 总时长 40min,过程如下: 1、自我介绍,学习、项目啥的,简单提一提。 2、接触到的微服务架构啥的,怎么解决问题等等。 3、 数据 库索引,对索引的理解&#x…

idea汉化

所有的jetbrains 汉化包下载地址, 包括leda ,pycharm /,datagrip 等软件,,所有方法都一样:搜索对应的版本需要的包 下载后,在idea的插件中选择从磁盘加载,然后重启 ,即可…

循环队列的结构设计和基本操作的实现(初始化,入队,出队,判空,获取长度,清空,销毁)

目录 1.队列的定义 2.循环队列的设计图示 3.循环队列的结构设计 4.循环队列的实现 5.循环队列的总结 1.队列的定义 和栈相反,队列(queue)是一种先进先出(first in first out,缩写为FIFO)的线性表.它只允许在表的一端进行插入,而在另一端删除元素. 在队列中,允许插入的一…

根文件系统构建-busybox中文支持

一. 简介 根文件系统里面就是一堆的可执行文件和其他文件组成的?难道我们得一个一个的从网上去下载这些文件?显然这是不现实的!那么有没有人或者组织专门干这个事呢? 他们负责“收集”这些文件,然后将其打包&#xf…

阿里云域名解析到非默认端口处理方式

1.需配置两条解析记录,如下图 2.第一条配置A记录,ip指向部署服务器 3.第二条配置隐形记录,指向第一条的网址,并附带端口号,最终访问第二条的网址就不用带非默认端口号了。 4.最终浏览器访问

[FC][常见Mapper IRQ研究]

本次IRQ研究了如下表所示Mapper的IRQ操作: 卡带名Mapper号VRC373VRC421,23,25VRC624 & 26VRC785MMC34MMC410MMC55Sunsoft FME-769Namco16319Jaleco SS 8800618RAMBO-164 共计11种Mapper的IRQ操作使用例子 代码内有详细注释, 希望能帮助到感兴趣的人. Mapper控制代码(MMC3…

matplotlib学习

显示两个figure 坐标上刻度修改 plt.xlim() 下标范围 plt.xticks() 替换新的下标 图例显示 散点图 subplot多合一显示

docker部署frp穿透内网

文章目录 (1)部署frps服务器(2)部署frpc客户端(3)重启与访问frp(4)配置nginx反向代理 (1)部署frps服务器 docker安装参考文档:docker基本知识 1…

Tmux奇技淫巧

Tmux奇技淫巧 在日常的开发工作中,终端是我们最常用的工具之一。在终端中我们可以调用各种解释器,来执行命令,完成我们的工作。然而,对于只使用终端的默认功能的开发者来说,他们可能会错过一些强大的工具和技巧&#…

业余做UE开发顾问

有家小公司找到我,聊了两次,其实,我的水平也很菜,也真看得起我。只是他们公司的人只会蓝图,我指出并解决了他们在软件设计上的一个问题。 周末闲着也是闲着,光想打游戏,效率不高,还…

王学岗网络监听

网上找了好久,终于找到啦,自己改了下 private var networkCallback object : ConnectivityManager.NetworkCallback() {override fun onCapabilitiesChanged(network: Network,networkCapabilities: NetworkCapabilities) {super.onCapabilitiesChanged…

【SLAM十四讲-9.3 实践Ceres BA-BAL数据集problem-16-22106-pre.txt分析】

数据集Dubrovnik Dataset 杜布罗夫尼克数据集的链接:Bundle Adjustment in the Large https://grail.cs.washington.edu/projects/bal/ problem-16-22106-pre.txt.bz216 22106 83718(这里是第1行) 0 0 -3.859900e02 3.871200e02&#x…

C#后台发送Get和Post请求的几种方法总结

本文主要介绍分别通过HttpClient、Flurl.Http、WebRequest和WebClient发送Get和Post请求的方法。 1、通过HttpClient发送Get和Post请求 适用平台:.NET Framework 4.5, .NET Standard 1.1, .NET Core 1.0 其它平台的移植版本可以通过Nuget来安装。 命名空间&…

Gson的用法详解

一、简介 Gson(又称Google Gson)是Google公司发布的一个开放源代码的Java库,主要用途为序列化Java对象为JSON字符串,或反序列化JSON字符串成Java对象。 Gson官网:gson Gson源码地址:google/gson 二、依赖…

STM32F407-14.3.8-01强制输出模式

强制输出模式 在输出模式(TIMx_CCMRx 寄存器中的 CCxS② 位 00)下,可直接由软件将每个输出比较信号(OCxREF④ 和 OCx⑥/OCxN⑦)强制设置为有效电平或无效电平,而无需考虑输出比较寄存器和计数器之间的任何…

CMake构建工具

文章目录 CMake构建工具1.概念2.mk文件3.CmakeList4.预编译 CMake构建工具 1.概念 Android构建原始库的工具,对mk构建工具封装,还是makefile。 加载lib库 2.mk文件 //call调用test-dir这个方法,返回mk文件的路径,LOCAL_PATH这…

一文搞懂设计模式之责任链模式

大家好,我是晴天。我们又见面了,本周我们继续学习设计模式,本周将同大家一起学习责任链模式。 场景引入 我们回想一下自己曾经经历过的考学场景,我们是如何一步一步考上大学的(为了简化过程只提取核心环节&#xff09…

如何使用vue组件

目录 1:组件之间的父子关系 2:使用组件的三个步骤 3:components组件的是私有子组件 4:在main.js文件中使用Vue.component全局注册组件 1:组件之间的父子关系 一:首先封装好的组件是不存在任何的关系的…

springboot(ssm家政服务系统 家政预约平台Java(codeLW)

springboot(ssm家政服务系统 家政预约平台Java(code&LW) 开发语言:Java 框架:ssm/springboot vue JDK版本:JDK1.8(或11) 服务器:tomcat 数据库:mysql 5.7(或8.0&#xff09…

强化学习简明教程

到目前为止,我们主要关注监督学习问题(主要是分类)。 在监督学习中,我们得到某种由输入/输出对组成的训练数据,目标是能够在学习模型后根据一些新输入来预测输出。 例如,我们之前研究过 MNIST 的卷积神经网…