多态
1. 概念
从广义上讲,多态可以分为静态多态和动态多态。
静态多态(编译时多态)发生在程序的编译阶段,主要包括函数重载和运算符重载,在编译时就能确定调用关系。
动态多态(运行时多态),本章讨论的主要是动态多态。因此从狭义上讲,多态指的是动态多态。
多态(polymorphism)按照字面的意思是“多种状态”,简单的概括为“一个接口,多种状态”,一个函数接口,在运行时根据传入的参数类型执行不同的策略。
多态的实现需要有三个前提条件:
- 公有继承
- 函数覆盖(函数重写)override
- 基类引用/指针指向派生类对象
2.2 函数覆盖
函数覆盖(函数重写)的前提是虚函数,虚函数使用关键字virtual修饰成员函数实现,普通的虚函数目的是实现函数覆盖。
虚函数的格式:
virtual 返回值类型 函数名 (参数表){}
一句话表达:在之前函数隐藏的前提下,把被隐藏的基类函数使用virtual修饰,就变成了函数覆盖。
虚函数具有以下性质:
- 在Qt Creator中斜体表示虚函数
- 虚函数具有传递性,基类被覆盖的虚函数会自动传递给派生类覆盖的新函数,使后者也变为虚函数。
- 成员函数和析构函数可以设置为虚函数,静态成员函数和构造函数不能设置为虚函数。
- 如果函数的声明与定义分离,virtual只需要修饰在声明处。
- C++11中,可以在派生类新覆盖的函数后面使用override关键字修饰,如果函数覆盖成功则不会报错。
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void eat()
{
cout << "吃东西" << endl;
}
virtual void speak();
};
void Animal::speak()
{
cout << "fgsjkfghklsj" << endl;
}
class Dog:public Animal
{
public:
void eat()
{
cout << "吃屎" << endl;
}
void speak() override
{
cout << "哼" << endl;
}
};
int main()
{
return 0;
}
3. 实现
多态往往伴随着函数的调用和传参,基类引用/指针指向派生类对象通常出现在函数传参中。
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void eat()
{
cout << "吃东西" << endl;
}
virtual void speak();
};
void Animal::speak()
{
cout << "fgsjkfghklsj" << endl;
}
class Dog:public Animal
{
public:
void eat()
{
cout << "吃屎" << endl;
}
void speak() override
{
cout << "哼" << endl;
}
};
class Cat:public Animal
{
public:
void eat()
{
cout << "吃鱼" << endl;
}
void speak()
{
cout << "害" << endl;
}
};
class Mouse:public Animal
{
public:
void eat()
{
cout << "吃老鼠药" << endl;
}
void speak()
{
cout << "吱吱吱" << endl;
}
};
/**
* @brief test_polymorphysism
* @param a 基类引用,传递栈内存
*/
void test_polymorphysism(Animal& a)
{
a.eat();
a.speak();
}
/**
* @brief test_polymorphysism
* @param a 基类指针,传递堆内存对象
*/
void test_polymorphysism(Animal* a)
{
a->eat();
a->speak();
}
int main()
{
Animal a;
Dog d;
Cat c;
Mouse m;
// 根据传入参数的类型不同,执行不同代码
test_polymorphysism(a);
test_polymorphysism(d);
test_polymorphysism(c);
test_polymorphysism(m);
Animal* a1 = new Animal;
Dog* d1 = new Dog;
Cat* c1 = new Cat;
Mouse* m1 = new Mouse;
// 根据传入参数的类型不同,执行不同代码
test_polymorphysism(a1);
test_polymorphysism(d1);
test_polymorphysism(c1);
test_polymorphysism(m1);
delete a1;
delete d1;
delete c1;
delete m1;
return 0;
}
4. 原理
当使用多态时,上面代码中的Animal类对象会增加一个隐藏的成员指针,指向Animal类的虚函数表,虚函数表与之前的虚基类表类似,也是只有一份,属于Animal类持有,而非某个对象持有。
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void eat()
{
cout << "吃东西" << endl;
}
virtual void speak();
};
void Animal::speak()
{
cout << "fgsjkfghklsj" << endl;
}
class Dog:public Animal
{
public:
void eat()
{
cout << "吃屎" << endl;
}
virtual void sleep()
{
cout << "呼呼呼" << endl;
}
};
class Cat:public Animal
{
public:
void eat()
{
cout << "吃鱼" << endl;
}
void speak()
{
cout << "害" << endl;
}
};
class Mouse:public Animal
{
public:
void eat()
{
cout << "吃老鼠药" << endl;
}
void speak()
{
cout << "吱吱吱" << endl;
}
};
/**
* @brief test_polymorphysism
* @param a 基类引用,传递栈内存
*/
void test_polymorphysism(Animal& a)
{
a.eat();
a.speak();
}
/**
* @brief test_polymorphysism
* @param a 基类指针,传递堆内存对象
*/
void test_polymorphysism(Animal* a)
{
a->eat();
a->speak();
}
int main()
{
Animal a;
Dog d;
Cat c;
Mouse m;
// 根据传入参数的类型不同,执行不同代码
test_polymorphysism(a);
test_polymorphysism(d);
test_polymorphysism(c);
test_polymorphysism(m);
Animal* a1 = new Animal;
Dog* d1 = new Dog;
Cat* c1 = new Cat;
Mouse* m1 = new Mouse;
// 根据传入参数的类型不同,执行不同代码
test_polymorphysism(a1);
test_polymorphysism(d1);
test_polymorphysism(c1);
test_polymorphysism(m1);
cout << sizeof(a) << endl; // 4 虚函数表指针
cout << sizeof(d) << endl; // 4 虚函数表指针,存在优化
// delete a1;
// delete d1;
// delete c1;
// delete m1;
return 0;
}
当Dog继承Animal时,也会有一张专属于Dog的虚函数表,这样表一开始会复制Animal的虚函数表。Dog对象内部会新增一个隐藏指针指向Dog的虚函数表,并且根据Dog的代码修改Dog的虚函数表。
动态类型绑定:
当基类引用或指针指向派生类对象时,编译器内部会产生一段代码,用于检查对象的真正类型,这段代码会在程序运行时通过对象的隐藏指针指向的虚函数表找到调用地址。因此多态本质上也是程序在运行期间查表的过程,会降低程序的执行效率。
5. 隐患
在使用多态时,可能会出现内存泄漏。
#include <iostream>
using namespace std;
class Animal
{
public:
~Animal()
{
cout << "Animal析构" << endl;
}
};
class Dog:public Animal
{
public:
~Dog()
{
cout << "Dog析构" << endl;
}
};
int main()
{
// 为了示例,强行触发多态
Animal* a = new Dog;
delete a; // 跳过了Dog的析构函数,可能出现内存泄漏
return 0;
}
解决的方法是给基类设置虚析构函数,让析构函数也被虚函数表管理。
#include <iostream>
using namespace std;
class Animal
{
public:
virtual ~Animal()
{
cout << "Animal析构" << endl;
}
};
class Dog:public Animal
{
public:
~Dog()
{
cout << "Dog析构" << endl;
}
};
int main()
{
// 为了示例,强行触发多态
Animal* a = new Dog;
delete a; // 解决了内存泄漏的问题
return 0;
}
如果一个类是基类,建议把析构函数设置虚析构函数,因为编译器自动添加的析构函数没有virtual修饰。