初识多态
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;
重写
前面我们已经谈过重载和隐藏了,今天我们需要在认识一个东西,重写,所谓的重写就是父子类中出现同名的函数,并且这个函数的返回值和参数也是一样的(有两个特殊,后面说.)
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的 返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数
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;
它们三个的关系是这样的,我们花了一张图片.
多态的条件
是不是父类调用了子类的方法一定会构成多态?不是的,构成多态需要满足下面的两个条件.
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
我们先来测试一下,先看看现象,至于原理,我们留在后面说,这个虚函数表有关.
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;
如果只是简单的把子类对象给父类,是不会构成多态的.
int main()
B b;
A a = b; // 简单的切片
a.func();
return 0;
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;
但是子类重写父类的虚函数可以不带,但是我们建议带上,感觉是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;
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;
;
除此之外,final还可以用来修饰类,表明这个类不能够被继承.
class A final
public:
virtual void func()
cout << "A:: func()" << endl;
;
class B : public A
public:
virtual void func()
cout << "B:: func()" << endl;
;
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;
;
如果不是的话,就会出现编译错误.
class A
public:
void func()
cout << "A:: func()" << endl;
;
class B : public A
public:
virtual void func() override
cout << "B:: func()" << endl;
;
重写的例外
前面我说了,重写的函数必须函数名,返回值,和参数必须完全一样,但是这里面存在两个例外.
协变
大家先看现象,这里面返回值就不一样.
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;
;
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变 注意 协变 不重要 ,而且用处很少.
析构函数
这里我直接给出结论,父子类的析构函数也是可以构成重写的,析构函数会被编译器童统一的更名为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;
你会发现出问题了,内存泄露,严格的编译器还会自动的检查出来,原因就是现在两个析构函数构成隐藏,我们又用父类来调用,只能调用到父类的析构函数.如果父类的析构函数是虚函数的话,这样就避免这个问题了.
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;
这样的话,构成多态,编译器会自动去子类的析构函数,子类的析构函数同时又会去调用父类的析构函数,这也是 析构函数的函数名会被统一处理的原因. 这里我们建议吧父类的析构函数写成虚函数.
抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承.
下面这个类就是抽象类,里面存在一个纯虚函数.
class Person
public:
virtual void func() = 0;
private:
;
特性
抽象类是不能够被继承的,它生来就是被继承的的,而且继承的类必须重写所有的纯虚函数.
不能被实例化
int main()
Person per;
return 0;
继承的类必须重写纯虚函数
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;
接口继承 和 实现继承
我们先来说一下实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现,简单的理解就是 子类可以调用父类的函数,这是 实现继承,所谓的是实现继承就是 函数名和函数体都被继承了下来.
class A
public:
void func()
cout << "A:: func()" << endl;
;
class B : public A
;
int main()
B b;
b.func();
return 0;
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数.说人话接口继承就是 只继承了函数的声明,没有继承实现
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;
大家这里可以看到了吧,接口继承只是继承的函数的名(包含缺省值),所以这一点一定要牢记.
多态原理
一般情况下,我们学到上面就可以了,但是我们需要往深处谈点,这样面试官问到的时候就可以提高面试官的印象了.
虚函数表指针
在谈这个之前我们需要看一个现象.下面的结果是哈?
#include <iostream>
using namespace std;
class A
public:
virtual void func()
private:
int _a;
;
int main()
cout << sizeof(A) << endl;
return 0;
这里我们就疑惑了,里面不就一个整型的成员变量吗?为何它的大小是8,这是由于有一个虚函数,那么就存在一个指针.
虚函数表
这个指针就是指向一个表,这个表就是虚函数表,简称虚表.表里面的存放的是各个虚函数的地址,记住,每一个虚函数都会放到虚函数表中.
class A
public:
virtual void func1()
virtual void func2()
private:
int _a;
;
int main()
A a;
return 0;
多态原理
看到了虚函数表,我们就可以看一下多态的原理,这里面就涉及到虚表.
大家仔细的看一下子类的虚函数表,这里你就会发现变了,你重写的虚函数表里面村的指针地址变化,这就是多态的原理.我用简单的话来说,子类继承父类的时候把虚函数表也给继承下来了,然后编译查看子类是否存在重写父类里面的虚函数,重写了,就把虚函数里对应的函数指针给改了,这就是多态.
class A
public:
virtual void func1()
virtual void func2()
private:
int _a;
;
class B : public A
void func1()
;
int main()
B b;
return 0;
虚函数都放在虚函数表中吗
我们现在有一个疑惑,是不是每一个虚函数都会放在虚函数表中?这里先来看一下监视窗口.
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;
这里出问题了,我们子类里面是存在这三个三个虚函数的,为何监视窗口里面只有两个,是不是我们的结论错了.不是的,这是由于编译器的监视窗口是优化过的,这里我们要看的是实际内存.
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;
现在我们已经确定了虚函数表里面存在三个指针,而且这三个指针很相似,最关键的是,他们中有两个已经确定了,我们有理由怀疑这第三个也是一个是一个虚函数指针.我们现在再来验证一下,这里的指针会发生变化,毕竟我又重新编译了一下,不过不要担心,大家还是可以看懂的.
注意这是给大家验证的,里面的内容看懂更好,不懂也行,不过这里面用的都是我们学过的,函数指针和强制类型类型转化...
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;
这里就证明了每一个虚函数都会把地址放在虚表中,同时也证明了VS的监视的出窗口是被优化过的
虚表存
以上是关于初识多态的主要内容,如果未能解决你的问题,请参考以下文章