C++之多态

C++之多态,第1张

多态

目录

多态

多态的定义

多态的构成条件

虚函数

虚函数的重写

C++11 override和final

重载、重写(覆盖)、隐藏的对比

抽象类

概念

接口继承和实现继承

多态的原理

虚函数表

动态绑定和静态绑定

单继承和多继承关系的虚函数表

单继承的虚函数表

多继承的虚函数表


多态的定义

多态:多态是在不同继承关系的类对象,去调同一函数,产生了不同的行为。

也就是说,多态就是函数调用的多种形态,调用函数更加灵活,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。

下面我们来看一下代码:

#include
using namespace std;
class Person
{
public:
    virtual void BuyTicket()
    {
        cout << "全价买票" << endl;
    }
protected:
    int _age;
    string _name;
};
class Student : public Person
{
public:
    virtual void BuyTicket()
    {
        cout << "半价买票" << endl;
    }
protected:
    //...
};
void Func(Person& pr)
{
    //pr为父类对象别名时调用父类的虚函数,为子类对象时调用子类虚函数
    pr.BuyTicket();
}
int main()
{
    Person p;
    Student s;

    Func(p);
    Func(s);
	return 0;
}

 我们而可以看到,分别打印了基类和派生类中的内容,所以这就是多态的一个运用。

所以我们知道了形成多态的两个要素:

  1. 子类重写父类的虚函数。

  2. 必须是父类的指针或者引用去调用虚函数。

如果没有这两个条件呢,那么会构不成多态,则会只调用父类的参数。

多态更细致的划分分为静态的多态和动态的多态。

静态的多态:函数重载,调用同一个函数,传不同的参数,就有不同的行为/形态。

动态的多态:父类的指针或者引用调用重写函数,不同的对象调用,就会产生不同的行为。

int main()
{
    int i = 0,j = 1;
    double d = 2.1, e = 3.2;
    swap(i,j);
    swap(d,e);
    return 0;
}

以上即是一种静态的多态,这里的静态是指编译时确定的,在编译阶段通过函数修饰规则去找函数。

动态的多态:不同的类型对象去完成同一件事情,产生的动作是不一样的。

多态的构成条件

在继承中要构成多态有两个条件:

  • 必须通过基类的指针或者引用调用虚函数
  •  被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

这里牵扯到两个知识点:

虚函数

虚函数就是在类的成员函数前面加virtual关键字。

class Person
{
public:
    virtual void BuyTicket()//这里就是虚函数
    {
        cout << "全价买票" << endl;
    }
protected:
    int _age;
    string _name;
};
虚函数的重写

虚函数的重写(也叫做覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),这样我们称派生类重写了基类的虚函数。

#include
using namespace std;
class Person
{
public:
    virtual void BuyTicket()
    {
        cout << "全价买票" << endl;
    }
protected:
    int _age;
    string _name;
};
class Student : public Person
{
public:
    virtual void BuyTicket()
    {
        cout << "半价买票" << endl;
    }
protected:
    //...
};
void Func(Person& pr)
{
    //pr为父类对象别名时调用父类的虚函数,为子类对象时调用子类虚函数
    pr.BuyTicket();
}
int main()
{
    Person p;
    Student s;

    Func(p);
    Func(s);
	return 0;
}

但虚函数重写也有两个意外:

协变(基类与派生类虚函数返回值类型不同)

正常的虚函数重写时,要求虚函数的函数名,返回值,参数都要相同,但协变例外,其基类与派生类的返回值可以不同。

即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。

class A{};
class B:public A{};
class Person
{
public:
	virtual A* fun()
	{
		cout << "A* fun()" << endl;
		return new A;
	}
};
class Student : public Person
{
public:
	virtual B* fun()
	{
		cout << "B* fun()" << endl;
		return new B;
	}
};

int main()
{
	Person p;
	Student s;
	Person* ptr = &p;
	ptr->fun();

	ptr = &s;
	ptr->fun();
	return 0;
}

 析构函数的重写(基类与派生类析构函数的名字不同)

如果基类里的析构函数定义为虚函数,那么派生类里的析构函数则无需定义为虚函数就可以重写基类的析构函数,这里他们的函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,这也说明的基类的析构函数最好写成虚函数。

