1. 动态类型和静态类型
动态类型:对象在运行时的类型。
静态类型:对象被定义的类型或表达式产生的类型,该类型在编译时是已知的。
对象的静态类型和动态类型不一致:当我们使用引用和指针操作一个对象时,基类的指针或引用可以指向派生类,此时,静态类型是基类的引用(或指针),而动态类型是派生类的引用(或指针)。
class Base {/* int func() */}
class A : public Base {/* int func() */}
// !函数参数的静态类型是Base,具体的动态类型需要运行时才能知道
int Function(const Base &item) {/*
int ans = item.func();
*/}
// 正式调用
// 1. 传入Base对象,调用的函数版本是 Base::func()
Base b = new Base();
func(b);
// 2. 调用的版本是A::func()
A a = new A();
func(a);
在这段代码中,Function函数内要调用func,但具体调用哪个类的func(),在运行时由item的具体类型决定。
2. 私有继承和公有继承
这里涉及到继承的概念,不同的继承方式,主要改变了派生类对基类成员的访问权限。
私有继承:
基类的公有成员和保护成员在被派生类继承后,将变为派生类的私有成员,能够被派生类的成员函数直接访问;不能被这个派生类的子类访问。
同时基类的私有成员也会被派生类继承,但是这些成员不能被派生类的成员函数 or 派生类的对象(指针,引用)直接访问。
公有继承:
基类的公有成员和保护成员原样继承给派生类,能够直接被派生类中定义的成员函数访问;私有成员不能被派生类的成员函数 or 派生类的对象(指针,引用)直接访问。
3. 多重继承
从多个直接基类中产生派生类的能力。
构造函数调用顺序:最终基类(根部)—— 向派生类顺序遍历基类 —— 直接基类 —— 派生类。
析构函数:只负责清除派生类本身分配的资源。调用顺序与构造函数相反。
4. 虚继承
虚继承是多重继承的一种形式,基类被继承了多次,但是派生类共享该基类的唯一副本。
具体操作,使用virtual关键字,令某个类做出声明,承诺愿意共享它的基类,其中共享的基类子对象称为虚基类。在此策略中,无论虚基类在继承体系中出现多少次,在派生类中都只包含唯一一个共享的虚基类子对象。
// 表达等价
class A : public virtual Base {/* */}
class A : virtual public Base {/* */}
// Base是A,B的虚基类
class A : virtual public Base {/* */} // 虚继承
class B : virtual public Base {/* */} // 虚继承
class C : public A, public B {/* */}
// 这样C中只有一个Base基类部分
5. 虚继承的对象的构造顺序
构造函数调用顺序:根部虚基类 — 向派生方向依次构建间接虚基类 — 直接虚基类 — 第一个非虚基类 — 向派生方向依次构建非虚基类 — 派生类
析构顺序:相反
6. 虚函数表
定义:虚函数表属于类,不属于具体对象,本质是一个指针数组,数组中每个元素是类中虚函数的函数指针,调用虚函数需要经过虚函数表,它是动态绑定技术的核心。
6.1 C++实现了动态绑定的技术,即在运行中识别对象的具体类型,该技术核心是虚函数表。
6.2 每个包含虚函数的类都包含了一个虚函数表,派生类继承了包含虚函数的基类,那么这个类也拥有自己的虚表。
6.3 虚表是属于类的,不属于某个具体的对象,一个类只需要一个虚表,同一个类的所有对象共享一个虚表。
6.4 虚函数表是一个指针数组(数组中每个元素都是指针),其元素是虚函数的函数指针,每个元素对应一个虚函数的函数指针。
6.5 虚函数表指针 *__vptr
编译器在类中添加了一个指针,*__vptr,进一步
告知每个对象需要指向的虚函数表,对象内部包含一个指针,指向所使用的虚函数表。
// 类A的虚函数表,有a(),b()两个虚函数的函数指针
class A {
public:
virtual void a();
virtual void b();
void func();
}
// 类B继承自类A,因此也有自己的虚函数表
// B重写了虚函数a(),虚函数表指针分别指向A::b(), B::a()
class B : public A {
public:
virtual void a();
}
int main(){
// 当执行到这里,会发现a是一个指针,并且调用的是虚函数
// 此时会根据a->__vptr来访问B的虚函数表,进一步调用B中的a()
B b = new B();
A *a = &b;
a->a();
}
总结:把经过虚函数表调用虚函数的过程称为动态绑定,其表现出来的现象称为运行时多态。
!!!需要注意的是:普通函数即非虚函数,其调用并不需要经过虚表,所以虚表的元素不包括普通函数的函数指针。
7. 虚函数表和构造函数的调用 / 初始化顺序
7.1 在单独一个类、没有继承的情况
分配内存 — 虚函数指针赋值 — 列表初始化 — 执行构造函数体
7.2 有继承的情况
分配内存 — ( 基类虚函数表指针赋值 — 基类列表初始化 — 基类构造函数体执行 ) — 派生类虚函数指针赋值 — 派生类列表初始化 — 执行派生类构造函数体
!! 将虚表指针的赋值过程放置在初始化列表之前,防止在初始化列表出现调用虚函数的情况!!