侯捷C++-面向对象编程-OOP
Composition(组合)
Composition 表示 has-a 的关系,UE中的组件和Actor的关系就是组合。
1 |
|
构造由内而外,析构由外而内! 这个过程完全由编译器实现。
Delegation(委托)
委托也叫做 Composition by reference (这里叫by ref是因为学术界一般不叫by pointer,实现中用指针也完全可以),他的思想就是用一个指针指向真正的实现类,从而将接口和实现分离(也叫编译防火墙,之后修改实现不用再编译接口类),这种思想又叫做 pimpl(pointer to implement) 指针指向实现。
委托不光可以描述两个类之间的委托关系,也可以直接委托给某个函数,一般使用函数指针实现,出自一文理解透UE委托Delegate:
1 |
|
Inheritance(继承)
继承表示 is-a 的关系。
构造由内而外,析构由外而内! 父类的析构函数必须是虚函数。
继承和组合同时存在时,构造和析构的顺序:
可以看出,第一种情况派生类中有组合时,会先构造基类再构造组合类,最后构造派生类,按相反顺序析构;第二种情况基类中有组合时,会先构造最内层的组合类,再构造基类,最后构造派生类,按相反顺序析构。
虚函数(多态)
- 非虚函数:不希望派生类重新定义(override);
- 虚函数(Virtual函数):希望派生类重新定义,但不定义也没关系,已有默认定义;
- 纯虚函数(Pure Virtual函数):希望派生类一定要重新定义,因为没有默认定义;
虚函数的实现依靠虚函数表,虚函数表不存在类中,类中存的是指向这个虚函数表的指针;所以有虚函数的类的内存要加上一个指针的大小;当子类定义一个新的虚函数时,这个虚函数会添加到虚表的末尾。
子类中有与父类同名的函数,且这个函数不是虚函数,那就不会覆写父类中的同名函数,如图中的子类B、C 都有 同名函数 func2。
动态绑定
在C语言中,所有的函数调用都是静态绑定,即在编译阶段会编译为 call XXX,而在C++中会出现动态绑定(如图,不能写死为call XXX,而要写成一个指针调用的形式,在执行时会按照走上图流程确定调用哪个函数),需要满足以下条件:
- 使用指针调用
- 向上转型
1
A* pa = new B; // 向上转型,new出来是B,类型却是A的指针
- 调用的是虚函数
虚函数的经典用法:
这里的 Template Method 不是指C++中的模板,而是一种设计模式,先写好通用部分的函数,将不能通用的函数写成虚函数形式,由子类实现其特化的功能;
虚函数起作用的流程是:
子类对象调用成员函数,成员函数中有一个虚函数Serialize(),调用时将this指针当作参数传递(所有类函数都有隐藏的参数this),在成员函数中完成非虚函数部分,遇到虚函数使用this调用虚函数,查询虚函数表确定要调用哪个函数并调用。
上图展示静态绑定,虽然a对象的初值是b,但是没有使用指针调用,不会出现动态绑定,所以是静态绑定。
上图展示动态绑定,满足使用指针调用,向上转型,调用虚函数三点;
虚函数与构造析构函数
1. 构造函数不能是虚函数!
2. 析构函数必须是虚函数!
构造函数不能是虚函数可以从两方面解释:
- 设计思想方面:虚函数是接口的思想,我们不想知道其内部是如何实现的,而构造函数要构造出一个对象,就想要知道其内部的实现,因此构造函数不该被定义为虚函数;
- 语言本身:虚函数在实现时需要用到vtpr虚表指针来确定虚函数的定义是哪个,这个虚表指针存在对象内存空间中,而构造函数被调用时还没有实例化对象,也就没有虚表指针;
析构函数(可能会被继承时)必须是虚函数:
析构函数会在对象生命周期结束时被自动调用,当用父类指针指向子类对象时,如果析构函数不是虚函数,只能顺利销毁子类对象中的父类部分,如果子类还有别的变量,可能会造成内存泄漏;