一、类虚函数杂谈 0.虚基类中若存在成员变量,应提供可初始化的构造函数或默认参数的构造函数,避免该成员变量无法被初始化到;当然若不是出于公共部分剥离到父类,则可以将其下层到子类中去,避免基类增加不必要的构造函数。 1.建议不要把基类中的虚析构函数置为存虚的。 2.不必要把子类基本不会覆写的函数设置为虚函数,也不应该全部函数加上virtual修饰由编译器来帮助取消一些成员函数的虚函数属性的操作。 3.虚函数是否用const修饰比较难以决定,因为很难保证继承体系中不会用到需要修改成员变量的情况,故而一般可暂时不考虑const修饰虚函数的情况。 4.的确,一般情况下类中有虚函数的声明,则一般会建议虚析构函数。 二、无继承时的对象构造 1.ADT抽象数据类型时,即没有虚函数的普通类时,编译器不一定会合成默认构造函数、析构函数、拷贝构造或拷贝赋值操作,若ADT类型为plain data,则直接按位逐次拷贝操作即可。 2.当类中由虚函数时,情况会变得不同,除了每个类对象实例需要增加一个vptr外,还会对构造函数、析构函数、拷贝构造函数、拷贝赋值等插入一些处理vptr和虚表的相关代码,使得代码被膨胀。在构造函数中时,带入插入到调用基类的构造函数之后,初始化列表和程序员的代码之前(当然若构造函数是编译器合成的,则同样的处理方式)。 三、继承体系下的对象构造 0.编译器在合成或是基于程序员的构造函数时,会扩充插入一些隐藏的代码,而插入的代码一般有以下几种: 1)构造函数中的成员初始化列表湖北放在构造函数中,并以声明的顺序排列; 2)若类成员没有在初始化列表中,但有自己的默认构造函数,则该默认构造函数会被调用; 3)若类中有虚函数,则需要设定一个vptr,此时需要设定其值,以指向适当的vtabl虚表; 4)所有上层的基类构造函数也会被调用,以基类声明的顺序排列;若基类在初始化列表中,则调用基类的对应构造函数;若基类没有在初始化列表且提供了默认构造函数,则调用其默认构造函数;另外若基类是多重继承下的第二或后继的基类,则this指针也必须被调整; 5)所有的虚基类构造函数也必须被调用,从左至右,由最深层到最浅层;若该需基类位于初始化列表中,则调用对应参数的构造函数,若没有在初始化列表中则调用其默认构造函数;此外,类对象模型中的每个虚基类部分的offset偏移量需要被设置,使其在执行期可被存取。 1.赋值拷贝函数,一般应最好加入条件筛选(if(this == &rhs) return *this;),避免多余的操作或者非预期的行为。 2.虚拟继承,因需要处理共享部分,所以子类还需对基类的构造函数单独初始化,而基于子类的新继承类时,则交由新子类来处理基类共享的那部分,此时上一层的子类则不再对基类的构造函数进行单独的初始化了(此方案实现需要在构造函数中增加一个额外的参数以判断是否为最深层的类)。 3.Vptr初始化,发生在所有的基类构造函数调用之后,成员初始化列表和程序员代码之前,此时若初始化列表中调用了虚函数,虽然vptr已被正确设置,但是有可能该虚函数依赖了类对象中未被初始化的成员变量,进而可能导致非预期的结果,故不建议在初始化列表中调用虚函数,而可以在程序员代码函数体中调用。
同样的由本类的虚函数在初始化列表中来初始化基类的构造函数更是不应该的,因为此时vptr都没有被正确设置。另外比较重要的一点,如果类继承体系中,每个类的构造函数中都调用了一个虚函数,则由构造顺序,每个类的仅能看见或使用自己类的那个虚函数。 四、对象拷贝赋值语意 1.若类的默认行为足够(安全且正确语意时)用一个对象指定给另一个对象时,应不需提供显式的拷贝赋值函数,默认行为(一般指的是按位逐次拷贝)效率更高;另外编译器也不会合成默认的拷贝赋值函数。 2.有几种情况使得不会使用按位逐次拷贝的操作机制,如下: 1)若类中有一个成员变量,该变量有个拷贝赋值函数操作时; 2)若类的基类中有拷贝赋值函数操作时; 3)若类中声明有虚函数时(或继承来的虚函数)或者继承于虚基类时。 五、对象的效率 一般情况下,POD、ADT、单一继承、多重继承、虚拟继承中,对象创建执行等会因一些额外的操作导致执行效率依次降低,POD、ADT中可能可以支持内联或按位逐次操作的机制可以实现高效的执行操作,
另外单一继承或多重继承若是可以支持按位逐次操作则性能的损失应该可能是临时对象变量的产生上,虚继承中明显会合成构造函数、析构函数以及拷贝构造和拷贝赋值等操作,其会明显降低执行效率。 六、析构语意 1.若类中没有定义析构函数,则仅当类中的成员变量或基类有析构函数时,编译器才会合成析构函数,纵使其有虚函数也如此; 2.本质上,析构函数是否需要提供除了合成的情况外,一般可不提供,除非需要做一些资源释放的操作,一般情况下对象删除时不需要做一些清理工作,也就意味着可以不需要析构函数。也不必追求要和构造函数对等存在的思想。 3.被合成或程序员提供了析构函数,若该析构函数需要被扩展,则扩展顺序和构造函数的顺序相反的,即: 1)析构函数本身被执行; 2)若类中拥有成员对象变量且有析构函数,则按照声明的相反顺序调用析构函数; 3)若类对象中有vptr,则重设相关的vtbl虚表,会被首先执行重设; 4)若非虚基类中有析构函数,则以继承的相反顺序调用其析构函数; 5)若有虚基类,且虚基类有析构函数,则按照原来构造的相反顺序调用其析构函数。