C/C++使用C语言实现多态

目录
  • 1.多态的概念
    • 1.1什么是多态?
    • 1.2为什么要用多态呢?
    • 1.3多态有什么好处?
  • 2.多态的定义及实现
    • 2.1继承中构成多态的条件
    • 2.2虚函数
    • 2.3虚函数的重写
    • 2.4C++11 override 和 final
    • 2.5 重载、覆盖(重写)、隐藏(重定义)的对比
  • 3.抽象类
    • 3.1概念
    • 3.2实现继承和接口继承
  • 4.多态的原理
    • 4.1虚函数表
    • 4.2多态的原理
    • 4.3 动态绑定与静态绑定
  • 5.单继承和多继承关系的虚函数表
    • 5.1 单继承中的虚函数表
    • 5.2 多继承中的虚函数表
  • 总结

1.多态的概念

1.1什么是多态?

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。
简单的说:就是”一个接口多种实现“。

1.2为什么要用多态呢?

我们知道,封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类);它们的目的都是为了代码重用。而多态除了代码的复用性外,还可以解决项目中紧偶合的问题,提高程序的可扩展性.。耦合度讲的是模块与模块之间,代码与代码之间的关联度,通过对系统的分析把他分解成一个个的子模块,子模块提供稳定的接口,达到降低系统耦合度的的目的,模块与模块之间尽量使用模块接口访问,而不是随意引用其他模块的成员变量。

1.3多态有什么好处?

1.应用程序不必为每一个派生类编写功能调用,只需要对抽象基类进行处理即可。大大提高程序的可复用性。//继承

2.派生类的功能可以被基类的方法或引用变量所调用,这叫向后兼容,可以提高可扩充性和可维护性。 //多态的真正作用,

2.多态的定义及实现

2.1继承中构成多态的条件

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

2.被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

2.2虚函数

虚函数:即被virtual修饰的类成员函数称为虚函数。

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

2.3虚函数的重写

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派生类的虚函数重写了基类的虚函数。

//void BuyTicket (),返回值类型、函数名字、参数列表完全相同
class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后
基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用*/
/*void BuyTicket() { cout << "买票-半价" << endl; }*/
};

虚函数重写的两个例外:

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

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

class Person {
public:
	virtual A* f() {return new A;}    //基类的返回值为A*
};

class Student : public Person {
public:
	virtual B* f() {return new B;}    //派生类的返回值为B*
};

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

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然析构函数名字不同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处

理成destructor。

class Person {
public:
	virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
	virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
	Person* p1 = new Person;
	Person* p2 = new Student;
	delete p1;
	delete p2;
	return 0;
}

2.4C++11 override 和 final

C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

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.5 重载、覆盖(重写)、隐藏(重定义)的对比

3.抽象类

3.1概念

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

class Car
{
public:
	virtual void Drive() = 0;  //纯虚函数
};
class Benz :public Car
{
public:
	virtual void Drive()
{
	cout << "Benz-舒适" << endl;
}
};
class BMW :public Car
{
public:
	virtual void Drive()
{
	cout << "BMW-操控" << endl;
}
};
void Test()
{
	Car* pBenz = new Benz;
	pBenz->Drive();
	Car* pBMW = new BMW;
	pBMW->Drive();
}

3.2实现继承和接口继承

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

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

4.多态的原理

4.1虚函数表

// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
	private:
	int _b = 1;
};
int main()
	{
		Base b;
		return 0;
	}

通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr,我们称之为虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。

// 针对上面的代码我们做出以下改造
// 1.我们增加一个派生类Derive去继承Base
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和一个普通函数Func3
class Base  //基类
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()   //虚函数
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()   //普通函数
	{
		cout << "Base::Func3()" << endl;
	}
private:
	int _b = 1;
};

class Derive : public Base  //派生类
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};

int main()
{
	Base b;
	Derive d;
	return 0;
}

1.派生类对象d中也有一个虚表指针,d对象由两部分构成:父类继承下来的成员和自己的成员。

2.基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。

3.另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。

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

5.总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

6.还有一个很容易混淆的问题:虚函数存在哪的?虚表存在哪的?

注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?

4.2多态的原理

1.观察下图的红色箭头我们看到,p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚函数是Person::BuyTicket。

2.观察下图的蓝色箭头我们看到,p是指向johnson对象时,p->BuyTicket在johson的虚表中找到虚函数是Student::BuyTicket。

3.这样就实现出了不同对象去完成同一行为时,展现出不同的形态。

