c++多态
一,多态
多态是c++面向对象三大特性之一,关于什么是多态,我们需要先来了解一下虚函数和重写。
1.虚函数
在类的成员函数前面加上virtual
关键字,就构成了虚函数。
2.虚函数的重写
当在子类定义一个与父类完全相同的虚函数时,我们就称,子类的虚函数重写(覆盖)了父类的虚函数。
重写(也称覆盖),想要构成重写,需要如下几个条件:
- 不在同一个作用域(分别在父类和子类)
- 函数名相同,参数相同,返回值相同(协变除外)
- 基类函数必须带有virtual关键字
- 访问限定符不限
重写是专门为多态而生的,至于虚函数重写能够实现什么功能,我们在第三点多态的简单举例中详细明。
3.多态的简单举例
下面是实现多态的例子:
#include<iostream>
#include<stdlib.h>
using namespace std;
class Person//父类
{
public:
virtual Person& BuyTicket()
{
cout << "买票全价" << endl;
return *this;
}
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person//子类
{
public:
virtual Student& BuyTicket() //重写父类的虚函数
{
cout << "买票半价" << endl;
return *this;
}
~Student()
{
// free
cout << "~Student()" << endl;
}
};
void Func(Person& p)
{
// 多态调用:对象有关,指向哪个对象,调用就是谁的虚函数
// 1.对象父类指针/引用
// 2.重写的虚函数
p.BuyTicket();
}
int main()
{
Person p;
Student s;
// 普通调用:跟类型有关
p.BuyTicket();
s.BuyTicket();
//多态调用:跟传入对象有关
Func(p);
Func(s);
system("pause");
return 0;
}
在上面的代码中,子类student继承了父类people,且父子类中都有虚函数BuyTicket,构成虚函数重载。又在类外定义了一个函数Func,在里面同样调用BuyTicket。
让我们运行一下:
我们发现,前两句输出来此与对象p和对象s的成员函数调用,后两次输出来自func函数。
这里就需要重点注意一下func函数了,func函数的参数是一个父类对象的引用,但是如果给他传一个子类对象,他会调用子类中的BuyTicket函数,输出“买票半价”,这就是多态,“一类代码,多种形态”。
当使用基类的指针或引用调用重写的虚函数时,使用父类对象调用调的就是父类的虚函数,子类对象调用的就是子类的虚函数。
4.构成多态的充分条件
1.函数的参数一定是父类的指针或引用
2.子类一定要重写父类的虚函数
这两个条件缺一不可,少任意一个都不能构成多态。
5.探索多态的底层—虚表
5.1什么是虚表
虚函数表是通过一块连续内存来存储虚函数的地址,这张表解决了继承,虚函数(重写)的问题,在有虚函数的对象实例中都存在一张虚函数表,虚函数表就像一张地图,指明了实际应该调用的虚函数。
#include<iostream>
#include<stdlib.h>
using namespace std;
class Person//父类
{
public:
virtual Person& BuyTicket()
{
cout << "买票全价" << endl;
return *this;
}
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person//子类
{
public:
virtual Student& BuyTicket() //重写父类的虚函数
{
cout << "买票半价" << endl;
return *this;
}
~Student()
{
// free
cout << "~Student()" << endl;
}
};
void Func(Person& p)
{
// 多态调用:对象有关,指向哪个对象,调用就是谁的虚函数
// 1.对象父类指针/引用
// 2.重写的虚函数
p.BuyTicket();
}
int main()
{
Person p;
Student s;
// 普通调用:跟类型有关
p.BuyTicket();
s.BuyTicket();
//多态调用:跟传入对象有关
Func(p);
Func(s);
system("pause");
return 0;
}
为了更方便观察虚表,在上面的代码中,我向父类person中多定义一个虚函数vfunc,该函数没有进行任何操作。
在父类person和子类student中,都有一个变量_vfptr存储在头部,这是一个数组指针(虚表指针),所指的数组,就是我们说的虚表。
由上面的图片可以看出,虚表中依次存储了各个虚函数的地址,且存放的顺序和代码中定义的虚函数的顺序一致。当进行多态调用时,编译器根据传入对象的类别,找到对应的vfptr(虚表指针),再查看你要调用的函数在类中定义的位置,来找到该虚函数在虚表中存储的位置,实现调用。
5.2虚表(重点!)
为了更加深入的认识虚表,我又多创建了一个对象s1,它和s都是student类的实例化。
我们根据上图可以看到:
-
s和s1都继承了p,但他两个的vfptr与p的vfptr值不同,所以得出结论:编译器会根据类的不同而分别为2种类各自构建一个虚表。即虚表是从属于类的!
-
而同一类的s和s1,他们的vfptr值相同,且对应虚函数的地址也相同,所以得出结论:虚表指针是从属于对象的。也就是说,如果一个类含有虚表,则该类的所有对象都会含有一个虚表指针,并且该虚表指针指向同一个虚表。
-
再看p和s,s继承p,s中重写了虚函数Buyticket,而不重写虚函数vfunc,在监视中,我们发现:s类中的虚函数Buyticket地址与p中的不同,而虚函数vfunc地址与p中的相同,所以得出结论:当子类继承父类的虚函数,不进行重写的话,虚函数地址不变,而如果重写(覆盖),那么就会改变对应虚函数的地址,分配一块新内存存储重写后的虚函数,这很像写时拷贝。这也说明了为什么重写又叫覆盖(新的虚函数地址覆盖了虚表中原有的父类虚函数地址)。
5.3虚表存储在什么地方?
虚表在编译时就开始创建,构造函数实际是初始化_vfptr指向的位置。也就是说虚表不可能放在堆,栈中的。虚表存放在代码段和数据段都可以,都有可能。
一般虚表放在静态区(数据段中)。
关于多态里面一些需要注意的地方(次重点!)
- 子类重写父类的虚函数实现多态,要求函数名,参数列表,返回值完全相同。(协变除外)
- 父类中定义了虚函数,在子类中该虚函数始终保持虚函数的特性。
- 只有类的成员函数才能被定义为虚函数。
- 各种类型的函数能否被定义成虚函数:
4.1 静态成员函数:不能!
静态函数不需要创建对象就可以调用,而虚表要进行初始化后,虚函数才能调用,所以没有必要;静态成员函数属于一个类而非某一对象,没有this指针,它无法进行对象的判别。
4.2 构造函数:不能!
上面提到过,虚表是在构造函数中进行初始化的,你把构造函数弄成虚函数,此时虚表里还什么东西都没有呢,怎么找到他的地址呢,故不能定义成虚函数。
4.3 赋值运算符重载函数 :不能!
赋值运算符重载operator=虽然在定义上可以设为虚函数,但是最好不要这么做,容易在使用时引起混淆。
4.4内联函数 :不能!
我们知道内联函数的优势是在指定位置直接展开代码,省去了创建栈帧的开销,但这也同时使得内联函数没有地址,无法放进虚标内。
4.5析构函数:能!
最好把父类的析构函数声明为虚函数,子类析构和父类析构尽管不同名,但是在编译后实际会改成一个名为destructor的函数,构成同名函数。
因为在某些情况下,如果不定义成虚函数,可能会发生错误,比如:
Student s;
Person *p=s;//父类指针可以指向子类对象,子类对象发生切片行为
delete p;//此时对p调用析构函数,如果析构函数不是虚函数,则调用的是父类的析构,没有释放全部空间
//倘若定义成虚函数,则调用的是子类的析构函数,空间全部释放
- 如果在类外定义虚函数,只能在声明函数时加上virtual,类外定义函数时不能加virtual。
- 不要在构造函数和析构函数中调用虚函数,在构造函数和析构函数中,对象是不完整的,可能会发生未定义的行为。
7.协变
上面谈到重写的条件时说到,必须函数名,参数列表,返回值都一样才能构成重写,但是协变除外。
协变是一种特殊的重写方式,他可以允许返回值不同,但是返回的一定要是父类的指针或引用。
8.纯虚函数和抽象类
在成员函数的形参后面写上=0,则成员函数为纯虚函数,包含纯虚函数的类叫抽象类(也叫接口类),抽象类不能实例化对象,纯虚函数在子类中重新定义后,子类才能实例化出对象。
class Person
{
virtual void Display() = 0;//纯虚函数
protected:
string _name;
};
class :Student : public Person//抽象类会强制让子类重写他的纯虚函数,否则子类就无法创建
{};