Effective C++ 之《构造/析构/赋值运算》
- 条款05:了解C++默默编写并调用了哪些函数
- 条款06:若不想使用编译器自动生成的函数,就该明确拒绝
- 条款07:为多态基类声明virtual析构函数
- 1. 带多态性质的父类,应该声明一个virtual析构函数
- 2. 类的设计目的不作为父类使用,不该声明virtual函数
- 3. 不要继承一个没有声明虚析构函数的类
- 4. 纯虚函数
- 条款08:别让异常逃离析构函数
- 条款09:绝不在构造和析构过程中调用virtual函数
- 条款10:令operator= 返回一个reference to *this
- 条款11:在operator= 中处理"自我赋值"
- 条款12:复制对象时勿忘其每一个成分
条款05:了解C++默默编写并调用了哪些函数
考虑如下一个类:
class Empty{};
这个类其实等价于:
class Empty {public:Empty();Empty(const Empty& other);~Empty();Empty& operator=(const Empty& other);
}
也就是说,当我们编写一个空类时,编译器会在我们调用这些函数时,自动为我们创建出来。那这些函数对应我们平时做的哪些操作呢:
{Empty a1; //调用Empty(),默认构造函数Empty a2(a1); //调用Empty(const Empty& other),拷贝构造函数a2 = a1; //调用Empty& operator=(const Empty& other),赋值函数
} //作用域结束,调用~Empty(),析构函数
所以在写一个类的时候,如果没有自定义这几个函数的话,编译器会自动为我们创建,这些默认的函数也有自己默认的操作内容。
- 当类中有const或引用的成员变量时,编译器拒绝自动生成赋值函数
- 如果父类中的赋值函数被声明为private,那么编译器拒绝为子类生成一个赋值函数。
总结
编译器可以暗自为class创建默认构造函数、拷贝构造函数、赋值函数、以及析构函数。
条款06:若不想使用编译器自动生成的函数,就该明确拒绝
在项目开发中,我们会经常编写一些复杂的类、或者单例,或者某些类从业务逻辑上是不允许被拷贝和赋值的。那么这个时候,如果不去明确拒绝,那么正如条款05所描述,编译器会自动为我们生成这些函数。所以如果不想这样做的话,我们需要明确拒绝。
那么如何明确拒绝呢,一般有两种方法:
- 类中声明拷贝构造函数和赋值函数为private,且不实现它们.
class HomeForSale {public:...
private:HomeForSale(const HomeForSale&);HomeForSale& operator=(const HomeForSale&);
}
如上在头文件中仅仅声明它即可,不用去实现。主要原因如下:
- 使用private修饰为了防止外部进行调用
- 不去实现,是为了防止在内部的成员函数或者friend函数内调用。
- 如果这种类过多,那么需要对每一个类做这样的操作,这样会比较麻烦。有一种方式是编写一个父类,让拥有这种属性的类继承父类,从而达到它们的实例不允许拷贝的效果。
class Uncopyable {protected:Uncopyable(); //不可实例化~Uncopyable();
private:Uncopyable(const Uncopyable&);Uncopyable& operator=(const Uncopyable&);
}
这样我们只需要HomeForSale 类继承Uncopyable即可:
class HomeForSale : public Uncopyable {...
}
这种方式比较常用,boost中有noncopyable提供了该功能:
#ifndef BOOST_NONCOPYABLE_HPP_INCLUDED
#define BOOST_NONCOPYABLE_HPP_INCLUDED namespace boost { // Private copy constructor and copy assignment ensure classes derived from
// class noncopyable cannot be copied. // Contributed by Dave Abrahams namespace noncopyable_ // protection from unintended ADL
{ class noncopyable { protected: noncopyable() {} ~noncopyable() {} private: // emphasize the following members are private noncopyable( const noncopyable& ); const noncopyable& operator=( const noncopyable& ); };
} typedef noncopyable_::noncopyable noncopyable; } // namespace boost #endif // BOOST_NONCOPYABLE_HPP_INCLUDED
总结
为驳回编译器自动提供的功能,可以将相应的成员函数声明为private并且不予以实现。使用像Uncopyable这样的父类也是一种做法。
条款07:为多态基类声明virtual析构函数
1. 带多态性质的父类,应该声明一个virtual析构函数
看如下的一个例子:
class A {A();~A();
};class B : public A {B();~B();
}A *a = new B();
delete a;
如上类B是类A的派生类,在实例化的时候使用A类型的指针指向B生成的对象,那么在析构的时候会出现什么情况呢。
答案是类B的析构函数没有被调用。这是为什么呢?这是因为C++明确指出,当派生类对象经由一个父类指针删除,而该父类带着一个非虚析构函数,其结果是未定义的。也就是派生类对象析构函数未被调用。
从另外一个角度思考也是合理的,程序在调用析构函数的时候,此时该指针是A类的,但实际指向B的实例。那么它首先会调用覆盖析构函数的派生类B的析构函数,但是A的析构函数未声明为虚函数,那么就不存在覆盖它的析构函数了,所以派生类B的析构函数未能执行。
如果B的析构函数未被执行,那就意味这,对象未全部销毁。如果B类中申请了一些其他类的实例,那么显然的,这会出现内存泄漏的问题。
2. 类的设计目的不作为父类使用,不该声明virtual函数
如果一个类的设计目的不是用来作为基类的,那么我们最好不应该声明虚函数(包括虚析构函数)。
这是因为当我们声明虚函数的时候,申请出来的对象中含有一个vptr,它指向了一个虚函数表(vpbl),它存储了类中每一个虚函数的函数指针地址。所以在我们每申请一次这个对象就会多出一个占4字节(32位)的vptr。这无疑是增加了内存的开销。
3. 不要继承一个没有声明虚析构函数的类
在实际开发中,我们可能会写出这样的代码:
class MyString : public std::string {}
这样的写法是危险的,因为std::string类并没有声明自己析构函数为virtual。如果发生1所描述的情况,那么就会出现问题。
4. 纯虚函数
如果你的基类的虚函数本身什么都不做,可以将其声明为纯虚函数:
class AWOV {public:virtual ~AWOV() = 0;
}
它的好处在于,我们知道先构造的后析构,后构造的先析构。那么编译器在先析构派生类的时候,如果发现派生类没有定义虚析构函数,那么链接器就会发出错误信息。这有助于我们提前知道自己所编写的代码的问题所在。
总结
- polymorphic(带多态性质的)base classes 应该声明一个virtual析构函数,如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
- Classes的设计目的如果不是作为base classes使用,或不是为了具备多态性(polymorphically),就不该声明virtual析构函数。
条款08:别让异常逃离析构函数
考虑如下代码:
class Widget {public:
~Widget() { // 假定这个析构函数可能会吐出异常
};
void doSomething() {std::vector<Widget> v;
}
这里如果在析构函数中对vector中的第一个元素析构时,抛出异常,那么就会导致程序结束执行或者出现不明确行为。
对于这种情况,一般情况采用如下方式:
class DBConn {public:void close() {db.close();closed = true;}~DBConn() {if(!closed) {try {db.close();}catch {...} {}}}
private:DBConnection db;bool closed;
};
如上代码所示,它是一个在析构函数中关闭数据库链接的操作,为了防止db.close()函数在析构函数中发生异常,可以定义一个外部接口,供用户自己去关闭数据库连接,将异常的情况交给用户进行处理。
但是如果用户忘记调用close,在析构函数中为了保险期间,需要try catch将异常吞掉,防止出现程序异常退出的情况。
这种双保险时解决异常逃离析构函数的方法。
总结
- 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
- 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么 class应该提供一个普通函数(而非在析构函数中)执行该操作。
条款09:绝不在构造和析构过程中调用virtual函数
且看如下实例:
class Transaction {public:Transaction();virtual void LogTransaction() const = 0;...
};Transaction::Transaction() {...LogTransaction();
}class BuyTransaction : public Transaction {public:virtual void LogTransaction() const;
};class SellTransaction : public Transaction {public:virtual void LogTransaction() const;
};
这是一个股票买进卖出的系统,不同的操作都会记录自己的日志信息。在Transaction父类的构造函数中调用了LogTransaction() 虚成员方法。这种情况会出现什么问题呢。
链接器会报错,无法找到LogTransaction定义的版本。
我们知道当实例化BuyTransaction对象的时候,程序先调用父类Transaction的构造方法,此时BuyTransaction对象并没有被初始化,这个时候调用了LogTransaction虚方法并不是BuyTransaction的实现版本。就算是BuyTransaction的实现版本,那么BuyTransaction的构造方法没有执行,也就是类中的成员变量未初始化,这个时候BuyTransaction方法中如果使用了未初始化的成员变量,同样会使程序运行出现问题。
同样的在析构函数中,如果在父类调用了虚函数,也会出现问题。因为父类的析构函数执行顺序在派生类之后。如果在父类的析构函数中调用了虚函数,此时,派生类中的成员变量已经变成未定义状态,这样同样会造成程序执行到不可知的方向。
有时候我们也会这样做:
class Transaction {public:Transaction() {Init();}virtual void LogTransaction() const = 0;...
private:void Init() {LogTransaction();}
};
这种情况和上面的一样会有问题,因为其本质上还是在构造函数中出现了对虚函数的调用。
所以我们在coding的时候一定要注意构造函数中是否有对虚函数的调用,这样的做法使相当危险的。
那么有什么办法解决这种问题呢。
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(parameters) : Transaction(createLogString(parameters)){}
private:static std::string createLogString(parameters);
};
- 声明LogTransaction为非虚函数。
- 将变化的部分作为LogTransaction的形参传递给父类。
- 使用static函数让初始化的实参是已经定义的。
使用用static方法,同样是因为在传递参数的时候,该实参如果作为派生类的成员变量传递的话,此成员变量并未被初始化,同样会出问题。所以使用createLogString静态方法,确保传递的实参是已经被定义的。
当然以上只是举个例子,在实际开发中我们可能不会这样去写。
总结
在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数的那层)。
条款10:令operator= 返回一个reference to *this
该条款说明的是一种大家都遵循的协议,即我们在class中实现operator=、+=、-=、*=等操作符的时候,最好使它的返回类型是一个reference to *this。
class widget {public:widget& operator= (const widget& other) {...return *this;}
}
总结
令赋值(assignment)操作符返回一个reference to *this。
条款11:在operator= 中处理"自我赋值"
该条款使用最经典的String拷贝面试题来解释会更好一些。这个在《剑指Offer》中也提到过的面试题。
关于这个面试题,基本的解法如下:
EMyString& EMyString::operator = (const EMyString& str) {if (this == &str) {return *this;}delete[] m_pData;m_pData = nullptr;m_nLen = str.Len();m_pData = new char[m_nLen];memcpy(m_pData, str.m_pData, m_nLen);return *this;
}
那么我们看到的:
if (this == &str) {return *this;}
就是条款中所说的,在operator =中处理自我赋值。因为如果不处理的话,很可能在后面delete[] m_pData,delete的是自己,这会出现很严重的问题。
所以这个条款在我们的实际开发中要谨记。
条款中也提到了另一个风险:
delete[] m_pData;m_pData = nullptr;m_nLen = str.Len();m_pData = new char[m_nLen];
m_pData = new char[m_nLen];这句可能出现的风险是,当内存不足时,可能申请不到这块内存。但是此时我们已经将this对象中的数据释放掉了,此时等于破坏掉了原始的this对象,这就会出现异常安全。所以有更好的实现方式如下:
const EMyString& EMyString::operator=(const EMyString& str) {if (this != &str) {EMyString strTmp(str);char* tempData = strTmp.m_pData;strTmp.m_pData = m_pData; //作用域之后调用析构释放m_pData = tempData; //tempData是在EMyString构造函数中申请的内存}return *this;
}
这里使用交换的方式,先申请临时的strTmp实例,然后通过str拷贝构造出来的对象内容与this对象内容进行交换。当出了if作用域后,它会释放原本this对象中的m_pData。这样当EMyString拷贝构造函数中如果申请不到内存的话,也不会破坏原来this对象的内容。
总结
- 确保当前自我赋值时operator=有良好行为,其中技术包括比较”来源对象“和”目标对象“的地址、精心周到的语句顺序、以及copy-and-swap。
- 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
条款12:复制对象时勿忘其每一个成分
该条款表明的是,在实现拷贝构造函数和赋值函数时,我们需要考虑到类中的每一个成员是否被拷贝了,根据业务逻辑,我们需要使用的时浅拷贝还是深拷贝。
同时,当该类时子类的时候,需要考虑到父类的成员变量是否被拷贝到。
总结
Copying函数应该确保复制”对象内的所有成员变量“及”所有base class成分“。
不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个coping函数共同调用。
Effective C++ 之《构造/析构/赋值运算》相关推荐
- 读书笔记 Effective C++: 02 构造析构赋值运算
条款05:了解C++默认编写并调用的哪些函数 编译器会为class创建: 1. default构造函数(前提是:没有定义任何构造函数): 如果已经声明了一个构造函数,编译器就不会再创建default构 ...
- Effective C++ --2 构造/析构/赋值运算
上一部分Effective C++ --1 让自己习惯C++ 5. 了解C++默认编写并调用哪些函数 (1) 编译器暗自为类创建默认构造函数.拷贝构造函数.拷贝赋值函数和析构函数. (2) 拷 ...
- 构造/析构/赋值运算--龙之介《Effective C++》实验室
条款5:了解C++默默编写并调用哪些函数 编译器可以暗自为class创建default构造函数,copy构造函数,copy assignment操作符 但是c++拒绝编译那一行赋值动作.你不会自动co ...
- Effective C++ -- 构造析构赋值运算
05.了解C++默默编写并调用哪些函数 编译产生的析构函数时non-virtual,除非这个类的基类析构函数为virtual 成员变量中有引用和const成员时,无法自己主动生成copy assign ...
- Effective C++学习笔记——构造/析构/拷贝运算
条款9:决不再构造和析构过程中调用virtual函数,包括通过函数间接调用virtual函数. 应用:想在一个继承体系中,一个derived class被创建时,某个调用(例如生成相应的日志log)会 ...
- 声明及赋值_重述《Effective C++》二——构造、析构、赋值运算
关于本专栏,请看为什么写这个专栏.如果你想阅读带有条款目录的文章,欢迎访问我的主页. 构造和析构一方面是对象的诞生和终结:另一方面,它们也意味着资源的开辟和归还.这些操作犯错误会导致深远的后果--你需 ...
- 《深度探索C++对象模型》读书笔记第五章:构造析构拷贝语意学
<深度探索C++对象模型>读书笔记第五章:构造析构拷贝语意学 对于abstract base class(抽象基类),class中的data member应该被初始化,并且只在constr ...
- 【从零学习OpenCV 4】Mat类构造与赋值
本文首发于"小白学视觉"微信公众号,欢迎关注公众号 本文作者为小白,版权归人民邮电出版社所有,禁止转载,侵权必究! 经过几个月的努力,小白终于完成了市面上第一本OpenCV 4入门 ...
- C++学习笔记-----在重载的赋值运算函数中调用拷贝构造函数
类的拷贝构造函数与赋值运算不同,拷贝构造函数是对这个类进行初始化的过程,而赋值是删除原有的东西,赋予它新的东西. 但是二者在实现上是互通的. template<class T> graph ...
最新文章
- IT规划中的技术体系架构
- AMD发布“全球单核性能最快”CPU,参数碾压英特尔,性能提升47%
- leetcode 743. Network Delay Time | 743. 网络延迟时间(邻接矩阵,Dijkstra 算法)
- 基于 FPGA 的数字抢答器设计
- MFC界面库BCGControlBar v25.3新版亮点:Dialogs和Forms
- BZOJ - 2186 欧拉函数
- mcldownload文件夹_《我的世界》中国版游戏空间精简教程 多余文件删除方法
- Selenium 显示等待和隐式等待
- PID控制(三)(位置式和增量式PID)
- 配置ouster雷达过程
- 居民身份证号码的编码规则
- 最全最新的的Java核心知识点整理!!! 【推荐】
- 项目进度控制的主要任务是什么?
- js中出现错误:Uncaught TypeError: date.getDay is not a function
- 使用adb从手机拉取apk包
- 计算机慢怎么解决6,解决电脑运行慢卡顿问题的六种方法
- 基于Spark实现电影点评系统用户行为分析—RDD篇(一)
- python打包总出错,解决Pyinstaller打包软件失败的一个坑
- 使用python爬取猫眼电影、房王、股吧论坛、百度翻译、有道翻译、高德天气、华夏基金、扇贝单词、糗事百科(糗事百科)
- CSS字体、文本属性、CSS 盒模型
热门文章
- 推荐一款免费的Markdown编辑器,GitHub斩获22.8k Star
- Vue中读取md文件
- 第16届JOLT大奖获奖书籍名单最新揭晓
- 诚之和:《鱿鱼游戏》普通人与恶的距离,究竟有多远
- 神策军丨那个在神策跨城转岗的小伙子,现在怎么样了?
- mysql idataparameter_[转]另一个SqlParameterCollection 中已包含 SqlParameter[解决方案]
- 物流企业的类型有哪些?物流企业分类
- python课程介绍-少儿Python编程课程的具体介绍
- 剩余电流继电器ASJ20-LD1A自恢复式过欠压保护器
- Vue3使用element-plusUI解决菜单高度自动自适应的问题,使用CSS3的vh单位