C++ 多态

C++ 多态,第1张

C++ 多态
  • 虚函数
  • 多态
  • 虚函数的析构函数
  • 纯虚函数
  • override 和 final
  • 总结

这并不是常规的虚函数介绍,如果没有了解C++继承,还请移步C++继承,否则下面地内容可能会有些困难


虚函数

接下来,看看虚函数是什么样的吧

虚函数一旦被声明,则必须有定义,否则链接时会报错,除非不实例化这个类。


一般来说,如果我们没有使用到一个函数,则无需定义它。


但虚函数最终会以一个函数指针的方式存在,仅在调用时将其取出。


因为函数指针是一个变量,所以编译器也不清楚是否会使用到某个虚函数(编译器难以预测变量的值,也就是函数指针的值),所以我们必须为每一个虚函数提供定义。


虚函数的声明:

virtual type func();

虚函数会被类存放入虚函数表,并将虚函数表放置在类的开头的位置。


下面我们来验证虚函数表的存在

typedef void func(void); //要与虚函数的函数类型对应
typedef int DWORD; //习惯把要看作十六进制的int 写作 DWORD

class Animal {
public:
    virtual void move() { cout << "动物在动" << endl; }
    virtual void breath() { cout << "动物在呼吸" << endl; }
};

void main() {
    Animal a;
    DWORD* aThis = (DWORD*)&a; 
    DWORD** pVirtualFuncTable = (DWORD**)(*aThis);//等会还要运算所以先写成int
    //func* pVirtualFuncTable[] = a.(*this);//这相当于这样的声明
    //DWORD** pVirtualFuncTable = *(DWORD***)&a; //上面两句的替代写法,能理解一种就行
	//为什么写出*(DWORD***)&a这样的代码,主要是我们想将 a 转换成DWORD**,但系统不让写(DWORD**)a

    //第一种调用方式,常规写法
    a.move();
    a.breath();
    //第二种调用方式
    
    ((func*)*pVirtualFuncTable)();//(func*)pVirtualFuncTable[0]; //DWORD* --> func*
    ((func*)*(pVirtualFuncTable + 1))();//(func*)pVirtualFuncTable[1]
}
//打印结果:
//动物在动
//动物在呼吸
//动物在动
//动物在呼吸

两种方法都能正确调用虚函数

我们可以聚焦到第二种调用方法上,我们采用一个DWORD*也就是int*来读取a.this


接着我们对aThis进行了解引用,并将解引用的值当做一个指针转存起来,起名pVirtualFuncTable(虚函数表)


其实可以想象这是一个结构体,结构体的第一个元素存放的是一个数组,而我们获取的是这个数组的首地址,所以pVirtualFuncTable所指向的就是虚函数数组的首地址,这个数组里存放的都是函数的地址。


struct cls{
	int addrs[n];
	...
}

虚函数表里的内容是函数指针,也就是说*pVirtualFuncTable是一个函数指针(func*,但是我们暂时把它写成了DWORD*),可以简单理解成func* pVirtualFuncTable[]*pVirtualFuncTable相当于pVirtualFuncTable[0],能取出一个函数指针func*,但编译器也不会让我们写arr[] = &x这样的代码,所以只能写成上面的那种形式。


最后,我们将这个DWORD*转换成func*,并对其进行调用。


虽然类的实例不能直接转换成DWORD*,但是可以写(int*)0x1234,这样的代码,但是为了便于理解,我还是写成了func* arr[]这样的形式。


如果声明成func arr[] (也就是DWORD*),那么会少一级指针,最后也能让DWORD成功转换成func*


那么有的同学就要问了,如果我实例化多个类的时候,虚函数表会是同一个吗?

废话不多说,直接写代码好吧

void main(){
	Animal a1, a2;
	DWORD* pVirtualFuncTable1 = *(DWORD**)&a1;
    DWORD* pVirtualFuncTable2 = *(DWORD**)&a2;
    cout << (pVirtualFuncTable1 == pVirtualFuncTable2) << endl;
}//打印结果: true

确实,他们会指向同一个虚函数表


