C++中的“虚”

本文详细介绍了C++中的虚概念,包括虚函数、虚函数表、纯虚函数、虚析构函数、虚继承及其作用。强调了虚函数在实现多态性中的关键角色,以及动态联编的重要性。同时讨论了析构函数为虚函数的必要性,以及在构造和析构函数中调用虚函数的注意事项。

本博文整理的知识点是C++中有关虚的一切,包括:包括:虚函数、虚函数表、纯虚函数、纯虚类(抽象类)、虚析构函数、虚继承和虚基类

1.什么是虚函数,有什么作用?

前言

先理解多态:http://blog.csdn.net/BillCYJ/article/details/79523110,磨刀不误砍柴功。

若派生类内定义了一个与基类同名的成员函数(假设是非虚函数的成员函数),该现象称为同名覆盖。此时,无论派生类内部成员函数还是派生类的对象访问同名成员,如果未加任何特殊标识,则访问派生类中重新定义的同名成员。

1.如果派生类内部成员函数或派生类的对象需要访问基类的同名函数,则必须在同名函数前加上"基类名::"进行类名限定(也就是常规跨类访问方法)。
2.如果基类内部成员函数或基类对象访问同名成员,访问的一定是基类的同名成员。
3.无论基类指针指向基类对象还是派生类对象,无论引用是基类对象还是派生类对象的别名,始终调用基类成员。

那么,怎样才能使基类指针指向基类对象时调用基类同名成员,指向派生类对象时调用派生类同名成员?这就需要动态联编(也叫运行时基于对象类型的绑定)所能达到的效果。要实现动态联编,首先要将同名函数声明为虚函数。

虚函数定义
  1. 在基类用virtual声明成员函数为虚函数。当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数。因此派生类中的虚函数可不加 virtual
  2. 虚函数是多态性的基础,其调用的方式是动态联编(程序运行时才决定调用基类的还是子类的同名函数)。
  3. 虚函数的作用:可在派生类中重新定义此函数,为它赋予新的功能,要求函数名、函数类型、函数参数(个数和类型)必须与基类的虚函数相同,并根据派生类的需要重新定义函数体。
  4. 若派生类以public方式继承了基类虚函数,则可通过基类指针或引用(又叫指向基类对象的指针或引用)访问基类和派生类中被声明为虚函数的同名函数,达到多态的目的。例子如下:
void main()
{
	Base* pBase;
	pBase->Func();//调用基类的Func函数
	pBase=new Derived(1);//该句可用Derived Obj;pBase=&Obj;替代
	pBase->Func();//使其指向派生类对象后,pBase才能调用子类的Func()
//注意:不能写成pBase.Func(),因为.号左边是类/结构体的对象,->号左边是类/结构体的对象的指针
}
哪些函数可声明为虚函数

一般,可将类层次中具有共性的成员函数声明为虚函数,而个性的函数没有必要声明为虚函数。但以下情况是特例:

  1. 静态成员函数不能声明为虚函数。因为静态成员函数不属于某一个对象,没有多态性。
  2. 内联函数不能够声明为虚函数。因为内联函数的执行代码是明确的,没有多态性的特征。
  3. 构造函数不能是虚函数。因为构造函数声明为虚函数是完全没有意义的。
  4. 析构函数往往被声明为虚函数。当派生类中存在指针,又通过该指针进行了动态空间申请时,将析构函数声明为虚函数很必要。
补充

一个函数在基类和公有派生类中拥有相同的函数名,若函数返回值类型不同或形式参数表不同,即使在基类中被声明为虚函数,也不具备多态性。这种情况下,基类中的函数无虚函数特性,当做普通函数使用,而在派生类中存在同名函数就是前言中讲到的同名覆盖,无法通过基类的指针或引用实现动态多态性。

考题

下面的两道面试题来巩固上述的知识。

1.下面的C++代码输出是什么?(阿里巴巴)

