C++三大特性之多态

本文介绍了C++中的多态性,包括多态的概念、实现方式,如虚函数的使用,以及构成多态的条件。文章通过示例代码展示了如何通过虚函数实现不同对象执行同一函数产生不同行为。此外,还讨论了虚函数重写、析构函数为虚函数的重要性,以及C++11中的`override`和`final`关键字。文章还涉及了重载、覆盖、隐藏的区别,抽象类的定义及其作用,以及虚函数表在多态实现中的角色。最后,提到了多继承和虚继承的相关问题。
摘要由CSDN通过智能技术生成

1. 多态概念:

通俗来说,就是多种形态, 具体点就是去完成某个行为,当不同的对象去完成时会
产生出不同的状态

2. 多态的定义及实现

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了 Person。Person对象买票全价,Student对象买票半价。

class Person
{
public:
virtual void BuyTicket()
{
	cout << "全价买票" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
	cout << "半价买票" << endl;
}
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student sd;
Func(ps);
Func(sd);
return 0;
}
//运行结果:
//全价买票
//半价买票

2.1 构成多态的条件: 

a. 虚函数重写(返回类型相同,参数列表完全相同,函数名相同)

b. 父类的指针或者引用去调用虚函数

2.2 虚函数重写的俩个特例

a. 子类可以不加虚函数,一样构成虚函数重写,因为重写是接口继承,重写是实现。

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	Func(ps);
	Func(st);
	return 0;
}
//运行结果如下:
买票-全价
买票-半价

b. 协变,基类与派生类虚函数的返回值不同,但必须基类返回值类型是基类类型的返回值或者引用,派生类返回值类型是派生类类型的返回值或者引用,而且基类和派生类返回值类型,可以不是自身类型,但返回类必须构成父子关系,才能构成协变。

//协变1
class Person {
public:
	virtual Person* f() { return nullptr; }
};
class Student : public Person {
public:
	virtual Student* f() { return nullptr; }
};
//协变2
class A 
{};
class B : public A 
{};
class Person {
public:
	virtual A* f() { return new A; }
};
class Student : public Person {
public:
	virtual B* f() { return new B; }
};

建议析构函数也要重写,原因如下:

class A 
{
public:
//virtual ~A()
~A()
{
	cout << "~A" << endl;
}
};
class B : public A 
{
public:
//virtual ~B()
//析构函数函数名会统一被处理成destructor
//构成重写
~B()
{
	cout << "~B" << endl;
}
};
int main()
{
A* a = new A;
A* b = new B;
delete a;
delete b;
return 0;
}
//不加virtual 运行结果:~A ~A
//加virtual 运行结果:~A ~B ~A

解释:不加virtual不构成多态,编译链接时确定地址,运行时call函数地址,没有虚表,调用的是A对象的析构函数,加virtual构成多态,运行时去虚函数表里面找,call函数地址,调用的时重写的B对象的析构函数。

学习了多态概念,请看题

class A
{
public:
virtual void func(int val = 1)
{
	cout << "A->" << val << endl;
}
virtual void test()
{
	func();
}
};
class B : public A
{
public:
virtual void func(int val = 0)
{
	cout << "B->" << val << endl;
}
};
int main()
{
B* p = new B;
p->test();
return 0;
}
//A:A->0  B:B->1  C: A->1  D: B->0  E: 编译出错  F: 以上都不正确

解析:

2.3 C++11 override final 

从上面可以看出, C++ 对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数
名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有
得到预期结果才来 debug 会得不偿失,因此: C++11 提供了 override fifinal 两个关键字,可以帮
助用户检测是否重写。
1. final :修饰虚函数,表示该虚函数不能再被重写
class Car
{
public:
 virtual void Drive() final {}
};
class Benz :public Car
{
public:
 virtual void Drive() {cout << "Benz-舒适" << endl;}
};
2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car{
public:
 virtual void Drive(){}
};
class Benz :public Car {
public:
 virtual void Drive() override {cout << "Benz-舒适" << endl;}
};

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

3. 抽象类

概念:在虚函数的后面写上 =0 ,则这个函数为纯虚函数。 包含纯虚函数的类叫做抽象类(也叫接口 类),抽象类不能实例化出对象 。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
class Car
{
virtual void Drive() = 0;
};
class BMW : public Car
{
virtual void Drive()
{
	cout << "BMW经济型" << endl;
}
};
class Benz : public Car
{
virtual void Drive()
{
	cout << "Benz豪华舒适" << endl;
}
};
int main()
{
//Car c;不能初始化对象,因为是抽象类
BMW b;//可以初始化对象,因为重写了Drive
//可以看出如果作为Car的派生类,就必须重写Drive
return 0;
}

4. 多态原理

4.1 虚函数表

class Base
{
public:
virtual void Func()
{
    cout<<"Func"<<endl;
}
int _a;
};

int main()
{
Base b;
cout<<sizeof(b)<<endl;
return 0;
}
//运行结果如下:
//8

