0%

马++参考书摘抄:多态与虚函数

一看就知道我是上课没有好好听课。

内容摘抄自《C++程序设计精要教程》(编著 马光志)。

8.1虚函数

8.1.1 虚函数的声明和定义

重载函数是一种静态多态函数, 虚函数是一种动态多态函数。

当派生类中含有与基类虚函数同名的实例成员函数,其显式参数类型都应相同;当基类虚函数返回类型为基类指针p或者引用r时,派生类同名函数成员返回类型必须为可向p或者r赋值的基类/派生类指针(或引用),其他情况两者的返回类型必须相同。

C++中不允许将构造函数定义为虚函数或者纯虚函数。

当基类中包含基类指针或者引用时,应将其析构函数定义为虚函数,所以一般可以将析构函数定义为虚函数。最好也将类的所有实例成员函数定义为虚函数。

当基类和派生类都定义了原型”相容”的虚函数时,如果基类指针指向的是派生类对象,则通过基类指针调用的是派生类的函数成员,否则调用基类的函数成员(基类引用引用了派生类对象同理)。

不能用virtual修饰类的友元函数,虚函数也不能定义为constexpr函数。

8.1.2 虚函数的重载和内联

基类与派生类定义的同名虚函数的访问权限可以不同。在C++的同一个类中,不能定义显式参数表(类型)完全相同、仅返回类型不同的静态成员函数或实例成员函数。 基类虚函数的特性会一直继承

基类指针指向派生类对象时,调用成员函数时优先调用离该派生类最近的实例成员函数,若在该对象继承的类中都找不到所找函数时,才会调用基类对应的成员函数。若想限定使用某一级中的成员函数,可以用类名加作用域运算符 “::” 进行限定。   

关于内联inline

虚函数可以申明为inline,具体到编译器解释时候,如果编译器在编译的时候就可以确定该虚函数的决议,则编译器以inline方式静态决议该虚函数。如果编译器在编译的时候不能决定,则必须在运行时决议虚函数,此时虚函数不能以inline函数的方式调用。

关于友元friend和virtual、static

因为friend声明的函数不是宿主类的函数成员,所以不能和virtual一起使用,除非被定义的函数已经是另一个类的虚函数成员;friend一般也不能跟static一起使用,除非static定义的函数是文件作用域内的静态函数或者其他类定义的静态函数。

virtual一定不能与static一起使用,因为static定义的任何函数都不含隐含参数this,virtual则恰好相反。

8.2 虚析构函数

如果为基类和派生类的对象分配了动态内存,或者为派生类对象的成员分配了动态内存,在一定要将基类喝派生类的析构函数定义为虚函数,否则极有可能造成内存泄漏。

8.3 类的引用

引用变量是被引用实体的别名,被引用的对象应该自己负责构造和析构。

8.3.1 类的引用变量及其析构

如果类A的引用变量r引用了通过new生成的对象x,而在退出r的作用域之前没将x传到作用域之外(转移),也没有主动去析构x和释放对象x所占用的内存,那么就应该用delete &r析构x并释放他占用的内存(注意r.~A()仅用于析构x,而不能释放x占用的内存,从而造成内存泄露)。

当然,如果有址引用变量引用的对象不是通过new产生的,则引用变量无需负责对被引用的对象进行析构。

无址引用变量引用的是对象常量,由于移动语义的引入,被无址引用的常量对象的析构将推迟到该引用变量生命期的结束。所以虽然无址引用变量不负责常量对象的析构,但被其引用的常量对象的析构确实与变量的生命期有关。

8.3.2 类的引用参数及其结构

对形参不是引用类型而是对于一般对象类型来说,形参相当于局限于当前函数的局部变量。这种形参对象的构造是在调用时通过值参传递完成的,其析构则是在函数调用返回时完成的。 如果类定义了拷贝构造函数,将调用对应的拷贝函数完成。值参传递将实参对象数据成员的值相应地赋给形参对象的数据成员,而指针类型的数据成员只浅拷贝给形参对应指针成员。
所以一般要给类提供自定义的深拷贝构造函数以及移动构造函数。其中,移动构造函数通常实现为浅拷贝构造函数。

8.4 抽象类

8.4.1 抽象类的定义

纯虚函数

纯虚函数是不必定义函数体的特殊虚函数,在定义虚函数时,在其后面添加 “=0” 表示其为纯虚函数。其他特性与一般虚函数区别不大。

抽象类

含有纯虚函数的类就是抽象类,抽象类通常作为派生类的基类。如果派生类继承了抽象类的纯虚函数,又未定义原型”相容”且带函数体的虚函数;或者派生类又定义了新的纯虚函数,则当前的派生类还是抽象类。直到派生类中有一个类给所有继承来的纯虚函数定义了具体的函数体,该派生类才能成为非抽象类。
只有非抽象类才能产生对象。抽象类不能定义或产生任何对象,包括使用new创建对象、定义对象数组等等。不过,抽象类可以作为父类引用或者指针,用于引用或者指向子类中的具体对象。
抽象类指针或引用可以调用抽象类的纯虚函数,此时调用的一定是子类中关于该函数的具体实现(虚函数)。

8.5 虚函数友元与晚期绑定

纯虚函数与一般虚函数一样,可以作为其他类的友元函数。不过一般不会将这种未定义函数体的函数作为友元。

8.5.1 虚函数作为友元

友元关系不能被传递或者继承。例如,类A的成员函数f( )是类B的友元,f( )可以访问B中所有成员,但是f()不能访问类B的派生类对象中的所有成员(除非f()也是其派生类的友元)。
同理,友元特性不能被继承关系传递,如A的函数void f1(C &)作为友元,可以访问C类对象中的所有成员,但是A的派生类B中的void B::f1(C &)不能访问类C的私有和保护成员。

8.5.2 虚函数的晚期绑定

举例说明,假定基类B及其派生类D都定义了虚函数,BD将分别产生虚函数地址表TB和TD。在构造D类对象d时,先将d作为基类B对象构造,故将TB首地址存放到d的起始单元,此时B::B()调用的虚函数将和TB中的虚函数绑定;当B::B()构造完毕,在执行D类构造函数D::D()之前,会将TD的首地址放到d的起始单元,此后D::D()调用的虚函数就会与TD中的虚函数进行绑定,即晚期绑定。
d析构的时候绑定顺序与构造时相反。这也说明子类对象的类型并不是固定的,在构造或者析构的某些时刻,子类对象可以被看作父类对象。

8.6 有虚函数时的内存布局

基类有虚函数时的内存布局

如果基类定义了虚函数或者纯虚函数,则派生类对象将共享基类对象的起始单元(存储单元),用于存放虚函数地址表首址。

基类无虚函数时的内存布局

如果基类没有定义虚函数,其单继承派生类定义了虚函数,则派生类的内存由三个部分组成:首先是继承自基类的内存,然后是派生类虚函数地址表首址,然后是该派生类中定义的实例数据成员。

-------------本文结束感谢您的阅读-------------