C++部分——C++继承和多态(2)

1.虚函数是怎么实现的

简单的说,虚函数是通过虚函数表实现的。那么,什么是虚函数表呢?
事实上,如果一个类中含有虚函数,则系统会为这个类分配一个指针成员指向一个虚函数表(vtbl),表中每一项指向一个虚函数的地址,实现上就是一个函数指针的数组。为了说明虚函数表,请看下面的程序用例:

class Parent
{
public:
    virtual void fool1(){}
    virtual void fool2(){}
    void fool3();
};

class Child1
{
public:
    void fool1(){};
    void fool3();
};

class Child2
{
public:
    void fool1(){}
    void fool2(){}
    void fool3();
};

下面列出了各个类的虚函数表(vtbl)的内容。
Parent类的vbtl:Parent::fool()的地址,Parent::fool();
Child1类的vbtl:Child1::fool()的地址,Parent::fool();
Child2类的vbtl:Child1::fool()的地址,Child2::fool();
可以看出,虚函数表既有继承性,又有多态性。每个派生类的vtbl继承了它各个基类的vbtl,如果基类vbtl中包含某一项,则其派生类的vbtl中也将包含同样的一项,但是两项的值可能不同。如果派生类覆盖(override)了该项对象的虚函数,则派生类vtbl的该项指向重载后的虚函数,没有重载的话,则沿用基类的值。
在类对象的内存布局中,首先是该类的vbtl指针,然后才是对象数据。在通过对象指针调用一个虚函数时,编译器生成的代码将先获取对象类的vbtl指针,然后调用vbtl中对应的项。对于通过对象指针调用的情况,在编译期间无法确定指针指向的是基类对象还是派生类对象,或者是哪个派生类对象。但是在运行期间执行到调用语句时,这一点已经确定,编译后的调用代码能够根据具体对象获取正确的vtbl,调用正确的虚函数,从而实现多态性。
分析一下这里的思想所在,问题的实质是这样,对于发出虚函数调用的这个对象指针,在编译期间缺乏更多的信息,而在运行期间具备足够的信息,但那时已不再进行绑定了,怎么在二者之间做一个过渡呢?把绑定所需的信息用一种通用的数据结构记录下来,该数据结构可以同对象指针相联系,在编译时只需要使用这个数据结构进行抽象的绑定,而在运行期间将会得到正真的绑定。这个数据结构就是vtbl。可以看到,实现用户所需的抽象和多态需要进行后绑定,而编译器又是通过抽象和多态实现后绑定的。

2.构造函数调用虚函数

(C++虚拟机制的理解)

#include<stdio.h>
class A
{
public:
    A(){doSth();}//构造函数调用虚函数
    virtual void doSth(){printf("I am A");}
};
class B:public A
{
public:
    virtual void doSth(){printf("I am B");}
};

int main()
{
    B b;
    return 0;
}

执行结果是什么?为什么?
解析如下:在构造函数中,虚拟机制不会发生作用,因为基类的构造函数在派生类构造函数之前执行,当基类构造函数运行时,派生类数据成员还没有被初始化。如果基类构造期间调用的虚函数向下匹配到派生类,派生类的函数理所当然会涉及本地数据成员,但是那些数据成员还没有被初始化,而调用涉及一个对象还没有被初始化的部分自然是危险的,所以C++会提示此路不通。因此,虚函数不会向下匹配到派生类,而是直接执行基类的函数。
所以,执行结果如下:
I am A

3.看代码写结果——虚函数的作用

#include<iostream>
using namespace std;
class A
{
public:
    virtual void print(void)
    {
        cout<<"A::print()"<<endl;
    }
};
class B:public A
{
pubic:
    virtual void print(void)
    {
        cout<<"B:print()"<<endl;
    }
};
class C:public A
{
public:
    void print(void)
    {
        cout<<"C::print()"<<endl;
    }
};
void print(A a)
{
    a.print();
}
void main(void)
{
    A a,*pa,*pb,*pc;
    B b;
    C c;

    pa=&a;
    pb=&b;
    pc=&c;

41  a.print();
    b.print();
    c.print();

45  pa->print();
    pb->print();
    pc->print();

49  print(a);
    print(b);
    print(c);
}

解析:
代码第41-43行,分别使用类A,类B和类C的各个对象来调用其print()成员函数,因此执行的是各个类的print()成员函数。
代码第45-47行,使用3个类A的指针来调用print()成员函数,而这3个指针分别指向类A,类B和类C三个对象。由于print()函数时虚函数,因此这里有多态,执行的是各个类的print()成员函数。
代码第49-51行,全局的print()函数的参数使用传值得方式(注意与传引用的区别,如果是引用,则又是多态),在对象a,b,c分别传入时,在函数栈中会分别生成类A的临时对象,因此执行的都是类A的print()成员函数。

4.看代码写结构——虚函数

#include<iostream>
#include<string>
using namespace std;

void println(const std::string& msg)
{
    cout<<msg<<"\n";
}

