侯捷C++-面向对象编程-OOP

Composition(组合)

Composition 表示 has-a 的关系,UE中的组件和Actor的关系就是组合。

1
2
3
4
5
6
7
8
class A{
private:
int m_x;
};
class B{
private:
A m_A;
};


构造由内而外,析构由外而内! 这个过程完全由编译器实现。

Delegation(委托)

委托也叫做 Composition by reference (这里叫by ref是因为学术界一般不叫by pointer,实现中用指针也完全可以),他的思想就是用一个指针指向真正的实现类,从而将接口和实现分离(也叫编译防火墙,之后修改实现不用再编译接口类),这种思想又叫做 pimpl(pointer to implement) 指针指向实现。

委托不光可以描述两个类之间的委托关系,也可以直接委托给某个函数,一般使用函数指针实现,出自一文理解透UE委托Delegate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using DelegateType = void (*)(int x, int y); // 给这种函数指针起一个别名DelegateType

void ArtilleryAction(int x, int y){ // 实现功能的函数
printf("Drop a bomb on the (%d,%d) position\r\n", x, y);
}

class Scout{
public:
Scout(DelegateType Delegate) :ArtilleryDelegate(Delegate){} // 构造函数
void FindInformation(){ // 调用委托
(*ArtilleryDelegate)(rand(), rand());
}
private:
DelegateType ArtilleryDelegate; // 类中成员变量就是一个指向功能实现的指针
};

int main(){
Scout OneScout(ArtilleryAction);
OneScout.FindInformation();
return 0;
}

Inheritance(继承)

继承表示 is-a 的关系。


构造由内而外,析构由外而内! 父类的析构函数必须是虚函数。

继承和组合同时存在时,构造和析构的顺序:

可以看出,第一种情况派生类中有组合时,会先构造基类再构造组合类,最后构造派生类,按相反顺序析构;第二种情况基类中有组合时,会先构造最内层的组合类,再构造基类,最后构造派生类,按相反顺序析构。

虚函数(多态)

  1. 非虚函数:不希望派生类重新定义(override);
  2. 虚函数(Virtual函数):希望派生类重新定义,但不定义也没关系,已有默认定义;
  3. 纯虚函数(Pure Virtual函数):希望派生类一定要重新定义,因为没有默认定义;

虚函数的实现依靠虚函数表,虚函数表不存在类中,类中存的是指向这个虚函数表的指针;所以有虚函数的类的内存要加上一个指针的大小;当子类定义一个新的虚函数时,这个虚函数会添加到虚表的末尾。

子类中有与父类同名的函数,且这个函数不是虚函数,那就不会覆写父类中的同名函数,如图中的子类B、C 都有 同名函数 func2

动态绑定


在C语言中,所有的函数调用都是静态绑定,即在编译阶段会编译为 call XXX,而在C++中会出现动态绑定(如图,不能写死为call XXX,而要写成一个指针调用的形式,在执行时会按照走上图流程确定调用哪个函数),需要满足以下条件:

  1. 使用指针调用
  2. 向上转型
    1
    A* pa = new B; // 向上转型,new出来是B,类型却是A的指针
  3. 调用的是虚函数

虚函数的经典用法:

这里的 Template Method 不是指C++中的模板,而是一种设计模式,先写好通用部分的函数,将不能通用的函数写成虚函数形式,由子类实现其特化的功能;
虚函数起作用的流程是:
子类对象调用成员函数,成员函数中有一个虚函数Serialize(),调用时将this指针当作参数传递(所有类函数都有隐藏的参数this),在成员函数中完成非虚函数部分,遇到虚函数使用this调用虚函数,查询虚函数表确定要调用哪个函数并调用。


上图展示静态绑定,虽然a对象的初值是b,但是没有使用指针调用,不会出现动态绑定,所以是静态绑定。

上图展示动态绑定,满足使用指针调用向上转型调用虚函数三点;

虚函数与构造析构函数

1. 构造函数不能是虚函数!
2. 析构函数必须是虚函数!

构造函数不能是虚函数可以从两方面解释:

  1. 设计思想方面:虚函数是接口的思想,我们不想知道其内部是如何实现的,而构造函数要构造出一个对象,就想要知道其内部的实现,因此构造函数不该被定义为虚函数;
  2. 语言本身:虚函数在实现时需要用到vtpr虚表指针来确定虚函数的定义是哪个,这个虚表指针存在对象内存空间中,而构造函数被调用时还没有实例化对象,也就没有虚表指针;

析构函数(可能会被继承时)必须是虚函数:
析构函数会在对象生命周期结束时被自动调用,当用父类指针指向子类对象时,如果析构函数不是虚函数,只能顺利销毁子类对象中的父类部分,如果子类还有别的变量,可能会造成内存泄漏


侯捷C++-面向对象编程-OOP
https://kenny-hoho.github.io/2023/12/18/侯捷C++-面向对象编程-OOP/
作者
Kenny-hoho
发布于
2023年12月18日
许可协议