上面那个小实验的补充内容:(可选则跳过这段内容)

有的人可能会好奇到打印一下函数的第一条指令是不是一样的,但是很遗憾,是不一样的。


如果对这个问题不感兴趣,那么就跳过吧。


int main(){
	...
	
	//尝试打印这些值,但是很遗憾,每个函数指针打印出来的值并不能很好地对应
	//需要把 跳转表 关掉才行,捣鼓了半天,也没搞懂怎么关
	printf("%p\t", &Animal::move); 
	printf("%p\n", &Animal::breath);
	printf("%p\t", *pVirtualFuncTable);
	printf("%p\n", *(pVirtualFuncTable + 1));
}

这一段打印代码不能打印出一样的地址的原因如下:

这里涉及到跳转表,函数调用时首先会跳掉一个全是jmp指令的地方,然后才跳到函数开始的地方。


然而每次调用的时候,都是不同的地址,然而都会跳到同一个地方去执行,这意味着两个跳转表都能跳到正确的位置执行,有兴趣的同学可以自己想办法关掉跳转表,再打印


多态

首先回顾一下,之前所学的内容

之前说过,如果出现同名函数,那么重名的函数会被重写,但是并不会将其删除,而是被隐藏起来了。


所以我们会优先调用当前类型的同名函数,People* p = new People,那么,当前类型就是People

如何实现多态?

当我们对一个虚函数进行重写的时候,就能实现多态

class Animal {
public:
    virtual void move() { cout << "动物在动" << endl; }
    virtual void breath() { cout << "动物在呼吸" << endl; }
};

class Bird : public Animal {
public:
    void move() { cout << "鸟儿在飞" << endl; }
    void breath() { cout << "鸟儿在空中呼吸" << endl; }
};

void main() {
    Animal* a  = new Animal;
    Animal* a2 = new Bird;
    a->move();
    a->breath();
    a2->move();
    a2->breath();
    delete a;
    delete a2;
}
//打印结果:
//动物在动
//动物在呼吸
//鸟儿在飞
//鸟儿在空中呼吸

如果没有写virtual关键字的话,声明是什么类的变量,就会调用哪个类的函数。


Animal a不管后面写的是什么,如果函数不是virtual的,那么就会调用Animal::func()


如果声明了virtual的,那么就会按照定义时(new)的类型进行调用。



到底是什么使得virtual,能调用new的类的函数呢?

之前我们说过了,virtual的函数会装入一个虚函数表里。


那么有没有可能是在new的时候,就直接把自己的虚函数写入了这个表呢?

下面我们来试着验证一下:

稍微修改忆点点main函数

void main() {
    Animal* a  = new Animal;
    Animal* a2 = new Bird;
    Bird*   b = new Bird;
    DWORD** a_virTable = *(DWORD***)a;
    DWORD** a2_virTable = *(DWORD***)a2;
    DWORD** b_virTable = *(DWORD***)b;
    //func* virTable[] = *(DWORD***)a; //这样的写法是错误的,只是便于理解
    printf("a  point to move:\t %p\n", *a_virTable); //打印函数指针
    printf("a  point to breath:\t %p\n", *(a_virTable + 1));
    printf("a2 point to move:\t %p\n", *a2_virTable);
    printf("a2 point to breath:\t %p\n", *(a2_virTable + 1));
    printf("b  point to move:\t %p\n", *b_virTable);
    printf("b  point to breath:\t %p\n", *(b_virTable + 1));
    ((func*)*a_virTable)();         //a->move();
    ((func*)*(a_virTable + 1))();   //a->breath();
    ((func*)*a2_virTable)();        //a2->move();
    ((func*)*(a2_virTable + 1))();  //a2->breath();
    ((func*)*b_virTable)();         //b->move();
    ((func*)*(b_virTable + 1))();   //b->breath();
    delete a;
    delete a2;
    delete b;
}
//打印结果:
//a  point to move:        010E1537
//a  point to breath:      010E1762
//a2 point to move:        010E1609
//a2 point to breath:      010E1992
//b  point to move:        010E1609
//b  point to breath:      010E1992