class Base
{
public:
    Base()
    {
        println("Base::Base()");
        virt();
    }
    void f()
    {
        println("Base::f()");
        virt();
    }
    virtual void virt()
    {
        println("Base::virt()");
    }
};
class derived:public Base
{
public:
    Derived()
    {
        println("Derived::Derived()");
        virt();
    }
    virtual void virt()
    {
        println("Derived::virt()");
    }
};

int main(int argc,char *argv[])
{
    Derived d;
    Base *pB=&d;
    pB->f();
    return 0;
}

这里写图片描述
代码第54行,构造Derived对象d。首先调用Base的构造函数,然后调用Derived的构造函数。在Base类的构造函数中,又调用了虚函数virt(),此时虚拟机制还没有开始作用(因为在构造函数中),所以执行的是Base类的virt()函数。同样,在Derived类的构造函数中,执行的是Derived类的virt()函数。
代码第47行,通过Base类的指针pB访问Base类的公共成员函数f()。f()函数又调用了虚函数virt(),这里出现多态。由于指针pB是指向Derived类对象的,因此实际执行的是Derived类中的virt()成员。

5.虚函数相关的选择题

#include<iostream>
#include<complex>
using namespace std;
class Base
{
public:
    Base(){cout<<"Base-ctor"<<endl;}
    ~Base(){cout<<"Base-dtor"<<endl;}
    virtual void f(int){cout<<"Base::f(int)"<<endl;}
    virtual void f(double){cout<<"Base::f(double)"<<endl;}
    virtual void g(int i=10){cout<<"Base::g()"<<i<<endl;}
};
class Derived:public Base
{
public:
    Derived(){cout<<"Derived-ctor"<<endl;}
    ~Derived(){cout<<"Derived-dtor"<<endl;}
    void f(complex<double> c){cout<<"Derived::f(complex)"<<endl;}
    virtual void g(int i=20){cout<<"Derived::g()"<<i<<endl;}
};
Base b;
Derived d;
Base* pb=new Derived;

从4个选项中选择正确的一个:
1)cout《sizeof(Base)《endl;
2)cout《sizeof(Derived)《endl;
3)pb->f(1.0);
4)pb->g();
解析:
题(1),Base类没有任何数据成员,并且含有虚函数,所以系统会为它分配一个指针指向虚函数表。指针的大小是4个字节。
题(2),Derived类没有任何数据成员,它是Base类的派生类,因此它继承了Base的虚函数表。系统也会为它分配一个指针指向这张虚函数表。
题(3),Base类中定义了两个f()的重载函数,Derived只有一个f(),其参数类型为complex,因此Derived并没有Base的f()进行覆盖。由于参数1.0默认是double类型的,因此调用的是Base::f(double)。
题(4),Base和Derived都定义了含有相同参数列表的g(),因此这里发生多态了。pb指针指向的是Derived类的对象,因此调用的是Derived类的g()。这里要注意,由于参数的值在编译期就已经决定的,而不是在运行期,因此参数i应该取Base类的默认值,即10.

6.为什么需要多重继承?它的优缺点是什么?

实际生活中,一些事物往往会拥有两个或两个以上事物的属性,为了解决这个问题,C++中引入了多重继承的概念。
多重继承的优点是对象可以调用多个基类中的接口。
多重继承的缺点是容易出现继承向上的二义性。解决措施:1)加上全局符确定调用哪一份拷贝;2)使用虚拟继承。使得多重继承只拥有父类的一份拷贝。

7.多重继承中的二义性?

#include<iostream>
class cat
{
public:
    void show()
    {
        cout<<"cat"<<endl;
    }
};
class fish
{
public:
    void show()
    {
        cout<<"fish"<<endl;
    }
};
class   catfish:public cat,public fish
{
};

int main()
{
    catfish obj;
    obj.show();

    return 0;
}

程序中catfish类多重继承cat类和fish类,因此继承了cat的show()方法和fish的show()方法。由于这两个方法同名,代码第27行直接用obj.show()时,无法区分应该执行哪个基本类的show方法,因此会出现编译错误。
可以改成obj.cat::show()访问cat中的show()成员。

8.多重继承二义性的消除

类A派生B和C,类D从B,C派生,如何将一个类A的指针指向一个类D的实例?
代码如下:

class A{};
class B:public A{};
class C:public A{};
class D:public B,public C{};

int main()
{
    D d;
    A *pd=&d;//编译错误
    return 0;
}

由于B,C继承自A,B,C都拥有A的一份拷贝,类D多重继承自B,C,因此拥有A的两份拷贝。如果此时一个类A的指针指向一个类D的实例,会出现“模糊的转换”之类的编译错误。解决办法如下。

class A{}
class B:virtual public A {} //B虚拟继承自A
class C:virtual public A {} //C虚拟继承自A
class D:public B,public C {};

int main()
{
    D d;
    A *pd=&d;//成功转换
    return 0;
}

将B,C都改为虚拟继承自A,则类D多重继承自B,C时,就不会重复拥有A的拷贝了,因此也就不会出现转换错误了。消除了继承的二义性。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值