結構體是C/C++兩種語言中的基礎語法, C語言中的結構體只是一個存粹的數據集合類型的描述,它只有數據成員而沒有成員方法。C++中的結構體則被賦予爲一個類定義的角色,它能夠有數據成員也能夠有成員方法。OC語言源自於C語言,它是面向對象的C語言,天然結構體的概念就和C語言中的定義保持一致。html
結構體中的數據成員能夠是基本類型,也能夠是數組,也能夠是指針,還能夠是其餘的結構體。下面是一個結構體的定義示例:git
struct Student {
bool sex;
short int age;
char *address;
float grade;
char name[9];
};
複製代碼
一個被常常討論的問題就是求結構體的尺寸(Size)大小,也就是結構體實例佔用的內存字節數。結構體的尺寸受操做系統字長、編譯器、對齊方式等衆多因素的影響。所以要確認一個結構體的尺寸時若是沒有上述的約束前提則是沒有統一結果的。通常狀況下計算結構體尺寸大小有以下規則:程序員
按照上述的規則,就能夠得出上面示例結構體在64位系統下的尺寸了:github
在上面的佈局圖中能夠看出:數組
從上面的例子能夠看出由於須要對齊,結構體中的數據成員並不必定是連續保存的,而是有可能會存在一些padding空隙。 這也引出了另一個問題就是: 當咱們在定義結構體時若是數據成員的定義順序安排的不合理就有可能會致使多餘內存空間的佔用和浪費。 爲了達到最佳內存空間佔用,能夠將上述結構體中數據成員的定義順序進行調整以下:bash
struct Student {
bool sex;
char name[9];
short int age;
float grade;
char *address;
};
複製代碼
就能夠得出優化後的內存佈局:函數
那麼如何才能獲得最優的數據成員佈局順序呢?一個建議就是:按基礎數據類型的尺寸從小到大的順序進行排列。佈局
💡OC類中屬性的定義順序會引起內存佔用的差別嗎?這個問題留在後面詳細說明。大數據
最後再來看看結構體有嵌套的狀況下尺寸的計算規則,如下面的結構體定義爲例:優化
struct A {
int a1;
char a2;
};
struct B {
char b1;
struct A b2;
};
複製代碼
結構體A的尺寸在64位系統下佔用8個字節,那麼結構體B的尺寸以及b2的偏移又是多少呢?
根據前面的嵌套規則定義能夠得出: 全部結構體中最大的基礎數據類型是A中的int a1 ,它佔用了4個字節。所以得出B的尺寸是12,而b2的偏移則是int長度的倍數,這裏應該是4。
結構體中除了能夠定義基本數據類型外,還可使用位域來構建數據成員,也就是說某個數據成員可能只佔用結構體中某幾個bit位的存儲空間。結構體中定義位域的目的主要是爲了節省內存空間。假如某個結構體中有8個BOOL類型的數據成員用來描述8種狀態。那麼咱們須要定義8個BOOL類型的數據成員,這樣這個結構體實例就佔用了8個字節的內存空間,而若是咱們使用位域來定義的話則能夠用一個字節的內存空間就能夠表述出來。定義位域的格式以下:
struct Test {
int a:1; //冒號後面指定數據成員佔用的bit位的位數。
int b:2;
};
複製代碼
您也能夠參考這篇文章:www.cnblogs.com/zzy-frisrtb… 有對位域的詳細介紹。
在使用位域時須要注意兩點:
使用位域結構的一個經典應用就是用它來定義CPU指令。下面是用位域結構體來定義一條arm64的add加法指令:
//定義add當即數指令結構
struct arm64_add_immediate {
uint32_t Rd:5; //目標
uint32_t Rn:5;
uint32_t imm12:12;
uint32_t shift:2; //00
uint32_t opS:7; //0010001
uint32_t sf:1; //1
};
複製代碼
在通訊領域最多見的就是報文傳輸了。通常狀況下報文的結構由報文頭和報文體組成。報文頭的結構一般是固定的並且具備特定的格式,而報文體則一般是長度是可變的一串數據。報文頭結構中會有一個數據成員來指定報文體的長度,而報文體則一般是跟在報文頭後面。 對於這種報文頭和報文體的定義咱們仍然能夠用一個結構體來進行統一描述。這時候稱這種結構體爲變長結構體。變長結構體通常定義以下:
struct Test {
//其餘任意字段
int bodySize;
unsigned char body[0];
};
複製代碼
能夠看到結構體的最後定義的是一個長度爲0的字節數組數據成員,同時還定義了一個bodySize數據成員來指定body所佔用的字節。對於這種可變長度的結構體實例一般按以下方式來構建的:
int bodySize = 100;
//爲結構體實例pTest分配內存,內存的大小爲結構體的固定長度和body中的數據長度。
struct Test *pTest = (struct Test*) malloc (sizeof( struct Test) + bodySize);
//賦值可變長度
pTest->bodySize = bodySize;
//咱們就能夠通訪問其餘數據成員同樣來訪問body數據成員了。
pTest->body
free(pTest);
複製代碼
定義變長結構體的規則要求可變長部分的數據成員必須放到最後位置,同時結構體中還應該有一個數據成員來指定這個可變長度成員的所佔用的內存字節數。
當咱們用結構體來描述通訊的數據包信息時,就可能會由於不一樣操做系統中字長的差別或者CPU體系結構體的差別而致使發送方和接收方沒法匹配而出現異常。
出現這種問題的緣由之一就是不一樣平臺對數據類型的定義是不同的 ,好比int和long這兩種類型是平臺相關的類型。所以當咱們在開發跨平臺通訊的應用時就不能使用平臺相關的基本數據類型做爲結構體的數據成員,而應該明確的指定固定寬度的類型以及平臺無關的類型來定義數據成員。
除了數據類型的約束外,還有就是對齊的問題。就如上面介紹的對齊規則,由於不一樣系統或者編譯器的對齊規則不一致,就會致使當咱們將結構體序列化進行傳輸時出現異常。所以最佳的實踐是將結構體中的padding進行統一的去除。這須要在結構體定義中加入以下:
//告訴編譯器保存當前的對齊方式,並將對齊方式設置爲1字節
#pragma pack(push,1)
struct Student {
bool sex;
short int age;
char *address;
float grade;
char name[9];
};
//告訴編譯器恢復保存的對齊方式
#pragma pack(pop)
複製代碼
上述的編譯指令#pragma pack,能夠用來設置和恢復一個結構體成員的對齊方式。經過上述的編譯指令設置後最終的Student結構體的數據成員中將不會再出現padding空間了。結構體的尺寸就等於全部數據成員的尺寸之和了。
除此以外,不一樣的CPU在處理整數的字節序上也有差別,有的是Big Endian有的是Little Endian的。所以若是結構體中定義有整數數據成員時,也會出現由於雙方字節序不一致而出現異常。所以在通訊時若是結構體中有整數數據類型,通常狀況下咱們都會約定爲某種統一的字節序進行處理(最多見的就是約定爲Big Endian來處理)。
正是由於上述的總總限制,所以通常咱們在傳輸數據時不多直接對結構體進行序列化和反序列化處理。而是藉助一些平臺無關的數據組織格式來進行傳輸,好比JSON、XML、PB、ASN等等。固然若是通訊的雙方都是用C/C++語言來編寫的那麼序列化和反序列化效率最高的仍是結構體!!
不管是結構體仍是類其實都是一些數據的集合的聲明和描述,OC類也是如此。只不過在OC類中除了聲明數據成員外,還能夠定義方法。固然方法自己是不會佔用對象的存儲空間的。
在OC類中聲明的實體屬性最終會轉化爲數據成員。每一個OC類中還會有一個隱式的數據成員isa,這是一個指針類型的數據成員,而且是做爲類的第一個數據成員被定義。 所以下面的OC類定義:
@interface Student
@property short int age;
@property NSString *address;
@property float grade;
@property BOOL sex;
@end
複製代碼
若是轉化爲結構體的話就會變成:
struct Student {
void *isa;
BOOL _sex;
int _age;
float _grade;
NSString *_address;
};
複製代碼
從上面的定義中能夠看出,除了會多出一個isa數據成員外,數據成員的順序也發生了變化,它再也不是按OC中定義的屬性順序進行排列了。編譯器會自動優化OC類中屬性的排列順序, 也就是說: OC類中定義的屬性順序會在編譯時進行優化調整,其調整的規則就是先按數據類型的尺寸從小到大進行排列,相同尺寸的數據成員則按字母順序進行排列。
所以咱們在定義OC類時不須要考慮屬性的定義順序,系統會優化這些順序以便達到最小的內存佔用。
最後再來講說OC類實例對象的內存佔用問題。OC類的對象內存尺寸佔用按以下規則進行計算:
OC語言中的對象基本是基於堆內存來構造的,所以咱們所訪問和操做的對象實際上是一個指針。在MRC時代這個指針對象是由程序員負責其生命週期的控制,到了ARC時代OC對象的生命週期控制被編譯器託管。
C語言的結構體對象沒有所謂的構造和析構的概念,因此結構體中的數據成員的生命週期必須由程序員來控制。在當前的Xcode編譯器中能夠支持將一個OC對象定義爲一個結構體的數據成員。爲了解決結構體中OC對象數據成員的生命週期問題。編譯器會爲每一個包含了OC對象數據成員的結構體自動生成一個隱式的構造函數和隱式的析構函數。每當一個結構體對象實例被建立時系統自動會調用這個結構體的隱式構造函數,隱式構造函數的實現也很簡單,就是將結構體中的全部數據成員的值清零處理。而每當一個結構體對象實例被銷燬時則會自動調用隱式的析構函數,隱式的析構函數的內部實現是會將其中的OC對象數據成員置爲nil來減小對象的引用計數。
須要明確的是結構體對象的構造和析構調用只會發生在棧內存中建立的結構體實例中。而經過堆內存構造的結構體對象是不會調用構造函數和析構函數的。好比下面的代碼:
struct A {
NSString *a1;
int a2;
};
void main() {
//當函數結束後將會調用結構體A的默認析構函數,析構函數會將a1的引用計數減1,是的a1所指的對象會在合適的時機被釋放。
struct A a;
a.a1 = @"Hello world!";
a.a2 = 10;
struct A *pA = (struct A *)malloc(sizeof(struct A));
pA->a1 = @"Hello, world!";
pA->a2 = 20;
//pA在銷燬時並不會調用析構函數,這樣就使得a1所指向的OC對象不會被釋放,從而致使內存泄露的發生。
//除非咱們在銷燬pA前,手動調用pA->a1 = nil; 來減小引用計數。
free(pA);
}
複製代碼
所以若是咱們在結構體中定義OC對象數據成員時有以下的使用限制:
C++類中能夠將一個OC對象聲明爲其數據成員。與結構體不一樣的是C++類中若是有OC對象數據成員時,老是會在構造函數中將OC對象數據成員值設置爲nil, 同時會在析構函數中再次將OC對象數據成員設爲nil並減小引用計數。 而且不管你是否重寫了構造函數和析構函數,上述的兩個行爲都會被插入到構造和析構代碼中。所以在C++類中能夠放心的使用OC對象數據成員。