初识多态

Posted 玄鸟轩墨

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了初识多态相关的知识,希望对你有一定的参考价值。

认识多态

大家可能是第一次接触到多态,这里我用一个现实的例子来举例子.在买火车票的时候,我们会发现学生优惠价,军人有优先买票的资格.如果让你写一个购买车票的系统,那么你会怎么做?首先我们已经学过继承了,肯定第一步就是创建几个角色,每一个角色都写一个购买票的函数,反正是可以形成隐藏的,我们先把自己的想法给实现出来.

#include <iostream>
#include <vector>
#include <string>
using namespace std;

class Person

public:
Person(string& name)
:_name(name)



void buyTicket()

cout << _name << " 普通人 票价 100 ¥" << endl;

protected:
string _name;
;

class Soldier : public Person

public:
Soldier(string& name)
:Person(name)



void buyTicket()

cout << _name << " 军人 优先购买 票价 100 ¥" << endl;

;

class Student:public Person

public:
Student(string& name)
:Person(name)



void buyTicket()

cout << _name << " 学生 半价 50 ¥" << endl;

;

int main()


while (true)

cout << "=====================" << endl;
cout << "1 学生 2 军人 3 普通人" << endl;
int ret = -1;
cout << "请选择 " ;
cin >> ret;
string name;
cout << "请输入你的名字: " << endl;
cin >> name;
switch (ret)

case 1:

Student s(name);
s.buyTicket();

break;
case 2:

Soldier s(name);
s.buyTicket();

break;

case 3:

Person s(name);
s.buyTicket();

break;
default:
cout << "输入错误,请重新输入" << endl;
break;



return 0;

初识多态_多态

什么是多态

我们已经完成的这个程序,那么你有没有感觉自己的代码存在些不妥.前面我们谈过,子类的对象是可以赋值给父类的,我们想想,反正父类和子类都存在买票的函数,我们是不是可以通过父类的买票函数来自动调用子类的呢?这是一个很好的思路.也是我们今天认识多态的开端.

virtual 关键字

我们已经见识过了virtual关键字了,是在虚继承那里,这里我们要谈它的其他的用法,修饰成员函数.被修饰的函数叫做虚函数.

在谈这个之前,我们先来看看个情况.这个情况就是我们的多态,所谓的多态就是父类的引用或者指针调用虚函数.

class A

public:
virtual void func()

cout <<"A:: void func()" << endl;

;

class B : public A

public:
virtual void func()

cout << "B:: void func()" << endl;

;

int main()

B b;
A& a = b;
a.func();
return 0;

初识多态_多态_02

重写

前面我们已经谈过重载和隐藏了,今天我们需要在认识一个东西,重写,所谓的重写就是父子类中出现同名的函数,并且这个函数的返回值和参数也是一样的(有两个特殊,后面说.)

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

class A

public:
virtual void func()

cout << "A:: func()" << endl;

;

class B : public A

public:
virtual void func()

cout << "B:: func()" << endl;


void func(int i)

cout << "B:: func(int i)" << endl;

;

int main()

B b;
A& a = b;
a.func();
b.func(2);
return 0;

初识多态_虚表、_03

它们三个的关系是这样的,我们花了一张图片.

初识多态_运行时绑定_04

多态的条件

是不是父类调用了子类的方法一定会构成多态?不是的,构成多态需要满足下面的两个条件.

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

我们先来测试一下,先看看现象,至于原理,我们留在后面说,这个虚函数表有关.

class A

public:
virtual void func()

cout << "A:: func()" << endl;

;

class B : public A

public:
virtual void func()

cout << "B:: func()" << endl;

;

必须是是指针或者是父类放热引用.

int main()

B b;
A& a = b;
A* p = &b;
p->func();
a.func();
return 0;

初识多态_虚表、_05

如果只是简单的把子类对象给父类,是不会构成多态的.

int main()

B b;
A a = b; // 简单的切片
a.func();
return 0;

初识多态_运行时绑定_06

virtual 一定要带吗

这里我先说明一下,父类的虚函数一定要带,否则构成不了多态的条件

class A

public:
void func1()

cout << "A:: func1()" << endl;

private:
int _a;

;
class B : public A

virtual void func1()

cout << "B:: func1()" << endl;



;

int main()

B b;
A& a = b;
a.func1();
return 0;

初识多态_虚函数_07

但是子类重写父类的虚函数可以不带,但是我们建议带上,感觉是C++的一个bug

class A

public:
virtual void func1()

cout << "A:: func1()" << endl;

private:
int _a;

;
class B : public A

void func1()

