C++多态

目录

1.多态的定义及实现

2.C++11 override 和 fifinal

3. 抽象类

4.多态的原理

4.1虚函数表

4.2动态绑定与静态绑定

5.多继承关系的虚函数表

6.继承和多态常见面试题 


1.多态的定义及实现

概念:多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。
在继承中要构成多态还有两个条件:
1. 必须通过基类的指针或者引用调用虚函数
2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
虚函数:virtual修饰的类成员函数
虚函数的重写 ( 覆盖 ) 派生类中有一个跟基类完全相同的虚函数 ( 即派生类虚函数与基类虚函数的
返回值类型、函数名字、参数列表完全相同 ) ,称子类的虚函数重写了基类的虚函数。
例如:
class Person {
public:
 virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
 virtual void BuyTicket() { cout << "买票-半价" << endl; }

补充:虚函数的重写有两个例外

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

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指 针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。

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

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字, 都与基类的析构函数构成重写,可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数称统一处理成destructor

举例:

class AA
{
public:
	AA(int a=5)
	{
		_a = new int[a];
	}
	virtual void person()
	{
		cout << "AAperson" << endl;
	}
	virtual ~AA()
	{
		delete[] _a;
		cout << "delete AA" << endl;
	}
private:
	int* _a;
};
class BB:public AA
{
public:
	BB(int b=9)
	{
		_b = new int[b];
	}
	virtual void person()
	{
		cout << "BBperson" << endl;
	}
	~BB()
	{
		delete[] _b;
		cout << "delete BB" << endl;
	}
private:
	int* _b;
};
void show(AA* p)
{
	p->person();
}
int main()
{
	AA* A = new AA;
	show(A);
	delete A;
	A = new BB;
	show(A);
	delete A;

	return 0;
}

结果:

AAperson
delete AA
BBperson
delete BB
delete AA

2.C++11 override fifinal

1. fifinal:修饰虚函数,表示该虚函数不能再被重写 ,修饰类,表示该类不能被继承。

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

class Car {
public:
	virtual void Drive()
	{}
};
class Benz :public Car {
public:
	virtual void Drive() override 
	{
		cout << "Benz-舒适" << endl;;
	}
};

3. 抽象类

概念:虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口 类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

例如:

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

4.多态的原理

4.1虚函数表

先看一下A的大小

class BB: public AA
{
public:
	 void person()
	{
		cout << "BBprosn" << endl;
	}
private:
	int _b;
};
int main()
{
	AA A;
	cout << sizeof(A) << endl;
	return 0;
}

 A对象里面有一个int型的数据和一个指针(64位平台下),内存对齐后为16个字节。

这个指针我们叫做虚函数表指针 (v virtual f 代表 function) 。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
再对上面代码更该下:

将AA类中 AAfun()继承下来了,person()完成了重写。

在监视窗口我们可以发现子类和父类都有一个虚表,当一个父类的指针或者引用调用虚函数时,不是编译时确定的,而是运行时到指向对象的虚表中去找对应的虚函数并调用,所以指向的是父类的对象调用的就是父类的虚函数,指向的是子类的对象调用的就是子类的虚函数。

           1.构成多态,指向谁调用的就是谁的虚函数,跟对象有关。

           2.不构成多态,对象类型是什么,调用那个函数就跟类型有关

那为什么必须是父类的指针或者引用调用虚函数时发生多态,不能是对象呢?对象切片时,子类对象只会拷贝成员给父类对象,不会拷贝虚表指针。

只要是虚函数就会进虚表指针,那么B对象中的BBfun函数的地址应该也在虚表。可在内存窗口查看。

 也可进一步验证,将虚函数的地址打印出来。(32位机器下)

1.派生类对象A 中也有一个虚表指针,A 对象由两部分构成,一部分是父类继承下来的成员,虚
表指针是自己的成员。
2. 基类A 对象和派生类B 对象虚表是不一样的,这里我们发现person 完成了重写,所以B 的虚表
中存的是重写的A ::person ,所以虚函数的重写也叫作覆盖 ,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个 nullptr
5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生
类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己
新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

4.2动态绑定与静态绑定

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

5.多继承关系的虚函数表

多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中 

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;
};

int main()
{
 Derive d;
 Base1* p1 = &d;
 Base2* p2 = &d;
 Derive* p3 = &d;
 return 0; 
}

6.继承和多态常见面试题 

1.

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;
}

按声明的顺序(继承的顺序)构造。

2.多继承中指针偏移问题?下面说法正确的是( )

class Base1 {  public:  int _b1; };
class Base2 {  public:  int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main(){
 Derive d;
 Base1* p1 = &d;
 Base2* p2 = &d;
 Derive* p3 = &d;
 return 0; }

p1==p3!=p2
问答题:
nline函数可以是虚函数吗?答:可以,不过编译器就忽略inline属性,这个函数就不再是
inline,因为虚函数要放到虚函数表中
静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数
的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表
构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表
阶段才初始化的
虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况
下存在代码段(常量区)的
​
 C++菱形继承的问题?虚继承的原理? 虚基表:存的是偏移量,解决数据冗余和二义性问题一
                                 虚函数表:存的是虚函数地址,是为了实现多态

​

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值