C++(六) 虚函数与多态

文章探讨了类对象通过虚函数指针和直接调用的区别,以及虚函数表的寻址机制。还分析了单继承和多继承下虚函数的调用行为,以及this指针的作用。最后提到了内存方位在多继承中的影响。
摘要由CSDN通过智能技术生成

虚函数和多态关系密切,只有掌握好了虚函数才能更好的理解多态

虚函数调用

函数的调用有以下两种形式:

直接调用:在反汇编中是call + 地址的形式,在硬编码中是E8 + 地址的形式

间接调用:在反汇编中是call + [.....]的形式,在硬编码中是FF + 地址的形式。其中[.....]中是一个地址,call + [.....]指的是将[.....]中地址的数据作为地址进行调用

现我们观察如下程序,该程序定义了一个类,类中有普通函数和虚函数,我们在main函数中通过对象去调用这两个函数

class Person
{
public:
    void method1()
    {
        printf("method1\n");
    }
    virtual void method2() //虚函数
    {  
        printf("method2\n");
    }
};
int main(int argc, char* argv[])
{            
    Person person;
    person.method1();
    person.method2();
    return 0;
}

我们进入反汇编观察函数调用的情况:

我们发现,虚函数和普通函数调用的情况是一致的。

我们已知ecx中存储的是this指针的地址,我们定义的类中并没有定义成员变量,但是this指针却指向了ebp – 4,这是一个奇怪的现象,我们将在之后的内容中讲解

现我们通过指针去调用两个函数,观察是否有所不同

class Person
{
public:
    void method1()
    {
        printf("method1\n");
    }
    virtual void method2()
    {
        printf("method2\n");
    }
};
int main(int argc, char* argv[])
{            
    Person person;
    Person* p = &person;  //用指针的方式调用
    p->method1();
    p->method2();
    return 0;
}

我们进入反汇编观察

我们可以发现,这时,在调用虚函数的时候和调用普通函数就有了区别。调用虚函数(第一个call)是间接调用(FF)。注意最后两行是调用堆栈平衡的代码,不要搞混了

纯虚函数

春旭函数的特点:

1.将成员函数声明为virtual,此时该函数为虚函数

2.该函数没有函数体,函数后跟=0

接下来我们对此进行一个举例

class Base

{

public:

virtual int Plus() = 0;//该函数为纯虚函数

}

抽象类

抽象类有这几种特点:

1.包含纯虚函数的类,就是抽象类;

2.抽象类可以包含普通的函数;

3.抽象类不能实例化,即创建对象。

我们可以把抽象类看作是对子类的一种约束,或者认为其(抽象类)就是定义一种标准。

举例:淘宝,有很多店铺,虽然每个店铺卖的东西都不一样,但是他们同样都可以下单、评论、购物车,也就是说他们都遵守了这种标准规则。也就是说淘宝就是一个作为抽象类的父类,其有很多成员:购物车、评论、商品展示区等等。但是淘宝都有定义实现这些成员,而是交给开淘宝店的人(子类)根据父类成员去定义实现。

现在我们实际应用纯虚函数

如此演示下来,我们不难发现,纯虚函数就是给一个模板。子类想要实现纯虚函数要求的相同的功能,比如函数名和父类的纯虚函数一样,这样的话,实际就是方便类的统一管理

虚函数表的引入

我们更新以下代码。在新的代码中我们为Person定义了两个成员变量,并在main()函数中查看该类的大小

class Person
{
	int x;
	int y;
public:
	void method1()
    {
		printf("method1\n");
	}
	virtual void method2()
    {
		printf("method2\n");
	}
};
int main(int argc, char* argv[])
{		
	Person person;
	printf("%d",sizeof(person));  //12
	return 0;	
}

程序运行以后打印了12,因此我们得知,这个类大小是12。而我们定义的类中只有两个int类型的成员变量,理应只有8字节大小,这时多出来的4字节大小自然跟虚函数有关。

