Tips05:了解C++悄悄编写并调用哪些函数

编译器自动生成的函数包括



  1. 1. 构造函数(default)

  2. 2. 拷贝构造函数(copy)

  3. 3. 析构函数

  4. 4. 拷贝复制运算符(copy assignment)



如果一个class的成员变量含有reference(引用)类型,那么必须自己定义copy assignment操作符。



编译器自动构造的copy assignment的默认操作,是逐一调用成员变量的copy assignment,但是reference没有copy assignment,也不允许改变。因此,如果不自己定义copy assignment,编译器就会报错。



reference一样一旦赋值就不可更改的const类型也一样,只要class的成员变量含有referenceconst类型,就必须自定义copy assignment操作符。





Tips06:若不想使用编译器自动生成的函数,就该明确拒绝

编译器自动生成的函数包括



  1. 1. 构造函数(default)

  2. 2. 拷贝构造函数(copy)

  3. 3. 析构函数

  4. 4. 拷贝复制运算符(copy assignment)



如果不想让编译器,自动生成上述函数,并且禁止调用这些函数,该将该函数声明为private,并且不必实现。



通常,禁止的函数为2.拷贝构造函数和4.拷贝赋值运算符




Tips07:为多态基类声明virtual析构函数

这里给多态基类特殊标记,因为不是所有的基类都需要virtual析构函数的,正因为用到了多态,才有了virtual析构函数的必要。





一、为什么要为多态基类声明virtual析构函数。



先来看一段简单的小程序



class TimeKeeper{           
public:          
       TimeKeeper(void){}           
       ~TimeKeeper(void){           
              std::cout << "delete TimeKeeper" << std::endl;           
       }           
};

   

class AtomicClock : public TimeKeeper{
public:
       ~AtomicClock(void){
              std::cout << "delete AtomicClock" << std::endl;
       }
};



客户端代码:    

TimeKeeper *timer = new AtomicClock();
delete timer;



p_w_picpath003

可以发现,程序并没有调用子类AtomicClock的析构函数,这就导致AtmoicClock对象并没有被销毁,而其父类TimeKeeper对象却被正常销毁,造成一种“局部销毁”,形成所谓的内存泄露。



现在为TimerKeeper的析构函数加上关键字“virtual

class TimeKeeper{       
public:      
       TimeKeeper(void){}       
       virtual ~TimeKeeper(void){       
              std::cout << "delete TimeKeeper" << std::endl;       
       }       
};

 





p_w_picpath006

可以看到,子类AtomicClock的析构函数也被正常调用,这样才是完成的对象销毁。





事实上,任何带有virtual方法的class,都需要virtual析构函数。很简单,既然class带有virtual方法,其目的就是想让其子类重载该virtual方法,也就说明该class是多态基类,既然是多态基类,只有带有virtual析构函数,才不会出现“局部销毁”。



二、不要继承没有virtual析构函数class,尤其是标准库的类。



三、如果一个class,不被任何类继承,不要将其析构函数设为virtual      



有时,为了方便,不管该类是否是基类(base class),都将其析构函数设为virtual。但是,设为virtual结构是有代价的。

class Point{
public:
       Point(int x, int y);
       ~Point();
private:
       int x, y;
};

如果int32bits,那一个Point对象就是64bits。如果Point的析构函数virtual,那么Point的大小就不止64bits

对象需要携带额外的信息,来决定运行期间到底运行那个virtual函数,而决定权是在一个叫vptr(虚函数表指针)的手上,vptr指向一个由函数指针构成的数组——vtbl(虚函数表)。每个带有virtual函数的class都有一个vtbl,例如virtual析构函数Point。当对象调用某一virtual函数时,实际被调用的是vptr指向的那个vtbl

因此,在32位机上,2*32bits(int)+32bits(vptr) = 96bits。无论何种类型的指针,大小均为32bits

64位机上,2*64bits(int)+64bits(vptr) = 128bits。同理在64位机上,指针为64bits

这就导致C++对象不再和其他语言(C)有着相同的结构,也就不再可能把他传递(或接受)其他语言缩写的函数。

总的来说,就是丧失的移植性

PS:其实我一直不太理解,书上这段话的含义,virtual本来就是C++的关键字,既然使用它,还谈什么和传递给CC本来就不认识virtual






四、如果class没有任何一个纯虚函数,却又想让该classabstract class

想让class成为abstract class(抽象类)的原因是,不想让该class被实例化,既不能new。但该类的所有普通方法均要实现,这又不构成抽象类的条件。



方法就是,将析构函数设置为纯虚函数(pure virtual)


class AWOV{
public:
       AWOV(void);
       virtual ~AWOV(void) = 0;
};
                     
class Child : public AWOV{
public:
       Child(void);
       ~Child(void);
};



客户端代码:


AWOV *a = new Child();
delete a;

如果仅仅是这样,运行程序,你会发现,程序报错



p_w_picpath009

这里就涉及到析构函数的运行方式,深层派生(most derived)class的析构函数最先被调用,然后是每一个base class的析构函数被调用。在本例中,当ddelete a时,Child的析构函数被调用,然后是AWOV的析构函数,但是AWOV纯虚析构函数并没有实现,因此造成链接错误。



因此,就算析构函数被定义为pure virtual,它也一定要有实现!从资源释放的角度来看,析构函数是释放资源和内存的关键,因此必须要实现它。



PS:为什么普通的方法声明为pure virtual后,就不要实现了呢?



  1. 1. 不需要实现



  1. 2. 实现也没用



普通的方法声明为pure virtual,就是希望有子类区重载该方法,如果子类重载该pure virtual方法,那子类也是抽象类,无法被实例化。因此,必须有子类重载该pure virtual方法,一旦该pure virtual方法被重载,那它的实现根本不会被调用!


PS:构造函数一定不能是virtual,因为用到虚函数,就要使用虚函数表(Virtual Table)。但此时,构造函数并没执行,当前对象还没构造出来,因此虚函数表更是不存在。


Tips08:别让异常逃离析构函数

这条Tips的含义就是,不要在析构函数中抛出异常。释放对象的过程会调用析构函数(这点没什么好说的),如果连续释放多个对象,而又有不止一个对象的析构函数抛出异常,就会导致程序中出现多个异常。事实上,在两个异常的存在下,C++程序不是结束执行就是产生不明确结果。



一、对会抛出异常的操作,class应提供普通的方法,这样客户才能有机会处理异常。



对于某些操作,当程序执行时,必须调用这些操作。例如,关闭数据库的连接。一般为了防止客户忘了关闭连接,会将关闭操作放在析构函数中。根据Tips的含义,如果发生异常,析构函数只能使程序关闭,或者吞掉该异常,让程序继续执行。但这两种方法,都会使客户无法对异常进行处理/响应



因此,唯一可行的办法,就是让客户自己处理异常,一旦程序因为异常而不能正常运行,客户就应该知道自己处理异常。所以,class应该为会抛出异常的操作提供方法。





析构函数对于会抛出异常操作的处理方式。



有两种处理方式



  遇到异常就结束程序,用过abort完成。

try{
}catch (...){       
       std::abort();       
}


  遇到异常就吞下

try{
}catch (...){       
             制作运转记录,记下对close的调用失败
}