Tips05:了解C++悄悄编写并调用哪些函数
编译器自动生成的函数包括
1. 构造函数(default)
2. 拷贝构造函数(copy)
3. 析构函数
4. 拷贝复制运算符(copy assignment)
如果一个class的成员变量含有reference(引用)类型,那么必须自己定义copy assignment操作符。
编译器自动构造的copy assignment的默认操作,是逐一调用成员变量的copy assignment,但是reference没有copy assignment,也不允许改变。因此,如果不自己定义copy assignment,编译器就会报错。
和reference一样一旦赋值就不可更改的const类型也一样,只要class的成员变量含有reference和const类型,就必须自定义copy assignment操作符。
Tips06:若不想使用编译器自动生成的函数,就该明确拒绝
编译器自动生成的函数包括
1. 构造函数(default)
2. 拷贝构造函数(copy)
3. 析构函数
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;
可以发现,程序并没有调用子类AtomicClock的析构函数,这就导致AtmoicClock对象并没有被销毁,而其父类TimeKeeper对象却被正常销毁,造成一种“局部销毁”,形成所谓的内存泄露。
现在为TimerKeeper的析构函数加上关键字“virtual”
class TimeKeeper{ public: TimeKeeper(void){} virtual ~TimeKeeper(void){ std::cout << "delete TimeKeeper" << std::endl; } };
可以看到,子类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; };
如果int是32bits,那一个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++的关键字,既然使用它,还谈什么和传递给C,C本来就不认识virtual。
四、如果class没有任何一个纯虚函数,却又想让该class为abstract 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;
如果仅仅是这样,运行程序,你会发现,程序报错
这里就涉及到析构函数的运行方式,最深层派生(most derived)的class的析构函数最先被调用,然后是每一个base class的析构函数被调用。在本例中,当ddelete a时,Child的析构函数被调用,然后是AWOV的析构函数,但是AWOV的纯虚析构函数并没有实现,因此造成链接错误。
因此,就算析构函数被定义为pure virtual,它也一定要有实现!从资源释放的角度来看,析构函数是释放资源和内存的关键,因此必须要实现它。
PS:为什么普通的方法声明为pure virtual后,就不要实现了呢?
1. 不需要实现
2. 实现也没用
普通的方法声明为pure virtual,就是希望有子类区重载该方法,如果子类不重载该pure virtual方法,那子类也是抽象类,无法被实例化。因此,必须有子类重载该pure virtual方法,一旦该pure virtual方法被重载,那它的实现根本不会被调用!
Tips08:别让异常逃离析构函数
这条Tips的含义就是,不要在析构函数中抛出异常。释放对象的过程会调用析构函数(这点没什么好说的),如果连续释放多个对象,而又有不止一个对象的析构函数抛出异常,就会导致程序中出现多个异常。事实上,在两个异常的存在下,C++程序不是结束执行就是产生不明确结果。
一、对会抛出异常的操作,class应提供普通的方法,这样客户才能有机会处理异常。
对于某些操作,当程序执行时,必须调用这些操作。例如,关闭数据库的连接。一般为了防止客户忘了关闭连接,会将关闭操作放在析构函数中。根据Tips的含义,如果发生异常,析构函数只能使程序关闭,或者吞掉该异常,让程序继续执行。但这两种方法,都会使客户无法对异常进行处理/响应
因此,唯一可行的办法,就是让客户自己处理异常,一旦程序因为异常而不能正常运行,客户就应该知道自己处理异常。所以,class应该为会抛出异常的操作提供方法。
二、析构函数对于会抛出异常操作的处理方式。
有两种处理方式
① 遇到异常就结束程序,用过abort完成。
try{ }catch (...){ std::abort(); }
② 遇到异常就吞下
try{ }catch (...){ 制作运转记录,记下对close的调用失败 }
本文出自 “若羽☆初见” 博客,请务必保留此出处http://youngcold.blog.51cto.com/6366865/1395991