天天看點

C++ 類 sizeof 計算大小 虛函數 虛繼承 虛基類 虛函數表c++類的大小計算

文章目錄

  • c++類的大小計算
    • 關于類/對象大小的計算
      • 一.簡單情況的計算
      • 二.空類的大小
      • 三.含有虛函數成員
        • (1)在派生類中不對基類的虛函數進行覆寫,同時派生類中還擁有自己的虛函數
        • 在派生類中對基類的虛函數進行覆寫
        • (3)多繼承:無虛函數覆寫
        • (4)多重繼承,含虛函數覆寫
      • 四.虛繼承的情況
    • 虛函數表
      • 建立時期
    • 五、 取消位元組對齊
#include <iostream>
using namespace std;
class onlyHaveAInt{
private:
	int a;
};
class HaveAIntAndDouble{
	int a;
	double b;
};
class IntDoubleChildren:public HaveAIntAndDouble{
	int c;
	double d;
};
class VirtualInherit:virtual HaveAIntAndDouble{

};
class OnlyAFunc{
	void func(){};
};
class VirtualFunc{
	virtual void func(){};
};
int main(){
	void *getByte=nullptr;
	int size=sizeof(getByte);
	if(size==8){
		cout<<"Bytes=64"<<endl;
	}else if(size==4){
		cout<<"Bytes=32"<<endl;
	}
	cout<<"sizeof(int)="<<sizeof(int)<<" sizeof(double)="<<sizeof(double)<<endl;
	cout<<"-	OnlyHaveAInt Size:"<<sizeof(onlyHaveAInt)<<endl;
	cout<<"-	HaveAIntAndDouble Size:"<<sizeof(HaveAIntAndDouble)<<endl;
	cout<<"-	OnlyHaveAFunc Size:"<<sizeof(OnlyAFunc)<<endl;
	if(sizeof(OnlyAFunc)==1){
		cout<<"Class with Func do not get ram"<<endl;
	}
	cout<<"-	VirtualFunc Size:"<<sizeof(VirtualFunc)<<endl;
	
	cout<<"-	IntDoubleChildren Size:"<<sizeof(IntDoubleChildren)<<endl;
	cout<<"-	VirtualInherit Size:"<<sizeof(VirtualInherit)<<endl;
}


           

輸出:

Bytes=64
sizeof(int)=4 sizeof(double)=8
-	OnlyHaveAInt Size:4
-	HaveAIntAndDouble Size:16
-	OnlyHaveAFunc Size:1
Class with Func do not get ram
-	VirtualFunc Size:8
-	IntDoubleChildren Size:32
-	VirtualInherit Size:24
           

c++類的大小計算

c++中類所占的大小計算并沒有想象中那麼簡單,因為涉及到虛函數成員,靜态成員,虛繼承,多繼承以及空類等,不同情況有對應的計算方式,在此對各種情況進行總結。

首先要明确一個概念,平時所聲明的類隻是一種類型定義,它本身是沒有大小可言的。 我們這裡指的類的大小,其實指的是類的對象所占的大小。是以,如果用sizeof運算符對一個類型名操作,得到的是具有該類型實體的大小。

關于類/對象大小的計算

首先,類大小的計算遵循結構體的對齊原則

struct/class/union記憶體對齊原則有四個:

1).資料成員對齊規則:結構(struct)(或聯合(union))的資料成員,第一個資料成員放在offset為0的地方,以後每個資料成員存儲的起始位置要從該成員大小或者成員的子成員大小(隻要該成員有子成員,比如說是數組,結構體等)的整數倍開始(比如int在32位機為4位元組, 則要從4的整數倍位址開始存儲),基本類型不包括struct/class/uinon。

2).結構體作為成員:如果一個結構裡有某些結構體成員,則結構體成員要從其内部"最寬基本類型成員"的整數倍位址開始存儲.(struct a裡存有struct b,b裡有char,int ,double等元素,那b應該從8的整數倍開始存儲.)。

3).收尾工作:結構體的總大小,也就是sizeof的結果,.必須是其内部最大成員的"最寬基本類型成員"的整數倍.不足的要補齊.(基本類型不包括struct/class/uinon)。

4).sizeof(union),以結構裡面size最大元素為union的size,因為在某一時刻,union隻有一個成員真正存儲于該位址。

類的大小與普通資料成員有關,與成員函數和靜态成員無關。即普通成員函數,靜态成員函數,靜态資料成員,靜态常量資料成員均對類的大小無影響

虛函數對類的大小有影響,是因為虛函數表指針帶來的影響

虛繼承對類的大小有影響,是因為虛基表指針帶來的影響

