多态
一、 多态概念
多态(polymorphism)是面向对象(OOP)的核心思想,按照字面意思去理解,就是多种形态。
是对于同一种指令,针对不同的对象,产生不一样的行为。简短来说就是一个接口,多个方法。
1 如何去理解多态
这里我们用一个例子去步步引入
- 假如我们定义了一个Animal的动物基类,该基类有两个成员函数,一个是eat()吃,一个是run()跑,并且还有一个数据成员name名字,这是基本上每个动物都会有的特征。
- 此时有两个具体的动物类Dog和Cat来继承我们的Animal动物基类。
- 此时我们的目的是通过基类的方式去创建不同的动物,并使用每个动物实现的方法。
#include <iostream>
#include <string>
using std::endl;
using std::cout;
using std::string;
class Animal {
public:
void eat() {
cout << "Animal eat()" << endl;
}
void run() {
cout << "Animal run()" << endl;
}
protected:
string name;
};
//Dog类继承了Animal类
class Dog : public Animal{
public:
void eat() {
cout << "Dog eat()" << endl;
}
void run() {
cout << "Dog run()" << endl;
}
};
//Cat类继承了Animal类
class Cat : public Animal{
public:
void eat() {
cout << "Cat eat()" << endl;
}
void run() {
cout << "Cat run()" << endl;
}
};
int main()
{
Animal animal = Dog();
animal.eat();
animal.run();
animal = Cat();
animal.eat();
animal.run();
return 0;
}
- 正如第三条所说,我们希望通过一个接口就能得到不同动物的不同方法的调用,这正是我们多态实现的意义所在。
2 如何去实现多态
然而我们上面的写的形式在C++语言中真的能够实现吗?
我们可能想的太过简单。当我们用上述代码去实现时会得到一下结果。
Animal eat()
Animal run()
Animal eat()
Animal run()
Animal eat()
我们发现,这并不是想要得到的效果,应该是创建Dog的对象,调用Dog的函数才对。这里调用的还是Animal,为了解决这个问题,需要引入一些概念。
2.1 C++的多态性(静态联编和动态联编)
C++支持两种多态性:编译时多态和运行时多态。
- 编译时多态:也称为静态多态,C++编译器根据传递给函数的参数和函数名决定要具体使用哪一个函数,又称为先期联编(early binding)或静态联编(static binding)。
- 运行时多态:有时,编译器无法在编译过程中完成联编,必须在程序运行时完成,因此编译器提供了一套“动态联编(dynamic binding)”的机制或晚期联编(late binding)。
而我们想要的多态和上面代码表现出来的不一样,我们一般想要的多态是运行是多态。
2.2 虚函数
我们发现上述例子显然实现的是静态多态,在这里需要介绍一种(表示还有其他方式实现动态多态)实现动态多态的方式:虚函数。
2.2.1 虚函数的定义
虚函数在基类中被声明为virtual的函数,并在其派生类中被重新定义的成员函数。
class Base {
virtual 返回类型 函数名(参数列表) { ... }
};
//或者是在类中声明之后在类外实现
virtual 返回类型 类名::函数名(参数列表) { ... }
2.2.2 派生类继承虚函数的使用事项:
-
如果一个基类的成员函数定义为了虚函数,那么它在所有派生类中也保持为虚函数,即使在派生类中省略了virtual关键字,也仍然是虚函数。
-
派生类要对继承来的虚函数进行重写(覆盖),但有要求:
- 与基类的虚函数有相同的参数个数及类型;
- 与基类的虚函数有相同的返回值类型;
- 与基类的虚函数有相同的函数名;
简单来说,除了函数体不同,其他都相同。
让我们根据这些概念来完善上述代码。
#include <iostream>
#include <string>
using std::endl;
using std::cout;
using std::string;
class Animal {
public:
//这里加了virtual
virtual void eat() {
cout << "Animal eat()" << endl;
}
virtual void run() {
cout << "Animal run()" << endl;
}
protected:
string name;
};
//Dog类继承了Animal类
class Dog : public Animal{
public:
void eat() {
cout << "Dog eat()" << endl;
}
void run() {
cout << "Dog run()" << endl;
}
};
//Cat类继承了Animal类
class Cat : public Animal{
public:
void eat() {
cout << "Cat eat()" << endl;
}
void run() {
cout << "Cat run()" << endl;
}
};
int main()
{
Animal animal = Dog();
animal.eat();
animal.run();
animal = Cat();
animal.eat();
animal.run();
return 0;
}
2.2.3 虚函数的访问
此时如果再次运行代码,我们能得到什么结果?
Animal eat()
Animal run()
Animal eat()
Animal run()
发现还是与我们想的不一样,这究竟是为什么?
我们发现,左边声明的是Animal类对象,右边初始化的是其派生类对象,按理说,应该调用的是派生类的函数才对。但为什么调用的还是Animal基类中的函数?
这里需要对虚函数的访问做一些解释:
- 1 对象访问:即
Animal animal = Dog();
- 如上述main函数中的使用,我们通过对象的方式去访问虚函数,此时编译器采用的是静态联编。
- 通过对象名访问虚函数时,调用哪个类的函数取决于定义对象的类型。
- 对象类型是基类型,那么就调用基类的函数;对象类型是派生类型,那么就调用派生类的函数。
- 2 指针访问:即
Animal *pAnimal = new Animal();
- 使用指针访问非虚函数时,编译器根据指针本身的类型决定要调用哪个函数,而不是根据指针指向的对象类型。(可以看出这点和对象访问一致)
- 使用指针访问虚函数时,编译器根据指针所指对象的类型决定要调用哪个函数(动态联编),而与指针本身的类型无关。
- 3 引用访问:即
Dog dog; Animal &animal = dog;
- 使用引用访问虚函数,与使用指针访问虚函数类似,都表现出动态多态的特性。
- 不同的是,引用一经声明后,引用变量本身无论如何改变,其调用的函数就不会再改变,始终指向其开始定义时的函数。(这点还是符合了引用的特性:引用就是初始化时对象的别名)。
- 因此虽然在使用上有一定限制,但是在一定程度上提供了代码的安全性,特别体现在函数参数传递等场合中。
- 4 成员函数中访问:在类的成员函数中访问该类层次中的虚函数,采用动态联编方式。
- 5 构造函数和析构函数中访问:构造函数和析构函数是特殊的成员函数
- 在其中访问虚函数时,C++采用静态联编,即在构造函数或析构函数内,即使是使用"this->虚函数名"的形式来调用,编译器仍将其解释为静态联编的”本类名::虚函数名“。
- 即它们所调用的虚函数是自己类中定义的函数,如果在自己类中没有实现该函数,则调用的是基类中的虚函数。但绝对不会调用任何在派生类中重写的虚函数。
2.2.4 访问格式的验证
- 对象访问:上述代码以及验证过,这里不多加赘述。
- 指针访问:发现此时的却可以访问到对应派生类的函数,指针所指对象的类型绝对要调用哪个函数。
int main() {
Animal *pAnimal = new Dog();
pAnimal->eat();
pAnimal->run();
delete pAnimal;
pAnimal = new Cat();
pAnimal->eat();
pAnimal->run();
}
/*
Dog eat()
Dog run()
Cat eat()
Cat run()
*/
- 引用访问:此时我们发现这也能访问到对应派生类的函数,但是重新改变指向,指为cat时却还是原来的dog类型。(从逻辑出发,对于引用来说,这种操作本身就是错的。但在可执行程序的生成过程中却没有报错,访问的时候与预期值不符合,这样行为称之为未定义行为。在C++中未定义行为有很多,你还知道哪些?)
int main() {
Dog dog;
Cat cat;
Animal &animal = dog;
animal.eat();
animal.run();
cout << endl;
animal = cat;
animal.eat();
animal.run();
}
/*
Dog eat()
Dog run()
Dog eat()
Dog run()
*/
- 成员函数的访问:在Animal定义一个bark()函数,其中调用了一个虚函数run()
class Animal {
public:
virtual void eat() {
cout << "Animal eat()" << endl;
}
virtual void run() {
cout << "Animal run()" << endl;
}
void bark() {
cout << "There is a animal barking now" << endl;
run();
}
protected:
string name;
};
class Dog : public Animal{
public:
void eat() {
cout << "Dog eat()" << endl;
}
void run() {
cout << "Dog run()" << endl;
}
/*
virtual void drink() {
cout << "the dog want to drink" << endl;
}
*/
};
int main() {
Animal *pAnimal = new Dog();
pAnimal->bark();
return 0;
}
/*
从结果发现,的确实动态联编,调用的还是Dog类中的run函数
There is a animal barking now
Dog run()
*/
- 构造函数和析构函数中访问:在Dog和Cat的构造函数中都调用eat()函数,其中Dog类重写了继承过来的eat()函数,Cat类没有重写eat()函数
class Animal {
public:
virtual void eat() {
cout << "Animal eat()" << endl;
}
virtual void run() {
cout << "Animal run()" << endl;
}
protected:
string name;
};
//重写了eat
class Dog : public Animal{
public:
Dog() {
this->eat();
cout << "Dog()" << endl;
}
void eat() {
cout << "Dog eat()" << endl;
}
};
//没有重写eat()
class Cat : public Animal{
public:
Cat() {
eat();
cout << "Cat()" << endl;
}
};
int main() {
Animal *pAnimal = new Dog();
delete pAnimal;
pAnimal = new Cat();
delete pAnimal;
}
/*
从结果中可以发现,若将继承过来的虚函数重写了那么会调用该类中重写过的函数,若没有重写,则调用基类中对应的虚函数。
Dog eat()
Dog()
Animal eat()
Cat()
*/
2.3 常见的多态类型(欢迎补充)
对于多态性我们可以做个小总结,对哪些属于动态多态,哪些属于静态多态进行分类。
2.3.1 静态多态
函数重载,运算符重载,类类型对象,模板等发生的时机都在编译的时候。
2.3.2 动态多态
类类型指针,类类型引用,成员函数调用虚函数等,发生的时机在运行的时候,通过虚函数体现。
3 如何去使用动态多态
- 基类要定义或声明虚函数
- 派生类要重写该虚函数
- 创建派生类对象
- 用基类的指针或引用指向派生类的对象
- 基类的指针或去调用虚函数