文章目录
- 前言
- [1. 虚函数](#1. 虚函数)
-
- [1.1 现象](#1.1 现象)
- [1.2 多态](#1.2 多态)
- [1.3 析构函数](#1.3 析构函数)
- [1.4 override和final](#1.4 override和final)
- [1.5 重载、隐藏、重写对比](#1.5 重载、隐藏、重写对比)
- [2. 抽象类](#2. 抽象类)
-
- [2.1 抽象类特性](#2.1 抽象类特性)
- [2.2 抽象类的应用场景](#2.2 抽象类的应用场景)
- [3. 多态实现的底层原理](#3. 多态实现的底层原理)
- [4. 静态绑定和动态绑定](#4. 静态绑定和动态绑定)
- [5. 总结](#5. 总结)
前言
多态是面向对象三大特性之一,也是细节最多的语法之一。学习类对象的多态,我们不仅仅要看到基础语法 ,也要体会到底层原理。从而我们结合类与对象的其它性质,才能更好地体会到多态的魅力。
下面小编会和大家一起探讨多态的基础语法细节!
关于虚函数,还有涉及很多很多的问题(例如:构造中的多态调用、初始化顺序......),小编会在一篇讲解习题的文章中谈到!
注:本文章的测试用例均在VS2022 x86环境下进行。
1. 虚函数
小编会由现象引入多态
1.1 现象
来看下面一个例子,例1:
cpp
#include<iostream>
#include<string>
using namespace std;
class animal
{
public:
animal(const string& name)
:_name(name)
{}
virtual void call()
{
cout << "name is : " << _name << " call : sound" << endl;
}
protected:
string _name;
};
class cat : public animal
{
public:
cat(const string& name = "")
:animal(name)
{}
virtual void call()
{
cout << "name is : " << _name << " call : meow" << endl;
}
};
class dog : public animal
{
public:
dog(const string& name = "")
:animal(name)
{}
virtual void call()
{
cout << "name is : "<< _name << " call : growl" << endl;
}
};
int main()
{
cat c("Lucy");
dog d("Juck")
animal* ptr1 = &c;
animal *ptr2 = &d;
ptr1->call();
ptr2->call();
return 0;
}
说明:
上面代码定义了一个
animal
的类,其中成员是animal
的名字和叫声。由
dog
和cat
来继承这个animal
的类,并且重写了叫声方法。创建了一个
cat
和dog
对象并且由animal
的指针来接受这两个指针。然后调用call
方法,观察现象!

结果是我们可以正常得到cat
和dog
的call
函数调用结果!!
这就是多态。
1.2 多态
多态的字面意思:对于同一种行为,不同对象去完成产生了不同的结果(形态)!
- 多态 :是在不同继承的类对象,去调用同一个函数产生了不同的行为。
根据刚刚的例1,我们可以得到的事实是:
cat
和dog
继承于同一个基类animal
。animal
中用virtual
声明了函数call
。cat
和dog
中都对函数call
再进行了重写。
看到的现象:
- 同一个类型的
animal*
指针指向了不同的对象,调用同一个call
函数,产生了不同的结果。
接下来我们正式步入多态语法的讲解:
-
多态的两个条件:
- 基类 的指针 或者引用,指向派生类。(原因后面会谈到)
- 被调用的函数一定是虚函数,派生类必须对虚函数进行重写。
-
虚函数 :在类里 被
virtual
关键字修饰的函数被称为虚函数。- 注意:这个
virtual
关键字和继承的虚拟继承没有关系。
- 注意:这个
-
重写(也叫覆盖):
- 函数必须是虚函数。
- 函数名 完全和基类相同、函数参数列 表完全和基类相同、函数返回值和基类相同。(三同)
-
需要注意的是:
virtual
关键字可以在派生类的时候不用声明,但是基类必须显示声明。- 协变 :返回值可以不同。但是必须满足父子关系的同类型的返回值 。例如:基类返回基类的指针,派生类虚函数就可以返回派生类的指针(不可以是引用)。(只要满足父子关系即可,并不用管是什么类型)
- 派生类方法的访问权限与基类方法的访问权限只会影响当前类型的访问
建议:基类的访问可以公有。 - 了解:派生类方法的抛出的异常不能大于基类方法的异常范围。
下面我们来进行解析:
- 必须是指针 或者引用 。那如果是基类对象呢?
例2:
cpp
#include<iostream>
#include<string>
using namespace std;
class animal
{
public:
animal(const string& name)
:_name(name)
{}
virtual void call()
{
cout << "name is : " << _name << " call : sound" << endl;
}
protected:
string _name;
};
class cat : public animal
{
public:
cat(const string& name = "")
:animal(name)
{}
virtual void call()
{
cout << "name is : " << _name << " call : meow" << endl;
}
};
int main()
{
cat c("Lucy");
animal a = c; //用基类的对象得到对象c
a.call(); //尝试调用call()函数
return 0;
}
没有产生预期的结果!(底层解析的时候会告诉读者为什么)
这就给我们提示:普通对象的调用看的是类型。(千万不要和隐藏弄混了)
- 一定是需要调用虚函数 。如果不是调用虚函数,那么就是满足继承体系中的隐藏(重定义)关系了!
例3:
cpp
#include<iostream>
#include<string>
using namespace std;
class animal
{
public:
animal(const string& name)
:_name(name)
{}
void func()
{
//无关键字virtual声明--->普通的成员函数
cout << "1" << endl;
}
virtual void call()
{
cout << "name is : " << _name << " call : sound" << endl;
}
protected:
string _name;
};
class cat : public animal
{
public:
cat(const string& name = "")
:animal(name)
{}
void func()
{
cout << "2" << endl;
}
void call()
{
cout << "name is : " << _name << " call : meow" << endl;
}
};
int main()
{
cat c("Lucy");
animal* ptr = &c;
ptr->func(); //普通调用
ptr->call(); //多态调用
return 0;
}
上面代码func函数的调用构成普通的隐藏关系。
- 协变演示:
例4:
cpp
#include<iostream>
#include<string>
using namespace std;
class A
{};
class B : public A
{};
class animal
{
public:
animal(const string& name)
:_name(name)
{}
virtual A* call()
{
cout << "name is : " << _name << " call : sound" << endl;
A* ptr = new A; //仅仅由于演示
return ptr;
}
protected:
string _name;
};
class cat : public animal
{
public:
cat(const string& name = "")
:animal(name)
{}
virtual B* call()
{
cout << "name is : " << _name << " call : meow" << endl;
B* ptr = new B;
return ptr;
}
};
int main()
{
cat c("Lucy");
animal* ptr1 = &c;
ptr->call();
return 0;
}
小编是为了演示而
new
了A
、B
对象,大家不要写出这样的代码。

- 访问权限问题。
例5:
cpp
#include<iostream>
#include<string>
using namespace std;
class animal
{
public:
animal(const string& name)
:_name(name)
{}
virtual void call()
{
cout << "name is : " << _name << " call : sound" << endl;
}
protected:
string _name;
};
class cat : public animal
{
public:
cat(const string& name = "")
:animal(name)
{}
private:
virtual void call()
{
cout << "name is : " << _name << " call : meow" << endl;
}
};
int main()
{
cat c("Lucy");
animal* ptr1 = &c;
ptr->call(); //没有影响
return 0;
}
上面代码的
animal
的call
方法访问权限为public
,而cat
的call
方法访问权限为private
,但是不影响animal*ptr
调用。
1.3 析构函数
说到多态,不得不提及一个默认成员函数了:析构函数。
来看下面一个场景:
例6(错误示例):
cpp
#include<iostream>
using namespace std;
class Base
{
public:
~Base()
{
cout << "~Base()" << endl;
}
};
class Derived : public Base
{
public:
~Derived()
{
cout << "~Derived()" << endl;
}
};
int main()
{
Base *ptr = new Derived;
delete ptr;
return 0;
}
说明:
上面代码
Derived
继承Base
类,并且由Base
指针指向new
的Derived
对象,然后delete
该指针指向的对象。来看运行结果:
我们明明是想调用
Derived
的析构函数,为什么会调用Base
的呢?
-
下面我们来分析一下:
delete
:调用析构 + 销毁空间。- 上面我们谈到:普通函数的调用只能看类型。(1.2的例3)
Base
和Derived
的析构函数很显然没有被声明为一个虚函数,那么调用就被当作普通函数来对待。- 那么对于一个
Base
的指针来说,如果以该指针类型来调用析构函数的话,就只会调用Base
类型的析构函数。
如果想要解决这个问题:
- 析构函数要被处理为统一的名字 --- 满足三同。这个任务已经帮助我们完成了,统一被处理为:
destructor
。 - 析构函数要被声明为虚函数 。
例7:
cpp
#include<iostream>
using namespace std;
class Base
{
public:
virtual ~Base()
{
cout << "~Base()" << endl;
}
};
class Derived : public Base
{
public:
virtual ~Derived()
{
cout << "~Derived()" << endl;
}
};
int main()
{
Base *ptr = new Derived;
delete ptr;
return 0;
}
来看运行结果:

-
注意:
编译默认生成的析构函数不会为你声明为一个虚函数。如果涉及以上想要使用
delete
的场景,还请显示声明析构函数为虚函数!
1.4 override和final
C++11提出这两个关键字,更好地规范了虚函数的重写
-
override:
- 用途:声明在派生类函数的参数列表后。检查派生类虚函数是否重写了基类的某个虚函数。如果没有重写,编译器会报错。
例8:
cpp
#include<iostream>
using namespace std;
class Base
{
public:
virtual ~Base()
{
cout << "~Base()" << endl;
}
virtual void func()
{
cout << "Base" << endl;
}
};
class Derived : public Base
{
public:
virtual ~Derived()
{
cout << "~Derived()" << endl;
}
virtual void func() override //声明override,并且完成了重写:三同
{
cout << "Derived" << endl;
}
};
int main()
{
Base *ptr = new Derived;
ptr->func();
delete ptr;
return 0;
}
-
final:
- 用途:修饰虚函数,表示该虚函数不能再被重写。
例9(错误样例,代码final
检查错误):
cpp
#include<iostream>
using namespace std;
class Base
{
public:
virtual ~Base()
{
cout << "~Base()" << endl;
}
virtual void func() final //声明
{
cout << "Base" << endl;
}
};
class Derived : public Base
{
public:
virtual ~Derived()
{
cout << "~Derived()" << endl;
}
virtual void func()
{
cout << "Derived" << endl;
}
};

1.5 重载、隐藏、重写对比
这三个是比较容易混淆的概率,小编在这里为大家对比一下
名称 | 特性 |
---|---|
重载 | 1、两个函数在同一作用域下 2、函数名和参数列表相同 |
隐藏(重定义) | 1、两个函数分别在父子类域中 2、函数名相同 |
重写(覆盖) | 1、两个函数分别在父子类域中 2、三同(协变例外)3、两个函数都是虚函数 |
-
我们可以得到一个小结论:
- 父子类域中的两个同名函数,不是构成重写就是隐藏!
2. 抽象类
有时候我们描述一个事物,但是这个事物在现实生活中是"抽象"的。那么对于它的方法而言就是一个抽象的方法,但是这个事物可以得到延展。例如:形状。
-
抽象类 :含有纯虚函数 的类被称为"抽象类"。
-
纯虚函数:
是在基类中声明 的函数,它在基类中没有定义,但要求 :任何该类的派生类都要重写自己的实现方法。
在基类中实现纯虚函数的方法是:在虚函数的方法函数原型后面添加
= 0
。
-
例10:
cpp
#include<iostream>
using namespace std;
class shape //该类为抽象类
{
public:
virtual double area() const = 0 // 声明为纯虚函数并且const修饰
{}
};
class circle : public shape
{
public:
virtual double area() const //const修饰指针也是参数列表中的一环
{
//业务处理
}
};
上面代码中,就是一个抽象类
shape
,由circle
继承这个抽象类,并且重写方法area
2.1 抽象类特性
-
抽象类是不能实例化对象出来的!
我们可以理解为:类似这样的抽象类在现实生活中也找不到实体!
- 也就是意味着:抽象类只能作为其它类的基类。
-
抽象类的本身类型 也不能 作为函数参数或者函数返回值,也不能 作为显示类型转换的类型。
原因同上:抽象类不能示例化出对象!
-
如果派生类没有重写该纯虚函数,那么该派生类也不能实例化出对象。
4. 可以定义抽象类类型 的指针 或者引用来指向其派生类。这里小编就不再列举例子了。
2.2 抽象类的应用场景
- 纯虚函数常用于定义接口 规范,强制派生类实现特定功能。抽象基类仅提供接口声明,不包含具体实现,确保所有派生类遵循统一的接口标准。抽象类体现了一种:接口继承的理念。
我们可以将抽象类用于描述一些抽象的事物:
- 图像
- 游戏角色
- ......
这些还是需要大家实战体会。
3. 多态实现的底层原理
关于这个问题,小编打算另起一文,如下:
在这篇文章中小编会和大家探讨:
- 多态调用的原理:解析虚函数指针,虚函数表,多态成立的两个条件
- 拓展虚函数在虚函数表中的存放位置:单继承和多继承
- ......
4. 静态绑定和动态绑定
上面链接那篇文章谈到了一个话题:多态调用的消耗。实际上这是一个动态绑定的。汇编层面上的差异我们已经可以得知了。
-
静态绑定(前期绑定):
- 在程序编译期间 就已经确定了程序的行为,这就是静态绑定。
- 例如:函数重载(也是一种静态多态)
-
动态绑定(后期绑定):
- 在程序运行期间 根据具体的类型确定程序的行为,这就是动态绑定。
- 例如:虚函数重写多态调用(动态多态)
5. 总结
我们总结一下这个部分的知识点
virtual
关键字声明虚函数,建议析构函数声明为虚函数。- 构成多态的两个条件:a、重写 b、指针和引用调用。
- 重载、隐藏、重写对比。
- 什么是抽象类?具体的性质?
- 多态调用的底层原理?虚函数指针、虚函数表?
- 了解静态多态和动态多态 。
完......
- 希望这篇文章能够帮助你!