C++中的多态

本文详细介绍了C++中的多态概念,包括多态的定义、虚函数的使用、重载、覆盖和隐藏的区别,以及C++11中的override和final关键字。此外,还讨论了抽象类和多态常见问题,如析构函数的虚函数特性及其重要性。

1.多态的概念

在面向对象语言中,接口的多种不同实现方式即为多态。具体来说就是去完成某个行为,当调用不同的对象去完成时会产生不同的状态。

例如:在车站买票这个行为,学生去购买学生票就是半价;普通人去买票则是全价。

2.多态的定义即实现

2.1多态定义的构成条件

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

构成多态的两个条件:

  1. 调用函数的对象必须是指针或者引用。
  2. 被调用的函数必须是虚函数,且完成了虚函数的重写。

2.2什么是虚函数

虚函数:被virtual关键字修饰的类的成员函数称为虚函数, (virtual不能加在内联函数和静态成员函数之前) 继承后可以被重写。

作用:实现多态性

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

2.3虚函数的重写

虚函数的重写:派生类中有一个跟基类的完全相同虚函数,我们就称子类的虚函数重写了基类的虚函数,(完全相同是指:函数名、参数、返回值都相同)另外虚函数的重写也叫作虚函数的覆盖。

#include<iostream>
using namespace std;
class Person {
public:
	virtual void BuyTicket() {
		//定义为虚函数
		cout << "全票" << endl;
	}
};
class Student :public Person {
public:
	virtual void BuyTicket() {
		//重写基类虚函数
		cout << "学生半价"<<endl;
	}
};
class Soldier :public Person {
public:
	virtual void BuyTicket() {
		//重写基类虚函数
		cout << "军人优先购票"<<endl;
	}
};
void Buy(Person& people) {
	people.BuyTicket();
}
int main() {
	Person p;
	Student st;
	Soldier so;
	Buy(p);
	Buy(st);
	Buy(so);
	system("pause");
	return 0;
}

在上述代码中,当我们调用Buy()函数传参时,把Student类和Solider类对象的引用赋值给Person的引用,这是因为继承中派生类对象可以赋值给基类的指针 / 基类的引用 / 基类的对象,把派生类中基类的那部分切割下来赋值过去(切片操作)。 这样,Student和Solider各自的对象调用Buy()函数就能调用到他们各自继承的Person类。
在这里插入图片描述
结果可以看到:继承同一个Person类, Buy中调用的是同一个函数, 却产生了不同的行为, 这就是多态。

2.4虚函数重写的例外

虚函数重写要求基类和派生类有完全相同(返回值类型、函数名字、参数列表完全相同)的虚函数, 但下面两种情况就是基类和派生类虚函数没有完全相同, 但也会发生重写。

1.协变
重写的虚函数的返回值可以不同,但是必须分别是基类指针和派生类指针或者基类引用和派生类引用。

#include<iostream>
using namespace std;
class Person {
public:
//返回值为基类指针和派生类指针
	virtual Person* f() {
		cout << "Person类的函数调用" << endl;
		return nullptr;
	}
};
class Student:public Person{
public:
	virtual Student* f() {
		cout << "Student类的函数调用" << endl;
		return nullptr;
	}
};
int main() {
	Person* p = new Student;//派生类对象赋值给基类指针(切片)
	p->f();
	return 0;
}

Student类对象的指针赋值给Person类的指针,发生切片。调用了Student类中被重写的函数。
在这里插入图片描述

2.析构函数的重写问题
基类中的析构函数如果是虚函数,在派生类中我们定义一个析构函数,无论有没有关键字virtual都会重写了基类的析构函数。
这里他们的函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,这也说明的基类的析构函数最好写成虚函数。

C++支持派生类对象指针赋给基类对象指针,当我们new出一个派生类对象, 将其指针赋给一个基类指针。最后本该释放派生类对象指针时我们释放了基类指针, 这时就会造成只释放基类内存和调用基类析构函数, 而派生类自己的部分却没有释放, 造成了内存泄漏。

#include<iostream>
using namespace std;
class Person {
public:
	 ~Person() {
		cout << "基类的析构函数调用" << endl;
	}
};
class Student :public Person {
public:
	 ~Student() {
		cout << "派生类的析构函数调用" << endl;
	}
};
int main() {
	Person* p = new Student;//将派生类指针赋值给基类指针
	delete p;  //只会调用基类析构
	return 0;
}