4.反过来思考我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数。

反思一下为什么?

5.再通过下面的汇编代码分析,看出满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译是确认好的。

4.3 动态绑定与静态绑定

1.静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载

2.动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

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

5.1 单继承中的虚函数表

5.2 多继承中的虚函数表

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

总结

本篇文章就到这里了,希望能给你带来帮助,也希望您能够多多关注我们的更多内容!

(0)

相关推荐

  • c++中为什么可以通过指针或引用实现多态详解

    引言: 在c++中司空见惯的事情就是:可以通过指针和引用可以实现多态,而对象不可以. 那为什么?让我们来解开这神秘的暗纱! 1. 类对象的存储方式: 在一个类的实例中,只会存放非静态的成员变量. 如果该类中存在虚函数的话,再多加一个指向虚函数列表指针-vptr. 例如声明如下两个类,并分别实例化两个对象,它们的内存分配大致如下:(vptr具体在什么位置,与编译器有关,大多数都在开始处) class base { public: virtual ~base() {}; virtual string

  • 虚函数表-C++多态的实现原理解析

    参考:http://c.biancheng.net/view/267.html 1.说明 我们都知道多态指的是父类的指针在运行中指向子类,那么它的实现原理是什么呢?答案是虚函数表 在 关于virtual 一文中,我们详细了解了C++多态的使用方式,我们知道没有 virtual 关键子就没法使用多态 2.虚函数表 我们看一下下面的代码 class A { public: int i; virtual void func() { cout << "A func" <<

  • 一篇文章带你了解C++多态的实现原理

    目录 虚函数和多态 多态的作用 多态的一个例子 构造函数和析构函数中存在多态吗? 多态的实现原理 虚函数表 虚析构函数 纯虚函数和抽象类 总结 虚函数和多态 虚函数: 在类的定义中,前面有 virtual 关键字的成员函数称为虚函数 virtual 关键字只用在类定义里的函数声明中,写函数体时不用 比如: class Base { virtual int Fun() ; // 虚函数 }; int Base::Fun() // virtual 字段不用在函数体时定义 { } 多态的表现形式 派生

  • C++中多态的定义及实现详解

    1. 多态概念 1.1 概念 多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态. 举个栗子:比如买票,当普通人买票时,是全价买票:学生买票时,是半价买票:军人买票时是优先买票.同一个事情针对不同的人或情况有不同的结果或形态. 2. 多态的定义及实现 2.1 多态的构成条件 多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为.比如Student继承了Person. Person对象买票全价,Student对象买票半价. 注意:那么在继

  • C++中的多态详谈

    1. 多态概念 1.1 概念 多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态. 举个栗子:比如买票,当普通人买票时,是全价买票:学生买票时,是半价买票:军人买票时是优先买票.同一个事情针对不同的人或情况有不同的结果或形态. 2. 多态的定义及实现 2.1 多态的构成条件 多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为.比如Student继承了Person. Person对象买票全价,Student对象买票半价. 注意:那么在继

  • 深入理解Go语言实现多态 

    目录 多态是什么 Go语言多态举例 总结 多态是什么 相信学过Java这种面向对象语言的同学对于多态来说都不陌生,在代码执行的时候,能够根据子类的类型去执行子类当中的方法.多态是指代码可以根据类型的具体实现采取不同行为的能力.如果一个类型实现了某个接口,所有使用这个接口的地方,都可以支持这种类型的值. Go语言多态举例 有这样一个场景,我们在应用开发中涉及到很多通知事件,通知的类型可以是通过微信.QQ.Email等,那么我们可以抽象出一个接口,定义一个通知的接口,然后微信通知类.QQ通知类.Em

  • C/C++使用C语言实现多态

    目录 1.多态的概念 1.1什么是多态? 1.2为什么要用多态呢? 1.3多态有什么好处? 2.多态的定义及实现 2.1继承中构成多态的条件 2.2虚函数 2.3虚函数的重写 2.4C++11 override 和 final 2.5 重载.覆盖(重写).隐藏(重定义)的对比 3.抽象类 3.1概念 3.2实现继承和接口继承 4.多态的原理 4.1虚函数表 4.2多态的原理 4.3 动态绑定与静态绑定 5.单继承和多继承关系的虚函数表 5.1 单继承中的虚函数表 5.2 多继承中的虚函数表 总结

  • C语言实现C++继承和多态的代码分享

    这个问题主要考察的是C和C++的区别,以及C++中继承和多态的概念. C和C++的区别 C语言是面向过程的语言,而C++是面向对象的过程. 什么是面向对象和面向过程? 面向过程就是分析解决问题的步骤,然后用函数把这些步骤一步一步的进行实现,在使用的时候进行一一调用就行了,注重的是对于过程的分析.面向对象则是把构成问题的事进行分成各个对象,建立对象的目的也不仅仅是完成这一个个步骤,而是描述各个问题在解决的过程中所发生的行为. 面向对象和面向过程的区别? 面向过程的设计方法采用函数来描述数据的操作,

  • C语言模式实现C++继承和多态的实例代码

    这个问题主要考察的是C和C++的区别,以及C++中继承和多态的概念. C和C++的区别 C语言是面向过程的语言,而C++是面向对象的过程. 什么是面向对象和面向过程? 面向过程就是分析解决问题的步骤,然后用函数把这些步骤一步一步的进行实现,在使用的时候进行一一调用就行了,注重的是对于过程的分析.面向对象则是把构成问题的事进行分成各个对象,建立对象的目的也不仅仅是完成这一个个步骤,而是描述各个问题在解决的过程中所发生的行为. 面向对象和面向过程的区别? 面向过程的设计方法采用函数来描述数据的操作,

  • PHP面向对象编程之深入理解方法重载与方法覆盖(多态)

    什么是多态? 多态(Polymorphism)按字面的意思就是"多种状态".在面向对象语言中,接口的多种不同的实现方式即为多态.引用Charlie Calverts对多态的描述--多态性是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作(摘自"Delphi4编程技术内幕").简单的说,就是一句话:允许将子类类型的指针赋值给父类类型的指针(没错这段话来自百度百科).那么多态的作用是什么,它有

  • 关于Java三大特性之多态的总结

    面向对象的三大特性:封装.继承.多态.从一定角度来看,封装和继承几乎都是为多态而准备的.这是我们最后一个概念,也是最重要的知识点. 1.定义: 多态:指允许不同类的对象对同一消息做出响应.即同一消息可以根据发送对象的不同而采用多种不同的行为方式.(发送消息就是函数调用) 2.实现多态的技术称为:动态绑定(dynamicbinding),是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法. 3.作用:消除类型之间的耦合关系. 4.现实中,关于多态的例子不胜枚举.比方说按下F1

  • C语言实现面向对象的方法详解

    目录 1.引言 2.封装 3.继承 4.多态 4.1 虚表和虚指针 4.2 在构造函数中设置vptr 4.3 继承 vtbl 和 重载 vptr 4.4 虚函数调用 4.5 main.c 5.总结 1.引言 面向对象编程(OOP)并不是一种特定的语言或者工具,它只是一种设计方法.设计思想.它表现出来的三个最基本的特性就是封装.继承与多态.很多面向对象的编程语言已经包含这三个特性了,例如 Smalltalk.C++.Java.但是你也可以用几乎所有的编程语言来实现面向对象编程,例如 ANSI-C.

  • 分析Swift性能高效的原因

    自从2014年Apple发布Swift语言以来,历时六年多,Swift已经发布到5.3版本,在5.0版本已经ABI stability,5.2版本也已经module stability,不管是语言还是基础库都日趋稳定,目前国内外大厂也都积极拥抱Swift阵营. 绝大多数公司选择Swift语言开发iOS应用,主要原因是因为Swift相比Objc有更快的运行效率,更加安全的类型检测,更多现代语言的特性提升开发效率:这一系列的优点使Swift语言的热度越来越高. 大多数人知道Swift语言相比于Obj

  • c++ 虚继承,多继承相关总结

    看这一篇文章之前强烈建议先看以下我之前发布的 虚指针,虚函数剖析 例1: 以下代码输出什么? #include <iostream> using namespace std; class A { protected: int m_data; public: A(int data = 0) {m_data=data;} int GetData() { return doGetData(); } virtual int doGetData() { return m_data; } }; class

  • Go语言实现类似c++中的多态功能实例

    前言 Go语言作为编程语言中的后起之秀,在博采众长的同时又不失个性,在注重运行效率的同时又重视开发效率,不失为一种好的开发语言.在go语言中,没有类的概念,但是仍然可以用struct+interface来实现类的功能,下面的这个简单的例子演示了如何用go来模拟c++中的多态的行为. 示例代码 package main import "os" import "fmt" type Human interface { sayHello() } type Chinese s

随机推荐