我们现在该类中定义两个虚函数,观察是否该类多出来8字节大小

class Person
{
	int x;
	int y;
public:
	virtual void method1()
    {
		printf("method1\n");
	}
	virtual void method2()
    {
		printf("method2\n");
	}
};
int main(int argc, char* argv[])
{		
	Person person;
	printf("%d",sizeof(person));  //12
	return 0;	
}

程序运行后,我们发现仍然打印了12,这也意味着该类大小仍然是12,并没有因为多出来一个虚函数而导致该类变大4字节

我们现将该类成员变量进行赋值,并只保留一个虚函数,为了方便在内存观察

class Person
{
	int x;
	int y;
public:
	Person()
    {
		x = 1;
		y = 2;
	}
	virtual void method1()
    {
		printf("method1\n");
	}
};
int main(int argc, char* argv[])
{		
	Person person;
	printf("%d",sizeof(person));
	return 0;	
}

现我们进到该类对象的内存空间,观察这四个大小的字节到底是什么

我们发现,后八个字节正是我们赋值的成员属性,前四个字节便是多出来的四个字节,它是虚函数表的地址,进入该地址便是虚函数表

虚函数表

现我们通过指针去查看虚函数表有什么用

class Person
{
	int x;
	int y;
public:
	Person()
    {
		x = 1;
		y = 2;
	}
	virtual void method1()
    {
		printf("method1\n");
    }
};
int main(int argc, char* argv[])
{		
	Person person;
	Person* p = &person;
	p->method1();
	return 0;	
}

进入反汇编

发现call要调用的函数的地址是虚函数表上前四个字节数据作为地址,该地址上存储的数据。

现在我们定义两个虚函数,观察虚函数表有什么变化

class Person
{
	int x;
	int y;
public:
	Person()
    {
		x = 1;
		y = 2;
	}
	virtual void method1()
    {
		printf("method1\n");
    }
    virtual void method2()
    {
	    printf("method2\n");
    }
};
int main(int argc, char* argv[])
{		
	Person person;
	Person* p = &person;
	p->method1();
    p->method2();
	return 0;	
}

我们再次进入反汇编

我们发现第二个虚函数地址正好在虚函数表的第二个四字节。由此我们也可以得知,虚函数表其实就是一个数组,这个数组依次排列着虚函数的地址

总结:当类中有虚函数时,类对象内存大小会多出4字节,这4个字节是指向虚函数表的地址,虚函数表里面依次存储了所有虚函数的地址

现我们可以用函数指针论证以下虚函数表中是不是真的虚函数地址

typedef int (*func)(int *p),这是一个函数指针,我们分析一下

已知func是一个指针,而int *p表示int*类型的函数形参,int表示返回值类型,所以推出*func是一个函数,因此func是一个指向这类函数的指针,即函数指针。该函数指针表示一个int*类型形参,int类型返回值的func类型函数指针。

class Person
{
	int x;
	int y;
public:
	Person()
    {
		x = 1;
		y = 2;
	}
	virtual void method1()
    {
		printf("method1\n");
	}
	virtual void method2()
    {
		printf("method2\n");
	}
};
int main(int argc, char* argv[])
{		
	Person person;
	typedef void (*pMethod)(void);   //函数指针声明
	pMethod pm1 = (pMethod)(*(int*)(*(int*)&person));  //定义pm1函数指针并将虚函数表中第一个地址值赋给pm1
	pMethod pm2 = (pMethod)(*((int*)(*(int*)&person) + 1));  //定义pm2函数指针并将虚函数表中第二个地址值赋给pm2
	//接着用函数指针的方式调用
    pm1();  //method1
	pm2();  //method2
	return 0;	
}

程序可以正常执行虚函数

如下是虚函数的图解

单继承无重写

图示如下

