一、了解C++默默编写并调用了哪些函数:
C++编译器会自动为一个空类生成8个成员函数:
- default构造函数
- 析构函数
- copy构造函数
- copy(赋值)运算符重载
- 取址运算符重载
- 常量取址运算符重载
- 移动构造函数(C++11)
- 移动赋值运算符重载(C++11)
其中,当你定义了某个函数,C++便不会生成相应的成员函数,值得注意的是,从C++11起,如果一个类有析构函数,为其生成拷贝构造函数和拷贝赋值运算符的特性被弃用了。
对于一些特殊情况,C++会拒绝生成这些成员函数,例如:reference(引用)的拷贝,base类的某个函数为private,derived类不会生成相应的函数
二、若不想使用编译器生成的函数,就该拒绝
若某个对象应具有唯一性,那对它进行copy显然是不合理的,这种情况下,自行定义或不定义copy,都没有禁止copy行为,正确的做法有两种:
将不想使用但必须拥有的成员函数声明为private而不去实现它:
1
2
3
4
5
6
7class houseforsale{
private:
houseforsale(const houseforsale& rhs); //声明而不实现
houseforsale& operator = (const houseforsale& rhs);
public:
......
}利用访问限制,定义一个base类,该类中必须具有的成员函数声明为private:
1
2
3
4
5
6
7
8class uncopyable{
private:
uncopyable(const uncopyable&);
uncopyable& operator = (const uncopyable&);
}
class houseforsale : public uncopyforsale{
......
}
三、为多态基类声明virtual析构函数
当一个base类指针指向derived类对象时,若直接delete掉这个基类指针,会导致derived类资源未被释放,从而使得内存泄漏,故而在写希望产生多态效果的基类时,要将基类析构函数写为虚函数:
1
2
3
4
5
6
7
8class base{
public:
base(){}
virtual ~base(){}
}
class derived : public base{
......
}虚函数为C++运行时多态(同名的函数,不同的效果)提供了支持,其基本原理是,具有虚函数的类中,除了成员数据部分,还会放一个虚函数表指针,指向一个放置虚函数指针的虚函数表,在运行阶段,程序通过虚函数表指针,找到虚函数表,再找到相应的函数,完成多态。
虚函数表指针是一个指针,是具有大小的,在32位系统中指针大小为32位bit(4byte),64位系统中为64位bit(8byte),因此,虽然虚函数很强大,但不必使每个类都有虚函数,通常做法是:当一个类中必不可少有一个虚函数时,才声明虚析构函数。
四、不要使析构函数抛出异常
C++不喜欢析构函数吐出异常,比如
1 | class Weight{ |
当v销毁时,它会销毁vector中的每一个Weight,但若有两个Weight在销毁时都抛出异常,会使程序终止或导致不明确行为,所以析构函数的异常应当自我消化,在捕捉到异常时,应当终止程序或记录异常采用鸵鸟策略,绝不应当在析构函数中抛出:
1 | class example{ |
另外,作为软件开发人员,我们可将析构中可能包含异常的行为写成一个函数,供用户调用,使得用户去处理这个异常(甩锅给客户),若析构时发现用户未调用该函数,则在析构时处理。
1 | class example{ |
五、不在构造或析构过程中调用virtual函数
举例游戏场景:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24class Game {
public:
Game();
~Game();
virtual void Log() const = 0;
};
Game::Game(){
Log();
std::cout << "Game() Log()" << std::endl;
};
Game::~Game(){
Log();
std::cout << "~Game() Log()" << std::endl;
};
class Kill : public Game {
public:
virtual void Log() const override { //const override表示重写父类虚函数
std::cout << "Kill Log()" << std::endl;
}
};
int main(void){
Kill a;
return 0;
}这段代码在编译时会报waning,运行时也会出错,因为是Log函数为纯虚函数,无法对它产生调用,因为在派生类构造阶段,会先调用父类构造,再调用成员对象构造,最后才是自己的成员变量构造,故而在第一阶段时,父类的构造函数调用的是它本身的纯虚函数,而不是派生类的函数
同样的道理也适用于析构函数,众所周知,析构函数和构造函数的顺序是反着来的
更奇怪的情况下,可能会让人摸不着头脑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Game{
public:
Game(){init();}
virtual void Log() const = 0;
private:
void init(){
Log();
}
}
class Kill : public Game {
public:
virtual void Log() const override { //const override表示重写父类虚函数
std::cout << "Kill Log()" << std::endl;
}
};对于这段代码,未经优化的编译器不会有任何反应(据说Visual Studio集成了整本Effective C++给出的建议),然而最终运行时,会报错:Kill调用了错误版本的Log()函数,使人在风中凌乱……所以,一定要确定自己的代码中,构造和析构函数中不调用虚函数
那么,我们想实现类似的功能该怎么办呢,一个好的方法将原函数写为非虚的,在构造时传参到Base类中
1 | class Game{ |
六、operator “ = “ 的重载
为了使自定义重载运算符和基本类型运算符保持一致,我们通常使” = “返回一个当前对象的reference to *this
对于一个含指针的类Weight,我们对它的赋值函数进行三次改版。
先是Weight类:
1
2
3
4
5class Weight{
Bitmap *pb;
public:
Weight& operator = (const Weight& rhs);
}第一版赋值函数:
1
2
3
4
5
6//version_1
Weight& Weight::operator = (const Weight& rhs){
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}这种情况下,当
lhs == rhs
时,逻辑上,这个函数已经出错了!Bitmap将指向一个已被删除的对象!由此引出第二版赋值构造函数:1
2
3
4
5
6
7//version_2
Weight& Weight::operator = (const Weight& rhs){
if(rhs == *this) return *this;
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}这个版本中,我们对于传入参数和自身进行了比较,有效地避免了
rhs == lhs
的问题。但是,这个新版本依旧有问题:若是new Bitmap时出现异常(可能由于内存不够,可能由于copy抛出异常),由于先删除了pb后赋值,对象内的指针会指向一块被删除的Bitmap,为此,出现了第三版:
1
2
3
4
5
6
7
8//version_3
Weight& Weight::operator = (const Weight& rhs){
if(rhs == *this) return *this;
Bitmap *tpb = pb;
pb = new Bitmap(*rhs.pb);
delete tpb;
return *this;
}这一版就没有上述两版的问题了,它既有证同测试,又有防异常行为,prefect version达成了!
对于性能提升上,Ecpp还给出了一个新的版本:
1
2
3
4
5
6
7void swap(Weight &rhs){
...
}
Weight& Weight::operator = (Weight rhs){
swap(rhs);
return *this;
}它利用了传值时会调用拷贝构造这一行为,其思想和移动很像,这应该是“移动”的前身了!
七、复制对象时切勿忘记复制它的每个部分
- copy系函数应当确保复制对象内所有的成员变量和所有Base类的成分
- 不要使copy构造函数和copy operator函数相互调用,通常将它们共通的部分写在一个函数中去调用。