文章目录
- 一、了解C++默默编调用了哪些函数
- 1、编译器默认调用的函数
- 2、请记住
- 二、若不想使用编译器自动生成的函数,就该明确拒绝
- 1、拒绝编译器自动生成的函数
- 2、请记住
- 三、为多态基类声明virtual析构函数
- 1、普通的基类的指针指向派生类的对象
- 2、virtual析构函数的作用
- 3、请记住
- 四、别让异常逃离析构函数
- 1、析构函数中发生异常有什么问题?
- 2、请记住
- 五、绝不在构造和析构过程中调用virtual函数
- 1、构造和析构过程中调用virtual函数有什么问题?
- 2、请记住
- 六、令operator=返回一个reference to*this
- 1、连锁赋值
- 2、请记住
- 七、在operator=里处理"自我赋值"
- 1、正确进行自我赋值
- 2、请记住
- 八、复制对象时务忘其每一个成分
- 1、正确复制对象
- 2、请记住
一、了解C++默默编调用了哪些函数
1、编译器默认调用的函数
编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符,以及析构函数。
class Empty
{
public:
Empty(){}
Empty(const Empty& rhs){}
Empty& operator=(const Empty& rhs){}
~Empty(){}
};
Empty e1; // default构造函数,析构函数。
Empty e2(e1); // copy构造函数。
e2 = e1; // copy assignment操作符
当需要这些函数时,这些函数会被编译器创建,这几个是最常见的、也是用的最多的。那么编译器创建的这些函数都是做了什么?
首先,如果没有构造函数,编译器将会创建一个默认构造函数,由它来调用基类和non-static成员变量的构造函数。
析构函数是否是虚函数,继承基类,如果没基类,那么默认是non-virtual。析构函数会调用基类和non-static成员变量的析构函数。编译器创建的拷贝构造函数和赋值构造函数是个浅拷贝。
在编译器创建的复制构造函数和赋值操作符中,给成员变量初始化或赋值,会调用成员变量的赋值构造函数和赋值操作符。
2、请记住
编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符,以及析构函数。
二、若不想使用编译器自动生成的函数,就该明确拒绝
1、拒绝编译器自动生成的函数
所有编译器产出的函数都是public。为了阻止这些函数被创建出来,你得自己声明它们,但这里并没有什么需求使你必须将它们声明为public。有的时候拷贝构造函数和赋值操作符是不应该使用的,所以可以将copy构造函数或copy assignment操作符声明为private或者使用delete。
class HomeForSale
{
public:
/....../
private:
HomeForSale(const HomeForSale&);
HomeForSale& operator=(const HomeForSale&);
};
// C++11中可以使用delete关键字
class HomeForSale
{
public:
HomeForSale(const HomeForSale&) = delete;
HomeForSale& operator=(const HomeForSale&) = delete;
};
将错误提前到编译阶段是最好的,毕竟越早出现错误越好。可以通过继承来实现,设计一个不可以复制的类。
class Uncopyable
{
protected:
Uncopyable() {} // 允许derived对象构造和析构
~Uncopyable() {}
private:
Uncopyable(const Uncopyable&); // 但阻止copying
Uncopyable& operator=(const Uncopyable&);
};
class HomeForSale:private Uncopyable
{
/......./ // class 不再声明copying函数
};
2、请记住
为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为private并且不予实现或者使用delete关键字。使用像Uncopyable这样的base class也是一种做法。
三、为多态基类声明virtual析构函数
1、普通的基类的指针指向派生类的对象
#include <iostream>
using namespace std;
class Virtualbase
{
public:
void Demon() { cout << "this is Virtualbase class" << endl; };
void Base() { cout << "this is farther class" << endl; };
};
class SubVirtual :public Virtualbase
{
public:
void Demon()
{
cout << "this is SubVirtual!" << endl;
}
void Base()
{
cout << "this is subclass Base" << endl;
}
};
void main()
{
Virtualbase* inst = new SubVirtual();
// 如果析构函数不是虚函数,将不会调用当前指针指向对象的函数。
inst->Demon();
inst->Base();
/....../
}
/*
运行结果:
this is Virtualbase class
this is farther class
*/
#include <iostream>
using namespace std;
class Virtualbase
{
public:
virtual void Demon() { cout << "this is Virtualbase class" << endl; };
virtual void Base() { cout << "this is farther class" << endl; };
};
class SubVirtual :public Virtualbase
{
public:
void Demon()
{
cout << "this is SubVirtual!" << endl;
}
void Base()
{
cout << "this is subclass Base" << endl;
}
};
void main()
{
Virtualbase* inst = new SubVirtual();
// 如果析构函数是虚函数,就会调用当前指针指向对象的函数了。
inst->Demon();
inst->Base();
/....../
}
/*
运行结果:
this is SubVirtual!
his is subclass Base
*/
2、virtual析构函数的作用
当derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,其结果未有定义——实际执行时通常发生的是对象的derived成分没被销毁,而其base class成分通常会被销毁,于是造成一个诡异的“局部销毁”对象。
这是因为在使用这些类时,往往是通过基类指针或者引用使用的(类的实例在堆上),通过delete指针析构对象时,这时如果析构函数不是虚函数,将不会调用当前指针指向对象的析构函数。这是多态的原理。同理可知,要实现多态的函数,在基类也要声明为虚函数。
【Note】:
(1)当一个类不用做基类时,如果把其析构函数声明为虚函数是个馊主意。因为虚构函数是通过虚函数表调用的,在调用虚函数时多一步指针操作;除此之外,其对象占用的内存空间也会多一个虚函数指针。
3、请记住
- polymorphic(带多态性质的)base classes应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
- Classes的设计目的如果不是作为base class使用,或不是为了具备多态性(如条款06-2中的基类Uncopyable),就不该声明virtual析构函数。
四、别让异常逃离析构函数
1、析构函数中发生异常有什么问题?
class DBConnection {
public :
/....../
static DBConnection create(); // 这个函数返回DBConnection 对象
void close(); //关闭联机;失败则抛出异常。
};
class DBConn { // 这个class用来管理DBConnection 对象
public :
/....../
~DBConn() // 确保数据库连接总是会被关闭
{
db.close();
}
private :
DBConnection db;
};
如果调用close成功,则一切都美好。但是如果出现异常,DBConn会抛出异常,也就是允许这个异常离开析构函数,这样会传播异常。
一个比较好的策略是重新设计DBCoon接口,是客户能对可能出现的异常做出反应。例如DBConn可以自己提供一个close函数,可以给客户一个机会来处理“因该操作而发生的异常”。DBConn也可以追踪其所管理的DBConnection是否已经关闭,并在答案为否的情况下由其析构函数关闭,这样可以防止遗失数据库连接。但是如果DBConnection的析构函数调用close失败,问题又回到了起点。
class DBConn {
public:
.....
void close() //供客户使用的新函数
{
db.close();
closed = true;
}
~DBConn()
{
if (!closed) {
try { db.close(); }
catch( ... ){
// 日志
....
}
}
}
private:
DBConnection db;
bool closed;
};
2、请记住
- 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
- 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。
五、绝不在构造和析构过程中调用virtual函数
1、构造和析构过程中调用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;
/....../
};
//考虑下面语句
BuyTransaction b;
由于base class 构造函数的执行更早于derived class构造函数,当base class构造函数执行时derived class的成员变量尚未初始化。在derived class 对象的base class构造期间,对象的类型是base class而不是derived class。
解决方案:在base class 内将virtual函数改为non-virtual,然后要求derived class构造函数传递必要信息给base class构造函数,而后base class 构造函数就可以安全的调用non-virtual函数了。如下:
class Transaction
{
public:
//单参数构造函数,最好使用explicit禁止其进行隐式类型转换
explicit Transaction (const std::string& logInfo);
//non-virtual函数
void logTransaction (const std::string& logInfo) const;
/....../
};
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
static std::string createLogString(parameters);
};
2、请记住
- 在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数的那层)。这就是所谓的:virtual函数在构造/析构期间的“失常表现”,也即,在此期间,virtual函数不是virtual函数。
六、令operator=返回一个reference to*this
1、连锁赋值
int x, y, z;
x = y = z = 15; // 赋值连锁形式
// 赋值采用右结合律,所以上述连锁赋值被解析为:
x = (y = (z = 15));
这里15先被赋值给z,然后其结果(更新后的z)再被赋值给y,然后其结果(更新后的y)再被赋值给x。为了实现“连锁赋值”,赋值操作符必须返回一个reference指向操作符的左侧。
class Widget
{
public:
/....../
Widget &operator=(const Widget &rhs)
{
/....../
return *this;
}
};
2、请记住
- 令赋值操作符返回一个reference to *this。
七、在operator=里处理"自我赋值"
1、正确进行自我赋值
class Widget
{
public:
void swap(const Widget& rhs);//交换rhs和this
Widget& operator=(const Widget& rhs)
{
Widget tmp(rhs); // 赋值一份数据
swap(tmp) // 交换
return *this; // 临时变量会自动销毁
}
int *p;
};
2、请记住
- 确保当对象自我赋值时operator=有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。
- 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
八、复制对象时务忘其每一个成分
1、正确复制对象
在一个类中,有两个函数可以给复制对象:拷贝构造函数和赋值操作符,统称为copying函数。如果我们自己不编写者两个函数,编译器会帮我们实现这两个函数,编译器生成的版本会将对象的所有成员变量做一份拷贝。编译器生成的copying函数的做法通常是浅拷贝。如果我们自己实现了copying函数,编译器就不再帮我们实现。但是编译器不会帮我们检查copying函数是否给对象的每一个变量都赋值。
class PriorityCustomer : public Cutsomer
{
public:
PriorityCustomer()
{
cout<<"PriorityCustomer Ctor"<<endl;
}
PriorityCustomer(const PriorityCustomer& rhs)
:priority(rhs.priority)
{
cout<<"PriorityCustomer Copy Ctor"<<endl;
}
PriorityCustomer& operator=(const PriorityCustomer& rhs)
{
cout<<"PriorityCustomer assign operator"<<endl;
priority=rhs.priority;
return *this;
}
private:
int priority;
};
PriorityCustomer(const PriorityCustomer& rhs)
:Cutsomer(rhs),priority(rhs.priority)
{
cout<<"PriorityCustomer Copy Ctor"<<endl;
}
PriorityCustomer& operator=(const PriorityCustomer& rhs)
{
cout<<"PriorityCustomer assign operator"<<endl;
Cutsomer::operator=(rhs);
priority = rhs.priority;
return *this;
}
2、请记住
- Copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分”。
- 不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。