在看《Effective C++》这本书的过程中,我无数次的发出感叹,这他妈写得太好了,句句一针见血,直接说到点上。所以决定把这本书的内容加上自己的理解写成5篇博客,我觉得不管你是否理解这些条款,都值得你先记下来。下面的索引对应的是书中的章节。
11:如果class内动态配置有内存,请为此class声明一个copy constructor和一个assignment运算符
12:在constructor中尽量initialization动作取代assignment工作
13:initialization list中的members初始化次序应该和其在class内的声明次序相同
14:总上base class拥有virtual destructor
15:令operator =传回*this的reference
16:在operator=中为所有data member赋值
17:在operator =中检查是否自己赋值给自己
11:如果class内动态配置有内存,请为此class声明一个copy constructor和一个assignment运算符。
默认的copy constructor和operator=不会对类的每个data member一一赋值,而是一个简单引用,让左边的对象指向右边对象所指的对象。再也没有起动作,如果这个类动态分配了内存,比如左边对象本来指向一块内存A,现在左边的对象指向右边对象所指的内存B了,而再也没有其他对象指向内存A,由于它是动态分配,不会自己回收,所以就出现内存泄露,还有就是两个对象指向同一块内存,如果其中一个对象出了其作用域,那么其析构函数将自动调用,其动态分配的内存将被回收,现在另一个对象却指向一块已经被回收的内存,只要一调用这个对象的数据,就会出现不可知的异常,还有一点值得一提,就算一个对象没有指向任何地址或是它所指的地址已经被回收了,在调用它的方法的时候,只要它的方法没有使用它的data member(肯定不存在),就不会出现任何问题,因为方法的内存是和对象类型一起分配,实例化一个对象的时候不会为方法分配内存,只会为data member及其他一些指针分配内存,如指向父类的指针,指向虚拟表的指针等。
如11所述,如果不声明那两个方法,在方法调用的时候也会出现问题,当这个对象是以传值的方式被调用时,会产生一个临时变量,这个临时变量会引用这个对象,当方法执行完成,这个临时变量超出它的作用域,析构函数被调用,这个对象就这样被销毁了。所以你必须遵守这一条规则。
12:在constructor中尽量initialization动作取代assignment工作
对象的构造分两个阶段:
1:data member被初始化
2:被调用的构造函数执行起来
如果在构造函数中对data member一一赋值,那么先要调用data member的构造函数,如果你没有为data member赋初值,那么调用的是默认的构造函数,如果你赋了初值调用的是copy constructor,但是我的编译器不允许data member在定义的时候赋初值,那么就是调用默认的构造函数,当你在构造函数内为data member赋值的时候调用operator =,相当于你调用了一次constructor和一次operator =,而initialization 只调用一次copy constructor,因为在data member初始化的时候已经为data member赋值了,在构造函数里面就不用为data member赋值了,经常会遇到这样的面试题:一个data member在定义的时候给他一个初始值,又在构造函数内赋另一个值,请问这个data member现在的值是多少?还有一些就是base class中的一个data member,多处赋值,然后问题最后它的值是多少?只要记住父类的构造函数在子类的构造函数之前执行,初始化参数在构造函数之前执行。
总之一句话initialization效率比在构造函数中赋值的效率高,如果data member很多且需要初始化成同一个值,而且效率不是那么重要的话,可以在构造函数中用连等式赋值,这样会清晰明了一点。效率不是永远都放在第一位的,代码的可读性也很重要。82法则还记得吗,我曾经因为多写了一个if,在代码评审中被批评,理由就是100万访问的时候会影响效率,在两家公司遇到过这种情况,什么都是这个理由,百万级访问时会影响效率。
13:initialization list中的members初始化次序应该和其在class内的声明次序相同
有时候data member的初始化是依赖别的data member的,那么data member的初始化顺序就必须弄清楚。data member的初始化顺序与其在在initialization中出现的顺序无关,只与它们定义的顺序有关,先定义的data member会在initialization中先初始化,在destructor中后析构,先初始化的data member后析构,所以base data member后析构,跟栈中的变量一样先定义的变量后析构一样。如果类继承多个类,那么base data member的初始化顺序由继承的先后顺序决定,先继承的先初始化。
14:总上base class拥有virtual constructor
在继承关系中,是调用父类的方法还是调用子类的方法,这个动态的实现是由虚拟函数来决定的,含有虚拟函数的类都有一个虚拟函数表,如果父类中的方法是virtual的,如果没有子类没有覆写这个方法那么就是直接继承过来,不管子类还是父类调用这个方法产生的结果是一样的,如果子类覆写了这个方法,那么子类的虚拟表中存的就是子类方法的地址,如果一个父类的指针指向子类,如果方法被子类覆写了,那么调用的方法就是子类的方法,如果没有覆写那么就是调用父类的方法。如果父类的方法不是virtual的,而且子类有一个一样的方法,那么父类的方法不会被子类覆写,也就是说:父类指向子类的指针调用的将不会是子类的方法,而是父类的方法。同样的父类的析构函数不是virtual的,那么在delete 这个指针的时候就会出现不可知的情况,反正之类的构造函数是不会调用的,所以当你决定让一个类成为父类,那么就让他的destructor为virtual。
但是也不需要让每一个类的destructor成为virtual的,因为含有virtual方法的类都有一个指向virtual table的指针,会让对象变大,如果对象本来就不太的话可能会出现成本翻倍的情况。只有当class中含有至少一个虚拟方法时才让他的析构函数成为虚拟的。
15:令operator =传回*this的reference
先看一个等式:(A=B)=C,为了实现这种连等式,operator=肯定是不能返回void的,你可以为*void赋值,但是你不能为void赋值,operator=的返回方式不能是by value,如果是这样的话,(A=B)返回的是A或是B的副本(正确的方式应该是A的reference),让后将C的值赋给这个副本,而A、B的值却没有发生任何变化,这当然不是我们想要的,为了不让这个副本的产生,返回值必须是引用的方式,你可以用指针或是reference的方式返回,指针必须加个*麻烦,所以就是以reference的方式返回,那么是返回A的引用还是B的引用,毫无疑问是A的,如果是B的,那么执行的顺序是这样的,先将B赋给A,然后将C的值赋给B,赋值其实就是将右边的值赋给左边的返回值吗!这样然不是我们想要的,我想要的其实是将B赋给A,然后将C在赋给A,当然写这样等式的绝对不是一个合格的程序员。所以operator=必须返回左边对象的reference。我一直在想this为什么是一个指针类型而不是一个reference呢?因为我们必须在operator=方法的最后一句加上return *this;而不是 return this;
16:在operator=中为所有data member赋值
这是毫无疑问的,如果有部分data member没有被赋值,那么被赋值的对象不就是残废了的吗!有时候我们可能会在为类加data member的时候忘了再operator=中为它赋值了,或是在子类的operator=中忘了为父类的data member赋值。当然这些都不是重点,不记得不是问题,出错了自然就知道了,为父类的data member赋值只需要在子类的operator=中加上Base::operator=(this);如果编译器不支持调用base的operator=的话,可以做类型转换啊,static_case<&Base>(*this)=Derived;我发现在类型转换中,转换成的类型一般都是指针或是引用,特别是转换类型在左边的时候就一定不能是by value的方式,会产生临时变量,然后给临时变量赋值,当然不是你想要的。在为指针类型的data member 赋值时要记住一点,是为指针所指的对象赋值,而不是为指针赋值,如果你让data member一会儿指向这一会指向那的,可能会出现某些地址不可到达而出现内存泄露。
17:在operator =中检查是否自己赋值给自己
在为一个对象赋值之前,先要确认这个对象是否动态配置内存,如果动态配置内存,先要回收掉这块内存,不然当这个对象被赋值,指向别的地址后就会出现内存泄露,当然也不能一发现它动态配置内存了就把它先回收掉,因为可能出现把自己赋给自己的情况,你总不能因为把自己赋给自己之后,自己就莫名其妙的被回收了吧,所以在operator=中药检查是否自己赋值给自己。
Effective C++系列:
Effective C++构造函数析构函数Assignment运算符
作者:陈太汉