我们发现,正如猜想的那样,a2的虚函数表的内容和b的一模一样。


如果虚函数没有被重写,那么会装载父类的虚函数,这和继承是一样的。


可以自己验证一下。


书上把填充虚函数表这个行为称之为绑定,但我们现在只看结果,不用去在意过程是怎么样的,不用关注具体到在什么时候进行的绑定,除非真的到了要了解这个的时候。


我们只需要大概了解到虚函数在new的时候会将虚函数填充完整, 如果遇到了没有重载的虚函数,那么就会将父类的虚函数填入表中,如果父类也没有向上进行寻找,直到没有父类为之。



虚函数的析构函数

上次谈到使用多态形式时,delete 实例只会调用当前类型的析构函数,因为子类的空间并不归父类管

那么想要调用子类的析构函数,就要另辟蹊径了。


上面我们讲到,如果函数被声明为虚函数,那么实例化(new)的类型的对应函数的函数指针就会被装载到虚函数表中。


不管当前类是什么,调用的都是这个虚函数表中的虚函数的指针。


那么如果我们把析构函数声明成虚函数,new的时候析构函数就会装载到虚函数表里,我们再调用这个类的析构函数的时候,就会查虚函数表,刚好能找到创建时放入的函数。


而子类的析构函数会调用父类的析构函数。


这一波简直完美。


class Animal {
public:
    virtual ~Animal() { cout << "Animal 析构函数" << endl; }
};
class Bird : public Animal {
public:
    ~Bird() { cout << "Bird析构函数" << endl; }
};
int main(){
	Animal* b = new Bird;
	delete b;
}
//打印结果:
//Bird析构函数
//Animal 析构函数
纯虚函数

有纯虚函数的类被当做是模板类(抽象类)。



这个类不能被实例化,因为纯虚函数并没有定义(实现),而之前提到虚函数必须要有定义。


纯虚函数的声明:

virtual void func() = 0;

没错,就是显示地写出 = 0,表示我不回去实例化它,如果要在声明纯虚函数的作用域定义它,会被编译器拒绝。


这样就能防止不小心定义了这个纯虚函数。


继承的父类中有纯虚函数,但不实例化,那么当前类也被认为是个模板类。


override 和 final

override关键字的主要作用是防止我们重载写错的

试想一下,我在父类中写了一个函数virtual void func(w_char c),然后我的子类继承了父类,我要重写这个函数。


但是一不小心我写错了,写成(virtual) void func(char c)


但编译器并不会报错,因为这是个全新的函数,我们为子类声明一个新的函数,或是一个新的虚函数,这并没有什么不妥,但这个结果却不是我们所想要的。


所以为了防止这种错误的发生,C++11起,可以使用override关键字,表示这个函数是被重写的。


当我们写void func(char c) override的时候,编译器会查找父类,父类的父类…如果没有找到,那么他就会报错,因为这个函数匹配不到可重写的函数,这是个新的函数,不符合override的语义

final则更为简单,字面上的意思是最后的

所以如果这个关键字写在函数后面,则表示我不希望子类再对这个函数进行重写了,这是最后的版本。


如果写在类的后面,则表示这个类是最终版本,不希望再被继承。


总结
  • 虚函数表会被放置在类空间的最上方的位置
  • 当new的时候,函数会将虚函数表填充完整
  • 当子类虚函数没有被重载的时候,那么会想父类进行查找填充
  • 如果父类,父类的父类…查到尽头都没有查到虚函数的定义,那么这个函数不能被实例化
  • 模板类(抽象类)不能被实例化,如果子类继承了抽象类,但没有重写纯虚函数,那么子类也是模板类,也不能被实例化
  • 善用override关键字,也许会在关键时刻省去许多麻烦,final表示这个最后一个版本
  • 对了,还有跳转表是个有意思的东西,函数不会直接call到函数本体里去,而是通过一个跳转表重新定位函数所在的地方

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/580244.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-11
下一篇 2022-04-11

发表评论

登录后才能评论

评论列表(0条)

保存