1. 程式人生 > 程式設計 >詳解C++ sizeof(下)

詳解C++ sizeof(下)

sizeof作用於基本資料型別,在特定的平臺和特定的編譯器中,結果是確定的,如果使用sizeof計算構造型別:結構體、聯合體和類的大小時,情況稍微複雜一些。

1.sizeof計算結構體

考察如下程式碼:

struct S1
{
char c;
int i;
};
cout<<”sizeof(S1)=”<<sizeof(S1)<<endl;

sizeof(S1)結果是8,並不是想象中的sizeof(char)+sizeof(int)=5。這是因為結構體或類成員變數具有不同型別時,需進行成員變數的對齊。《計算機組成原理》一書中說明,對齊的目的是減少訪存指令週期,提高CPU儲存速度。

1.1記憶體對齊原則

(1)結構體變數的首地址能夠被其最寬基本成員型別大小所整除;

(2)結構體每個成員相對於結構體首地址的偏移量都是成員大小的整數倍,如有需要編譯器會在成員之間加上填充位元組;

(3)結構體的總大小為結構體最寬基本成員型別大小的整數倍,如有需要編譯器會在最末一個成員之後加上填充位元組。

有了以上三個記憶體對齊的原則,就可以輕鬆應對巢狀結構體型別的記憶體對齊。如下:

struct S2
{
char c1;
S1 s;
char c2;
};

在尋找S2的最寬基本資料型別時,包括其巢狀的結構體中的成員,從S1中尋找出最寬結構體資料型別是int,因此S2的最寬資料型別是int。S1 s在結構體S2中的對齊也遵守前三個準則,因此sizeof(S2)=sizeof(char)+pad(3)+sizeof(S1)+1+pad(3)=1+3+8+1+3=16位元組,其中pad(3)表示填充3個位元組。

結構體某個成員相對於結構體首地址的偏移量可以通過巨集offsetof()來獲得,這個巨集也在stddef.h中定義,如下:

#define offsetof(s,m) (size_t)&(((s *)0)->m)

例如獲得S1中的偏移量,方法為

size_t pos = offsetof(S1,i); //pos等於4

1.2修改對齊方式

1.2.1#pragma pack

#pragma pack(n)中n為位元組對齊數,其取值為1、2、4、8、16,預設是8。結構體對齊時,

(1)成員的偏移量為成員本身大小和n二者最小值的整數倍;
(2)結構體最終大小是結構體中最寬基本型別成員大小和n二者中的最小值的整數倍。

考察如下程式碼:

#pragma pack(push) //將當前pack設定壓棧儲存
#pragma pack(2) //必須在結構體定義之前使用
struct S1
{
char c;
int i;
};
struct S2
{
char c1;
S1 s;
char c2
};
#pragma pack(pop) // 恢復先前的pack設定

//或者
#pragma pack(2)
...
#pragma pack()

因此,sizeof(S2)=sizeof(char)+pad(1)+sizeof(S1)+1+pad(1)=1+1+6+1=10位元組。

注意,#pragma pack不能指定變數的儲存地址,變數的首地址預設為最大基本成員型別大小的整數倍。

