C++之多态(上)

目录

多态的概念

多态的定义

虚函数

虚函数的重写

多态的实现

final和override关键字

纯虚函数和抽象类

 重载重写和重定义区别


C++三大特性封装,继承,多态我们已经学习了封装和继承,本期开始我们将开始多态的学习,多态是C++语法中很重要的知识点。

多态的概念

不同的对象去完成相同的行为却产生了不同的状态,我们称这种现象为多态。

生活中的例子也很多,比如说我们去海底捞吃火锅,有两个对象,一个为超级vip一个为vip在吃晚饭结账时,作为vip都享受了打折这一优惠,但是最终超级vip打了5折,vip打了6折,同样是打折这一行为 ,超级vip和vip最终却享受了不同的折扣,这一现象我们便可以称之为多态。

多态的定义

多态分为静态的多态动态的多态。静态的多态在编译时实现,而动态的多态在运行时实现。

静态的多态:函数重载就是静态时的多态。静态的多态如何实现呢?

在程序编译时,我们知道程序的编译分为预处理,编译,汇编,链接,在编译阶段,会将代码转为汇编代码,调用函数的代码(假设函数名为f),如果函数没有参数就会转为call fv(?),?为对应函数的地址。如果函数的第一个参数为int那么转为调用函数的汇编代码为call fi(?),?为对应函数的地址。重载的函数的地址会在符号表中存储,最终编译器在符号表中找到对应函数的地址之后会填回?处,这样就相当于是调用了不同的函数,这样函数重载就实现了。

动态的多态:简单来说就是父类的指针或者引用去调用同一函数,传递的对象不同时,所调用的函数不同。本期的重点我们要关注动态多态的实现。

动态多态的定义:满足动态的多态要符合两个条件。

1.必须通过基类的指针或者引用去调用虚函数。

2.子类的虚函数必须对父类的虚函数进行重写。重写即三同(函数名,函数参数,返回值相同)。

虚函数

在多继承中的菱形继承,我们使用了virtual关键字,实现了虚继承,解决了数据冗余和二义性的问题。在多态中,虚函数就是用virtual关键字修饰的成员函数。代码如下。

class Person
{
public:
	virtual void func()
	{
		cout << "我是虚函数" << endl;
	}
};

虚函数的重写

何为虚函数的重写?

即子类对父类继承下来的虚函数进行修改之后,与父类的虚函数的函数名,返回值和参数列表相同。代码如下。

class Person
{
public:
	virtual void func()
	{
		cout << "我是虚函数" << endl;
	}
};

class Student :public Person
{
public:
	virtual void func()
	{
		cout << "我是子类的虚函数" << endl;
	}
};

重写还有两个例外,一个是协变,何为协变呢?其实就改变三同中的返回值相同这一标准。保证子类中虚函数的返回值为子类的指针或者引用,父类中的虚函数的返回值为父类的指针或者引用。代码如下。

class A
{
private:
	int _a;
};

class B:public A
{
private:
	int _b;
};
class Person
{
public:
	virtual A* func()
	{
		cout << "我是虚函数" << endl;
	}
};

class Student :public Person
{
public:
	virtual B* func()
	{
		cout << "我是子类的虚函数" << endl;
	}
};

二是析构函数作为虚函数时,改变三同中的函数名称相同这一标准,析构函数的函数名称可以不相同。代码如下。

class Person
{
public:
	virtual A* func()
	{
		cout << "我是虚函数" << endl;
	}

	virtual ~Person()
	{
		cout << "~Person" << endl;
	}
};

class Student :public Person
{
public:
	virtual B* func()
	{
		cout << "我是子类的虚函数" << endl;
	}

	virtual ~Student()
	{
		cout << "~Student" << endl;
	}
};

其实析构函数的名称只是看起来不同,本质上仍然是相同的函数名,都是destructor(),所以本质上并没有改变三同这一标准。

多态的实现

我们已经说过了实现多态的原则:一是使用基类的指针或者引用去调用虚函数,二是派生类的虚函数必须对基类的虚函数进行重写。 代码如下

class Person
{
public:
	virtual void func()
	{
		cout << "我是父类虚函数" << endl;
	}

	virtual ~Person()
	{
		cout << "~Person" << endl;
	}
};

class Student :public Person
{
public:
	virtual void func()
	{
		cout << "我是子类的虚函数" << endl;
	}

	virtual ~Student()
	{
		cout << "~Student" << endl;
	}
};

void func(Person* p)
{
	p->func();
}
int main()
{
	Person p;
	Student s;
	func(&p);
	func(&s);
	return 0;
}

运行结果如下。

当给基类指针传递基类对象的地址时,调用基类的虚函数,当给基类指针传递派生类对象的地址时,调用派生类对象的虚函数,因此实现了多态。 

大家思考一个问题,这个析构函数我们有必要实现多态吗?

先不设置成虚函数,运行结果如下。

设置成虚函数,运行结果如下。

通过运行结果我们可以发现,其实不论我们是否设置析构函数为虚函数,析构函数的调用都是正确的,那么我们还有必要设置析构函数为虚函数吗?
一般情况下是不需要的,但是当我们new子类对象,将子类对象的地址传给父类的指针时,此时就需要设置成虚函数,去实现多态。