空類的大小是一個特殊情況,空類的大小為1

解釋說明

靜态資料成員之是以不計算在類的對象大小内,是因為類的靜态資料成員被該類所有的對象所共享,并不屬于具體哪個對象,靜态資料成員定義在記憶體的全局區。

空類的大小,以及含有虛函數,虛繼承,多繼承是特殊情況,接下來會一一舉例說明

注意:因為計算涉及到内置類型的大小,接下來的例子運作結果是在64位gcc編譯器下得到的。int的大小為4,指針大小為8

一.簡單情況的計算

#include<iostream>
using namespace std;
class base
{
    public:
    base()=default;
    ~base()=default;
    private:
    static int a;
    int b;
    char c;

};
int main()
{
    base obj;
    cout<<sizeof(obj)<<endl;
}
           

計算結果:8

靜态變量a不計算在對象的大小内,由于位元組對齊,結果為4+4=8

二.空類的大小

本文中所說是C++的空類是指這個類不帶任何資料,即類中沒有非靜态(non-static)資料成員變量,沒有虛函數(virtual function),也沒有虛基類(virtual base class)。

直覺地看,空類對象不使用任何空間,因為沒有任何隸屬對象的資料需要存儲。然而,C++标準規定,凡是一個獨立的(非附屬)對象都必須具有非零大小。換句話說,c++空類的大小不為0

為了驗證這個結論,可以先來看測試程式的輸出。

#include <iostream>
using namespace std;

class NoMembers
{
};

int main()
{
    NoMembers n;  // Object of type NoMembers.
    cout << "The size of an object of empty class is: "
         << sizeof(n) << endl;
}
           

輸出:

The size of an object of empty class is: 1

C++标準指出,不允許一個對象(當然包括類對象)的大小為0,不同的對象不能具有相同的位址。這是由于:

new需要配置設定不同的記憶體位址,不能配置設定記憶體大小為0的空間

避免除以 sizeof(T)時得到除以0錯誤

故使用一個位元組來區分空類。

但是,有兩種情況值得我們注意

第一種情況,涉及到空類的繼承。

當派生類繼承空類後,派生類如果有自己的資料成員,而空基類的一個位元組并不會加到派生類中去。例如

class Empty {};
struct D : public Empty { int a;};
           

sizeof(D)為4。

第二中情況,一個類包含一個空類對象資料成員。

class Empty {};
class HoldsAnInt {
    int x;
    Empty e;
};
           

sizeof(HoldsAnInt)為8。

因為在這種情況下,空類的1位元組是會被計算進去的。而又由于位元組對齊的原則,是以結果為4+4=8。

繼承空類的派生類,如果派生類也為空類,大小也都為1。

三.含有虛函數成員

首先,要介紹一下虛函數的工作原理:

虛函數(Virtual Function)是通過一張虛函數表(Virtual Table)來實作的。編譯器必需要保證虛函數表的指針存在于對象執行個體中最前面的位置(這是為了保證正确取到虛函數的偏移量)。

每當建立一個包含有虛函數的類或從包含有虛函數的類派生一個類時,編譯器就會為這個類建立一個虛函數表(VTABLE)儲存該類所有虛函數的位址,其實這個VTABLE的作用就是儲存自己類中所有虛函數的位址,可以把VTABLE形象地看成一個函數指針數組,這個數組的每個元素存放的就是虛函數的位址。在每個帶有虛函數的類 中,編譯器秘密地置入一指針,稱為v p o i n t e r(縮寫為V P T R),指向這個對象的vTable。 當構造該派生類對象時,其成員vPtr被初始化指向該派生類的vTable。是以可以認為vTable是該類的所有對象共有的,在定義該類時被初始化;而vPtr則是每個類對象都有獨立一份的,且在該類對象被構造時被初始化。

假設我們有這樣的一個類:

class Base {

public:

virtual void f() { cout << "Base::f" << endl; }

virtual void g() { cout << "Base::g" << endl; }

virtual void h() { cout << "Base::h" << endl; }

};
           

當我們定義一個這個類的執行個體,Base b時,其b中成員的存放如下:

C++ 類 sizeof 計算大小 虛函數 虛繼承 虛基類 虛函數表c++類的大小計算

指向虛函數表的指針在對象b的最前面。

虛函數表的最後多加了一個結點,這是虛函數表的結束結點,就像字元串的結束符”\0”一樣,其标志了虛函數表的結束。這個結束标志的值在不同的編譯器下是不同的。在vs下,這個值是NULL。而在linux下,這個值是如果1,表示還有下一個虛函數表,如果值是0,表示是最後一個虛函數表。