首先我们先不构成虚函数:

 我们可以看到先析构了派生类,再析构了派生类中的基类部分,最后调用基类的析构函数析构了基类部分。

当我们加上虚函数:

 我们发现仍然和不加一样,普通场景下加不加虚函数好像都一样,但比如当我们有new对象的场景:

 我们可以看到这里是错误的,为什么会这样呢?因为delete时底层会调用析构函数和operator delete,实际的形式是delete p;//p->destuctor()+operator delete(p1),不是虚函数时,它们构成隐藏,又因为p和s都是父类指针,所以都会调用父类的析构函数。

当构成析构重写时:

 当父类加了虚函数virtual后,子类可以不加virtual,子类继承下来以后也有了virtual的属性,我们需要注意的是尽量不要写协变,尽量严格按重写要求来,这样代码更容易进行维护。

C++11 override和final

C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

如果虚函数不想被重写,可以在虚函数后面加上关键字final:

class Person
{
public:
    virtual void BuyTicket() final//这里加上final函数可以禁止重写
    {
        cout << "买票-全价" << endl;
    }
};
class Student : public Person
{
public:
    //子类的虚函数重写了父类的虚函数
    void BuyTicket()
    {
        cout << "买票-半价" << endl;
    }
};
void f(Person& p)
{
    
    p.BuyTicket();
}

int main()
{
    Person p;
    Student s;

    f(p);
    f(s);
    return 0;
}

我们可以看到的是我们无法重写基类中的虚函数,因为这里被定义了final,final也可以在类名后加,如class A final {},表示该类无法被继承,也可以叫做最终类。

override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

class Person
{
public:
    virtual void BuyTicket() 
    {
        cout << "买票-全价" << endl;
    }
};
class Student : public Person
{
public:
    //子类的虚函数重写了父类的虚函数
    void BuyTicket(int i) override//这里我们修改了同一参数这个条件导致没有重写
    {
        cout << "买票-半价" << endl;
    }
};
void f(Person& p)
{
    
    p.BuyTicket();
}

int main()
{
    Person p;
    Student s;

    f(p);
    f(s);
    return 0;
}

重载、重写(覆盖)、隐藏的对比

 

 总结:

  •  多态:多态是在不同继承关系的类对象,去调同一函数,产生了不同的行为。

  • 静态多态:是在编译时确定地址。

    动态多态:条件:1、子类继承父类,完成虚函数的重写 2、父类的指针或者引用去调用这个重写的虚函数。

  • 虚函数的重写条件:1、要是虚函数 2、函数名、参数、返回值都相同
  • 例外:1、协变 2、析构函数 3、子类中的重写的虚函数可以不加virtual关键字(建议加上)
抽象类 概念

在虚函数后面写上=0,则这个函数称为纯虚函数。

包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。

派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。

纯虚函数规范了子类必须重写虚函数,纯虚函数更加体现了接口的继承。

#include 
using namespace std;
class Car
{
public:
	virtual void func() = 0;
};
class Benz :public Car
{
public:
	virtual void func()
	{
		cout << "Benz-舒适" << endl;
	}
};

int main()
{
	Car* ptr = new Benz;
	ptr->func();
	return 0;
}

 这里的基类Car是不能实例化出对象的,派生类Benz继承后完成重写虚函数才可以实例化出对象。

接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。

虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。

所以如果不实现多态,不要把函数定义成虚函数。

多态的原理 虚函数表

当我们遇到需要计算带有虚函数的类的大小时应该如何计算呢?我们来看下面这段代码:

 我们可以看到在32位情况下大小为12,根据内存对齐这里的大小应该为8,那么为什么是12呢?

那是因为包含虚函数的类中会有一个虚函数表指针(简称虚表指针),这个虚函数表指针就是用来实现多态的。

 我们可以看到这个虚表指针指向的是一个数组,数组里的内容是函数指针,函数指针指向的是该类中的虚函数,所以可以说指向的是函数指针数组。

虚函数被编译成指令后,还是和普通函数一样,存在代码段,只是它的地址放在虚表中。

