C++类大小确定,构造、析构、静态、动态绑定(分析一道Oracle面试题)

    技术2022-05-20  32

     题目给出一段C++代码,要求写出运行结果,代码如下,大家可以自己先试一下:

    #include <iostream> using namespace std; class A{ public: A(){ cout<<"A::A()"<<endl; } ~A(){ cout<<"A::~A()"<<endl; } }; class B:public A{ public: B():c(0){ cout<<"B::B()"<<endl; fun(); } virtual ~B(){ cout<<"B::~B()"<<endl; } virtual void fun(){ cout<<"B::fun"<<endl; } virtual void test(int a=10){ cout<<"a= "<<a<<endl; } private: char c; }; class C:public B{ public: C():i(0){ cout<<"C::C()"<<endl; } ~C(){ cout<<"C::~C()"<<endl; } void fun(){ cout<<"C::fun"<<endl; } void test(int a=20){ cout<<"a= "<<a<<endl; } private: int i; }; int main(){ cout<<"===Construct A==="<<endl; A *a=new C; cout<<"===Construct B==="<<endl; B *b=new C; cout<<"===Satrt Work==="<<endl; b->test(); cout<<"Size of A is "<<sizeof(A)<<endl; cout<<"Size of B is "<<sizeof(B)<<endl; cout<<"Size of C is "<<sizeof(C)<<endl; cout<<"==Delete b=="<<endl; delete b; cout<<"==Delete a=="<<endl; delete a; return 0; }  

    程序的大致意思就是给出了三个类A、B、C的定义,其中C继承B,B继承A。形成三级继承层次

    现在我们看main函数里发生的故事:

    首先是new表达式,它获取一块内存并在该区域构造一个C类型的对象,并返回新构造对象的指针,

    由于类B和类A都是C的基类,所以,这个指针可以赋值给类型为A或B的指针;

    但是new是对C的,所以要调用C的构造函数,C要完成自身的构造必须调用所有父类的构造函数

    来完成继承来的资源的构造,所以C++中会按照继承层次从高到低调用构造函数,所以前四句的输出如下:

    ===Construct A===

    A::A()

    B::B()

    B::fun

    C::C()

    ===Construct B===

    A::A()

    B::B()

    B::fun

    C::C()

    下面我们分析

    cout<<"===Satrt Work==="<<endl;

    b->test();

    首先我们说一下C++中的静态绑定和动态绑定:一个基类的引用或者指针具有静态类型和动态类型,

    而且两者可能不同,

    静态类型:在编译时可知的引用类型或者指针类型,如本例中指针变量a的静态类型是 A*,b的是B*

    动态类型:该指针真正指向的对象的实际类型,这个只有运行时才知道,如a的动态类型是C*,b的是C*

    所以通过基类的的指针和引用调用成员函数是,就产生了一个问题,是根据其静态类型调用呢还是根据动态

    类型,所以就有了静态绑定和动态绑定。

    对于a而言,如果是静态绑定,就要调用A的test函数,如果是动态绑定就要调用C的test函数。请注意我这里只是说函数。

    在C++中关于使用基类指针或引用调用成员函数的时候绑定的规则是:

    对于非virtual函数执行静态绑定,对于virtual函数执行动态绑定

    所以b->test调用的肯定是C类的test函数,所以我们认为该句的执行结果如下:a=20

    事实上结果是:a=10

    为什么呢,难道是调用的B类的test函数,其实不是,就是调用的C类的

    你可以将C类中的test函数稍作修改: void test(int a=20)

    {  cout<<"In class C: a= "<<a<<endl; }

    编译后运行,结果是:In class C: a=10

    现在你可以确定是调用的C类的test函数了吧,下面就要说说默认参数的传递了

    我觉得函数拥有默认参数,并不意味着函数自身携带该参数的默认值,而是由编译器维护的

    当编译器发现调用该函数是没有给出参数,就将其默认参数传递给该函数,而且编译器是根据静态类型

    检查确定给函数传递参数的,也就是说对b->test编译器认为b的静态类型是B*

    所以b->test的静态类型是B中的test函数,所以把其默认参数传给将要调用的函数,而实际上b->test却在运行时

    指向了C中test函数入口地址,所以就有了上面的执行结果。

     

    ok,下面我们就要看一下,C++中类的大小的确定,也就是下面三行代码的执行结果分析:

     cout<<"Size of A is "<<sizeof(A)<<endl; cout<<"Size of B is "<<sizeof(B)<<endl; cout<<"Size of C is "<<sizeof(C)<<endl;

    C++中类的大小主要是虚函数表的大小加上非静态成员的大小,以及考虑存储效率而采用的字节对齐,

    类的大小与构造函数,非虚成员函数,析构函数(如果不是virtual)无关(注意:如果析构函数是virtual的,则有关)

           对于一个空类,我们暂且把一个空类定义为没有成员变量没有虚函数,理论上不占有任何内存空间,但是为了表示这个类的

    存在,必须给出地址,所以就给一个字节的内存空间。所以对于一个空类而言,sizeof(空类)=1,本例中的类A就属于这种

    情况;

          对于非空类而言,大小就是由虚函数表的大小加上非静态成员变量,以及考虑字节对齐了。

    对于非静态成员和字节对齐没什么特别,和C中的struct一样。关于虚表的大小,就有单继承和多继承的不同了:

         在单继承层次中,类中如果有虚函数(有就行,与个数无关),则要有虚函数表,其实就是一个指向虚函数函数指针数组的指针,所以占一个指针大小。

         在多继承中,继承多个基类的那个派生类会拥有与其直接继承的父类个数相同的虚函数表,所以其虚标的大小就不是一个指针的大小了

    而是一个指针的大小乘直接继承的父类的个数;

    下面俩个程序来说明这一点:

    第一个,单继承中,虚表的大小,为了便于观察虚表,我没有加入任何成员(我用的是64位的系统,所以指针大小是8Byte):

     

    #include <iostream> using namespace std; class A { public: A(){ cout<<"A::A()"<<endl; } ~A(){ cout<<"A::~A()"<<endl; } virtual void hello() { cout<<"hello "<<endl; } virtual void helloworld() { cout<<"hello world"<<endl; } }; class B:public A { public: B(){ cout<<"B::B()"<<endl; } ~B(){ cout<<"B::~B()"<<endl; } void hello() { cout<<"hello "<<endl; } }; class C:public A { public: C(){ cout<<"C::C()"<<endl; } ~C(){ cout<<"C::~C()"<<endl; } void hello() { cout<<"hello "<<endl; } }; int main() { cout<<"Size of A is "<<sizeof(A)<<endl; cout<<"Size of B is "<<sizeof(B)<<endl; cout<<"Size of C is "<<sizeof(C)<<endl; return 0; }

    其输出时结果如下:

    [jim@gpu1 ctest]$  ./classSizetestSize of A is 8Size of B is 8Size of C is 8[jim@gpu1 ctest]$

    通过A的大小我们看出与虚函数的个数没有关系。

     

    下面是多继承的情况,其中C类继承类A和B

     

    #include <iostream> using namespace std; class A { public: A(){ cout<<"A::A()"<<endl; } ~A(){ cout<<"A::~A()"<<endl; } virtual void hello() { cout<<"hello "<<endl; } }; class B { public: B(){ cout<<"B::B()"<<endl; } ~B(){ cout<<"B::~B()"<<endl; } virtual void helloworld() { cout<<"hello world"<<endl; } }; class C:public A,public B { public: C(){ cout<<"C::C()"<<endl; } ~C(){ cout<<"C::~C()"<<endl; } void hello() { cout<<"hello "<<endl; } void helloworld() { cout<<"hello world"<<endl; } }; int main() {    cout<<"Size of A is "<<sizeof(A)<<endl; cout<<"Size of B is "<<sizeof(B)<<endl; cout<<"Size of C is "<<sizeof(C)<<endl; return 0; }

    其运行结果如下:

    [jim@gpu1 ctest]$  ./classSizetest1Size of A is 8Size of B is 8Size of C is 16[jim@gpu1 ctest]$

     

    好了,有了这些我们分析一下最开始的那个程序的输出吧,即下面三句的输出

     cout<<"Size of A is "<<sizeof(A)<<endl; cout<<"Size of B is "<<sizeof(B)<<endl; cout<<"Size of C is "<<sizeof(C)<<endl;

    对于A没有成员变量,没有虚函数,也就是我们定义的空类,好了,给一个字节大小说明一下它的存在吧

    对于B有虚函数,ok要占一个指针,又有一个成员占一个字节,按照指针大小填充字节以实现对齐,ok两个

    指针大小吧,在32为系统下就是8了,64位系统下就是16了

    对于C有一个虚表占一个指针,一个int占四个字节,还有继承来的一个char如果在32位系统中,那就是

    12了,在64为系统下就是16了

    所以结果如下:

    (32位系统)

    Size of A is 1Size of B is 8Size of C is 12

    (64位系统)

    Size of A is 1Size of B is 16Size of C is 16

     

    OK,下面我们讨论一下最后的那段析构吧

    一个派生类对象在构造自身的时候,依次调用了所以父类的构造函数,同理析构的时候也是要靠调用所以父类的析构函数

    来将所以继承来的资源析构掉。

    但是此处注意了,通过delete 父类的指针或者引用来析构派生类对象时,对析构函数的调用,遵循C++中函数成员函数调用的

    绑定规则,如果析构函数是virtual的则动态调用派生类的析构函数,若析构函数不是virtual则静态调用父类的析构函数,这时候就可能会产生问题了,因为派生类可能新加入了成员变量,这时候,这部分新加的变量将无法被正常析构,而形成一个部分析构的怪胎。

    所以在本例中,B类的析构函数是virtual的,所以真正调用的是C类的析构,delete b时会自动调用A和 B的析构函数,而A中析构函数是非virtual的,所以会静态调用A的析构函数,生成析构后的怪胎(在window是下程序会报警),所以输出如下:

    ==Delete b==C::~C()B::~B()A::~A()==Delete a==A::~A()

     

    好了,到此结束吧,希望大家发现错误之处务必批评指正,谢谢。

     

    2011-04-16

     


    最新回复(0)