只释放基类内存和调用基类析构函数, 而派生类自己的部分却没有释放, 造成了内存泄漏。
在这里插入图片描述
解决这个问题就需要用到重写析构函数。

#include<iostream>
using namespace std;
class Person {
public:
	 virtual ~Person() {
		cout << "基类的析构函数调用" << endl;
	}
};
class Student :public Person {
public:
	 virtual ~Student() {
		cout << "派生类的析构函数调用" << endl;
	}
};
int main() {
	Person* p = new Student;
	delete p;
	return 0;
}

在这里插入图片描述

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

在这里插入图片描述

4. C++11 override 和 final

  1. 在继承中我们说在定义类时在后面加上final关键字, 则这个类不能被继承, 而final还能加在虚函数后面, 限定这个虚函数不能被继承。

  2. override修饰派生类虚函数强制完成重写, 派生类对这个override修饰的基类虚函数必须重写 , 否则编译不通过。

5.抽象类

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

class Person
{
public:
 virtual void f() = 0;
};

抽象类中的纯虚函数对于其派生类来讲, 只继承了一个函数接口, 派生类要用的话, 只能重写。而基类虚函数对于其派生类而言, 接口、实现都有。在派生类使用这个函数时, 即可以重写, 也可以不重写。

接口继承和实现继承

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

虚函数的继承, 当派生类重写基类虚函数时, 可以看作是接口继承。当派生类没有重写基类虚函数, 可以看做实现继承。

6.多态常见的问题

  1. 什么是多态?

接口的多种不同实现方式即为多态。具体来说就是去完成某个行为,当调用不同的对象去完成时会产生不同的状态。

  1. 什么是重载、重写(覆盖)、重定义(隐藏)?

重载 : C++允许同一作用域中有同名函数, 这些同名函数的形参列表(参数个数 或 类型 或 顺序)必须不同(返回值类型无要求),在处理实现功能类似数据类型不同的问题上保证了接口的统一性。
重写 : 也叫覆盖。在继承关系中, 派生类中有跟基类完全相同(返回值类型、函数名字、参数列表完全相同) 的虚函数,称派生类的虚函数重写了基类的虚函数。
重定义 : 一般叫隐藏, 在继承关系中, 在派生类中有跟基类同名的普通成员函数(只要函数名相同, 与返回值类型和参数列表无关),则基类在派生类中被隐藏。 隐藏对成员变量也一样, 不管变量类型, 只要变量名相同, 基类成员变量在派生类中也会被隐藏。

  1. 多态的实现原理?

利用了虚函数可以重写的特性, 当一个有虚函数的基类有多个派生类时, 通过各个派生类对基类虚函数的不同重写, 实现通过指向派生类对象的基类指针或引用调用同一个虚函数, 去实现不同功能的特性。

  1. inline函数可以是虚函数吗?

不能,因为inline函数没有地址,无法把地址放到虚函数表中。

  1. 静态成员可以是虚函数吗?

不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式
无法访问虚函数表,所以静态成员函数无法放进虚函数表。

  1. 构造函数可以是虚函数吗?

不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始
化的。

  1. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?

可以,并且最好把基类的析构函数定义成虚函数。为了防止程序中new出的派生类对象, 将其赋给基类指针后, 只delete了基类指针, 而派生类自己的部分没有被释放, 造成内存泄漏的后果。

  1. 对象访问普通函数快还是虚函数更快?

如果是普通对象,是一样快的。
如果是指针对象或者是引用对象,则调用的普通函数快。因为构成多态,运行时调用虚函数需要到虚函数表中去查找。

  1. 虚函数表是在什么阶段生成的,存在哪的?

虚函数是在编译阶段就生成的,一般情况下存在代码段(常量区)的。

  1. C++菱形继承的问题?虚继承的原理?

当一个子类继承的多个父类中, 有两个以上的父类是同一个爷爷类(父类的的父类, 这里为了区分, 叫爷爷类), 则此时就发生了菱形继承, 产生了数据冗余性和二义性的问题, 用虚拟继承解决, 虚拟继承原理是:虚拟继承时, 派生类中引入了入一个虚基表来解决这两个问题, 虚基表中放着继承来的虚基类指针, 用来标识自己的父类, 当继承时通过虚基表中的指针来判别是否发生了菱形继承, 如果是, 则只继承一份。

  1. 什么是抽象类?抽象类的作用?

包含纯虚函数的类, 不能实例化处对象,抽象类(纯虚函数)强制重写了虚函数。作用: 体现了接口继承。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值