需要特别注意的是:这里和虚继承是不一样的,虽然都是virtual关键字,但解决的问题是不一样的,虚继承时产生了虚基表,虚基表里存的是距离虚基类的偏移量,而虚表指针指向的是一个函数指针数组。

class A
{
public:
	virtual void func()
	{
		cout << "funcA()" << endl;
	}
};
class B :public A
{
public:
	virtual void func()
	{
		cout << "funcB()" << endl;
	}
};
void f(A& a)
{
	a.func();
}
int main()
{
	A a;
	f(a);
	B b;
	f(b);
	return 0;
}

 

父子类无论是否完成虚函数的重写,都有各自的独立的虚表,一个类的所有对象共享一个虚表。

满足多态以后,构成多态:父类的指针或者引用 在调用虚函数时,不是在编译时决定的,是在运行时到指针或引用指向的对象的虚表中去找对应的虚函数调用,如果指向的是父类对象,则调用的就是父类的虚函数,指向的是子类的对象,调用的就是子类的虚函数。

如果不构成多态,那么这里调用的时候就是编译时确定的调用哪个函数,主要看的基类的类型,此代码则是调用的就是A的func,跟传什么类型对象过来没有关系。

注意:

为什么多态的条件之一必须是父类的指针或者引用去调用虚函数时发生多态,父类对象确不行呢?

当我们使用父类的指针或者引用,在切片时,指向或者引用父类对象时 或者 指向或者引用子类切成片出的父类部分,因为vfptr在对象的前四个字节保存,指向父类看到的是父类的虚表,指向子类看到的是子类的虚表。

但当父类为对象时,切片时只会拷贝成员变量,并不会拷贝vfptr指针,因为拷贝过去是不合理的,如果拷贝过去那么还要创建以一个父类才能有他的虚函数指针,但一个类共享一个虚表,这明显是不对的。

我们可以通过反汇编来更仔细观察:

满足多态调用的虚函数:

 不满足多态调用的虚函数:

 当我们将引用符去掉时,会发现和构成多态的虚函数是不同的。

多态的原理:基类的指针/引用,指向谁,就去谁的虚函数表里找到相应的位置的虚函数去调用。

动态绑定和静态绑定
  • 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如函数重载
  • 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态

普通函数的调用,编译(当在一个文件当中时在编译阶段确定)链接(当声明和定义分离时,在链接时确定)时确定地址,多态的调用是运行时确定地址,去指向对象的虚函数表中找到虚函数的地址。

那么如下的问题该如何解决呢?

对象中虚表指针是在什么阶段初始化的呢?虚表又是在什么阶段生成的呢?

对象中虚表指针是在构造函数初始化列表进行初始化,虚表是在编译时就生成好了。

虚函数是放在虚表里的吗?

其实这句话是不准确的,虚表里面放的是虚函数地址,虚函数跟普通函数一样,编译完成后,都是放在代码段。

 一个类中所有的虚函数地址,都会放在虚表中,这句话是对的吗?

这句话是正确的,因为一个类的所有虚函数是共享虚表的,我们可以从内存去观察,就可以发现这些虚函数的地址都会放在虚表中。

有一个情况是:vs下会在虚表结束位置放一个空指针表示虚表结束了。

当我们想知道虚表是存在哪里的?想办法写一段程序,论证一下虚表存在哪个区域的?那么怎么论证呢?我们定义各个区域的变量或者常量,通过看地址的方式看哪个地址和虚表指针的内容相近: 

class A
{
public:
	virtual void func()
	{
		cout << "funcA()" << endl;
	}
};
class B :public A
{
public:
	virtual void func()
	{
		cout << "funcB()" << endl;
	}
};
void f(A& a)
{
	a.func();
}
int j = 0;
int main()
{
    //取虚表地址打印一下
    A a;
    A* aa = &a;//pp指向整个对象p
    printf("vftptr:%p\n", *((int*)aa));//将pp强转为int*,即pp指向对象p的前四个字节,对它解引用就拿到了前四个字节,前四个字节就是vftptr(虚表指针)

    int i;
    printf("栈上地址:%p\n", &i);
    printf("数据段地址:%p\n", &j);

    int* k = new int;
    printf("堆地址:%p\n", k);
    char cp[] = "hello world";
    printf("代码段地址:%p\n", cp);
    return 0;
}

 我们可以看到vfptr更加接近代码段地址,所以我们可以得出虚表是存在代码段的。