class Base{	
public:	
    virtual void Function_1(){	
        printf("Base:Function_1...\n");	
    }	
    virtual void Function_2(){	
        printf("Base:Function_2...\n");	
    }	
    virtual void Function_3(){	
        printf("Base:Function_3...\n");	
    }	
};	
class Sub:public Base{	
public:	
    virtual void Function_4(){	
        printf("Sub:Function_4...\n");	
    }	
    virtual void Function_5(){	
        printf("Sub:Function_5...\n");	
    }	
    virtual void Function_6(){	
        printf("Sub:Function_6...\n");	
    }	
};

单继承有重写

图示如下

如图所示,被重写的父类函数被覆盖了

class Base{
public:
    virtual void Function_1(){
        printf("Base:Function_1...\n");
    }
    virtual void Function_2(){
        printf("Base:Function_2...\n");
    }
    virtual void Function_3(){
        printf("Base:Function_3...\n");
    }
};
class Sub:public Base{
public:
    virtual void Function_1(){
        printf("Sub:Function_1...\n");
    }
    virtual void Function_2(){
        printf("Sub:Function_2...\n");
    }
    virtual void Function_6(){
        printf("Sub:Function_6...\n");
    }
};

多个继承无重写

该情况属于有多个父类时,子类直接继承父类

如图所示,此时有了两个虚函数表。第一张表记录了第一个父类虚函数和子类的虚函数。第二张表记录了第二个父类的虚函数

class Base1{
public:
    virtual void Fn_1(){
        printf("Base1:Fn_1...\n");
    }
    virtual void Fn_2(){
        printf("Base1:Fn_2...\n");
    }
};
class Base2{
public:
    virtual void Fn_3(){
        printf("Base2:Fn_3...\n");
    }
    virtual void Fn_4(){
        printf("Base2:Fn_4...\n");
    }
};
class Sub: public Base1,public Base2{
public:
    virtual void Fn_5(){
        printf("Sub:Fn_5...\n");
    }
    virtual void Fn_6(){
        printf("Sub:Fn_6...\n");
    }
};

多个继承有重写

图示如下

如图所示该情况也有两张表,第一张表记录了第一个父类虚函数和子类的虚函数,但父类被重写的虚函数会被覆盖。第二张表记录了第二个父类的虚函数,同样的父类被重写的虚函数会被覆盖

class Base1{
public:
    virtual void Fn_1(){
        printf("Base1:Fn_1...\n");
    }
    virtual void Fn_2(){
        printf("Base1:Fn_2...\n");
    }
};
class Base2{
public:
    virtual void Fn_3(){
        printf("Base2:Fn_3...\n");
    }
    virtual void Fn_4(){
        printf("Base2:Fn_4...\n");
    }
};
class Sub:public Base1,public Base2{
public:
    virtual void Fn_1(){
        printf("Sub:Fn_1...\n");
    }
    virtual void Fn_3(){
        printf("Sub:Fn_3...\n");
    }
	virtual void Fn_5(){
        printf("Sub:Fn_5...\n");
    }
};

多重继承无重写

图示如下


class Base1{
public:
    virtual void Fn_1(){
        printf("Base1:Fn_1...\n");  
    }
    virtual void Fn_2(){
        printf("Base1:Fn_2...\n");
    }
};
class Base2:public Base1{
public:
    virtual void Fn_3(){
        printf("Base2:Fn_3...\n");
    }
    virtual void Fn_4(){
        printf("Base2:Fn_4...\n");
    }
};
class Sub:public Base2{
public:
    virtual void Fn_5(){
        printf("Sub:Fn_5...\n");
    }
    virtual void Fn_6(){
        printf("Sub:Fn_6...\n");
    }
};

多重继承有重写

图示如下

此时,依然一张虚表,依次记录从第一个父类到子类的虚函数地址。但父类被重写的依然会被覆盖