因為對象b中多了一個指向虛函數表的指針,而指針的sizeof是8,是以含有虛函數的類或執行個體最後的sizeof是實際的資料成員的sizeof加8。

例如:

class Base {

public:
int a;
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
           

sizeof(Base)為16。

vptr指針的大小為8,又因為對象中還包含一個int變量,位元組對齊得8+8=16。

下面将讨論針對基類含有虛函數的繼承讨論:

(1)在派生類中不對基類的虛函數進行覆寫,同時派生類中還擁有自己的虛函數

比如有如下的派生類:

class Derived: public Base
{
public:
virtual void f1() { cout << "Derived::f1" << endl; }
virtual void g1() { cout << "Derived::g1" << endl; }
virtual void h1() { cout << "Derived::h1" << endl; }
};
           

基類和派生類的關系如下:

這裡寫圖檔描述

當定義一個Derived的對象d後,其成員的存放如下:

這裡寫圖檔描述

可以發現:

1)虛函數按照其聲明順序放于表中。

2)基類的虛函數在派生類的虛函數前面。

此時基類和派生類的sizeof都是資料成員的大小+指針的大小8。

在派生類中對基類的虛函數進行覆寫

假設有如下的派生類:

class Derived: public Base
{
public:
virtual void f() { cout << "Derived::f" << endl; }
virtual void g1() { cout << "Derived::g1" << endl; }
virtual void h1() { cout << "Derived::h1" << endl; }
};
           

基類和派生類之間的關系:其中基類的虛函數f在派生類中被覆寫了

這裡寫圖檔描述

當我們定義一個派生類對象d後,其d的成員存放為:

這裡寫圖檔描述

可以發現:

1)覆寫的f()函數被放到了虛表中原來基類虛函數的位置。

2)沒有被覆寫的函數依舊。

派生類的大小仍是基類和派生類的非靜态資料成員的大小+一個vptr指針的大小

這樣,我們就可以看到對于下面這樣的程式,

Base *b = new Derive();
b->f();
           

由b所指的記憶體中的虛函數表的f()的位置已經被Derive::f()函數位址所取代,于是在實際調用發生時,是Derive::f()被調用了。這就實作了多态。

(3)多繼承:無虛函數覆寫

假設基類和派生類之間有如下關系:

C++ 類 sizeof 計算大小 虛函數 虛繼承 虛基類 虛函數表c++類的大小計算

對于派生類執行個體中的虛函數表,是下面這個樣子:

C++ 類 sizeof 計算大小 虛函數 虛繼承 虛基類 虛函數表c++類的大小計算

我們可以看到:

1) 每個基類都有自己的虛表。

2) 派生類的成員函數被放到了第一個基類的表中。(所謂的第一個基類是按照聲明順序來判斷的)

由于每個基類都需要一個指針來指向其虛函數表,是以d的sizeof等于d的資料成員加上三個指針的大小。

(4)多重繼承,含虛函數覆寫

假設,基類和派生類又如下關系:派生類中覆寫了基類的虛函數f

C++ 類 sizeof 計算大小 虛函數 虛繼承 虛基類 虛函數表c++類的大小計算

下面是對于派生類執行個體中的虛函數表的圖:

C++ 類 sizeof 計算大小 虛函數 虛繼承 虛基類 虛函數表c++類的大小計算

我們可以看見,三個基類虛函數表中的f()的位置被替換成了派生類的函數指針。這樣,我們就可以任一靜态類型的基類類來指向派生類,并調用派生類的f()了。如:

Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()
b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()
           

此情況派生類的大小也是類的所有非靜态資料成員的大小+三個指針的大小

舉一個例子具體分析一下大小吧:

#include<iostream>
using namespace std;
class A     
{     
};    
class B     
{  
    char ch;     
    virtual void func0()  {  }   
};   
class C    
{  
    char ch1;  
    char ch2;  
    virtual void func()  {  }    
    virtual void func1()  {  }   
};  
class D: public A, public C  
{     
    int d;     
    virtual void func()  {  }   
    virtual void func1()  {  }  
};     
class E: public B, public C  
{     
    int e;     
    virtual void func0()  {  }   
    virtual void func1()  {  }  
};  

int main(void)  
{  
    cout<<"A="<<sizeof(A)<<endl;    //result=1  
    cout<<"B="<<sizeof(B)<<endl;    //result=16      
    cout<<"C="<<sizeof(C)<<endl;    //result=16  
    cout<<"D="<<sizeof(D)<<endl;    //result=16  
    cout<<"E="<<sizeof(E)<<endl;    //result=32  
    return 0;  
}  
           

