初识C++ 之 多态性
在C++语言当中,多态性是通过虚函数(virtual)来实现的,虚函数允许子类重新定义成员函数,而子类重新定义父类的做法称为重写(Override)
1. 虚函数
虚函数是实现C++多态性必不可少的关键,首先我们来探究一下虚函数的具体作用:
比如说,我们现在定义一个基类Base0
,它内部成员如下:
class Base0
{
public:
int var0 = 0;
virtual void func()
{
cout<<"I am from Base0"<<endl;}
void method()
{
cout<<"I am a method from Base0"<<endl;}
virtual ~Base0()
{
cout<<"Destroy Base0"<<endl;}
};
现在我们有一个子类Base1
,和Base2
它继承了Base0
class Base1: public Base0
{
public:
int var1 = 0;
void func()
{
cout<<"I am from Base1"<<endl;}
// 参数是基类的指针
void check(Base0 *b0)
{
b0->func();
}
~Base1()
{
cout<<"Destroy Base1"<<endl;}
};
class Base2: public Base0
{
public:
int var2 = 0;
};
表面上Base1
,和Base2
中的func()
方法是基类中func()
方法的重写,我们打印看看:
int main()
{
Base0 b0;
Base1 b1;
Base2 b2;
cout<<"b1 inherit b0:";b1.func();
cout<<"b2 inherit b0:";b2.func();
return 0;
}
[Running] cd "c:\Users\S.E\Desktop\c++\test\" && g++ main.cpp -o main && "c:\Users\S.E\Desktop\c++\test\"main
b1 inherit b0:I am from Base1
b2 inherit b0:I am from Base2
但这并不能展现多态的魅力,假如Base1
,和Base2
没有继承Base0
,仍然能实现以上的输出。
事实上,多态最常见的用法就是声明基类类型的指针,利用该指针指向任意一个子类对象,并且可以根据指向的子类的不同而实现不同的方法。换句话说,多态的目的是为了“接口重用”。也即,不论传递过来的究竟是类的哪个对象,函数都能够通过同一个接口调用到适应各自对象的实现方法,比如下面的例子:
int main()
{
Base0 b0;
Base1 b1;
Base2 b2;
Base0 *b[] = {
&b1, &b2};
for (int i=0; i<2; i++){
b[i]->func();
}
return 0;
}
在上述代码块中,我们定义了一个数组型指针,左边的类型为基类,我们的期望的输出能够根据子类的不同实现方法的多样化,我们先打印看看结果:
[Running] cd "c:\Users\S.E\Desktop\c++\test\" && g++ main.cpp -o main && "c:\Users\S.E\Desktop\c++\test\"main
I am from Base0
I am from Base0
显然,这并不是我们想要的结果,这时候,virtual虚函数的作用就派上用场了:
将基类Base0
的fun
更改为虚函数:
virtual void func()
{
cout<<"I am from Base0"<<}
再打印输出结果:
[Running] cd "c:\Users\S.E\Desktop\c++\test\" && g++ main.cpp -o main && "c:\Users\S.E\Desktop\c++\test\"main
I am from Base1
I am from Base2
这便是我们所期待的效果。
可见,对于虚函数,是实现c++多态的一个重要途径:在基类中将被重写的成员函数设置为虚函数,其含义是:当**通过基类的指针或者引用调用该成员函数(普通的定义无法实现多态)**时,将根据指针指向的对象类型确定调用的函数,而非指针的类型。如果没有使用虚函数的话,即没有利用C++多态性,则利用基类指针调用相应的函数的时候,将总被限制在基类函数本身,而无法调用到子类中被重写过的函数。因为没有多态性,函数调用的地址将是固定的,因此将始终调用到同一个函数,这就无法实现“一个接口,多种方法”的目的了。
除此之外,只需将基类中的成员函数声明为虚函数即可,派生类中重写的virtual函数自动成为虚函数。
1.1 一般多态性函数
通过以上的例子以及实验,我们给出c++一般多态性函数的定义:
在子类中,多态方法的函数名称、参数、返回值完全相同,即重写。
需要通过将基类中的多态函数声明为虚函数。
产生多态时,由成员函数调用或者通过指针、引用来访问多态方法。
1.2 特殊多态性函数
实际应用中,我们不仅仅希望在实际的调用中实现多态性,同时,我们也希望在函数的传参过程中能够实现多态性。
比如说,有这样一个全局函数,在声明中我们定义传入的参数类型为基类Base0
,但在实际调用这个函数的过程中,假设传入的是派生类,我们也希望函数能够保留该派生类的多态方法:
void use_Base(Base0 b){
b.func();
}
int main()
{
Base0 b0;
Base1 b1;
Base2 b2;
use_Base(b0);
use_Base(b1);
use_Base(b2);
return 0;
}
[Running] cd "c:\Users\S.E\Desktop\c++\test\" && g++ main.cpp -o main && "c:\Users\S.E\Desktop\c++\test\"main
I am from Base0
I am from Base0
I am from Base0
显然,这样并不能实现传入参数的多态性,这时候,将传入的参数改为基类的引用或者指针即可:
void use_Base(Base0 *b){
b->func();
}
int main()
{
Base0 b0;
Base1 b1;
Base2 b2;
use_Base(&b0);
use_Base(&b1);
use_Base(&b2);
return 0;
}
或
void use_Base(Base0 &b){
b.func();
}
int main()
{
Base0 b0;
Base1 b1;
Base2 b2;
use_Base(b0);
use_Base(b1);
use_Base(b2);
return 0;
}
结果均为:
[Running] cd "c:\Users\S.E\Desktop\c++\test\" && g++ main.cpp -o main && "c:\Users\S.E\Desktop\c++\test\"main
I am from Base0
I am from Base1
I am from Base2
这样便实现了函数传参的多态性。
1.3 析构函数的多态性
在c++的多态性中,析构函数同样也不例外,假如我们用基类的指针创建一个子类的对象,那么在delete的时候执行的就会是基类的析构函数,子类虽然继承了基类,但是却没有重写基类的析构函数,这样一来,delete就无法析构子类的指针,有可能造成内存泄漏:
int main()
{
Base0 *bb = new Base1;
bb->func();
delete bb;
return 0;
}
[Running] cd "c:\Users\S.E\Desktop\c++\test\" && g++ main.cpp -o main && "c:\Users\S.E\Desktop\c++\test\"main
I am from Base1
Destroy Base0
解决的办法同样是将析构函数设置为virtual虚函数,以保证析构函数的多态性:
virtual ~Base0()
{
cout<<"Destroy Base0"<<endl;}
[Running] cd "c:\Users\S.E\Desktop\c++\test\" && g++ main.cpp -o main && "c:\Users\S.E\Desktop\c++\test\"main
I am from Base1
Destroy Base1
Destroy Base0
添加virtual属性后,delete子类时就会先执行子类的析构函数,再执行父类的析构函数。
1.4 纯虚函数
对于一个类而言,若在成员函数后面加上=0,则该成员函数为纯虚函数:
class Base
{
public:
int bvar = 0;
virtual void abstract() const = 0;
};
纯虚函数具有如下性质:纯虚函数没有函数体,它只能有一个函数声明
- 纯虚函数只有函数的名字而不具备函数的功能,不能被调用。
- 纯虚函数的作用是在基类中为其派生类保留一个函数的名字,以便派生类根据需要对他进行定义。
- 如果在一个类中声明了纯虚函数,在其派生类中没有对其函数进行定义,则该虚函数在派生类中仍然为纯虚函数
可以发现,含有纯虚函数的类无法创建实例对象:
如果一个类中含有纯虚函数,那么这个类也叫做抽象类(接口类), 抽象类存在的意义在于不用定义对象而只作为一种基本类型用作继承,因此抽象类不能实例化出对象。和java类似,java中抽象类中的方法叫做抽象方法。
现在我们创建一个Base抽象类的派生类:
class BaseB : public Base
{
public:
int bvar = 0;
};
可以发现,抽象类的派生类必须实现抽象方法,如下:
class BaseB : public Base
{
public:
int bvar = 0;
virtual void abstract() const
{
cout<<"achieve abstract method"<<endl;}
};
顺带一提,通常会遇到在纯虚函数后加const
,这其实和声明纯虚函数本身无关。只是为了更加严谨。
用const
关键字是用来说明这个函数是 "只读(read-only)"函数,也就是说明这个函数内部无法修改任何数据成员,否则编译器会报错 。
为了声明一个const
成员函数, 把const
关键字放在函数括号的后面。声明和定义的时候都应该使用const
关键字。
2. 虚继承
假设我们有如下一种继承关系:
即,Base3
多继承了Base1
和Base2
,而Base1
和Base2
继承自同一个基类Base0
,在这种情况下,Base3
继承Base0
就存在两条继承路径:Base3->Base2->Base0以及Base3->Base1->Base0。此时,如果在Base3
中调用基类Base0
的方法,就会产生访问不明确的错误,这是因为编译器认为两条继承路径下的Base0是两个类,因此编译器不知道你所访问的Base0中的方法到底是哪条继承路径下的方法:
class Base1: public Base0
{
public:
int var1 = 0;
~Base1()
{
cout<<"Destroy Base1"<<endl;}
};
class Base2: public Base0
{
public:
int var2 = 0;
~Base2()
{
cout<<"Destroy Base2"<<endl;}
};
class Base3: public Base2, public Base1
{
};
int main()
{
Base3 b3;
b3.func();
return 0;
}
报错信息:
这时候,虚继承就派上用场了,
class Base1: virtual public Base0{
...};
class Base2: virtual public Base0{
...};
若Base1与Base2虚继承Base0, 则在派生类中只保留一份间接基类的成员,即,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员,此时在Base3中调用Base0虚基类的方法就不会产生模糊调用的情况。
[Running] cd "c:\Users\S.E\Desktop\c++\test\" && g++ main.cpp -o main && "c:\Users\S.E\Desktop\c++\test\"main
I am from Base0
一个距离我们并不遥远的虚继承的例子便是c++标准库的iostream
类,iostream由istream
和ostream
直接继承而来,并且这两个类又都共同继承自base_ios
类,属于典型的菱形继承。此时 istream 和 ostream 必须采用虚继承,否则将导致 iostream 类中保留两份 base_ios 类的成员:
3. 总结
本次实验详细研究了C++的多态性以及如何实现多态,c++的多态性体现在使用基类的指针或引用创建派生类的对象时产生的多态性。这时候就要求对应的多态方法声明为虚函数。
除此之外,由于c++拥有多继承的特性(这是有别于Java的,Java语言为了避免产生麻烦,只支持单继承)。为了解决继承过程中容易产生的菱形继承的问题,c++还引入了虚继承的概念,防止多条继承路径产生的模糊引用的问题。
在继承关系中如果一个基类的存在十分必要但也没必要实例化出对象,我们称该类为抽象类,c++中使用纯虚函数可以构造出抽象类以及抽象方法。