【C++】多态

目录

1. 多态的概念

2. 多态的定义及实现

3. 抽象类

4. 多态的原理


1. 多态的概念

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

举个栗子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人 买票时是优先买票

再举个栗子:同样是动物叫这个行为,猫猫叫是"喵喵"(哈基米)的叫,狗狗叫是"汪汪"的叫

总结一下:同样是一个行为,不同类型的对象会得到不一样的结果,这就是一种多态行为。

2. 多态的定义及实现

多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。

比如Student继承了 Person。Person对象买票全价,Student对象买票半价。

代码如下:

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 Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person p;
	Student st;
	Soldier so;

	Func(p);
	Func(st);
	Func(so);

	return 0;
}

在继承中要构成多态有两个条件:

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

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

为了弄清上面这两个条件,我们必须先了解一下什么是虚函数

虚函数及其重写

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

比如我们上面出现过的这个:

virtual void BuyTicket()
{
	cout << "普通人买票——全价" << endl;
}

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

比如:

virtual void BuyTicket()
{
	cout << "学生买票——半价" << endl;
}
	
virtual void BuyTicket()
{
	cout << "军人买票——优先" << endl;
}

关于重写,还有一个结论:如果基类的函数为虚函数,此时派生类函数只要定义,无论是否加virtual关键字, 都与基类的函数构成重写

下面我们来对比一下普通调用和多态调用:

普通调用:跟调用对象类型有关

多态调用: 通过指针/引用,跟指向对象有关

比如还是上面这个买票的例子,我们将引用去掉,就不构成多态了:

下面来看一个虚函数重写的例外 : 析构函数的重写(基类与派生类析构函数的名字不同)

我们上面讲过,在继承中要构成多态有两个条件:

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

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

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

而如果基类的析构函数为虚函数,此时派生类析构函数只要定义,虽然基类与派生类析构函数名字不同。虽然函数名不相同, 但也构成重写,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

所以本质上,这还是符合重写的规则的

C++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:
    //父类不是虚函数
	void Drive()
	{

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

int main()
{
	return 0;
}

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

下面对这3个概念放在一起进行一个对比:

重载 : 2个函数在同一作用域 函数名,参数不同

重写(覆盖): 2个函数分别在基类和派生类的作用域,满足三同,两个函数必须是虚函数

重定义(隐藏): 2个函数分别在基类和派生类的作用域,函数名相同,基类和派生类的同名函数不构成重写就是隐藏

重写是隐藏的一种特殊情况

3. 抽象类

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

接口继承和实现继承:

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

讲了这么多,下面我们来做一道题目练习一下吧~

以下程序输出结果是什么() A: A->0  B: B->1  C: A->1  D: B->0  E: 编译出错

class A
{
    public:
    virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
    virtual void test(){ func();}
};

class B : public A
{
    public:
    void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }
};

int main(int argc ,char* argv[])
{
    B*p = new B;
    p->test();
    return 0;
}

这道题目很多人会选 D 或者 E

但是答案是选B

func()函数是重写(缺省值没有要求),test()函数调用func()函数,此时的func()函数是this调用的,this是A*类型,func()是多态调用,此时this指向的是子类对象,同时根据接口继承,完成重写,用的是父类的接口,缺省值用的是父类的,所以是1

4. 多态的原理

虚函数表

//Q: sizeof(Base)是多少?
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};

int main()
{
	Base b;
	cout << sizeof(b) << endl;

	return 0;
}

我们发现b对象是8个字节,除了_b成员,还多一个_vfptr放在对象的前面,对象中的这个指针我们叫做虚函数表指针。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数 的地址要被放到虚函数表中,虚函数表也简称虚表

那么派生类中这个表放了些什么呢?我们接着往下分析

// 针对上面的代码我们做出以下改造
// 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. 虚函数表本质是一个存虚函数指针的指针数组
5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

最后总结一下虚函数以及虚函数表存在的位置:

虚函数和普通函数一样的,都是存在代码段的

虚表存的是虚表指针,不是虚函数   ,虚表也是存在代码段的

虚表指针 存在对象中

要理解多态的原理,我们就必须知道2个概念:动态绑定与静态绑定
 

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

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

我们今天学的多态,就是动态多态

我们前面学过,要达到多态,有两个条件:一个是虚函数覆盖,一个是对象的指针或引用调
用虚函数。反思一下为什么?

普通调用——静态绑定/编译时确定

多态调用——动态绑定/运行时确定

无论是父类部分还是子类部分,都是取到虚表指针,在虚表中找到对应的虚函数进行调用,也就是动态绑定/运行时确定,通过指向的对象的虚表之中找到对应的虚函数

所以说,满足多态以后的函数调用,是运行起来以后到对象中去找的。不满足多态的函数调用时编译时确认好的

继承和多态常见的问题
 

下面程序输出结果是什么? ()
 

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

A:class A class B class C class D           B:class D class B class C class A
C:class D class C class B class A           D:class A class C class B class D

解析:结果选A

我们可以看到D类先继承B,再继承C,而B类中先继承了A,所以D中构造函数的初始化顺序为A B C D

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

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

A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3

解析:结果选C , 如图:

总结:

inline函数可以是虚函数吗?

答:可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去

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

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

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

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

 析构函数可以是虚函数吗?

答:可以,而且我们最好把基类的析构函数定义成虚函数

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

虚表是在什么阶段生成的?存在哪的?

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

相关推荐

  1. <span style='color:red;'>C</span>++<span style='color:red;'>多</span><span style='color:red;'>态</span>

    C++

    2024-07-11 23:04:01      43 阅读
  2. 八股文 c++

    2024-07-11 23:04:01       39 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-11 23:04:01       67 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-11 23:04:01       72 阅读
  3. 在Django里面运行非项目文件

    2024-07-11 23:04:01       58 阅读
  4. Python语言-面向对象

    2024-07-11 23:04:01       69 阅读

热门阅读

  1. 一句话回答的前端面试题

    2024-07-11 23:04:01       22 阅读
  2. 使用Python进行计算机视觉

    2024-07-11 23:04:01       20 阅读
  3. 从零手写实现 nginx-25-directive map 条件判断指令

    2024-07-11 23:04:01       19 阅读
  4. OWASP ZAP

    OWASP ZAP

    2024-07-11 23:04:01      19 阅读
  5. 【小超嵌入式】C++实现简单计算器

    2024-07-11 23:04:01       12 阅读
  6. pom.xml中重要标签介绍

    2024-07-11 23:04:01       24 阅读
  7. 科技的成就(六十一)

    2024-07-11 23:04:01       19 阅读
  8. 全球网络战市场规模未来十年将超过万亿元

    2024-07-11 23:04:01       19 阅读
  9. 使用kubeadm重置k8s集群

    2024-07-11 23:04:01       17 阅读
  10. k8s中使用cert-manager生成自签名证书

    2024-07-11 23:04:01       18 阅读
  11. k8s中控制器DaemonSet简介及用法

    2024-07-11 23:04:01       23 阅读