class Base
{
public:
    int Bar(char x) { return (int)x;}
    virtual int Bar(int x) { return (2*x);}
};
 
class Derived : public Base
{
public:
    int Bar(char x) { return (int)(-x);}
    int Bar(int x) { return (x/2);}
};
 
void main()
{
    Derived Obj;
    Base* pObj=&Obj;
    printf("%d,",pObj->Bar((char)(100)));
    printf("%d,",pObj->Bar(100));
}

解析:
对于pObj->Bar((char)(100)):基类中的Bar(char x)不是虚函数,所以基类指针不管指向基类还是派生类,都调用基类的这个函数。
对于pObj->Bar(100),基类中的Bar(int x)是虚函数,自然该调用派生类中的Bar(int x)。

答案是:100,50

2.What’s the output of the following code?(微软)

class A
{
public:
    virtual void f()
    {
        cout << "A::f()" << endl;
 
    }
    void f() const
    {
        cout << "A::f() const" << endl;
    }
};
class B : public A
{
public:
    void f()
    {
        cout << "B::f()" << endl;
    }
    void f() const
    {
        cout << "B::f() const" << endl;
    }
};
void g(const A* a)
{
    a->f();
}
int main()
{
    A* a = new B();
    a->f();
    g(a);
//a传进g()后,会被转化为const A*类型,a指向常量对象。
    delete a;
}

答案是:
B::f()
A::f() const

函数的形参括号后面加const的目的:这些函数是"只读"函数,即不改变数据成员的函数都加上const关键字进行标识,显然,可提高程序的可读性。实际上,const成员函数还有另外一项作用,即常量对象相关,如上题。

2.什么是虚函数表?

为实现动态联编,编译器会为每个包含虚函数的类创建一个虚函数表,称为vtable。**在vtable中,编译器放置了该类的虚函数地址。**编译器还会设置一个虚函数表指针,称为vptr,指向vtable。通过基类指针做虚函数调用时,编译器取得这个vptr,并在vtable表中查找函数地址,这样就能调用正确的函数。

3.什么是纯虚函数,有什么作用?什么是纯虚类,有什么作用?

纯虚函数:只有函数的名字而不具备函数的功能,不能被调用。
作用:它只是通知编译系统:“在这里声明一个虚函数,待派生类中定义”。在派生类中必须定义此函数,所以,它在派生类中才具备函数的功能才可被调用。声明纯虚函数的一般形式是virtual 函数类型 函数名 (参数表列)=0;

纯虚类也称抽象类,凡是包含纯虚函数的类都是抽象类。因为纯虚函数是不能被调用的,包含纯虚函数的类是无法建立对象的。
作用:是作为一个类族的共同基类,或者说,为一个类族提供一个公共接口。

4.析构函数是否可以是虚函数?

为什么有的类的析构函数需要设为虚函数?

这样做是为了当用一个基类指针删除一个派生类的对象时,也就是用基类指针调用派生类的析构函数时,只有将父类的析构函数设为virtual,派生类的析构函数才会真正被调用。

是否每个类的析构函数都要设置成虚函数?

当然,并不是要把所有类的析构函数都写成虚函数。因为当类里面有虚函数的时候,编译器会给类添加一个虚函数表,里面来存放虚函数指针,这样就会增加类的存储空间。所以,只有当一个类被用来作为基类时,析构函数最好要写成虚函数。

5.析构函数是否可以是纯虚函数?

可以,当需定义一个抽象类时,若其中没其他合适的函数可定义为纯虚的,可把析构函数定义为纯虚的。

6.是否可以将构造函数设置为虚函数?

