1.final
final在继承和多态中都可以使用,在继承中是指不想将自己被继承,在多态中是指不想该函数被重写,比较简单,下面是一些使用例子。
2.纯虚函数
当我们需要抽象一个类的时候,我们就需要用到纯虚函数。所谓抽象的类是指高度概括的,需要针对不同事物有不同处理的。如植物是一种抽象的类,而像苹果、香蕉就是具象的,单独讨论植物太过庞大,没有太大意义,因此我们的重心放在由植物具象出来的苹果,我们可以具体讨论它的成分、营养价值等。理解了这个例子,就能理解为什么有抽象类,纯虚函数的存在了。
这就是一个纯虚函数,就是在虚函数后面加上 = 0,它对应的类就叫抽象类。注意,只要有一个纯虚函数,这个类就叫抽象类,抽象类不能被实例化,就算你不打算用这个纯虚函数。唯一能做的就是调用这个类里面的static成员,因为它们不需要实例化就能调用
这么做的意义就在于纯虚函数对应的类本身就高度抽象,实例化它没有意义。但我们可以讨论将它具象化的事物,这就要用到虚函数的重写功能。我们可以理解,纯虚函数存在的意义是依赖于虚函数的性质存在的,这里需要我们深刻思考。
3.继承、多态难点
继承、多态的用法、意义几乎讲的差不多了,绝大多数情况下已经够用了,只不过在极少数情况下仍有一些坑。
(1)多态调用重写的函数
先看下面的代码,想想结果是什么
#include <iostream> using namespace std; class A { public: virtual void test(int a = 0) {} }; class B final : public A { public: void test(int a) { cout << a << endl; } }; int main() { A* a = new B; a->test(); return 0; }
不少人会想,这难道不报错吗?但结果是
我们需要知道,当构成多态和重写时,调用函数是以父类声明+子类定义进行的,对于三个类及以上都是如此,这个父类指的是构成多态的父类
我们也可以进一步理解为什么只需要父类写virtual,子类可以不写,因为子类的函数声明根本没有意义(在多态中),写不写都是以父类的声明为标准。但是在多态语法以外就不会出现这种反直觉处理情况了。
(2)继承调用父类函数时this的类型变化
先看看下面的代码,想想test2的隐含的this指针是B*还是A*
#include <iostream> using namespace std; class A { public: virtual void test(int a = 0) {} void test2() { test(); } }; class B final : public A { public: void test(int a = 1) { cout << a << endl; } }; int main() { B* a = new B; a->test2(); return 0; }
既然是B*调用函数,那理所应当应该是B*为形参来接受啊,但实际不是这么理解的。
当子类去调用父类的成员函数时,隐含的指针类型始终是父类的。要理解这里,我们假设这个指针的类型是子类的,那如果子类又写了一个一模一样的函数构成隐藏,那么就会因为参数和假设的函数完全相同而报错,所以是行不通的。
当子类调用父类时,this指针会发生一次赋值兼容转换,这里是从B*赋值兼容转换为A*,赋值兼容转换为指针只会影响访问的方式,指针的值,指向的内容都不会改变。但学了多态之后,我们是否可以将这种特性和多态的形成条件结合起来呢?上面这段代码就是如此。
结合上一个易错点,这段代码的最终结果是
(3)多态访问限制的特殊处理
先看看下面的代码,看看是否能够正常访问
#include <iostream> using namespace std; class A { public: virtual void Test() { cout << "A" << endl; } }; class B : public A { private: void Test() { cout << "B" << endl; } }; int main() { A* p = new B; p->Test(); return 0; }
很多人以为p的类型是A*,A访问不了B,但其实程序运行没有问题
我们要理解访问限定符限制的是什么,是防止其它类调用private的函数,这里p是一个指针,本身就指向B对象的空间,只不过访问方式按A进行。由于符合多态的条件,就按虚函数表进行访问。那么问题在于:B会不会阻止呢?
我们先看看什么情况是会阻止的
我们发现无论在A还是在main函数中,都没有办法调用B中的private成员,这也符合我们之前的预期。但是为什么A* p = new B; p->Test();这种操作就可以呢?
事实上,这是多态中的特殊处理,当我们用父类的指针或引用来访问子类的虚函数时,是会以父类的访问限定符为标准的。子类的限制不会起到作用。同理,就算子时public,父是private,那么就无法访问。如何理解?
当以父类的指针和引用调用虚函数时,是按照虚函数表的地址直接去调用函数,根本不会经过类调用函数,只会受到调用方(父类)的访问限定符的限制。
一般建议都设为public
4.动静态绑定
动静态绑定都是为了定位一个函数,从反汇编的角度上讲就是确定call的对应的地址是什么,只不过两者的方式有一定的区别。
(1)动态绑定:多态调用函数的核心时动态绑定,也叫运行时绑定。也就是借助虚函数表,在这个函数指针数组中确定函数的地址。
(2)静态绑定:我们平时写的函数都可以认为是静态绑定(包括函数重载、普通函数、模板函数),函数如果声明定义在一起就在编译后进符号表,如果声明定义分离在两个文件则在链接时进符号表,运行时是根据符号表来查找函数。
在多态中,在满足动态绑定的情况下我们指定类域调用函数那就自动转为静态绑定,就失去了多态的特性。
#include <iostream> using namespace std; class A { public: virtual void Test() { cout << "A" << endl; } }; class B final : public A { public: void Test() { cout << "B" << endl; } }; int main() { A* p = new B; p->Test(); p->A::Test(); return 0; }
运行结果
5.继承、多态的一些知识点和处理技巧
(1)多用const修饰函数,保证匿名对象传参可以调用函数
(2)函数第一句的指令理解为函数的地址,成员函数要打印它们的地址函数名前要加&,其余函数函数名就是它的地址(&可加可不加,但成员函数一定要加)
(3)cout打印地址很麻烦,char*不会打印地址,会按字符串去打印,这跟流插入的重载有关。有几个关于函数指针的重载会导致出现bug,打印地址很受阻,最好使用printf
(4)关联性强的类型之间支持隐式类型转换,如整型家族+double(内置类型)、指针之间,有的支持强转,如int和int*。
关联性弱的自定义类型,想取头地址,可以使用*((int*)&Base),虚函数表的地址就在类的开头
(5)只有virtual修饰的成员函数才能叫作虚函数,而像static修饰的成员函数、全局函数都不能定义为虚函数,全局定义的虚函数没有意义,static修饰的成员函数不属于对象,就算加了virtual,也进不了虚函数表,没有意义。
(6)virtual修饰的成员函数声明定义分离时定义处不写virtual
(7)友元不是成员函数,所以不能用virtual修饰
(8)多继承可能有多张虚函数表,按继承顺序排序,但单继承对应的就只有一张虚函数表,如果多继承后自己又写了虚函数,则默认放在第一张虚函数表后面
(9)如果不重写虚函数,那共用同一个函数,如果所有的函数都不重写,两个类存的函数的地址都相同,但是这对应两张虚函数表,开辟的是不同的空间
(10)虚函数表是在编译期间就形成了。而多态是动态绑定(运行时绑定/晚期绑定),是因为编译时编译器只负责检查语法错误,而不负责读取内容,只有运行起来才知道函数调用的地址。