C++语法(16)---- 多态

https://blog.csdn.net/m0_63488627/article/details/130106690?spm=1001.2014.3001.5501https://blog.csdn.net/m0_63488627/article/details/130106690?spm=1001.2014.3001.5501

目录

1. 多态的概念

2.多态的实现

1.虚函数

2.多态条件

得到的多态条件

特殊条件

3.虚函数析构函数

4.override和final

 3.抽象类

1.概念

2.接口继承和实现继承

4.多态的原理

1.虚函数表

2.父子类虚表关系

3.多态的实现原理

1.静态绑定和态绑定

2.父子在多态调用的原理

3.虚表重要记忆点

5.单继承和多继承的多态

单继承

多继承

菱形继承

菱形虚拟继承


1. 多态的概念

1.概念

多态:就是多种形态,传不同的对象处理,会出现不同的状态

例子如,学生买票半价,普通人买票全价,军人优先买票

辨析:函数重载是通过传入不同的参数,随后能调用不同的同名函数;而多态是面对不同的对象出现不同的行为进行处理

2.多态的实现

1.虚函数

class Person 
{
public:
    //虚函数
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

class Student :public Person
{
public:
    //虚函数
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};

虚函数构成重写,也叫做覆盖;

三同:函数名相同,参数相同,返回值相同。

如果不是虚函数,则两个函数构成隐藏;

2.多态条件

得到的多态条件

1.类继承

2.虚函数重写

3.父类指针或者引用调用虚函数

void Func(Person& p)
{
	p.BuyTicket();
}

void Func(Person* p)
{
	p->BuyTicket();
}

比较

普通调用:跟调用对象类型有关,传入对象,运行的是对象中的函数

多态调用:与指向对象有关,指向父类调用父类虚函数,指向子类调用子类虚函数

void Func(Person p)
{
	p.BuyTicket();
}

特殊条件

1.子类可以不加virtual关键字,只要父类有virtual那依然是虚函数

2.协变:三同中,返回值可以不同,都是要求返回值为一个父子类关系的指针或者引用

 此外,传出其他类的父子关系的指针或者引用也可以。

破坏多态条件:

1.父类没有virtual那就破坏了虚函数,调用为普通调用

2.取消引用和指针,就算是虚函数也会变为普通调用

3.虚函数析构函数

推荐:在继承中,将析构函数作为虚函数

原因:

class Person 
{
public:
	~Person()
	{
		cout << "Person delete:" << _p << endl;
		delete _p;
	}
protected:
	int* _p = new int[10];
};

class Student :public Person
{
public:
	~Student()
	{
		cout << "Student delete:" << _s << endl;
		delete _s;
	}
protected:
	int* _s = new int[10];
};

 1.如果是传统的,对象的作用域结束调用析构,父子类都没什么问题。

2.但是如果我们用到了指针,那么会出现问题,其调用就是普通调用,不会指定全部删除;因为析构函数父子同名重定义,那么当传入指针进行析构,那自然是普通调用,父类普通调用还好,可能能清除;但是子类使用父类的指针或者引用则一定只调用了父类的析构。

3.所以,析构函数作为虚函数,那么指针析构,指向的是父类就父类析构,子类就子类析构

修改:

class Person 
{
public:
	virtual ~Person()
	{
		cout << "Person delete:" << _p << endl;
		delete _p;
	}
protected:
	int* _p = new int[10];
};

class Student :public Person
{
public:
	virtual ~Student()
	{
		cout << "Student delete:" << _s << endl;
		delete _s;
	}
protected:
	int* _s = new int[10];
};

满足三同原则,为重写,指向父类调父类,指向子类调子类 

结论:写继承,无脑给父类的析构写virtual关键字

4.override和final

如何实现出一个不被继承的类

1.构造函数设为私有;原因是子类需要父类的构造但是父类的构造设为私有无法访问;那么就无法被继承了 -- 此外析构私有不太行,虽然不能直接构造,但是可以new出来指针,绕过了判断

2.类定义时加final关键字,该类称为最终类

class A final
{ };

此外,final也可以修饰虚函数,这样函数就不能被重写。

class A final
{ 
    virtual func final();
};

override:检查函数是否完成了重写

class A
{ 
    virtual func final(int);
};

class B : public A
{ 
    virtual func final() override;
};

