多态
概念
多态,简单说就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。比如,在乐队演奏时,钢琴演奏是一种声音,二胡演奏时另一种声音等等,这就是生活中的多态。
#include<iostream>
using namespace std;
class A
{
int m_a = 10;
public:
//当不加virtual时,调用的时A
virtual void show()
{
cout << "A::show():" << m_a << endl;
}
};
class B :public A
{
int m_b = 20;
public:
void show()
{
cout << "B::show():" << m_b << endl;
}
};
int main()
{
B b;
A* pa = &b; //基类指针指向派生类
pa->show();
A& ra = b; //基类引用绑定派生类
ra.show();
return 0;
}
运行结果
当不加virtual时,运行结果是A::show():20 \n A::show():10
当家virtual时,运行结果是B::show():20\n,B::show():10
多态的基本概念
1. 基类指针只能调用基类的成员函数,不能调用派生类的成员函数。
2. 如果在基类的成员函数前加virtual 关键字,把它声明为虚函数,基类指针就可以调用派生类中 同名的成员函数, 通过派生类中同名的成员函数,就可以访问派生对象的成员变量。
3. 有了虚函数,基类指针指向基类对象时就使用基类的成员函数和数据,指向派生类对象时就使用派生类的成员函数和数据,基类指针表现出了多种形式,这种现象称为多态。
4. 基类引用也可以使用多态。
演示
#include<iostream>
using namespace std;
class A
{
int m_a = 10;
public:
//当不加virtual时,调用的时A
virtual void show() { cout << "A::show():" << m_a << endl; }
};
class B :public A
{
int m_b = 20;
public:
void show() { cout << "B::show():" << m_b << endl; }
};
int main()
{
B b;
A* pa = &b; //基类指针pa指向派生类
pa->show(); //若加了关键字virtual,则调用派生类
A& ra = b; //基类引用ra绑定派生类
ra.show();
return 0;
}
我们试想一些,声明的虚函数支持派生类中的重载函数吗?
在派生类中添加一个成员函数
void show(int) { cout << "B::show(int)" << endl; }
void test()
{
B b;
A* pa=&b;
pa->show(10); //报错,不支持派生类中的重载函数
}
若要使用派生类的重载函数,在基类中也要写出重载的基类函数;
注意
1)只需要在基类的函数声明中加上virtual关键字,函数定义时不能加。
2)在派生类中重定义虚函数时,函数特征要相同。
3)当在基类中定义了虚函数时,如果派生类没有重定义该函数,那么将使用基类的虚函数。
4)在派生类中重定义了虚函数的情况下,如果想使用基类的虚函数,可以加类名和域解析符。
5)如果要在派生类中重新定义基类的函数,则将它设置为虚函数;否则,不要设置为虚函数,有两方面的好处:首先 普通函数效率更高;其次,指出不要重新定义该函数。
多态的应用
#include<iostream>
using namespace std;
class Person
{
public:
virtual void hello() { cout << "hello" << endl; }
virtual void wave() { cout << "向你挥手" << endl; }
};
class A:public Person
{
public:
void hello() { cout << "A:hello" << endl; }
void wave() { cout << "A:向你挥手" << endl; }
};
class B :public Person
{
public:
void hello() { cout << "B:hello" << endl; }
void wave() { cout << "B:向你挥手" << endl; }
};
int main()
{
//不使用多态 1->A,2->B
int num;
cin >> num;
//if (num == 1)
//{
// A a;
// a.hello();
// a.wave();
//}
//else if (num == 2)
//{
// B b;
// b.hello();
// b.wave();
//}
//使用多态
Person* ptr = nullptr;
if (num == 1)
{
ptr = new A;
}
else if(num==2)
{
ptr = new B;
}
ptr->hello();
ptr->wave();
return 0;
}
多态分类
C++中的多态分为两种:静态多态与动态多态。
静态多态:也成为编译时的多态;在编译时期就已经确定要执行了的函数地址了;主要有函数重载和函数模板。
动态多态:即动态绑定,在运行时才去确定对象类型和正确选择需要调用的函数,一般用于解决基类指针或引用派生类对象调用类中重写的方法(函数)时出现的问题。
对于上述程序我么你打开内存模型(左:虚继承,右:普通)
原来是4字节,在虚继承后,变为8字节,存放了一个虚基类表,存放着基类三个虚函数的地址,如果没有虚函数,编译器直接把成员函数的地址链接到二进制文件中,直接调用基类成员函数,如果右虚函数,编译器不会把成员函数的地址链接到二进制文件中,而是在创建对象时,多创建一个虚函数表;在调用虚函数时,先查虚函数表,得到函数地址,再执行函数,所以调用普通函数比虚函数效率高。
继续查看派生类A的内存模型
如果创建了派生类对象,在虚函数表中,会用派生类成员函数的地址取代基类成员函数的地址。如果用基类指针指向派生类对象,调用虚函数时,由于函数地址已经被替换成派生类的了,则相当于调用派生类的成员函数。
在这里,把派生类A的wave函数删除,则A的内存模型发生改变,如下
可以看到wave函数的地址没有被替换,还是基类的成员函数。
如何析构派生类对象(虚析构函数)
1. 构造函数不能继承,创建派生类对象时,先执行基类构造函数,再执行派生类构造函数。
2. 析构函数不能继承,而销毁派生类对象时,先执行派生类析构函数,再执行基类析构函数。
派生类的析构函数在执行完后,会自动执行基类的析构函数。
3. 如果手工的调用派生类的析构函数,也会自动调用基类的析构函数。
要点
1)析构派生类对象时,会自动调用基类的析构函数。与构造函数不同的是,在派生类的析构函数中不用显式地调用基类的析构函数,因为每个类只有一个析构函数,编译器知道如何选择,无需程序员干涉。
2)析构函数可以手工调用,如果对象中有堆内存,析构函数中以下代码是必要的:
delete ptr;
ptr=nulllptr;
3)用基类指针指向派生类对象时,delete基类指针调用的是基类的析构函数,不是派生类的,如果希望调用派生类的析构函数,就要把基类的析构函数设置为虚函数。
4)C++编译器对虚析构函数做了特别的处理。
5) 对于基类,即使它不需要析构函数,也应该提供一个空虚析构函数。
6)赋值运算符函数不能继承,派生类继承的函数的特征标与基类完全相同,但赋值运算符函数的特征标随类而异,它包含了一个类型为其所属类的形参。
7)友元函数不是类成员,不能继承。
示例如下
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class AA { // 基类
public:
AA() { cout << "调用了基类的构造函数AA()。" << endl; }
virtual void func() { cout << "调用了基类的func()。" << endl; }
virtual ~AA() { cout << "调用了基类的析构函数~AA()。" << endl; }
//~AA() { cout << "调用了基类的析构函数~AA()" << endl; }
};
class BB :public AA { // 派生类
public:
BB() { cout << "调用了派生类的构造函数BB()。" << endl; }
void func() { cout << "调用了派生类的func()。" << endl; }
~BB() { cout << "调用了派生类的析构函数~BB()。" << endl; }
};
int main()
{
//AA a;
//BB b;
AA* a = new BB;
delete a;
}
我们查看内存模型
基类
派生类
之前我们说过,如果要在派生类中重定义基类的虚函数,函数名和函数列表需要一致才可以,但是连个类的虚析构函数肯定不能一样,这怎么处理??
这里时C++编译器会给其进行特殊处理,我们大可不用深究。