結果分析:

1.A為空類,是以大小為1

2.B的大小為char資料成員大小+vptr指針大小。由于位元組對齊,大小為8+8=16

3.C的大小為兩個char資料成員大小+vptr指針大小。由于位元組對齊,大小為8+8=16

4.D為多繼承派生類,由于D有資料成員,是以繼承空類A時,空類A的大小1位元組并沒有計入當中,D繼承C,此情況D隻需要一個vptr指針,是以大小為資料成員加一個指針大小。由于位元組對齊,大小為8+8=16

5.E為多繼承派生類,此情況為我們上面所講的多重繼承,含虛函數覆寫的情況。此時大小計算為資料成員(按單個類對齊)的大小+2個基類虛函數表指針大小

考慮位元組對齊,結果為8+8+2*8=32

四.虛繼承的情況

虛拟繼承是多重繼承中特有的概念。虛拟基類是為解決多重繼承而出現的。如:類D繼承自類B1、B2,而類B1、B2都繼承自類A,是以在類D中兩次出現類A中的變量和函數。為了節省記憶體空間,可以将B1、B2對A的繼承定義為虛拟繼承,而A就成了虛拟基類。

虛繼承要求子類不和父類共享虛表指針

對虛繼承層次的對象的記憶體布局,在不同編譯器實作有所差別。

在這裡,我們隻說一下在gcc編譯器下,虛繼承大小的計算。

它在gcc下實作比較簡單,不管是否虛繼承,GCC都是将虛表指針在整個繼承關系中共享的,不共享的是指向虛基類的指針。

class A {
    int a;
};

class B:virtual public A{
    virtual void myfunB(){}
};

class C:virtual public A{
    virtual void myfunC(){}
};

class D:public B,public C{
    virtual void myfunD(){}
};
           

以上代碼中sizeof(A)=16,sizeof(B)=24,sizeof©=24,sizeof(D)=32.

解釋:A的大小為int大小加上虛表指針大小。

B,C中由于是虛繼承是以大小為int大小加指向虛基類的指針的大小。

B,C雖然加入了自己的虛函數,但是虛表指針是和基類共享的,是以不會有自己的虛表指針,他們兩個共用虛基類A的虛表指針。 B/C的虛基類指針+虛表指針+A類

D由于B,C都是虛繼承,是以D隻包含一個A的副本,于是D大小就等于int變量的大小+B中的指向虛基類的指針+C中的指向虛基類的指針+一個虛表指針的大小,由于位元組對齊,結果為8+8+8+8=32。

虛函數表

  • 擁有虛函數的類會有一個虛表,而且這個虛表存放在類定義子產品的資料段中。子產品的資料段通常存放定義在該子產品的全局資料和靜态資料區,這樣我們可以把虛表看作是子產品的全局資料或者靜态資料
  • 類的虛表會被這個類的所有對象所共享。類的對象可以有很多,但是他們的虛表指針都指向同一個虛表,從這個意義上說,我們可以把虛表簡單了解為類的靜态資料成員。值得注意的是,雖然虛表是共享的,但是虛表指針并不是,類的每一個對象有一個屬于它自己的虛表指針。
  • 虛表中存放的是虛函數的位址。

建立時期

虛函數和虛函數表是兩個東西,虛函數調用是在Run-Time時确定,虛函數表是在compile-Time時期決定的。

五、 取消位元組對齊

c/c++下取消結構體位元組對齊方法

在c/c++下編譯器會預設地對結構體進行對齊,其對齊的方法跟平台具體的特性有關,本文主要介紹結構體不進行對齊的方法。

1、結構體位元組不進行對齊的用途

(1)、減小記憶體占用的空間

結構體預設進行對齊,占用的空間比結構體内部成員變量位元組加起來大,如果取消位元組對齊,可以減小一部分空間。見下面具體例子。

(2)、直接将結構體作為通信協定(在低帶寬下通訊)

在不同的平台下,保證結構體内基本資料的長度相同,同時取消結構體的對齊,就可以将定義的資料格式結構體直接作為資料通信協定使用。

2、結構體位元組不對齊的方法

利用僞指令 #pragma pack (n) 可以動态的調整記憶體對齊的方式:

#pragma pack (n)  // 編譯器将按照n個位元組對齊;
#pragma pack()   // 恢複先前的pack設定,取消設定的位元組對齊方式
#pragma  pack(pop)// 恢複先前的pack設定,取消設定的位元組對齊方式
#pragma  pack(1)  // 按1位元組進行對齊 即:不行進行對齊
           

繼續閱讀