没有将析构函数设置成虚函数,代码和运行截图如下。

我们发现,我们创建了子类对象,最终却只调用了父类的析构函数,这就会导致子类中的属于子类本身那一部分的资源没有被释放,就会导致内存泄漏,产生隐患。 

将析构函数设置成虚函数,代码和运行截图如下。 

我们发现,当把析构函数设置成虚函数,创建子类对象,并且最终调用了父类的析构函数清理从父类继承的资源,调用了子类的析构函数清理属于子类本身的资源,最终不会造成资源泄露,所以使用new创建子类对象将子类对象的地址传给父类指针时,此时就要将析构函数设置成虚函数。 

注意:父子类中同名的虚函数,父类中的虚函数的virtual关键字必须写上,但是子类中的virtual关键字可以不写,因为子类中没写virtual关键字虚函数可以理解就是从父类继承下来的虚函数,所以具有了虚函数的性质,所以可以不用写virtual关键字。但是一般建议子类和父类中的虚函数都写上关键字virtual。 代码如下。

final和override关键字

我们以往要使一个类不被继承,就会设置当前类的构造函数为私有,因为在派生类的构造函数中无法调用基类的构造函数进行初始化,所以就会报错,无法创建出派生类对象也就会导致基类无法被继承。

在C++11中,引入了一个新的关键字为final关键字。在类名称的后面加上final关键字,意味着当前类不能被继承。代码如下。

如果我们要使一个类的虚函数不能被其子类重写,可以在其参数列表后加上关键字final。代码如下。

在C++11中,同样引入了override关键字,可以检查派生类的虚函数是否对基类的虚函数进行了重写。代码如下。

纯虚函数和抽象类

在虚函数后加上=0,表示该函数为纯虚函数,具有纯虚函数的类为抽象类。抽象类无法实例化出对象,所以纯虚函数一般不会有对象去调用,所以纯虚函数一般只声明不实现。抽象类的子类因为也继承了抽象类的虚函数,所以抽象类的子类也无法实例化出对象,要想实例化出对象,必须重写抽象类的虚函数。 

没有重写从父类继承下来的纯虚函数,所以子类也是抽象类,无法实例化出对象。代码如下。

对从父类继承下来的纯虚函数进行了重写,所以子类中不再存在抽象函数,所以子类不再是抽象类,所以可以实例化出对象。代码如下。

 重载重写和重定义区别

重载:指函数重载,函数名相同,函数参数的顺序,类型,个数不同。

重写:子类对从父类继承下来的虚函数进行重写,及最终保证三同,函数名,参数,返回值相同。重写还有两个例外,一个是协变,即父子类中的虚函数分别返回父子指针或者引用。析构函数的函数名只是看起来不相同,本质上是相同的。

重定义:继承中,如果父类和子类有同名函数,那么子类把父类中的同名函数继承下来之后,自身的同名函数就对父类的同名函数进行了隐藏,也叫做重定义,可以使用类作用域限定访问。

以上便是多态的基本概念,下节课我们将进一步探讨多态的基本原理。

本期内容到此结束^_^ 

  • 13
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
C++中的多态性是面向对象编程的一个重要概念,它允许使用基类的指针或引用来调用派生类的方法。C++中的多态性可以通过两种方式实现:类多态和函数多态。 1. 类多态: 类多态是通过继承和虚函数来实现的。当基类的指针或引用指向派生类的对象时,可以通过虚函数来调用派生类中的方法。这种调用方式是动态绑定的,即在运行时确定调用的是哪个方法。这种动态绑定是通过虚函数表和虚函数表指针来实现的[^1]。 范例: ```cpp #include <iostream> class Shape { public: virtual void draw() { std::cout << "Drawing a shape." << std::endl; } }; class Circle : public Shape { public: void draw() override { std::cout << "Drawing a circle." << std::endl; } }; class Rectangle : public Shape { public: void draw() override { std::cout << "Drawing a rectangle." << std::endl; } }; int main() { Shape* shape1 = new Circle(); Shape* shape2 = new Rectangle(); shape1->draw(); // 输出:Drawing a circle. shape2->draw(); // 输出:Drawing a rectangle. delete shape1; delete shape2; return 0; } ``` 2. 函数多态: 函数多态是通过函数重载和模板来实现的。函数重载允许在同一个作用域中定义多个同名函数,但它们的参数类型或个数不同。当调用这些同名函数时,编译器会根据实参的类型或个数来选择合适的函数进行调用。这种调用方式是静态绑定的,即在编译时确定调用的是哪个函数。模板是一种通用的函数或类,它可以根据实参的类型自动生成对应的函数或类。 范例: ```cpp #include <iostream> void print(int num) { std::cout << "Printing an integer: " << num << std::endl; } void print(double num) { std::cout << "Printing a double: " << num << std::endl; } template <typename T> void print(T value) { std::cout << "Printing a value: " << value << std::endl; } int main() { print(10); // 输出:Printing an integer: 10 print(3.14); // 输出:Printing a double: 3.14 print("Hello"); // 输出:Printing a value: Hello return 0; } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

棠~

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

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

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

打赏作者

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

抵扣说明:

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

余额充值