1.2.2__declspec(align(#))

VC++支援__declspec(align(#)),在GNU C++並不支援。#的取值為1~8192,為2的冪。使用示例如下:

__declspec(align(256)) struct TestSize
{
char a;
int i;
};
cout<<sizeof(TestSize)<<endl; //輸出256

__declspec(align(#))要求#為2的整數次冪,作用主要有兩個方面:
(1)使結構體或類成員按#pragma pack確定記憶體佈局之後,在末尾填充記憶體使得整個物件的大小至少是#的整數倍。
(2)作用於變數時,強制要求編譯器將變數放置在地址是#整數倍的記憶體位置上。這點在呼叫原生API等要求嚴格對齊的方法時十分重要。

1.3空結構體

C/C++中不允許長度為0的資料型別存在。對於“空結構體”(不含資料成員)的大小不為0,而是1。“空結構體”變數也得被儲存,這樣編譯器也就只能為其分配一個位元組的空間用於佔位了。如下:

struct S3 { };
sizeof(S3); // 結果為1

1.4位域結構體

有些資訊在儲存時,並不需要佔用一個完整的位元組, 而只需佔一個或多個二進位制位。例如在存放一個開關量時,只有0和1 兩種狀態, 用一位即可表示。為了節省儲存空間,並使處理簡便,C語言又提供了一種資料結構,稱為”位域”或”位段”。包含位域變數的結構體叫作位域結構體。位域結構體的定義形式:

struct 位域結構體名
{ 
型別說明符 位域名:位域長度;
...
};

注意,位域長度不應該大於該型別說明符對應的資料型別的位長度。
使用位域的主要目的是壓縮儲存,其大致規則為:

(1)如果相鄰位域欄位的型別相同,且其位寬之和小於型別的sizeof大小,則後面的欄位將緊鄰前一個欄位儲存,直到不能容納為止;
(2)如果相鄰位域欄位的型別相同,但其位寬之和大於型別的sizeof大小,則後面的欄位將從新的儲存單元開始,其偏移量為其型別大小的整數倍;
(3)如果相鄰位域欄位的型別不同,則各編譯器的具體實現有差異,VC++採取不壓縮方式,GNU C++採取壓縮方式;
(4)如果位域欄位之間穿插著非位域欄位,則不進行壓縮;
(5)整個結構體的總大小為最寬基本型別成員大小的整數倍;
(6)位域可以無位域名,這時它只用作填充或調整位置,不能使用。例如:

struct BitFiledStruct
{ 
int a:1;
int :2; //該2位不能使用
int b:3;
int c:2;
};

關於位域結構體的sizeof大小,考察如下程式碼:

#include <iostream>
using namespace std;

struct BFS1
{
char f1 : 3;
char f2 : 4;
char f3 : 5;
};
struct BFS2
{
char f1 : 3;
int i : 4;
char f2 : 5;
};
struct BFS3
{
char f1 : 3;
char f2;
char f3 : 5;
};

int main()
{
cout<<sizeof(BFS1)<<endl;
cout<<sizeof(BFS2)<<endl;
cout<<sizeof(BFS3)<<endl;
}

執行上面的程式,VC++和GNU C++輸出結果如下:

//VC++輸出結果
2
12
3

//GNU C++輸出結果
2
4
3

考察以上程式碼,得出:

(1)sizeof(BFS1)==2。當相鄰位域型別不同,在VC++中sizeof(BFS2)=1+pad(3)+4+1+pad(3)=12,採用不壓縮方式,位域變數i的偏移量需要是4的倍數,並且位域結構體BFS2的總大小必須是sizeof(int)的整數倍。在GNU C++中為sizeof(BFS2)=4,相鄰的位域欄位的型別不同時,採取了壓縮儲存,位域變數i緊隨位域變數f1的剩餘位進行儲存,位域變數f2同樣是緊隨位域變數i的剩餘位進行儲存,並且位域結構體BFS2的總大小必須是sizeof(int)的整數倍,所以最終結果sizeof(BFS2)=1+pad(3)=4。

(2)sizeof(BFS3)==3,當非位域欄位穿插在其中,不會產生壓縮,在VC++和GNU C++中得到的大小均為3,如果壓縮儲存,則sizeof(BFS3)==2。

2.sizeof計算共用體

結構體在記憶體組織上是順序式的,共用體則是重疊式,各成員共享一段記憶體,所以整個共用體的sizeof也就是每個成員sizeof的最大值。結構體的成員也可以是構造型別,這裡,構造型別成員是被作為整體考慮的。所以,下面例子中,假設sizeof(s)的值大於sizeof(i)和sizeof(c),那麼sizeof(U)等於sizeof(s)。

union U
{
int i;
char c;
S1 s;
};

3.sizeof計算類

類是C++中常用的自定義構造型別,有資料成員和成員函式組成,進行sizeof計算時,和結構體並沒有太大的區別。考察如下程式碼:

#include <iostream>
using namespace std;

class Small{};

class LessFunc
{
int num;
void func1(){};
};

class MoreFunc
{
int num;
void func1(){};
int func2(){return 1;};
};

class NeedAlign
{
char c;
double d;
int i;
};

class Virtual
{
int num;
virtual void func(){};
};

int main(int argc,char* argv[])
{
cout<<sizeof(Small)<<endl; //輸出1
cout<<sizeof(LessFunc)<<endl;//輸出4
cout<<sizeof(MoreFunc)<<endl;//輸出4
cout<<sizeof(NeedAlign)<<endl;//輸出24
cout<<sizeof(Virtual)<<endl; //輸出8
return 0;
}

注意一點,C++中類同結構體沒有本質的區別,結構體同樣可以包含成員函式,建構函式,解構函式,虛擬函式和繼承,但一般不這麼使用,沿用了C的結構體使用習慣。類與結構體唯一的區別就是結構體的成員的預設許可權是public,而類是private。

基於以上這點,再考察從程式的輸出結果,得出如下結論:

(1)類同結構體一樣,C++中不允許長度為0的資料型別存在,雖然類無任何成員,但該類的物件仍然佔用1個位元組。
(2)類的成員函式並不影響類物件佔用的空間,類物件的大小是由它資料成員決定的。
(3)類和結構體一樣,同樣需要對齊,具體對齊的規則見上文結構體的記憶體對齊。
(4)類如果包含虛擬函式,編譯器會在類物件中插入一個指向虛擬函式表的指標,以幫助實現虛擬函式的動態呼叫。

所以,該類的物件的大小至少比不包含虛擬函式時多4個位元組。如果考慮記憶體對齊,可能還要多些。如果使用資料成員之間的對齊,當類物件至少包含一個數據成員,且擁有虛擬函式,那麼該物件的大小至少是8B,讀者可自行推導。

以上就是詳解C++ sizeof(下)的詳細內容,更多關於C++ sizeof的資料請關注我們其它相關文章!