 3.抽象类

1.概念

在虚函数的后面写上 =0 ,则这个函数为纯虚函数;包含纯虚函数的类叫做抽象类。

1.抽象类不能实例化出对象

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

在我看来,抽象类算是一种模板,如果想继承该模板,必须重写虚函数后可以实例化。那么现实中一个不存在的概念性的对象,可以用抽象类定义。

2.接口继承和实现继承

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

2.虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

class A
{
public:
    virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
    virtual void test(){ func();}
};

class B : public A
{
public:
    void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }
};

int main(int argc ,char* argv[])
{
    B*p = new B;
    p->test();
    return 0;
}

A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确

p调用的test()是B从A中继承的,那么test中调用func函数,是this指针调用的,this是test的指针即A的指针,但是A指针是p切割后得到的,那么func是满足虚函数要求,调用func时是B的函数是多态情况,但是要知道虚函数是接口继承,可认为A中func函数被替换为B中的,但是val=1没有变,所以答案选B

4.多态的原理

1.虚函数表

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};

int main()
{
	cout << sizeof(Base) << endl;
	return 0;
}

虚表指针指向虚表,虚表存放虚函数的地址,有几个虚函数就有几个虚函数地址存储

2.父子类虚表关系

1.父类结构其实跟上面说的一样,一个虚表指针指向虚表,虚表里有虚函数地址

2.子类继承了父类,那子类中的父类有虚指针,但是虚表的内容发生变化;拷贝父类,将完成重写的虚函数进行覆盖,覆盖原来指向

3.虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。

3.多态的实现原理

1.静态绑定和态绑定

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

静态调用:就是编译器能确定调用的函数,以至于它call的是函数的地址

动态调用:编译器看不出来,需要算出函数的地址,再调用之

2.父子在多态调用的原理

1.对于一个指针指向无论是父类还是子类,指针能读到的都是父类的那一部分

2.父类的虚表就是上面说过的那样的,子类是覆盖了虚表

3.调用方式都是取虚表指针,在虚表中找到虚函数的地址取实现调用

4.也就是说,多态的实现取决于虚表

3.虚表重要记忆点

1.虚表放在常量区/代码段

2.同一个类型的虚表是一样,不同类型的虚表是不一样的

5.单继承和多继承的多态

单继承

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; }
private:
	int _b;
};

typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[],int num)
{
	cout << " 虚表地址>" << vTable << endl;
	for (int i = 0; i<num; ++i)
	{
		printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}

int main()
{
	Base b;
	Derive d;
	VFPTR * vTableb = (VFPTR*)(*(void**)&b);
	PrintVTable(vTableb,2);
	VFPTR* vTabled = (VFPTR*)(*(void**)&d);
	PrintVTable(vTabled,3);
	return 0;
}

此时我们发现:子类没有构成虚函数的函数也在虚表中 

多继承

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(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
	cout << " 虚表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}
int main()
{
	Derive d;
	VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
	PrintVTable(vTableb1);
	VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
	PrintVTable(vTableb2);
	return 0;
}

1.多继承,只有有虚函数的父类,才会生成虚表 

2.子类自己的函数,存在第一个虚表中

菱形继承

其模型就是菱形继承的虚函数,其实就是D中有BC;而BC还要A的虚表,所以BC自带两个虚表

菱形虚拟继承

1.如果是菱形虚拟继承,则D一定要写虚函数;因为如果不写,A虚函数不知道写在B表还是C表,其存在二义性编译器看不懂。

2.D类会多一个虚表

3.B的第一个表是虚表,第二个是虚基表,C亦是如此

4.虚基表的开头找的是虚函数的偏移量。

class A{
public:
    A(char *s) { cout<<s<<endl; }
    ~A(){}
};

class B:virtual public A
{
public:
    B(char *s1,char*s2):A(s1) { cout<<s2<<endl; }
};

class C:virtual public A
{
public:
    C(char *s1,char*s2):A(s1) { cout<<s2<<endl; }
};

class D:public B,public C
{
public:
    D(char *s1,char *s2,char *s3,char *s4):B(s1,s2),C(s1,s3),A(s1)
    { cout<<s4<<endl;}
};

int main() {
    D *p=new D("class A","class B","class C","class D");
    delete p;
    return 0;
}

A:class A class B class C class D 
B:class D class B class C class A
C:class D class C class B class A 
D:class A class C class B class D

1.A只有一个,所以A只构造一次

2.优先A构造

3.初始化列表优先于括号内,所以D最后

4.构造按继承顺序,即使先C(s1,s3),B(s1,s2) 也是B先输出,C再输出

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

灼榆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值