虚函数的意思就是开启动态绑定,会根据对象的动态类型而不是函数名来选择要调用的方法,但在调用构造函数之前,这个对象根本就不存在,它怎么动态绑定?(也就是说:在构造函数运行的之前,这个对象的动态类型还不完整,没办法确定它到底是什么类型,故构造函数不能动态绑定。

7.是否可在析构或构造函数中调用虚函数?

可以,但都达不到想要的效果,应尽量避免在构造函数和析构函数中调用虚函数。

在构造函数避免调用虚函数。定义一个derived实例对象时,当在构造base部分时,derived并不是一个完整的实例对象,derived部分的成员变量甚至没有被初始化。如果在构造base部分时,调用的是derived的虚函数并且该虚函数引用了尚未构造好的成员变量,试想会发生什么。所以,从安全性考虑,经由构造中的对象来调用一个虚函数,这个虚函数就是正在构造的对象的所属函数。

在析构函数中也避免调用虚函数。析构的时候,会先调用derived的析构函数,析构掉对象中的derived部分,再调用base的析构函数析构base部分。如果在base的析构函数里面调用derived的虚函数,并且该虚函数引用了已经被析构了的成员变量。所以,此时,调用的虚函数是base的。

8.什么叫虚继承?有什么作用?

虚继承,就是在被继承的类前面加上virtual关键字,这时被继承的类称为虚基类。虚继承在多重继承时可以防止二义性。 主要用在如下的菱形继承:

class A  
class B1:public virtual A;//A为虚基类,在多重继承时可防止二义性
class B2:public virtual A;  
class D:public B1,public B2;  //B1和B2共享

由于B1和B2共享, 所以虚基类的子对象虚基类的孙子对象中只存一份,这样较之多继承节省空间,还可以避免二义性。例如:

假设D中有公有变量p=10,B1将p的值改为12,B2将p的值改为13:

class D:public B1,public B2;
//情况1:此时,B2中对p的修改覆盖了B1,即p=13
class D:public B2,public B1;
//情况2:此时,B1中对p的修改覆盖了B2,即p=12

如果,当初class B1:public virtual A;class B2:public virtual A;都没加virtual,那么就会产生二义性以产生错误,如下:

//B1和B2都没加virtual时
int main()
{
    D d;            // 没有问题
    d.func();       // 若func是一个继承来的虚函数,若D没有重写func,会编译报错
    A* a = new D(); // 会编译报错,无法使用多态
    return 0;
}

分析:若D没有重写func(),后面d.func()会由于不知道调用B1的func还是B2的func而报错。同理,在使用动态的多态时,指针a并不知道使用哪条继承链。

9.什么是动态联编?

也叫运行时绑定,动态联编对函数的选择不是基于指针或者引用,而是基于对象类型,不同的对象类型将做出不同的编译结果。 C++中一般情况下联编也是静态联编,但是一旦涉及到多态和虚函数就必须要使用动态联编了。

10.什么是RTTI?

RTTI(Run-Time Type Information)运行时类型检查的英文缩写,它提供了运行时确定对象类型的方法。C++通过下面两个操作符提供RTTI:

(1)typeid:返回指针或引用所指对象的实际类型。
(2)dynamic_cast:将基类类型的指针或引用安全的转换为派生类型的指针或引用。主要用于类层次间的上行转换和下行转换,具有类型检查功能,如下例子:

classB
{
public:
	int m_iNum;
	virtual void foo();
};
classD:publicB
{
public:
	char* m_szName[100];
};
void func(B* pb)
{
	D* pd2=dynamic_cast<D*>(pb);
}

B为基类,D为派生类。pb和pd2都会指向B类型的对象,但用pd2来对D类型进行操作是不安全的,比如访问m_szName,所以,有了dynamic_cast,的作用,pd2将会自动成为一个空指针。

对于带虚函数的类,在运行时执行RTTI操作符,返回动态类型信息;
对于其他类,在编译时执行RTTI操作符,返回静态类型信息。

###参考
【1】剑指XX游戏(四) - 轻松搞定面试中的“虚”
http://blog.csdn.net/silangquan/article/details/18322087
【2】C++虚函数-同名访问
https://www.cnblogs.com/Rosanna/p/3346046.html

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值