虚函数编译时和普通函数一样,存在代码段,虚函数地址又被放到虚函数表中。

单继承和多继承关系的虚函数表 单继承的虚函数表
class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a;
};
class Derive :public Base {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
	virtual void func4() { cout << "Derive::func4" << endl; }
private:
	int b;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

我们发现我们通过调试发现监视窗口看不到子类自己的虚函数func3和func4。

 所以监视窗口不一定真实,但是内存上实际上是有的:

 我们通过程序来打印一下func3和func4:

class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a;
};
class Derive :public Base {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
	virtual void func4() { cout << "Derive::func4" << endl; }
private:
	int b;
};
typedef void(*VFunc)();//重命名函数指针类型,VFunc是函数指针
//void PrintVFT(VFunc ptr[])
void PrintVFT(VFunc ptr[])//存函数指针的数组指针,ptr指向函数指针数组
{
	for (int i = 0; ptr[i] != nullptr; ++i)
	{
		printf("VFT[%d]:%p\n", i, ptr[i]);
		ptr[i]();//调用该函数,确认地址是哪个函数的地址
	}
	printf("\n");
}
int main()
{
	Base b;
	PrintVFT((VFunc*)(*(int*)&b));//将b的虚表指针传过去,*(int*)&b)拿到虚表指针
	Derive d;
	PrintVFT((VFunc*)(*(int*)&d));//将d的虚表指针传过去

	return 0;
}

我么可以看到成功打印func3和func4。

多继承的虚函数表

我们通过代码来理解:

class Base1 {
public:
    virtual void func1() { cout << "Base1::func1" << endl; }
    virtual void func2() { cout << "Base1::func2" << endl; }
private:
    int b1;
};
class Base2 {
public:
    virtual void func1() { cout << "Base2::func1" << endl; }
    virtual void func2() { cout << "Base2::func2" << endl; }
private:
    int b2;
};
class Derive : public Base1, public Base2 {
public:
    virtual void func1() { cout << "Derive::func1" << endl; }
    virtual void func3() { cout << "Derive::func3" << endl; }
private:
    int d1;
};
typedef void(*VF_PTR)();//VF_PTR是函数指针
void PrintVFT(VF_PTR* table)
{
    for (int i = 0; table[i] != nullptr; ++i)
    {
        printf("vft[%d]:%p->", i, table[i]);
        VF_PTR f = table[i];
        f();
    }
    cout << endl << endl;
}
int main()
{
    Base1 b1;
    Base2 b2;

    Derive d;
    PrintVFT((VF_PTR*)*((int*)&d));//打印第一个虚表(d对象中起始位置为虚表指针)
    PrintVFT((VF_PTR*)*((int*)((char*)&d + sizeof(Base1))));//打印第二个虚表

    return 0;
}

 通过调试我们可以看到,多继承虚表的位置:

 我们可以看到派生类中的func3并没有显示,我们可以通过打印来观察:

PrintVFT((VF_PTR*)*((int*)&d));//打印第一个虚表(d对象中起始位置为虚表指针)
PrintVFT((VF_PTR*)*((int*)((char*)&d + sizeof(Base1))));//打印第二个虚表

我们可以通过这种方式来打印虚函数的位置:

PrintVFT((VF_PTR*)*((int*)&d));我们可以看到我们取d的地址,因为要打印第一张虚表所以我们取前四个字节,并且强转为VF_PTR*类型,因为虚表指针指向的类型是函数指针数组。

但我们想要打印第二张虚表时则有不同:PrintVFT((VF_PTR*)*((int*)((char*)&d + sizeof(Base1)))),首先将取地址d将他转为char*类型,加上Base1的大小就到了Base2,Base2的前四个字节是虚表指针,所以再强转为int*,然后解引用拿到这四个字节,最后强转为VFunc*,这样我们就打印出第二张虚表了。

 

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

原文地址: https://outofmemory.cn/langs/673275.html

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

发表评论

登录后才能评论

评论列表(0条)

保存