结构和联合--结构体内存和位段内存开辟规则
一.??結構的基本知識
聚合數據類型能夠存儲多個數據,C語言提供了兩種類型的聚合數據類型,數組和結構。數組是相同的數據,結構是不同類型的數據聚合。結構也是一些值得集合,這些值成為它的成員,每個結構都有它的名字,他們是通過名字來訪問的。
1.??????結構聲明
在結構聲明時,必須列出它包含的所有成員,這個列表包括每個成員的類型和名字。
struct tag {member-list}variable-list;
結構體的相關語法:
strict {
int a;
char b;
float c;
} x;
這個聲明創建了一個名字叫做x的結構體變量。它包含了三個元素。
struct {
int a;
char b;
float c;
}? y[20], *z;
這個聲明中y創建了一個結構體數組,它包含了20個結構體,z是一個指向結構體的指針變量。
警告:雖然上面的兩種結構的成員的數據類型一樣,但是編譯器會把它們當成是完全不同的數據類型,所以下面的這條語句是非法的:z=&x;
如果想定義一個指針變量使它可以指向x應該怎么定義呢,下面提供了兩種方式:
我們可以在定義的時候給這個結構體加上一個標簽,比如:
首先定義一個結構體變量
struct SIMPLE {
int a;
char b;
float c;
} x;
接著定義一個指向結構體變量的指針
struct SIMPLE *p;
這個時候我們就可以使用p =&x;了。
為了方便使用我們可以給這個變量定義一個新的類型
typedefstruct SIMPLE {
int a;
char b;
float c;
} SIMPLE;
這個時候,SIMPLE就是一個新的數據類型,如果我們想定義一個結構體變量,就可以使用SIMPLE x;定義一個指向結構體的指針就可以使用SIMPLE *p;這個時候我們可以使用p =&x;
2.??????結構成員
結構體中可以有很多不同的類型,如:
struct COMPLEX{
float f;
int a[20];
long *lp;
struct SIMPLE s;
struct SIMPLE sa[10];
struct SIMPLE *sp;
};
這里我們不必要擔心不同的結構體中的成員名相同,因為我們的結構體是不同的。
3.??????結構成員的直接訪問
如果我們已經知道結構變量的名字,這里我們通過操作符(.)進行訪問,比如:struct COMPLEX comp;如果我們想要訪問a,則可以通過以下的方式comp.a;這里我們還可以通過下面的方式訪問結構體里面的結構體的一個成員(comp.s).a;這里就是我們上面說的不同的結構體中可以定義名字相同的變量。我們甚至可以使用下面更為復雜的方式進行訪問((comp.sa)[4]).c;又因為下標引用和點操作符具有相同的優先級,他們的結合性都是從左到右。
4.??????結構成員的間接訪問
如果我們有一個指向結構體的指針,我們想通過這個指針訪問這個結構體,我們可以使用間接訪問操作符。舉個例子,假定一個函數的參數是個指向結構的指針,如下面的原型所示:
voidfunc(struct COMPLEX *cp );
這個函數可以使用下面這個表達式來訪問這個變量所指向的結構的成員f:
(*cp).f????
但是我們平時并不使用這種方式訪問一個結構體的成員變量,C語言提供了一個->操作符(也稱箭頭操作符)使用的方式如下:
cp-> f;
5.??????結構體的自引用
結構體的自引用是否合法呢,請看下面的一個例子:
structSELF_REF1 {
int a;
int c;
structSELF_REF1 b;
}
這種類型的定義是非法的,因為成員b是另一個完整的結構,其內部還將包含其他的成員b。這樣就永無止境的重復下去。
再來看下面的這種定義:
structSELF_REF1 {
int a;
int c;
structSELF_REF1 *b;
}
這個聲明和上面的區別在于,聲明b的時候不是聲明的一個結構體,而是聲明了一個指向結構體的一個指針,編譯器在結構體的長度確定之前就已經知道了指針的長度,所以這種自引用是合法的。(這其實就是我們在學習數據結構的時候,使用鏈表的一種很好的體現)
但是我們需要警惕下面的這種聲明:
typedefstruct {
int a;
int c;
SELF_FER3 *b;
}SELF_FER3;
這種聲明是錯誤的,類型名在聲明的末尾才定義,所以在結構體聲明的時候,它還沒有被定義。
解決的方法很簡單,
typedefstructSELF_FER3_TAG {
int a;
int c;
struct SELF_FER3_TAG *b;
}SELF_FER3;
6.??????不完整的聲明
偶爾我們需要聲明相互之間存在依賴關系的結構。即是一個結構中包含了另一個結構體的一個或者多個成員。和自引用結構體一樣,至少有一個結構必須在另一個結構體內部以指針的形式存在。
這樣我們就必須要使用不完整的聲明,它聲明一個作為結構標簽的標識符。然后我們可以把這個標簽于成員列表聯系在一起??聪旅娴倪@個例子:
structB;
struct A{
struct B*partner;
};
struct B{
struct A*partner;
}
7.??????結構體的初始化
一個位于一對花括號內部、由逗號分隔的初始值列表課用于結構體的各個成員的初始化。如果初始值列表的值不夠,剩余的結構體成員將使用缺省的值進行初始化。例如:
struct INIT_EX{
int a;
short b[10];
Simple c;
} x = {10,{1,2,3,4,5},{25,’x’,1.9}}
二.??結構、指針和成員
這里建議大家到《C和指針》這本書中尋找答案吧。
三.??結構體的存儲分配
1.??????結構體中存在對齊的原則,即開辟結構體空間的時候并不是按照每個成員的總大小開辟存儲空間,(在C++中類實例化的對象也是按照結構體的方式分配存儲空間的)看下面這樣的一個簡單的例子:
struct ALIGN {
char a;
int b;
char c;
};
假設我們的機器是32位的,這個時候首先開辟一個字節,用于存儲a,然后看下一個成員是4個字節,為了對齊,前面空3個字節,這時前面的字節數就是int字節數的倍數,接著就是從第五個字節開始開辟4個字節用于存儲b,然后是c,它的字節是1個字節,前面已經有八個字節正好是1的倍數,所以直接在b的后面開辟一個字節用于存儲c。
優化:我們在定義結構體的時候可以把復雜的放在前面,把簡單的放在后面,比如上面的結構體可以如下定義:
struct ALIGN {
int b;
char a;
char c;
};
如果我們用sizeof測試第一個結構的時候,它所測出來的結構包括了空白的結構,即為了對齊而跳過的內存空間,它也被包含進結構體中了。
如果我們想要確定某個成員的實際存儲位置,這個時候我們可以使用offsetof宏(定義于stddef.h)
offsetof(type,member)
type是結構體的類型,member是需要測試的成員名,表達式的結構是一個size_t值(無符號整型),表示這個結構成員開始的位置距離整個結構開始的位置偏移的字節數。例如對第一個結構進行下面的使用,offsetof(structALIGN,b);返回值是4。
2.??????結構體的內存對齊問題(對上面問題的詳細解讀)
內存對齊的含義:當結構體中每次放入一個新的成員的時候,它的前面需要補充的空白的字節數。即是:先對齊,再放入。
(1)????對齊數:自身字節數和編譯器默認字節數中的較小值(vs:8?? linux:4)
(2)????第一個成員放入的時候不需要對齊
(3)????接下來的成員放入的時候,需要對齊到對齊數的整數倍(補充空白字節,使前面的所有字節數的和為對齊數的整數倍)
(4)????上面對齊完之后需要對整個結構體進行對齊,即在所有的成員開辟完空間之后,還需要加入空白的空間,以使得整個結構體對齊,方式為:結構體的總大小是最大對齊數的整數倍。結構體的對齊數是該結構體中的最大對齊數。(我們可以把結構體當成是一個數據類型,這個時候結構體也是有對齊數的,但是這個對齊數不是結構體的大小,而是結構體的最大對齊數的大小)
(5)????數組拆成單個元素進行對齊
?
修改對齊數的大小,在源文件的一開始處輸入:#pragma(4),這條語句的作用是改變系統默認的對齊數,一般往小的改。
四.??作為函數參數的結構
把結構作為參數傳遞給一個函數是合法的,但是這種做法往往是并不適宜的。
這里我們定義一個結構體
typedefstruct
{
…
…
}Transaction;
這個結構體可能很大,我們編寫一個傳遞結構體的一個函數
void print(Transaction trans)
{
…
…
}
如果我們在主函數中定義了一個定義了一個結構體Transaction current_trans;
然后調用函數print(current_trans);我們知道C語言參數傳址調用方式要求把參數的一份拷貝傳遞給函數。如果這個結構體太大,我們必須把很大一個字節復制到堆棧中,以后再丟棄,這樣很影響效率。
再來看下面的函數
void print(Transaction *trans)
{
…
…
}
調用的時候使用
print(¤t_trans);這時我們傳遞的是一個指針,指針傳遞的時候要比直接把結構體傳遞過去小的多,這樣壓棧的效率就高,傳遞指針訪問結構體也是需要付出一定的代價的,就是我們在函數中要使用間接訪問來訪問結構體成員。但這對我們來說影響不大。
使用指針傳遞的時候還有一個缺陷就是,函數內可以對主函數中的結構體進行修改,如果我們想要防止在外部函數修改結構體,我們可以在函數中使用const進行修飾。
還有一點需要主要的是,結構體在傳參的時候不會退化成地址,只會發生原始的拷貝,這也是我們使用指針傳參的一個原因。
綜上我們建議在使用結構體傳遞參數的時候,可以使用地址傳遞的方法。
五.??位段
位段的聲明和任何普通的結構體成員相同,但是有兩個例外。首先位段成員必須聲明為int、signed int或者是unsigned int類型。其次,在成員名的后面是一個冒號和一個整數,這個整數指定該位段所占用的位的數目。
這里建議用signed 或者unsigned進行聲明,因為如果直接聲明為int,它究竟是有符號的還是無符號的整型,這是由編譯器而定的,可能會給我們造成不必要的麻煩。
?這里通過程序來給大家說明一下位段的內存開辟規則,這里是在結構體內存開辟的規則基礎之上的
#include<stdio.h>
#include<windows.h>typedef struct Stu
{char name : 2;char age : 2;char sex : 5;
}Stu;int main()
{Stu student;printf("%d\n", sizeof(student));system("pause");return 0;
}
這里的結果是多少呢,答案是2,這里位段的作用就是想充分的利用內存空間,首先開辟一個字節大小即是八個位,用于存儲name,但是這里name只占用了兩個位,然后age需要占用兩個位,回去看,剛剛開辟的字節還有六個位,于是拿出兩個位給了age,接著是sex,它需要五個位,但是剛剛開辟的字節只有四個位了,不夠五個位,于是又開辟了一個字節,拿出其中的五個位存放sex,第一次開辟的一個字節剩余的四個位就浪費掉了。這里我們再次把結構體給改一下,再來看一下測試的結果是多少呢
typedef struct Stu
{char name : 1;int age : 2;char sex : 5;char address : 3;int add :1;
}Stu;
這里的測試結果是16,為什么呢,這是有原因的對吧,首先開辟一個字節給name,name占用了其中的一位,剩余七個位,然后遇到age,大家請注意age的類型是int型的,雖然只需要用到一個位,但是上面剩余的七個位不能給這個age,于是編譯器開辟了四個字節,大家這里還容易犯的一個錯誤是,認為這四個字節直接在剛剛開辟的一個字節后面開辟,這是錯誤的。正確的方式是,需要對齊,前面開辟了一個字節,這里需要對齊到四,所以跳過了剩下了的三個字節,開辟了四個字節,這樣之后就開辟了八個字節,然后下面的方式一樣,在開辟一個字節用于存放sex和address,然后跳過三個字節,在開辟一個字節用于存放add,這樣的結果就是16個字節,這里大家可以結合上文中的結構體對齊問題好好的探索一下。
提示
注重可移植性的程序應該注意,由于下面的這些實現相關的依賴性,位段在不同的系統中可能有不同的結果。
六.??聯合體
聯合體的所有成員引用的是內存中的相同位置,當我們想在不同的時候把不同的東西存儲在同一個位置時,就可以使用聯合體。聯合體也需要對齊,它的對齊方式要適合所有的成員。
1.??????變體記錄
2.??????聯合的初始化
聯合變量可以被初始化,但是這個初始值必須是聯合第一個成員的類型,而且它必須位于一對花括號里面。例如
union{
int a;
float b;
char c;
} x = {5};
把x.a初始化為5.
我們不能把這個類量初始化為一個浮點值或者字符值。如果給出的初始值是任何其他類型,它就會轉換(如果有可能的話)為一個整數并賦值給x.a。
?
?
?
?
?
?
?
總結
以上是生活随笔為你收集整理的结构和联合--结构体内存和位段内存开辟规则的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《春雪》第二十一句是什么
- 下一篇: 杂记4