多态性与虚函数
面向对象程序设计中的多态性
多态性(polymorphism)
调用同一个函数名,根据需要实现不同的功能。
- 编译时的多态性:静态多态(函数重载、运算符重载)
- 运行时的多态性:动态多态(虚函数)
函数重载和运算符重载实现的多态性属于静态多态性,在程序编译时就能决定调用的是哪个函数。静态多态性又称编译时的多态性。静态多态是通过函数重载、运算符重载实现的。
运行时的多态性是指在程序执行之前,根据函数名和参数无法确定应该调用哪一个函数,必须在程序的执行过程中,根据具体的执行情况来动态地确定。动态多态是通过虚函数(virtual function
)实现的。
派生类与基类对象之间的赋值
- 1
可以将派生类对象的值赋给基类对象,但只对从基类继承来的成员赋值。
class Base{
};
class Derived:public Base{
};
Base b;
Derived d;
b = d;
- 派生类对象可以初始化基类的引用
Derived d;
Base &b = d;
//但b只能引用从基类继承来的成员
- 派生类对象的地址赋给基类的指针变量。
bp只能引用从基类继承来的成员。
Base *bp;
Derived d;
bp = &d;
公有派生类对象可以被当作基类的对象使用。反之则不可。
通过基类对象名、指针只能使用从基类继承的成员。
class A{
public:
void show(){cout<<"A"<<endl;}
};
class AA:public A
{
public:
void show(){cout<<"AA"<<endl;}
};
void f(A *pa){
pa->show();
}
int main()
{
A a;
AA aa;
f(&a);
f(&aa);
return 0;
}
函数f()
的形参是类A的指针,所以调用的方法只能是基类A中的方法。
A
A
这和java
的多态大不一样。
public class Polymorphism {
public static void main(String[] args) {
A a = new A();
AA aa = new AA();
f(a);
f(aa);
}
static void f(A a){
a.show();
}
}
class A {
void show() {
System.out.println("A");
}
}
class AA extends A{
void show()
{
System.out.println("AA");
}
}
A
AA
- 这种赋值与访问性的关系。发现只有共有派生的子类才能向上转型。
虚函数
在程序运行时通过基类指针调用相同的函数名,而实现不同功能的函数称为虚函数。
一旦把基类的成员函数定义为虚函数,由基类所派生出来的所有派生类中,该函数均保持虚函数的特性。
在派生类中重新定义基类中的虚函数时,可以不用关键字virtual来修饰这个成员函数 。
虚函数的声明
virtual关键字
virtual 函数类型 函数名(形参表)
{
函数体
}
若要通过基类指针访问派生类中相同名字的函数,必须将基类中的同名函数定义为虚函数,这样,将各种派生类对象的地址赋给基类的指针变量后,就可以动态地根据这种赋值调用不同类中的函数。
若要达成java的效果,只需
在基类A的同名函数show()
前加上virtual
关键字。
class A{
public:
virtual void show(){cout<<"A"<<endl;}
};
若要通过基类引用访问派生类中相同名字的函数,也是可以的。
class A{
public:
virtual void show(){cout<<"A"<<endl;}
};
class AA:public A
{
public:
void show(){cout<<"AA"<<endl;}
};
void f(A &a){
a.show();
}
int main()
{
A a;
AA aa;
f(a);
f(aa);
return 0;
}
A
AA
如果去掉f(A &a)的&
呢?
输出结果。
A
A
所以通过虚函数的实现的多态,必须要通过指针或引用。
只有在程序的执行过程中,依据指针具体指向哪个类对象,或依据引用哪个类对象,才能确定调用哪一个版本,实现动态绑定。
虚析构函数
如果要通过基类指针调用派生类对象的析构函数(里面包含delete),就需要让基类的析构函数成为虚函数。
class A{
public:
A(){cout<<"A"<<endl;}
~A(){cout<<"~A"<<endl;}
};
class AA:public A{
public:
int *p;
AA(){cout<<"AA"<<endl;p = new int;}
~AA(){cout<<"~AA"<<endl;delete p;}
};
int main()
{
A *pa = new AA;
delete pa;
return 0;
}
A
AA
~A
解决办法就是在基类的析构函数前加上关键字virtual
class A{
public:
A(){cout<<"A"<<endl;}
virtual ~A(){cout<<"~A"<<endl;}
};
class AA:public A{
public:
int *p;
AA(){cout<<"AA"<<endl;p = new int;}
~AA(){cout<<"~AA"<<endl;delete p;}
};
虚函数的几点说明:
-
当在基类中把成员函数定义为虚函数后,在其派生类中定义的虚函数必须与基类中的虚函数同名,参数的类型、顺序、参数的个数必须一一对应,函数的返回类型也相同。
若函数名相同,但参数的个数不同或者参数的类型不同时,则属于函数重载,而不是虚函数。
若函数名不同,显然是不同的成员函数。 -
虚函数必须是类的一个成员函数,不能是友元函数,也不能是静态的成员函数。
-
可把析构函数定义为虚析构函数。但是,不能将构造函数定义为虚函数。
-
在派生类中如果没有重新定义虚函数时,与一般的成员函数一样,当调用这种派生类对象的虚函数时,则调用基类中的虚函数。
-
实现动态多态性时,必须使用基类类型的指针变量或引用,指向不同的派生类对象,调用虚函数实现动态多态性。
通过对象名访问虚函数时,只能静态绑定。即由编译器在编译的时候决定调用哪个函数。 -
虚函数与一般的成员函数相比较,调用时的执行速度要慢一些。为了实现多态性,在每一个派生类中均要保存相应虚函数的入口地址表,函数的调用机制也是间接实现的。因此,除了要编写一些通用的程序,必须使用虚函数才能完成其功能要求外,通常不必使用虚函数。
-
一个函数如果被定义成虚函数,则不管经历多少次派生,仍将保持其虚特性(根据实际的类型来调用相应的函数,而不是声明的类型),以实现“一个接口,多个形态”。
但一定要注意virtual
在类派生结构中的位置:
class Base0{
public:
void v(){cout<<"Base0\n";}
};
class Base1:public Base0{
public:
virtual void v(){cout<<"Base1\n";}
};
class A1:public Base1{
public:
void v(){cout<<"A1\n";}
};
class A2:public A1{
public:
void v(){cout<<"A2\n";}
};
class B1:private Base1{
public:
void v(){cout<<"B1\n";}
};
class B2:public B1{
public:
void v(){cout<<"B2\n";}
};
int main()
{
Base0 *pb0;
A1 a1;
(pb0=&a1)->v();
A2 a2;
(pb0=&a2)->v();
return 0;
}
原因在于pb0接受了它的派生类的赋值,只能调用自己的函数,virtual
只是修饰了它的派生类。
Base0
Base0
纯虚函数(抽象类)
在基类中不对虚函数给出有意义的实现,只在派生类中有具体的意义。这时基类中的虚函数只是一个入口,具体的操作由派生类中的对象实现。这种虚函数称为纯虚函数(pure virtual)。
至少包含一个纯虚函数的类,称为抽象类(abstract classes)。抽象类只能作为派生类的基类,不能用来声明这种类的对象。
class <抽象类名>
{
virtual <类型> <函数名>(<参数表>) = 0;
……
}
-
在定义纯虚函数时,不能定义虚函数的实现部分。
-
把函数名赋于0,本质上是将指向函数的指针值赋为0。与定义空函数不一样,空函数的函数体为空,即调用该函数时,不执行任何动作。在没有重新定义这种纯虚函数之前,是不能调用这种函数的。
-
因为纯虚函数没有实现部分,所以不能产生对象。但可以定义指向抽象类的指针,即指向这种抽象基类的指针。当用这种基类指针指向其派生类的对象时,必须在派生类中给出纯虚函数的实现,否则会导致程序运行错误。
-
抽象类的唯一用途是为派生类提供基类。
纯虚函数是作为派生类中的成员函数的基础,实现动态多态性。
demo
class A{
public:
virtual void f()=0;
};
class A1:public A{
public:
void f(){cout<<"A1\n";}
};
class A2:public A{
public:
void f(){cout<<"A2\n";}
};
void fun(A *pa){
pa->f();
}
int main()
{
A1 a1;
A2 a2;
fun(&a1);
fun(&a2);
return 0;
}
虚函数与虚基类
- 虚基类的用处:派生类只会有唯一一份基类的拷贝。
要注意virtual
修饰的是那唯一的一份基类,在派生的时候声明。
demo
class Base{
};
class A1:public Base{
};
class A2:public Base{
};
class B:public A1,public A2{
};
int main()
{
B b;
Base *base = &b; //error , Ambiguous conversion from derived class 'B' to base class 'Base':
return 0;
}
要改为:
class Base{
};
class A1:virtual public Base{
};
class A2:virtual public Base{
};
class B:public A1,public A2{
};
int main()
{
B b;
Base *base = &b;
return 0;
}
- 虚函数的作用
用基类的指针或引用去根据实际对象的类型进行函数的调用。
这就是virtual
关键字在c++中主要的两个用处。
派生类中重定义基类中的重载函数
C++中继承层次的函数调用遵循以下四个步骤:
- 首先确定进行函数调用的对象、引用或指针的静态类型。
- 在该类中查找函数,如果找不到,就在直接基类中查找,如此循着类的继承链往上找,直到找到该函数或者查找完最后一个类。如果不能在类或其相关基类中找到该名字,则调用是错误的。
- 一旦找到了该名字,就进行**常规类型检查,**查看如果给定找到的定义,该函数调用是否合法。
- 假定函数调用合法,编译器就生成代码。如果函数是虚函数且通过引用或指针调用,则编译器生成代码以确定根据对象的动态类型运行哪个函数版本,否则,编译器生成代码直接调用函数。
下面的写法会报错。
class Base{
public:
void print(){
cout<<"print() in Base"<<endl;
}
void print(int x){
cout<<"print(int x) in Base"<<endl;
}
void print(string s)
{
cout<<"print(string s) in Base"<<endl;
}
};
class Derived:public Base{
public:
void print(){
cout<<"print() in Derived"<<endl;
}
};
int main()
{
Derived d;
d.print();
d.print(""); // error
d.print(1); // error
return 0;
}
当仅重写了其中的一个重载函数时,在做函数名匹配时,在本类中就可以匹配到了,就不会向其父类查找了。在派生类中,仅记录了这个被重写的函数的信息,没有其他重载函数,就导致了上述错误的出现。即:派生类中的函数会将其父类中的同名函数屏蔽掉。
- 如果派生类想通过自身类型使用基类中重载版本,派生类最好重定义所有重载版本。
解决方案1:
在派生类中通过using
为父类函数成员提供声明,这样,派生类不用重定义所继承的每一个基类版本。
class Derived:public Base{
public:
using Base::print; // 使用这条语句即可继承父类所有print()函数的重载
void print(){
cout<<"print() in Derived"<<endl;
}
};
解决方案2:
通过基类指针调用
int main() {
Derived d;
Base* bp = &d;
d.print();
bp->print(10); // 这里可没有进行虚函数,执行的还是父类中的函数。
bp->print(""); //
return 0;
}
解决方案3:
在派生类中需要重载的那个版本在基类中声明为virtual,实现动态绑定,就能统一的使用基类指针来调用了:
class Base{
public:
virtual void print(){
cout<<"print() in Base"<<endl;
}
void print(int x){
cout<<"print(int x) in Base"<<endl;
}
void print(string s)
{
cout<<"print(string s) in Base"<<endl;
}
};
class Derived:public Base{
public:
void print(){
cout<<"print() in Derived"<<endl;
}
};
int main()
{
Derived d;
Base *pb = &d;
pb->print(); //动态绑定到Derived的函数
pb->print(1);
pb->print("");
return 0;
}
个人觉得,第三种方法最为自然,并且最好还是自己显式地将函数全部进行重载(不要只重载一个)。
虚函数动态绑定的实现原理
-
多态类型与非多态类型
- 有虚函数的类类型称为多态类型
- 其它类型皆为非多态类型
-
二者的差异
多态类型支持运行时类型识别;多态类型对象占用额外的空间。 -
虚表(virtual table)
每个多态类有一个虚表(virtual table),虚表中有当前类的各个虚函数的入口地址 -
虚指针(virtual point)
每个对象有一个指向当前类的虚表的指针(虚指针vptr) -
动态绑定的实现
构造函数中为对象的虚指针赋值
通过多态类型的指针或引用调用成员函数时,通过虚指针找到虚表,进而找到所调用的虚函数的入口地址,通过该入口地址调用虚函数。