文章目录
- 5、term5:Know what functions C++ sliently writes and calls
- 6、term6:Explicitly disallow the use of compiler-generated functions you do not want
- 7、term7:Declare destructors virtual in polymorphic base classes
- 8、term8:Prevent exceptions from leaving destructors
- 9、term9:Never call virtual functions during construction or destruction
- 10、term10:Have assignment operators return a reference to *this
- 11、term11:Handle assignments to self in operator=
- 12、term12:Copy all parts of an object
- 4、总结
- 5、参考
5、term5:Know what functions C++ sliently writes and calls
编译器是一个“无名英雄”,当你创造一个简单的类的时候,他会将构造函数,解析函数,copy构造函数,copy assignment运算符都默默生成出来。
举个栗子:
class Empty { };
//实际上等价于
class Empty{
public:
Empty(){...} //default 构造函数
Empty(const Empty& rhs) {...} //copy构造函数
~Empty(){...} //析构函数
Empty& operator=(const Empty&rhs){...}//copy assignment 操作符
}
拷贝运算符在使用中需要注意,如果成员变量中有引用,或者被const修饰,则拷贝运算符不可被调用(运行时出错)
举个栗子:
templete<class T>
class NamedObject{
public:
...
NamedObject(std::string& name,const T& value);
...
private:
std::string & nameValue; //reference
const T objectValue; //const值
}
std::string newDog("yebei");
std::string oldDog("wangjing");
NamedObject<int> p(newDog,2);
NamedObject<int> q(oldDog,25);
p = q; //error!
//const的数值无法被修改
//引用的指向也无法被修改
面对p=q的赋值操作,编译器的回应很简单“办不了”。原因:
(1)C++不允许让reference改指向不同的对象;
(2)在class内,更改const成员也是不合法的。
6、term6:Explicitly disallow the use of compiler-generated functions you do not want
由条款5知道,当你声明一个类的时候,他会自动为你生成默认的构造函数,析构函数,copy构造函数,copy assignment操作符。
如果你不想要你的类支持copy构造函数,copy assignment操作符的操作,能想到的是在private中对其进行单独声明,这样别人就不能随意调用,但是这个方法并不绝对安全,因为成员函数以及friend函数还是可以对其进行调用。可以打一个不恰当的比方,默认的操作,就像一个死皮赖脸的追求者,你不说话,他就认为你是默认;所以对于这些“厚脸皮”的追求者,大声说“滚!!”他们才听得懂。
举个栗子:
#include <iostream>
#include <string>
class Student{
public:
//自定义构造函数
Student(int id,const std::string& name):studentID(id),studentName(name){
std::cout <<" Parameterized constructor called\n";
}
//自定义析构函数
~Student(){
std::cout << "Custom destructor called\n"
}
//拒绝默认构造函数
Student() = delete;
//拒绝默认拷贝构造函数
Student(const Student&) = delete;
//拒绝默认的拷贝赋值操作服
Student& operator=(const Student&) = delete;
void displayInfo()const{
std::cout << "Student ID: " << studentID <<", Name:" << studentName <<std::endl;
}
private:
int studentID;
std::string studentName;
}
7、term7:Declare destructors virtual in polymorphic base classes
带有多态性质的基类应该声明一个virtual析构函数;如果class内带有任何virtual函数,它就该拥有一个virtual析构函数;
举个反面栗子:
class TimeKeeper{
public:
TimeKeeper();
~TimeKeeper();
...
}
class AtomicClock:public TimeKeeper{...} //原子钟
class WaterClock:public TimeKeeper{...} //水钟
class WristWatch:public TimeKeeper{...} //腕表
TimeKeeper* getTimeKeeper();
//返回一个指针,指向一个TimeKeeper派生类的动态分配对象
TimeKeeper* ptk = getTimeKeeper();
//从TimeKeeper继承体系获得一个动态分配的对象
...
delete ptk;
//释放他,避免资源泄漏
先来说一下这为什么是一个反面教材,第二个getTimeKeeper()返回的指针指向一个derived对象,而那个对象却被一个base class指针删除。因为析构的顺序,是先析构derived对象,然后再析构base对象。这就会造成一个“局部销毁”的现象,进而导致一系列不好的后果:资源泄漏,败坏数据结构等诸多不便。
消除这个问题的做法很简单:给base class内声明一个virtual对象,这样删除对象,就会“如你所愿”。
举个栗子:
class TimeKeeper{
public:
TimeKeeper();
virtual ~TimeKeeper();
...
};
TimeKeeper* ptk = getTimeKeeper();
//从TimeKeeper继承体系获得一个动态分配的对象
...
delete ptk; //行为正确
virtual函数的目的就是允许derived class的实现得以客制化;任何class只要带有virtual函数几乎确定应该也有一个virtual析构函数。
8、term8:Prevent exceptions from leaving destructors
C++并不禁止析构函数吐出异常,但是他也不鼓励你这样做。
原因有以下几点:
- 异常安全性和资源管理:析构函数的主要职责是释放对象所占用的资源,如内存、文件句柄等。如果析构函数抛出异常,可能会导致这些资源无法正确释放,从而造成资源泄漏。因此,C++要求析构函数在执行过程中不能抛出异常。
- 避免多重删除:如果一个对象的析构函数抛出异常,而这个异常没有被捕获和处理,那么这个对象可能会被多次删除。这会导致未定义的行为,因为一个已经被删除的对象不应该再次被删除。
- 避免破坏栈展开:在C++中,当一个异常被抛出时,会启动栈展开(stack
unwinding)过程。如果析构函数抛出异常,那么这个异常可能会在栈展开的过程中被抛出,这会导致程序的执行流程变得不可预测。 - 减少异常处理复杂性:如果析构函数可以抛出异常,那么在处理这些异常时需要考虑更多的情况,这会增加代码的复杂性。
首先举一个反面栗子:
class Widget{
public:
...
~Widget(){...} //假设这个可能会吐出一个异常
};
void doSomething(){
std::vector<Widget> v;
...
}; //v在这里自动销毁;
- 当vector v被销毁时,可能会导致资源无法被正确释放;
- 当函数发生析构时,如果报出的异常没有被处理,可能就会导致这个对象被多次删除;
- 本身析构函数就是在释放资源,但是在抛出异常的过程中,会有栈展开的行为,这样会破坏栈展开的流程,程序的行为会变的不可预测;
- 析构函数抛出异常如何处理,这是代码需要考虑的事情,这样会增加代码的复杂性。
为了直观说明这个问题,假设你的析构函数必须执行一个动作,该动作可能会在失败时抛出异常,举个例子:使用class负责数据库连接:
class DBConnection{
public:
...
static DBConnection create(); //这个函数返回DBConnection对象;
void close(); //关闭联机;失败则抛出异常;
}
class DBConn{
public:
...
~DBConn(){
db.close();
}
private:
DBConnection db;
};
//客户写出这样的代码来进行调用
{
DBConn dbc(DBConnection::create());
...
}
只要调用close()成功,一切都好说;但是如果调用导致异常,析构函数会传播异常,这就会造成问题。
针对这类问题,提出3种解决办法;
解决办法1:
//如果close抛出异常,通过调用abort进行及时终止,强迫其结束;
DBConn::~DBConn()
{
try{db.close();}
catch(..){
//记录下对close的调用失败
std::abort();
}
}
这个方法的好处是阻止异常从析构函数中传播出去,不会让程序由不明确的行为发生。
解决办法2:
//吞下因调用close而发生的异常
DBConn::~DBConn()
{
try{db.close();}
catch(..){
//记录下对close的调用失败
}
}
方法一起码将有风险的程序进行终止;方法二,把问题掩埋,比方法一更加糟糕,不是很可取。
解决办法3:
//改头换面,重新设计DBConn接口
class DBConn{
public:
...
void close(){ //如果有需要客户可以自己进行调用
db.close();
closed = true;
}
~DBConn()
{
if(!closed){ //双重保险,关闭连接
try{
db.close();
}
catch(...){
//记录对close的调用失败
...
}
}
}
private:
DBConnection db;
bool closed;
}
如果某个操作可能会在失败时抛出异常,而又存在某种需要必须处理该异常,那么这个异常必须来自析构函数以外的某个函数。
9、term9:Never call virtual functions during construction or destruction
在构造和析构函数中使用virtual函数可能会出的问题。
- 1、运行时开销:虚函数调用的开销比非虚函数调用要大,因为它们需要使用虚函数表(vtable)来查找正确的函数实现。在构造和析构函数中频繁调用虚函数会增加额外的运行时开销。
- 2、派生类析构函数不正确调用:在构造和析构函数中使用虚函数时,如果派生类的析构函数没有正确地被调用,可能会导致资源泄漏或其他问题。例如,如果基类的析构函数中调用了虚函数,而派生类的析构函数没有正确地处理资源,那么资源可能会被泄漏。
- 3、不正确的多态行为:在构造和析构函数中使用虚函数可能会干扰多态行为。例如,如果在派生类的构造函数中调用了虚函数,而该函数在基类中没有正确地实现,那么可能会得到错误的结果。
- 4、与纯虚函数相关的问题:如果基类中定义了纯虚函数,而在派生类中没有实现该纯虚函数,那么在构造和析构函数中使用纯虚函数可能会导致编译错误。
先举一个反面栗子:
class Transaction{ //所有交易的base class
public:
Transaction();
virtual void logTransaction() const = 0;//日志记录,因类型不同,自身会有不同的操作
...
};
Transaction::Transaction() //base class构造函数的实现
{
...
logTransaction();
}
class BuyTransaction:public Transaction{ //derived class
virtual void logTransaction() const; //记录此交易类型
...
};
class SellTransaction:public Transaction{ //derived class
virtual void logTransaction() const; //记录此交易类型
...
};
//现在做出这个操作
BuyTransaction b;
关于父类以及子类存在这样一种情况,父类比子类先构造,而子类比父类先析构;所以在构造父类的时候,子类对象还未进行初始化,在析构父类的时候,子类已经被销毁。
构造BuyTransaction时,首先会调用Transaction构造函数,引发冲突的点是构造函数调用virtual函数logTransaction。你构造BuyTransaction b,一定是想调用BuyTransaction的logTransaction函数,但实际上,他只能调用Transaction类中的默认函数;非正式的说法:在base class 构造期间,virtual函数不是virtual函数。
解决方法:
在base class内将logTransaction函数改成non-virtual,并要求derived class构造函数传递必要信息给Transaction构造函数。
举个栗子:
class Transaction{
public:
expilcit Transaction(const std::string& logInfo);
void logTransaction(const std::string& logInfo) const; //non-virtual函数
...
};
Transaction::Transaction(const std::string& logInfo)
{
...
logTransaction(logInfo); //non-virtual调用
}
class BuyTransaction:public Transaction{
public:
//将log信息,传递给base class构造函数
BuyTransaction(parameters):Transaction(createLogString(parameters))
{...}
...
private:
static std::string createLogString(parameters);
};
在函数构造期间,你是无法使用virtual函数从base class向下调用;但是你可以令derived class 将必要的构造信息向上传递至base class构造函数来进行弥补。
这个条款的重要启示是,在构造和析构期间不要在base class中调用virtual函数,因为这类调用不会下降至derived class。
10、term10:Have assignment operators return a reference to *this
令赋值操作符(assignment),返回一个reference to *this。
在C++中,赋值操作符operator=的默认实现通常返回一个指向调用对象的引用。这意味着,当你对一个对象使用赋值操作符时,它会返回一个指向该对象的引用,以便你可以链式地调用其他操作符或函数。
举一个简单的栗子:
#include <iostream>
class MyClass {
public:
int value;
MyClass(int val = 0) : value(val) {}
MyClass& operator=(const MyClass& other) {
value = other.value;
return *this;
}
};
int main() {
MyClass obj1(10);
MyClass obj2(20);
obj1 = obj2; // 使用赋值操作符
std::cout << "obj1.value: " << obj1.value << std::endl; // 输出 20
return 0;
}
11、term11:Handle assignments to self in operator=
所谓的自我赋值,指得就是一个对象赋值给自己的简单行为,但这种看起来人畜无害的动作,在某些情形下却可能会使得你的代码崩溃。
举个栗子:
class Teacher{...};
Teacher w;
...
w = w
这就是自我赋值,虽然语法支持,但是愚蠢,毫无意义,而且还可能会出错。就好比我是GG_Bone,GG_Bone是我,所以我是GG_Bone,虽然看上去很愚蠢,实际上也不是很聪明。但是有的时候人会难以避免,会有一些潜在的自我赋值:
//潜在的自我赋值
a[i] = a[j]
*px = *py
举个有问题的栗子:
class student{...};
class teacher
{
...
private:
student* s;
};
teacher& teacher::operator=(const teacher& teach)
{
if(s != NULL)
{
delete s;
s = NULL;
}
s = new student(*teach.s);
return *this;//便于链式操作
}
上述代码就是一个典型的反面例子,如果*this 和teach是同一个对象,客户在删除s的时候,就会把teach也给删除了,这个时候,s就会指向一个被删除的对象,这肯定是不被允许的。
我们有三种方法可以避免这种问题:
方法一:证同检测
teacher& teacher::operator=(const teacher& teach)
{
if (this == &teach)
return *this;
//证同检测
if (s != NULL)
{
delete s;
s = NULL;
}
s = new student(*teach.s);
return *this;//便于链式操作
}
局限性:不能保证“异常安全性”。即,如果 new student 抛出异常,则 s 就会指向一个被删除的对象,这是一个有害指针,我们无法删除,甚至无法安全读取它。
方法二:记住原指针
teacher& teacher::operator=(const teacher& teach)
{
student* stu = s; //记住原指针
if(s != NULL)
{
delete s;
s = NULL;
}
s = new student(*teach.s); //如果抛出异常,s 也可以找回原来地址
delete stu; //删除指针
return *this;//便于链式操作
}
方法三:copy and swap
void swap(const teacher& teach)
{......}
teacher& teacher::operator=(const teacher& teach)
{
teacher temp(teach); //拷贝一个副本
swap(temp); //将副本和 *this 交换
return *this;//便于链式操作
}
交换操作不要考虑原本指针内容,可以保证赋值安全性,同时也能保证异常安全性。
12、term12:Copy all parts of an object
条款12的诉求主要是2点:
(1)Copying 函数应该确保复制“对象内的所有成员变量”以及所有的“base class”成分;
(2)不要尝试以某一个Copying函数来实现另一个Copying函数,若想要消除重复的代码,具体的做法就是创建一个新的成员函数给copy构造和copy assignment函数来调用。这个策略可以安全地消除copy构造函数和copy assignment操作符之间的重复代码。
举一个栗子:
#include<iostream>
using namespace std;
#include<string>
class Base
{
public:
Base(){}//默认构造
Base(int age, string name) :m_Age(age), m_Name(name)
{ cout << "this is base class 构造函数" << endl; };
Base(const Base& b)
{
this->m_Age = b.m_Age;
this->m_Name = b.m_Name;
cout << "this is base class copy 构造函数" << endl;
}
Base& operator=(const Base& b)
{
this->m_Age = b.m_Age;
this->m_Name = b.m_Name;
cout << "this is base class copy assignment 函数" << endl;
return *this;
}
virtual void showInfo() {}
int& getAge()
{
return this->m_Age;
}
string& getName()
{
return this->m_Name;
}
private:
int m_Age;
string m_Name;
};
class Son :public Base
{
public:
Son(){}//默认构造
Son(double sco, int age, string name):m_score(sco),Base(age,name)
{
cout << "this is Son class 构造函数" << endl;
};
Son(const Son& s):m_score(s.m_score), Base(s)
{
cout << "this is Son class copy 构造函数" << endl;
}
Son& operator=(const Son& s)
{
this->m_score = s.m_score;
Base::operator=(s);
cout << "this is Son class copy assignment 函数" << endl;
return *this;
}
virtual void showInfo()
{
cout << "分数:" << this->m_score << "\t年龄:" << this->getAge() <<
"\t姓名:" << this->getName() << endl;
}
private:
double m_score;
};
void test()
{
Son s1(100.0, 25, "zwh");//当然,你需要按照你子类的构造函数的参数顺序来实例化
Son s2;
s2 = s1;
Son s3(s1);
cout << "s1的Info:" << endl;
s1.showInfo();
cout << "s2的Info:" << endl;
s2.showInfo();
cout << "s3的Info:" << endl;
s3.showInfo();
}
int main(void)
{
test();
return 0;
}
运行结果:
this is base class 构造函数
this is Son class 构造函数
this is base class copy assignment 函数
this is Son class copy assignment 函数
this is base class copy 构造函数
this is Son class copy 构造函数
s1的Info:
分数:100 年龄:25 姓名:zwh
s2的Info:
分数:100 年龄:25 姓名:zwh
s3的Info:
分数:100 年龄:25 姓名:zwh
这种运行结果就是简洁的,优雅的,符合要求的。在每个derived class之前都先生成了一个base class。
雷点1:
Son(const Son& s):m_score(s.m_score), Base(s)
{
cout << "this is Son class copy 构造函数" << endl;
}
如果一开始就用初始化列表的方式给derived类中的 base class成分赋值,这是完全正确的;如果不赋值的话,在拷贝操作的时候就没法把Base class中的成分,此时通过拷贝生成的derived类中的成员属性就会乱码,
//反面例子
Son(const Son& s):m_score(s.m_score)
{
cout << "this is Son class copy 构造函数" << endl;
}
输出结果:
this is base class 构造函数
this is Son class 构造函数
this is base class copy assignment 函数
this is Son class copy assignment 函数
this is Son class copy 构造函数
s1的Info:
分数:100 年龄:25 姓名:zwh
s2的Info:
分数:100 年龄:25 姓名:zwh
s3的Info:
分数:100 年龄:3 姓名:
雷点2:用一个copying函数去实现另一个copying函数
//反面例子
Son(const Son& s):m_score(s.m_score)
{
Base::Base(s)//调用了base class的copying函数
cout << "this is Son class copy 构造函数" << endl;
}
直接不能运行,在Son的copy构造函数中,传入的参数本来就是 const Son& s,对于引用类型,必须在定义的时候初始化,而且不能重新赋值。所以在调用Base的copy构造函数初始化derived对象时中的Base class成分时,必须要在derived class的构造函数初始化列表中进行初始化。
4、总结
书山有路勤为径,学海无涯苦作舟。
5、参考
5.1 《Effective C++》
5.2 Effective C++ 条款11:在operator=中处理” 自我赋值“时的代码规范