cout << "B:: func1()" << endl;



;

int main()

B b;
A& a = b;
a.func1();
return 0;

初识多态_多态_08

C++11 override 和 final

这是C++11新增的两个关键字,作用还是可以的

final 关键字

final 主要的作用便是禁止重写,它有最终的的意思,也就是被他修饰的函数是不能被子类重写的.

class A

public:
virtual void func() final

cout << "A:: func()" << endl;

;

class B : public A

public:
virtual void func()

cout << "B:: func()" << endl;

;

初识多态_运行时绑定_09

除此之外,final还可以用来修饰类,表明这个类不能够被继承.

class A final

public:
virtual void func()

cout << "A:: func()" << endl;

;

class B : public A

public:
virtual void func()

cout << "B:: func()" << endl;

;

初识多态_运行时绑定_10

override 关键字

override更简单,是去检查和看看子类的这个函数再父类里面是否是虚函数.是就没事,不是就报错.

class A 

public:
virtual void func()

cout << "A:: func()" << endl;

;

class B : public A

public:
virtual void func() override

cout << "B:: func()" << endl;

;

初识多态_运行时绑定_11

如果不是的话,就会出现编译错误.

class A 

public:
void func()

cout << "A:: func()" << endl;

;

class B : public A

public:
virtual void func() override

cout << "B:: func()" << endl;

;

初识多态_虚表、_12

重写的例外

前面我说了,重写的函数必须函数名,返回值,和参数必须完全一样,但是这里面存在两个例外.

协变

大家先看现象,这里面返回值就不一样.

class A

;
class B : public A

;

class C

public:
virtual A* func()

cout << "A:: func()" << endl;

;

class D : public C

public:
virtual B* func()

cout << "B:: func()" << endl;

;

初识多态_父类_13

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变 注意 协变 不重要 ,而且用处很少.

析构函数

这里我直接给出结论,父子类的析构函数也是可以构成重写的,析构函数会被编译器童统一的更名为destructor(),所以可以构成重载.至于的作用,那可就大了去了.

class Person 
public:
~Person()

cout << "~Person()" << endl;

;

class Student : public Person
public:
virtual ~Student()

cout << "~Student()" << endl;
delete[] _name;


private:
char* _name = new char[10] j, a, c, k ;
;

int main()


Person* ptr = new Person;
delete ptr; // ptr->destructor() + operator delete(ptr)

ptr = new Student;
delete ptr; // ptr->destructor() + operator delete(ptr)

return 0;

初识多态_父类_14

你会发现出问题了,内存泄露,严格的编译器还会自动的检查出来,原因就是现在两个析构函数构成隐藏,我们又用父类来调用,只能调用到父类的析构函数.如果父类的析构函数是虚函数的话,这样就避免这个问题了.

class Person 
public:
virtual ~Person()

cout << "~Person()" << endl;

;

class Student : public Person
public:
virtual ~Student()

cout << "~Student()" << endl;
delete[] _name;


private:
char* _name = new char[10] j, a, c, k ;
;

int main()


Person* ptr = new Person;
delete ptr; // ptr->destructor() + operator delete(ptr)

ptr = new Student;
delete ptr; // ptr->destructor() + operator delete(ptr)

return 0;

初识多态_运行时绑定_15

这样的话,构成多态,编译器会自动去子类的析构函数,子类的析构函数同时又会去调用父类的析构函数,这也是 析构函数的函数名会被统一处理的原因. 这里我们建议吧父类的析构函数写成虚函数.

抽象类

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

下面这个类就是抽象类,里面存在一个纯虚函数.

class Person

public:
virtual void func() = 0;
private:

;

特性

抽象类是不能够被继承的,它生来就是被继承的的,而且继承的类必须重写所有的纯虚函数.

不能被实例化

int main()

Person per;
return 0;

初识多态_父类_16

继承的类必须重写纯虚函数

class Person

public:
virtual void func() = 0;
private:

;
class Student : public Person

public:
void func()




private:

;

int main()

Student stu;
Person& per = stu; // 允许
return 0;

初识多态_多态_17

接口继承 和 实现继承

我们先来说一下实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现,简单的理解就是 子类可以调用父类的函数,这是 实现继承,所谓的是实现继承就是 函数名和函数体都被继承了下来.

class A

public:
void func()

cout << "A:: func()" << endl;

;

class B : public A


;

int main()

B b;
b.func();
return 0;

初识多态_虚函数_18

虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数.说人话接口继承就是 只继承了函数的声明,没有继承实现

class A

public:
virtual void func(int a = 10)

