本文已参与「新人创作礼」活动,一起开启掘金创作之路。
游戏开发之多态及虚函数(C++基础)
以下概念是建立在32位操作系统之上的。
1.多态
1.1 多态的分类
- C++多态从表现形式来看,可以分为虚函数、模板、函数重载、运算符重载等。
- C++多态从绑定时间来看,可以分为静态多态和动态多态,也称编译器多态和运行期多态。
- 静态多态:函数重载、运算符重载等。对于相关的对象类型,直接实现它们各自的定义,不需要公有基类。
- 动态多态:虚函数等。对于相关的对象类型,确定它们之间的一个共同功能集,然后在基类中把这些共同的功能声明为多个公共的虚函数接口。各个子类重写这些虚函数,实现其具体的功能。
1.2 多态的概念
- 多态即多种状态,一个函数内部可以实现多种不同的功能。
- 可以解决继承的二义性问题。
- 一个接口实现多种功能,为未来开发预留接口。
2.虚函数
- 虚函数由两部分组成:虚函数指针和虚函数表。
- 虚函数的定义:使用virtual关键字可以在类中生成虚函数表,类对象中包含虚函数表指针(虚函数表指针记录各个虚函数的虚函数指针位置)。虚函数表本身不占用一个类对象的内存,而在类对象中生成的虚函数表指针指向该类的虚函数表且占该类对象的内存,为4个字节。
- 如果一个派生类继承(多继承)很多个基类(每个基类都有虚函数)会生成多个虚函数表。如果是普通的继承,即单继承,派生类只会生成一个虚函数表。
- 虚函数的定义:virtual 数据类型 函数名(形参列表){函数体}
2.1 虚函数指针及虚函数表指针
- 虚函数指针本质上就是一个指向函数的指针,它指向用户所定义的虚函数,具体是在子类里的实现,当子类调用虚函数的时候,实际上是通过调用该虚函数指针从而找到接口。虚函数表指针就是指向所有虚函数构成的虚函数表的指针。
- 拥有虚函数的类对象会拥有虚函数表指针,每个虚函数都会对应一个虚函数指针。在一个被实例化的对象中,它总是被存放在该对象的地址首位。
2.2 虚函数表
- 每个类的实例化对象会拥有虚函数表指针并且在对象的地址首位有一个虚函数指针指向所有的虚函数按照顺序排列而成的一个表。它们按照一定的顺序组织起来,从而构成了一种表状结构,成为虚函数表。
- 虚函数表属于类,不属于某个具体的类对象。
- 同一个类的所有对象都是用同一个虚函数表。公用一张虚函数表。
- 虚函数表是动态绑定的,类对象在创建的时候进行绑定。
2.3 实例1
- 定义一个基类
class Base
{
public:
virtual void f(){cout<<"Base::f"<<endl;}
virtual void g(){cout<<"Base::g"<<endl;}
virtual void h(){cout<<"Base::h"<<endl;}
};
TIPS:首先对于Base基类本身它的虚函数表记录的只有自己定义的虚函数。
2.再定义一个子类
class Derived:public Base
{
public:
virtual void f(){cout<<"Derived::f"<<endl;}
virtual void g1(){cout<<"Derived::g1"<<endl;}
virtual void h1(){cout<<"Derived::h1"<<endl;}
}
一般的C++编译器采取的是覆盖继承的原理(针对不同的C++编译器处理方式可能不同)。 将Base基类的虚函数进行覆盖继承。
子类对象及其虚函数表如下:
首先基函数的表项仍然保留,而得到正确继承的虚函数其指针将会被覆盖,子类自己的虚函数将依次push到虚函数表末尾。
而当多继承时,虚函数表将会增多,类对象保存的虚函数表指针顺序将会体现为继承的顺序,并且类本身的虚函数仅仅只跟着第一个虚函数表末尾。
多继承下,类对象及其虚函数表如下:
2.4 实例2
class Base
{
public:
virtual void func() const
{
std::cout << "Base!" << std::endl;
}
};
class Derived :public Base
{
public:
virtual void func()
{
std::cout << "Derived!" << std::endl;
}
};
void show(Base& b)
{
b.func();
}
Base base;
Derived derived;
int main()
{
//不属于动态多态,而只是属于静态多态中的函数重载,C++编译器只遵循就近原则。
show(base);
show(derived);
base.func();
derived.func();
return 0;
}
原因是因为虚函数的声明与定义要求非常严格,只有在子函数中的虚函数与父函数一模一样的时候(包括限定符)才会被认为是真正的虚函数,不然的话就只能是重载。这被称为虚函数定义的同名覆盖原则,意思是只有名称完全一样时才能完成虚函数的定义。
即此情况下,只能算是函数重载。而函数重载的情况下,C++编译器遵循就近原则,即类本身是什么类对象类型就调用该作用域下的函数。我们可以发现show函数里面,形参默认转换为了Base类,即编译器按照就近原则只调用Base的Print。base.func();和derived.func();也是同样的原理,调用类对象类型本身作用域下的Print。
2.5 实例3
class A
{
public:
int a;
//虚函数的定义
virtual void Print()
{
std::cout << __FUNCTION__ << std::endl;
}
//虚函数的定义
virtual void Print1()
{
std::cout << __FUNCTION__ << std::endl;
}
};
class B :public A
{
int b;
//在派生类中,可以不添加virtual关键字。
void Print()
{
std::cout << __FUNCTION__ << std::endl;
}
void Print1()
{
std::cout << __FUNCTION__ << std::endl;
}
};
class C :public B
{
public:
int c;
void Print() override
{
}
};
int main()
{
A *p = new B;
//如果子类有成员和父类同名的时候,子类访问其同名成员时候默认访问子类的成员(就近原则,本作用域),即删掉class A类中的Print和Print1的virtual,普通继承。
//p->Print(); A,就近原则
//sizeof(B); 4+4 = 8
//sizeof(C); 4+4+4 = 12
//而如果基类是虚函数,派生类继承之后通过虚函数指针指向的虚函数表是B,即在虚函数表中继承覆盖。
p->Print(); //B
sizeof(B);// 4+8 = 12,虚函数表指针占4个字节。且位于类对象首位
sizeof(C);// 4+4+8 = 16,虚函数表指针占4个字节。且位于类对象首位
sizeof(A);// 4 + 4 = 8,虚函数表指针占4个字节。且位于类对象首位
return 0;
}
TIPS:override关键字可以规范程序员,避免错误书写函数而造成无法预知的bug,一旦重写父类的虚函数出现问题便会报错,比如函数名写错等。类似于实例2。 语法:数据类型 函数名 override{函数体}
版本声明:本文为CSDN博主[ufgnix0802]的原创文章。
原文链接:(blog.csdn.net/qq135595696…)