class Base1{
public:
    virtual void Fn_1(){
        printf("Base1:Fn_1...\n");
    }
    virtual void Fn_2(){
        printf("Base1:Fn_2...\n");
    }
};
class Base2:public Base1{
public:
    virtual void Fn_1(){
        printf("Base2:Fn_1...\n");
    }
    virtual void Fn_3(){
        printf("Base2:Fn_3...\n");
    }
};
class Sub:public Base2{
public:
    virtual void Fn_3(){
        printf("Sub:Fn_3...\n");
    }
	virtual void Fn_5(){
        printf("Sub:Fn_5...\n");
    }
};

绑定与多态

绑定:当调用函数时,将函数与函数地址关联到一起的过程     

多态:当子类重写父类函数时,编译器可以调用正确的同名函数                                             

有如下代码:

class Base{
public:
    int x;
    Base()
    {
        x = 100;
    }
    void Function_1()//Func1为普通函数
    {  
        printf("Base:Function_1...\n");
    }
    virtual void Function_2()//Func2为虚函数
    { 
        printf("Base:Function_2...\n");
    }
};
class Sub:public Base
{
public:
    int x;
    Sub()
    {
        x = 200;
    }
    void Function_1()
    {
        printf("Sub:Function_1...\n");
    }
    virtual void Function_2()
    {
        printf("Sub:Function_2...\n");
    }
};
void Test(Base* pb)
{
    int n = pb->x;
	printf("%d\n",n);  //100
	pb->Function_1();  //Base:Function_1...
	pb->Function_2();  //Base:Function_2...
}
int main(int argc, char* argv[])
{
    Base base;  //创建父类对象
    Test(&base);
	return 0;	
}

注意:一个程序的完整执行流程是先编译后运行

当程序编译完成以后,函数的地址就已经确定(如上述代码Test函数),这种情况就叫做编译期绑定或前期绑定。在反汇编中,调用该函数的格式是call + 直接地址

当在程序运行时去调用一个函数时,这个函数地址才会确定,这种情况叫做运行期绑定或动态绑定或晚绑定。在反汇编中,调用该函数的格式是call + 间接地址

我们针对上述代码中Test函数进一步讲解绑定问题。现在进入反汇编观察Test函数

此处多注意父类指针和子类指针能都表示的范围,可以帮助我们理解

由反汇编可知,Test函数中x是已经确定的,是类base的x值为10

我们继续观察反汇编

由E8我们可以发现,Function_1()函数在编译时就已经确定了函数地址。

由FF我们可以发现,Function_2()函数的函数地址并没有被确定,call调用的[edx]的值是一个地址,该地址上的值可能被重写:可能因为虚函数重写导致改变

因此我们可以发现,在一个类中,其普通函数地址和成员变量,在编译期便已经写死不变了。而虚函数表会在执行期再被确定

现在我们分别以Base对象和Sub对象具体化的探究绑定

现以Base对象传入Test函数,观察程序运行,发现调用的Func2函数是父类Base的函数,其他也都是Base的函数

如果以以Sub对象传入Test函数,观察程序运行,发现调用的Func2函数是子类Sub的函数,其他同样也都是Base的函数没有改变

如此验证,可以发现虚函数是动态绑定的,而动态绑定的另一个名字便是多态。一种类型体现出不同的行为,这便是多态

注意:普通函数的重写也属于多态绑定,普通函数的重载属于编译期绑定

现在我们再看一个程序去更深刻理解多态