通过观察测试我们发现b 对象是 8bytes 除了 _b 成员,还多一个 __vfptr 放在对象的前面 ( 注意有些
平台可能会放到对象的最后面,这个跟平台有关 ) ,对象中的这个指针我们叫做虚函数表指针 (v
virtual f 代表 function) 。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数
的地址要被放到虚函数表中,虚函数表也简称虚表,。那么派生类中这个表放了些什么呢?我们
接着往下分析:
我们重新创建俩个类,Student是Person的派生类,观察派生类的变化:
class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
	virtual void Func() { cout << "Person::Func()" << endl;}
	void Drive() { cout << "Person::Drive()" << endl; }
};
class Student : public Person {
public:
	void BuyTicket() { cout << "买票-半价" << endl; }
};
int main()
{
	Person p;
	return 0;
}


 如果我们再派生类Student中加入一个虚函数,那么这个虚函数地址是否也会放入虚表中呢?

通过观察可得:vs下和上述运行结果是一样的,那么我们该如何探究这个问题?
//typedef 函数指针数组类型为VFptr
typedef void(*VFptr)();
void PrintVFTable(VFptr* ptr)
{
for (int i = 0; ptr[i] != nullptr; i++)
{
	//打印虚表中的函数地址
	printf("VFptr[%d]->%p ", i, ptr[i]);
	//调用虚函数
	ptr[i]();
}	
cout << endl;
}
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
virtual void Func() { cout << "Person::Func()" << endl;}
void Drive() { cout << "Person::Drive()" << endl; }
};
class Student : public Person {
public:
void BuyTicket() { cout << "买票-半价" << endl; }
virtual void Add() { cout << "Student::Add()" << endl; }
};
int main()
{
Student s;
PrintVFTable((VFptr*) * (int*)&s);
return 0;
}

打印结果如下:

我们可以知道子类的虚函数地址也是放入继承父类的虚表中 。

4.2 多态原理:

class Person {
public:
 virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
 virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person Mike;
 Func(Mike);
Student Johnson;
Func(Johnson);
 return 0;
}

 为什么初始化Mike对象就能掉用Mike的BuyTicket()函数,初始化Johnson对象就能掉用Mike的BuyTicket()函数,明明传递给Func()函数的都是父类的指针或者引用,不应该都是调用Mike的BuyTicket()函数嘛?

原理如下:

5.多继承的虚函数表

//typedef 函数指针数组类型为VFptr
typedef void(*VFptr)();
void PrintVFTable(VFptr* ptr)
{
for (int i = 0; ptr[i] != nullptr; i++)
{
	//打印虚表中的函数地址
	printf("VFptr[%d]->%p ", i, ptr[i]);
	//调用虚函数
	ptr[i]();
}
cout << endl;
}
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1 = 1;
};

class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2 = 2;
};
class Derive : public Base1, public Base2 {
public:	
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d = 3;
};

多继承会同时继承多个父类的虚表,上述代码中写了Derive继承了俩个子类,所以有俩个虚表

但是有一个疑问的点是,d对象中自身的func3虚函数地址是放在Base1虚表还是 放在Base2虚表中呢?

//typedef 函数指针数组类型为VFptr
typedef void(*VFptr)();
void PrintVFTable(VFptr* ptr)
{
for (int i = 0; ptr[i] != nullptr; i++)
{
	//打印虚表中的函数地址
	printf("VFptr[%d]->%p ", i, ptr[i]);
	//调用虚函数
	ptr[i]();
}
cout << endl;
}

像单继承类似打印可得:func3虚函数地址存放于先继承Base1的虚表中。

像上述图片运行结果显示可知:

Derive中的func1()虽然在子类中被重写,但是在Base1()和Base2()虚表中俩个func1()的地址是不一样的,为什么呢?

 面试题:

1. 什么是多态? 答:不同对象做同一件事展现出不同的形态或者结果。
2. 什么是重载、重写 ( 覆盖 ) 、重定义 ( 隐藏 ) ?答: 如上表
3. 多态的实现原理?答: 博客里面有
4. inline 函数可以是虚函数吗?答: 可以,不过编译器就忽略inline属性,这个函数就不再是
inline,因为虚函数要放到虚表中去。(inline在调用的地方直接展开,没有地址)
5. 静态成员可以是虚函数吗?答: 不能,因为静态成员函数没有this指针,使用类型::成员函数
的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。static函数相当于普通函数调用,编译时决议,多态运行时决议
6. 构造函数可以是虚函数吗?答: 不能,因为对象中的虚函数表指针是在构造函数初始化列表
阶段才初始化的。
7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答: 可以,并且最好把基类的析
构函数定义成虚函数。参考博客内容
8. 对象访问普通函数快还是虚函数更快?答: 首先如果是普通对象,是一样快的。如果是指针
对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函
数表中去查找。
9. 虚函数表是在什么阶段生成的,存在哪的?答: 虚函数表是在编译阶段就生成的,一般情况
下存在代码段(常量区)的。

 10. C++菱形继承的问题?虚继承的原理?答:参考另外一篇博客继承,注意这里不要把虚函数表和虚基 表搞混了。

11. 什么是抽象类?抽象类的作用?答:参考:抽象类,抽象类强制重写了虚函数,另外抽 象类体现出了接口继承关系。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

疯狂的小码农

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

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

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

打赏作者

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

抵扣说明:

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

余额充值