2、指针与引用类型的兼容性将源代码中的函数调用解释为执行特定的函数代码块被称为函数编联。在C语言中这很简单,因为每个函数名都对应一个函数。在C++中,由于函数重载的原因,这项任务更复杂。编译器必须查看函数名与函数参数才能决定调用哪个函数。C/C++编译器可以在编译过程完成这种编联。在编译过程中进行编联称为静态编联(static binding),又称为早期编联。然而,C++中的虚函数使这项工作变的更加的复杂。使用虚函数时,使用哪一个函数不是在编译器确定的,因为编译器不知道用户选择哪种类型的对象。所以,编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为动态编联(dynamic binding),又称为晚期编联。
通常C++不允许将一种类型的地址赋给另一种类型的指针或引用,下面的做法编译时都会出错:
int num = 10; double *p = # double *rnum = num;
但是指向基类的指针或引用可以指向派生类对象,而不必进行显示类型转换
Person *p;
Student s("student", "11041722");
p = &s;
3、向上类型转换
把一个派生类指针,转换成基类指针,称为向上类型转换。向上类型转换是安全的,也不需要进行强制类型转换。因为在派生类对象中,包含一个基类对象实例,向上类型转化可以理解为,把指向派生类对象的指针转换成指向派生类对象中包含的基类对象。
lass base{
private:
int b;
public:
base(int b){
this->b = b;
}
virtual void test(){
cout<<"base test."<d = d;
}
virtual void test(){
cout<<"Derived test."<test();
}
4、向下类型转换
把一个基类指针转换成派生类指针,称为向下类型转换。向下类型转换存在安全隐患,必须进行强制类型转换,因为在派生类对象中可能会包含基类对象没有的成员函数或数据成员。如果指向基类对象的指针实际指向的是派生类对象,则向下类型转换可以成功;否则,不能成功。
class base{
private:
int b;
public:
base(int b){
this->b = b;
}
virtual void test(){
cout<<"base test."<d = d;
}
virtual void test(){
cout<<"Derived test."<(pbaseObj);
if(pDerivedObj){
pDerivedObj->test();
} else {
cout<<"std::bad_cast"<(pbase);
if(pDerived){
pDerived->test();
} else {
cout<<"std::bad_cast"<
注意:如果基类指针实际指向的是派生类对象,则向下类型转换可以成功;如果基类指针实际指向的是基类对象,则向下类型转换不能成功。
5、虚成员函数与动态编联
Person *p;
Student s("student", "11041722");
p = &s;
p->show();
如果在基类中没有把show()函数声明为虚函数,则p->show()将根据指针类型(Person*)调用Person::show()。指针类型在编译时已知,因此编译器在编译时,可以将show()关联到Person::show()。总之,编译器对非虚函数使用静态编联。如果在基类中把show()函数声明为虚函数,则p->show()将根据对象的类型(Student)调用Student::show()。通常,只有在运行程序时才能确定对象的类型。所以编译器生成的代码将在程序执行时,根据对象类型将show()关联到Person::show()或Student::show()。总之,编译器对虚函数使用动态编联。
二、多态
1、什么是多态?
多态(polymorphism)的字面意思是多种表现形式,多态性可以简单地概括为”一个接口,多种方法”,程序在运行时才决定调用的函数,换句话说,方法的行为应取决于调用方法的对象,它是面向对象编程领域的核心概念。多态的目的是为了实现接口重用,也就是说,不论传递过来的究竟是那个类的对象,函数都能够通过同一个接口调用到对应于各自对象的实现方法。关于多态,简而言之就是用父类型的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时绑定,要么试图做到运行时绑定。因此C++的多态分为静态多态(编译时多态)和动态多态(运行时多态)两大类。静态多态通过重载、模板来实现;动态多态就是通过本文的主角虚函数来体现的。
2、虚函数
虚函数是实现多态的重要机制
class Person{
protected:
string name;
public:
Person():name("default"){}
Person(const string &name):name(name){}
virtual void show(){
cout<<"name: "<
虚函数的特点
- 当通过对象指针或引用调用虚函数时,会根据指针或引用实际指向的对象的类型来选择方法(动态编联)
- 在基类的方法声明中使用关键字virtual可使该方法在基类以及所有的派生类中是虚的。
注意:关键字virtual只用于类声明的方法原型中,而没有用于方法的定义中。
3、虚函数的工作原理
道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的,编译器处理虚函数的方法是:给每个对象添加一个隐藏成员,隐藏成员是一个指向虚函数表的指针,虚函数表中存放虚函数的地址。调用虚函数时,程序将通过对象中的vptr指针,找到虚函数表,然后在虚函数表中查找要调用的函数的地址。
4、继承时虚函数表的样子
4.1、单继承(无虚函数覆盖)
下面是一个类的继承图,以及派生类的虚函数表:
通过上面的图可以分析出下面两点:
- 虚函数按照其声明顺序放于表中
- 父类的虚函数在子类的虚函数前面
4.2、单继承(有虚函数覆盖)
下面是一个类的继承图,以及派生类的虚函数表:
通过上面的图可以分析出下面两点:
- 覆盖的f()函数被放到了虚表中原来父类虚函数的位置
- 没有被覆盖的函数依旧
4.3、多继承(无虚函数覆盖)
下面是一个类的继承图,以及派生类的虚函数表:
通过上面的图可以分析出下面两点:
- 每个父类都有自己的虚函数表
- 子类的虚函数放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数
4.4、多继承(有虚函数覆盖)
下面是一个类的继承图,以及派生类的虚函数表:
通过上面两图可以分析出
- 三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,就可以任一静态类型的父类来指向子类,并调用子类的f()
5、虚函数调用过程
当调用一个虚函数时,首先根据指针或引用实际指向的对象的内存中的vptr找到虚函数表vtbl,接着通过vtbl找到对应虚函数的实现区域并进行调用。当一个类声明了虚函数或者继承了虚函数,这个类就会有自己的vtbl,vtbl核心就是一个函数指针数组。vtbl数组中的每一个元素对应一个函数指针指向该类的一个虚函数,同时该类的每一个对象都会包含一个vptr,vptr指向该vtbl的地址。
6、使用虚函数带来的开销
使用虚函数时,在内存与执行速度方面都有一定的开销。虽然非虚函数的效率比虚函数高,但是不具有动态编联功能。
- 每个对象都将增大,增大量为存储隐藏成员(是一个指针)的空间。
- 对于每一个类,编译器都将创建一个虚函数表。
- 对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址。
7、使用虚函数应注意的问题
- 内联函数不能是虚函数。虚函数用于实现运行时的多态,或者称为晚绑定或动态绑定。而内联函数用于提高效率。内联函数的原理是,在编译期间,对调用内联函数的地方的代码替换成函数代码。内联函数与虚函数的实现原理不同,因此,内联函数不能声明为虚函数。
- 静态成员函数不能是虚函数。static成员不属于任何类对象或类实例,所以即使给此函数加上virutal也是没有任何意义的。此外静态与非静态成员函数之间有一个主要的区别,那就是静态成员函数没有this指针,从而导致两者调用方式不同。虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它,因为它是类的一个成员,并且vptr指向保存虚函数地址的vtable。虚函数的调用关系:this -> vptr -> vtable ->virtual function,对于静态成员函数,它没有this指针,所以无法访问vptr. 这就是为何static函数不能为virtual。
- 构造函数不能是虚函数。虚函数基于虚表vtable(内存空间),而虚函数表在构造函数中进行初始化,即初始化vptr,让它指向正确的虚函数表。而在构造对象期间,虚函数表还没有被初始化,因此,不能根据vptr找到对应的虚函数,也就是说构造函数不能是虚函数。
- 析构函数应当是虚函数,除非类不用作基类。析构函数可以是虚函数,因为此时vptr已经通过构造函数初始化完成,可以在析构函数里面通过this -> vptr -> vtable ->virtual function调用对应的虚函数。如果析构函数不是virtual的,派生类析构的时候调用的是基类的析构函数,而基类的析构函数只要对基类部分进行析构,从而可能导致派生类部分出现内存泄漏问题。
- 友元不能是虚函数。虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它,友元没有this指针,因此,不能声明成虚函数。
8、虚函数与覆盖、隐藏
8.1、什么是覆盖(override)?
覆盖是指派生类中存在重新定义的函数,其函数名、参数列、返回值类型必须同父类中的相对应被覆盖的函数严格一致,覆盖函数和被覆盖函数只有函数体(花括号中的部分)不同,覆盖是一个类的多态性的体现。要想实现多态,派生类就要覆盖基类的函数,并且基类的虚函数前面必须有virtual关键字,如果基类对应的函数部署虚函数,那就是隐藏。
8.2、什么是隐藏?
如果派生类中的成员(包括成员变量和成员函数)和基类中的成员重名,那么就会隐藏从基类继承过来的成员。所谓隐藏,就是在派生类中使用该成员(包括在定义派生类时使用,也包括通过派生类对象访问该成员)时,实际上使用的是派生类新增的成员,而不是从基类继承来的。
8.3、发生隐藏时不体现多态性(不进行动态编联)
如果派生类的虚函数隐藏了基类的虚函数,使用的是静态编联,不体现多态性,例如:
class base
{
public:
virtual void Show(int x){
cout << "I am base, x = " << x << endl;
}
};
class Derived : public base
{
public:
virtual void Show(float x) {
cout << "I am Derived, x = " << x << endl;
}
};
void test(base &obj){
int a = 10;
obj.Show(a);
float b = 20;
obj.Show(b);
}
int main()
{
base b;
Derived d;
test(b);
test(d);
return 0;
}
输出结果
I am base, x = 10
I am base, x = 20
I am base, x = 10
I am base, x = 20
Process returned 0 (0x0) execution time : 0.014 s
Press any key to continue.
问题一:对象能不能直接调用虚函数?
可以调用,但是体现不出来多态效果,可以从下面两个角度来理解
- 派生类对象是一个特殊的基类对象,派生类对象里面拥有一个基类对象实例。当把一个派生类对象赋值给基类对象时,是把派生类对象里面的基类对象对应的值赋值给新创建出来的对象。此时,通过基类对象调用的虚函数就是基类自己的虚函数。
- 对象的类型来编译期已经决定,因此,在编译期具体要调用对应的哪个函数都已经决议好
class base{
public:
virtual void func(){
cout << "I am base" << endl;
}
};
class Derived : public base{
public:
void func() {
cout << "I am Derived" << endl;
}
};
int main()
{
Derived dObj;
base bObj = dObj;
bObj.func(); // I am base
base &obj = dObj;
obj.func(); // I am Derived
return 0;
}
问题二:构造函数与析构函数中是否可以调用虚函数?
从语法上讲,调用完全没有问题。但是,往往不能达到多态的效果
class base{
public:
base() {
func();
}
virtual void func(){
cout << "I am base" << endl;
}
};
class Derived : public base{
public:
void func() {
cout << "I am Derived" << endl;
}
};
int main()
{
Derived obj; // I am base
return 0;
}
当创建一个派生类对象时,派生类的构造函数会先调用基类的构造函数,在基类的构造函数执行时,此时的this指针指向的是基类对象,通过this->vptr调用的是基类的虚函数。