class Base
{
public:
    int x;
    int y
    Base()
    {
        x = 1;
		y = 2;
    }
    void Print () // Print没有加virtual
    { 
        printf("Base:%x %x\n",x, y);
    }
};
class Sub1:public Base
{
public:
    int x;
    int y;
    Sub1()
    {
        x = 3;
		y = 4;
		A = 
    }
    void Print()
    {
        printf("Sub1:%x %x \n", x, y);
    }
};
class Sub2:public Base
{
public:
    int x;
    int y;
    Sub2()
    {
        x = 6;
		y = 7;
		B = 8;
    }
    void Print()
    {
        printf("Sub2:%x %x \n", x, y);
    }
};
void Test(Base* pb) //父类的指针指向子类的对象
{  
	Base b;
	Sub1 s1;
	Sub2 s2;
	Base* arr[] = {&b, &s1, &s2};
	for(int i = 0; i < 3; i++)
	{
		arr[i]->Print();
    }
}
int main(int argc, char* argv[])
{
    Sub sub;  //创建子类对象
    Test(&sub);  //父类的指针指向子类的对象
	return 0;	
}

运行程序后,我们发现,调用的Print()都是父类的Print(),这并没有体现多态的特性

我们对程序进行修改

class Base
{
public:
    int x;
    int y
    Base()
    {
        x = 1;
		y = 2;
    }
    virtual void Print () // Print没有加virtual
    { 
        printf("Base:%x %x\n",x, y);
    }
};
class Sub1:public Base
{
public:
    int x;
    int y;
    Sub1()
    {
        x = 3;
		y = 4;
		A = 
    }
    virtual void Print()
    {
        printf("Sub1:%x %x \n", x, y);
    }
};
class Sub2:public Base
{
public:
    int x;
    int y;
    Sub2()
    {
        x = 6;
		y = 7;
		B = 8;
    }
    virtual void Print()
    {
        printf("Sub2:%x %x \n", x, y);
    }
};

void Test(Base* pb) //父类的指针指向子类的对象
{  
	Base b;
	Sub1 s1;
	Sub2 s2;
	Base* arr[] = {&b, &s1, &s2};
	for(int i = 0; i < 3; i++)
	{
		arr[i]->Print();
    }
}
int main(int argc, char* argv[])
{
    Sub sub;  //创建子类对象
    Test(&sub);  //父类的指针指向子类的对象
	return 0;	
}

此时程序分别执行了父类和子类对应的Print()函数

析构函数的虚函数化

现有一个父类一个子类,并用父类指针指向父类对象和子类对象

Base b;
Sub s;
Base* pb = &b;  //Base类指针指向自己的对象
Base* ps = &s;  //父类指针指向子类的对象

假设父类和子类的析构函数都不是虚函数,那么当子类对象s进行释放的时候,由于是用Base*指针指向的s对象,所以会调用Base类中的析构函数,那么此时,对象s并没有释放

因此我们就需要将父类和子类的析构函数都设置成虚函数,这时候,释放对象s时,调用的就是s的析构函数,对象s正常被释放

作业

重载和重写

重载:一个类中,函数名一样,参数的个数或类型不同

重写:子类中的函数和其父类中的函数名字、参数、返回值一模一样(函数覆盖)

1.单继承无函数覆盖(打印Sub对象的虚函数表):

#include <iostream>
class Base {
public:
    virtual void Function_1() {
        printf("Base:Function_1...\n");
    }
    virtual void Function_2() {
        printf("Base:Function_2...\n");
    }
    virtual void Function_3() {
        printf("Base:Function_3...\n");
    }
};
class Sub :public Base {
public:
    virtual void Function_4() {
        printf("Sub:Function_4...\n");
    }
    virtual void Function_5() {
        printf("Sub:Function_5...\n");
    }
    virtual void Function_6() {
        printf("Sub:Function_6...\n");
    }
};
int main(int argc, char* argv[]) {
    Sub sub;
    Sub* subp = &sub;
    int* p  = (int*)*(int*)subp; //指向了虚函数表
    for (int i = 0; i < 6; i++)//打印虚函数表
    {
        std::cout << *(p + i) << std::endl;
    }
    return 0;
}

我们通过内存可以发现,该类只有一个虚函数表,虚函数表中有六个成员

2.单继承有函数覆盖(打印Sub对象的虚函数表)

