引言
這是一篇對結構體的詳細介紹,這篇文章對結構體聲明、結構體的自引用、結構體的初始化、結構體的內存分布和對齊規則、庫函數offsetof、以及進行內存對齊的原因、如何修改默認對齊數、結構體傳參進行介紹和說明。
?
? ? ? ? ? ? ? ???豬巴戒:個人主頁?
???????????????所屬專欄:《C語言進階》
? ? ? ? 🎈跟著豬巴戒,一起學習C語言🎈
目錄
引言
結構體的聲明
結構體的基礎
結構的聲明
匿名結構體類型
結構體的自引用
typedef作用于結構體的問題
?結構體變量的定義和初始化
多個元素的初始化要用大括號{ }
結構體的內存對齊
1.對齊規則
1.例子
2.例子
?3.例子
??????????
4.例子
offsetof
offsetof的使用
??編輯
?為什么要存在內存對齊
修改默認對齊數
?結構體傳參
結構體的聲明
??
結構體的基礎
結構是一些值的集合,這些值被稱為成員變量。結構的每個成員可以是不同類型的變量。
在一個變量中,要存放性別、年齡、成績、地址多種類型的數據時,C語言允許用戶自己建立由不同類型數據組成的組合型的數據結構,它稱為結構體。
? ??
結構的聲明
結構體是怎么聲明的呢?
struct tag
{member_list;
}variable_list; //分號不能丟struct Student
{//學生的相關信息char name[20];int age;
}s1,s2;
- tag,Student是結構體名
- member_list是成員表列
- struct是聲明結構體類型是必須使用的關鍵字,不能省略
- s1,s2變量就是學生變量。
- { }后面要記得把“ ;”帶上
struct tag就是一個結構體類型,我們可以根據自己的需要建立結構體類型,struct Teacher,struct Student等結構體類型,各自包含不同的成員。
如果將s1,s2放在main函數的外面,那么s1,s2就是全局變量。
struct Student
{//學生的相關信息char name[20];int age;
}s1,s2;int main()
{return 0;
}
????????
匿名結構體類型
結構體在聲明的時候省略了結構體標簽(tag),沒有名字的結構體類型只能使用一次,被稱為匿名結構體類型。
由于沒有名字,編譯器會把下面的兩個代碼當成完全不同的兩個類型。
所以,p = &x.
會因為類型不同報錯。
struct
{char name[20];int age;
}s1;struct
{char name[20];int age;
}a[20],*p;
????????
結構體的自引用
結構體的自引用用到數據結構中的鏈表。
數據結構中有順序表、鏈表的概念,
順序表
數據在內存中是順序排放的,可以逐個根據地址找到下一個數據。
鏈表
數據在內存中的存放是沒有規律的但是存放數據,會分為兩個部分,
一個部分叫數據域,存放有效數據,
另一個部分叫指針域,用來存放下一個數據的地址,可以通過地址直接找到下一個數據。
我們通過鏈表就可以實現結構體的自引用。
struct Node
{int data;struct Node* next;
};
????????
typedef作用于結構體的問題
下面在結構體自引用使用的改成中,夾雜了typedef對匿名結構體類型重命名,看看下面的代碼,有沒有問題?
typedef struct Node
{int data;Node* next;
}Node;
答案是不行的,因為Node是對前面的匿名結構體類型的重命名產生的,但是在匿名結構體內部提前使用Node類型來創建成員變量,這是不行的。
typedef struct Node
{int data;struct Node* next;
}Node;
??????????
?結構體變量的定義和初始化
struct Point是結構體類型,它相當于一個模型,是沒有占據具體空間的,
當我們建立結構體變量p1,它相當于具體的房屋,在內存中儲存數據。
struct Point
{int x;int y;
}p1 = { 2,3 };
????????
多個元素的初始化要用大括號{ }
在結構體中,如果存在多個元素的變量,我們初始化時要使用大括號。
像數組一樣,arr[] = { 0, 1, 2, 3, 4 };
- 打印結構體,s1是struct Stu的變量,name是s1的成員變量,用s1.name表示s1結構體的name變量
- s是struct Stu中的成員變量,用s1.s.n表示在結構體struct score的成員變量n。
struct score
{int n;char ch;
};
struct Stu
{char name[20];int age;struct score s;
};int main()
{struct Stu s1 = { "zhangsan",20,{100,'q' } };printf("%s %d %d %c\n", s1.name, s1.age, s1.s.n, s1.s.ch);return 0;
}
????????
結構體的內存對齊
如何計算結構體的大小?
結構體的內存分布是怎樣的?
????????
1.對齊規則
首先掌握結構體的對齊規則
1. 結構體的第?個成員對?到和結構體變量起始位置偏移量為0的地址處
2. 其他成員變量要對?到某個數字(對?數)的整數倍的地址處。
對?數 = 編譯器默認的?個對?數 與 該成員變量??的較?值。
- VS 中默認的值為 8
- Linux中 gcc 沒有默認對?數,對?數就是成員??的??
3. 結構體總??為最?對?數(結構體中每個成員變量都有?個對?數,所有對?數中最?的)的整數倍。
4. 如果嵌套了結構體的情況,嵌套的結構體成員對?到??的成員中最?對?數的整數倍處,結構體的整體??就是所有最?對?數(含嵌套結構體中成員的對?數)的整數倍。
??????????
只是文字的說明,免不了晦澀難懂,接下來用例子來給大家講解
1.例子
#include <stdoi.h>
struct S1
{char c1;int i;char c2;
};
int main()
{printf("%d\n", sizeof(struct S1));return 0;
}
解析:?
右邊表示的是偏移量,
1.第一個成員char c1要對齊到和結構體變量起始位置偏移量為0的地址處,占一個字節
2.其他成員要對齊到對齊數的整數倍的地址處
對?數 = 編譯器默認的?個對?數 與 該成員變量??的較?值。
VS中的默認對齊數是8.
? int i的大小是4個字節,對齊數就是4。int i 的地址要對齊到為偏移量整數倍的地址,也就是4的整數倍,偏移量為4的地址。int i 是4個字節,那占據的地址偏移量為4~7
char c2 的大小是1個字節,對齊數是1。1可以為任意偏移量的整數倍。所以char c2的地址的偏移量就是8.
3.結構體的大小為最大對齊數(結構體中每個成員變量都有一個對齊數,所有對齊數中最大的)的整數倍
成員變量有char c1,int i ,char c2。它們的對齊數分別是1,4,1。因此最大對齊數為4。
結構體總大小為最大對齊數的整數倍,現在偏移量是0~8,一共是9個字節,要湊成4的整數倍,就是12個字節,在浪費3個字節就可以了,地址偏移量9~11一共是3個字節。
這個結構體的內存就儲存在偏移量為0~11的空間。
??????????
2.例子
#include<stdio.h>
struct S2
{char c1;char c2;int i;
};
int main()
{printf("%d\n",sizeof(struct S2));return 0;
}
?
??????????
解析:? ?
右邊表示的是偏移量,
1.第一個成員char c1要對齊到和結構體變量起始位置偏移量為0的地址處,占一個字節
2.其他成員要對齊到對齊數的整數倍的地址處
對?數 = 編譯器默認的?個對?數 與 該成員變量??的較?值。
VS中的默認對齊數是8.
char c1? 的大小是1個字節,對齊數就是1。char c1的地址要對齊到為偏移量整數倍的地址,也就是1的整數倍,偏移量為1的地址。
int i 的大小是4個字節,對齊數是4。int i 的地址就要移到偏移量為4的倍數的地址。所以int i 的地址的偏移量就是4.int i 是4個字節,那占據的地址偏移量為4~7
3.結構體的大小為最大對齊數(結構體中每個成員變量都有一個對齊數,所有對齊數中最大的)的整數倍
成員變量有char c1,int i ,char c2。它們的對齊數分別是1,4,1。因此最大對齊數為4。
結構體總大小為最大對齊數的整數倍,現在偏移量是0~7,剛好是8個字節,是4的倍數。
這個結構體的內存就儲存在偏移量為0~7的空間。
????????
?3.例子
#include<stdio.h>
struct S3
{double d;char c;int i;
};
int main()
{printf("%d\n",sizeof(struct S3));return 0;
}
解析:?
1.第一個成員要對齊到結構體變量起始位置偏移量為0的地址處,double d占8個字節,所以占據的內存空間是偏移量為0~7的地址
2.其他成員要對齊到對齊數的整數倍的地址處
char c的大小是1個字節,任意偏移量都可以為1的整數倍,所以char c的地址是下一位,偏移量為8的地址。
int i 的大小是4個字節,要對齊到偏移量為4的倍數的地址,也就是偏移量為12,int i 占據的內存空間為偏移量為12~15的地址。
3.結構體的大小為最大對齊數的整數倍。
最大對齊數是double的對齊數,也就是8。現在的結構體占16個字節(偏移量為0~15),剛好是8的倍數。
??????????
4.例子
這個例子包括了嵌套結構體的情況,嵌套的結構體成員對?到??的成員中最?對?數的整數倍處,結構體的整體??就是所有最?對?數(含嵌套結構體中成員的對?數)的整數倍。
#include<stdio.h>
struct S3
{double d;char c;int i;
};
struct S4
{char c1;struct S3 s3;double d;
};
int main()
{printf("%d\n",sizeof(struct S4));return 0;
}
?
解析:?
1.第一個成員要對齊到結構體變量起始位置偏移量為0的地址處,char c1占1個字節,占據偏移量為0的空間。
2.嵌套的結構體成員對?到??的成員中最?對?數的整數倍處,結構體的整體??就是所有最?對?數(含嵌套結構體中成員的對?數)的整數倍。
接下來是struct s3,要對齊自己成員的最大對齊數,double d的對齊數為8個字節,對齊到偏移量為8的地址,
3.其他成員要對齊到對齊數的整數倍的地址處,嵌套的結構體成員也是這樣,double d占據8個字節,占據偏移量為8~15的地址。
char c對齊偏移量16,占據一個字節。
int i 的對齊數為4,對齊偏移量為20,占據4個字節,就是偏移量為20~23的空間。
struct S3整理完,繼續到struct S4,輪到double d
double d的對齊數為8,對齊偏移量24,占據8個字節,占據空間偏移量為24~31。
4.結構體的大小為最大對齊數的整數倍。
當前空間一共是32個字節(0~31),結構體struct S4,struct S3中的成員的最大對齊數是8。因此結構體的大小要是最大對齊數的整數倍。32剛好是8的整數倍。
??????????
offsetof
返回成員的偏移量 ,頭文件<stddef.h>
offsetof (type,member)
offsetof的使用
type是類型,
#include <stdio.h>
#include <stddef.h>
struct S1
{char c1;int i;char c2;
};
int main()
{printf("%d\n", offsetof(struct S1, c1));printf("%d\n", offsetof(struct S1, i));printf("%d\n", offsetof(struct S1, c2));return 0;
}
?
??????????
?為什么要存在內存對齊
總體來說:結構體的內存對?是拿空間來換取時間的做法。
?以32為機器為例,32位機器一次可以訪問32位比特位的數據,
如果沒有對齊規則,就像左邊,機器要訪問兩次才可以得到 int i 的值,
有對齊規則,就像右邊,想要訪問 i ,只需要訪問一次就足夠了。
對齊規則的思想:把數據放在機器可以一次訪問得到數據的空間內,使訪問更具效率。?
? ?
修改默認對齊數
當結構體的對齊方式不適合時,我們也可以修改默認對齊數。
- 在括號填寫數字,對默認對齊數進行修改。
- 如果()內沒有數字,則時將默認對齊數恢復到默認值。
#pragma pack()
下面的struct S原本是占據12個字節的空間,對默認對齊數進行修改后,只占據6個字節的空間。?
#include <stdio.h>
#pragma pack(1)//設置默認對?數為1
struct S
{char c1;int i;char c2;
};
#pragma pack()//取消設置的對?數,還原為默認
int main()
{//輸出的結果是什么?printf("%d\n", sizeof(struct S));return 0;
}

? ?????????
?結構體傳參
- 傳值調用,將數據通過參數傳過去,然后函數print會創立獨立的空間,對傳過來的數據進行存儲
- 傳址調用,將數據的地址傳過去,函數通過指向數據的地址對數據進行使用,不需要再建立空間對數據進行存放。
#include<stdio.h>
struct S
{int data[1000];int num;
};
void print1(struct S ss)
{int i = 0;for (i = 0; i < 3; i++){printf("%d ", ss.data[i]);}printf("%d\n", ss.num);
}
void print2(struct S* ps)
{int i = 0;for (i = 0; i < 3; i++){printf("%d ", ps->data[i]);}printf("%d\n", ps->num);
}
int main()
{struct S s = { {1,2,3},100 };print1(s);print2(&s);return 0;
}
????????
上面的傳值調用print1?和 傳址調用print2 函數那哪個更好?
原因:函數傳參的時候,參數是需要壓棧,會有時間和空間上的系統開銷。如果傳遞?個結構體對象的時候,結構體過?,參數壓棧的的系統開銷?較?,所以會導致性能的下降。