cout << "A:: func()" << endl;
cout << "a = " << a << endl;

;

class B : public A

void func(int a = 20)

cout << "B:: func()" << endl;
cout << "a = " << a << endl;

;

int main()

B b;
A& a = b;
a.func();
return 0;

初识多态_父类_19

大家这里可以看到了吧,接口继承只是继承的函数的名(包含缺省值),所以这一点一定要牢记.

多态原理

一般情况下,我们学到上面就可以了,但是我们需要往深处谈点,这样面试官问到的时候就可以提高面试官的印象了.

虚函数表指针

在谈这个之前我们需要看一个现象.下面的结果是哈?

#include <iostream>
using namespace std;
class A

public:
virtual void func()



private:
int _a;

;

int main()

cout << sizeof(A) << endl;
return 0;

初识多态_父类_20

这里我们就疑惑了,里面不就一个整型的成员变量吗?为何它的大小是8,这是由于有一个虚函数,那么就存在一个指针.

初识多态_多态_21

虚函数表

这个指针就是指向一个表,这个表就是虚函数表,简称虚表.表里面的存放的是各个虚函数的地址,记住,每一个虚函数都会放到虚函数表中.

class A

public:
virtual void func1()



virtual void func2()



private:
int _a;

;
int main()

A a;
return 0;

初识多态_虚表、_22

多态原理

看到了虚函数表,我们就可以看一下多态的原理,这里面就涉及到虚表.

大家仔细的看一下子类的虚函数表,这里你就会发现变了,你重写的虚函数表里面村的指针地址变化,这就是多态的原理.我用简单的话来说,子类继承父类的时候把虚函数表也给继承下来了,然后编译查看子类是否存在重写父类里面的虚函数,重写了,就把虚函数里对应的函数指针给改了,这就是多态.

class A

public:
virtual void func1()



virtual void func2()



private:
int _a;

;
class B : public A

void func1()



;

int main()


B b;
return 0;

初识多态_虚表、_23

虚函数都放在虚函数表中吗

我们现在有一个疑惑,是不是每一个虚函数都会放在虚函数表中?这里先来看一下监视窗口.

class A

public:
virtual void func1()



virtual void func2()



private:
int _a;

;
class B : public A

virtual void func1()



virtual void func2()



virtual void func3()



;

int main()


B b;
return 0;

初识多态_父类_24

这里出问题了,我们子类里面是存在这三个三个虚函数的,为何监视窗口里面只有两个,是不是我们的结论错了.不是的,这是由于编译器的监视窗口是优化过的,这里我们要看的是实际内存.

class A

public:
virtual void func1()



virtual void func2()



private:
int _a;

;
class B : public A

virtual void func1()



virtual void func2()



virtual void func3()



;

int main()


B b;
return 0;

初识多态_多态_25

现在我们已经确定了虚函数表里面存在三个指针,而且这三个指针很相似,最关键的是,他们中有两个已经确定了,我们有理由怀疑这第三个也是一个是一个虚函数指针.我们现在再来验证一下,这里的指针会发生变化,毕竟我又重新编译了一下,不过不要担心,大家还是可以看懂的.

注意这是给大家验证的,里面的内容看懂更好,不懂也行,不过这里面用的都是我们学过的,函数指针和强制类型类型转化...

class A

public:
virtual void func1()



virtual void func2()




private:
int _a;

;

// 一个 函数指针
typedef void (*ViPointer)();

class B : public A

virtual void func1()

cout << "func1()" << endl;

virtual void func2()

cout << "func2()" << endl;

virtual void func3()

cout << "func3()" << endl;

;

void printViPointer(ViPointer* f)

int i = 0;
while (f[i] != nullptr)

printf("[%d] : %p\\n", i, f[i]);
f[i]();
i++;



int main()


B b;
printViPointer((ViPointer*)(*(int*)(&b)));

// &b VS 编译器 虚表指针在头部取地址
// (int*)(&b)) 强制类型转化 int* 因为是 32 ,所以指针 4个字节
// *(int*)(&b) 解引用 得到 第一个 存放 虚函数的地址 的 地址
// (ViPointer*)(*(int*)(&b)) 前置类型转换
return 0;

初识多态_运行时绑定_26

这里就证明了每一个虚函数都会把地址放在虚表中,同时也证明了VS的监视的出窗口是被优化过的

虚表存

以上是关于初识多态的主要内容,如果未能解决你的问题,请参考以下文章

初识面向对象三(经典类/多态/鸭子类型/初识封装)

初识多态

初识继承和多态

zookeeper初识

探索C++对象模型

初识继承和多态