#include<stdio.h>
class Base
{
public:
    virtual void Function_1() {
        printf("Base:Function_1...\n");
    }
    virtual void Function_2() {
        printf("Base:Function_2...\n");
    }
    virtual void Function_3() {
        printf("Base:Function_3...\n");
    }
};
class Sub :public Base {
public:
    virtual void Function_1() {
        printf("Sub:Function_1...\n");
    }
    virtual void Function_2() {
        printf("Sub:Function_2...\n");
    }
    virtual void Function_6() {
        printf("Sub:Function_6...\n");
    }
};
int main(int argc, char* argv[]) {
    Sub sub;
    Sub* subp = &sub;
    int* p = (int*)*(int*)subp;
    for(int i = 0; i < 3; i++)
    {
        printf("%08x\n", *p);
    }
    return 0;
}

我们通过内存发现,该类只有一个虚函数表,虚函数表中只有三个成员

3.体会多态
定义一个父类Base:有两个成员X,Y;有一个函数Print(非virtul)能够打印X,Y的值

定义2个子类:Sub1,有一个成员A;Sub2,有一个成员B。每个子类有一个函数Print(非virtul),打印所有成员----Sub1:打印X Y A;Sub2:打印X Y B

定义一个数组,存储Base Sub1 Sub2对象;再使用一个循环语句调用所有的Print函数

#include<stdio.h>
class Base
{
public:
    int x;
    int y;
    Base()
    {
        x = 1;
        y = 2;
    }
    void Print()
    {
        printf("%d %d\n", x, y);
    }
};
class Sub1 : public Base
{
private:
    int A;
public:
    Sub1()
    {
        x = 3;
        y = 4;
        A = 5;
    }
    void Print()
    {
        printf("%d %d %d\n", x, y, A);
    }
};
class Sub2 : public Base
{
private:
    int B;
public:
    Sub2()
    {
        x = 6;
        y = 7;
        B = 8;
    }
    void Print()
    {
        printf("%d %d %d\n", x, y, B);
    }
};
int main(int argc, char* argv[]) {
    Base base;
    Base* pbase = &base;
    Sub1 sub1;
    Sub1* psub1 = &sub1;
    Sub2 sub2;
    Sub2* psub2 = &sub2;
    Base* arr[] = { pbase, psub1, psub2 };
    for (int i = 0; i < 3; i++)
    {
        arr[i]->Print();
    }
    return 0;
}

运行该程序以后,我们发现,该程序执行了三次父类Base的Print()函数。这是因为我们定义的数组类型是父类指针Base*,因此那么数组成员即使是子类指针,在调用函数时依然是父类指针进行调用,而父类指针只能调用父类的Print()函数

为了解决这个问题,我们将Print()函数定义为虚函数

#include<stdio.h>
class Base
{
public:
    int x;
    int y;
    Base()
    {
        x = 1;
        y = 2;
    }
    virtual void  Print()
    {
        printf("%d %d\n", x, y);
    }
};
class Sub1 : public Base
{
private:
    int A;
public:
    Sub1()
    {
        x = 3;
        y = 4;
        A = 5;
    }
    virtual void Print()
    {
        printf("%d %d %d\n", x, y, A);
    }
};
class Sub2 : public Base
{
private:
    int B;
public:
    Sub2()
    {
        x = 6;
        y = 7;
        B = 8;
    }
    virtual void Print()
    {
        printf("%d %d %d\n", x, y, B);
    }
};
int main(int argc, char* argv[]) {
    Base base;
    Base* pbase = &base;
    Sub1 sub1;
    Sub1* psub1 = &sub1;
    Sub2 sub2;
    Sub2* psub2 = &sub2;
    Base* arr[] = { pbase, psub1, psub2 };
    for (int i = 0; i < 3; i++)
    {
        arr[i]->Print();
    }
    return 0;
}

此时运行程序发现正常调用了各个类的Print()函数


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值