目錄
- 1. 結構體類型的聲明
- 1.1 結構的聲明
- 1.2 結構體變量的創建和初始化
- 1.3 結構的特殊聲明---匿名結構體
- 1.4 結構的自引用
- 2.結構體內存對齊(重點!!)
- 2.1 對齊規則
- 2.2 例題講解
- 2.3 為什么存在內存對齊?
- 2.4 修改默認對齊數
- 3. 結構體傳參
1. 結構體類型的聲明
結構是一些值的集合,這些值稱為成員變量。結構的每個成員可以是不同類型的變量。
1.1 結構的聲明
注意:
- 成員列表可以是不同類型的變量;
- 成員后一定要有分號;
- 花括號后也有一個分號。
例如描述一個學生:
struct Stu
{char name[20];//姓名int age;//年齡char tele[12];//電話char sex[3];//性別
};//分號不能丟
注意:上述代碼沒有創建變量,也沒有初始化,只是聲明了一個結構體類型,就像int,float一樣,只是一種類型。
1.2 結構體變量的創建和初始化
接下來我們可以用結構體類型創建變量
方式1:
聲明類型的同時創建,這是全局變量
struct Stu
{char name[20];//姓名int age;//年齡char tele[12];//電話char sex[3];//性別
}s1,s1;
方式2:
我們也可以在函數內部創建局部變量
# include <stdio.h>struct Stu
{char name[20];//姓名int age;//年齡char tele[12];//電話char sex[3];//性別
};int main()
{struct Stu s3;//s3是局部變量return 0;
}
我們再對結構體變量進行初始化:
方式1:直接初始化
struct Point
{int x;int y;
}p1 = { 2,3 };struct Stu
{char name[20];//姓名int age;//年齡}s1 = { "zhangsan",27 };
方式2:也可以再函數內部進行初始化
struct Stu
{char name[20];//姓名int age;//年齡
};int main()
{struct Stu s1 = { "zhangsan",24 };return 0;
}
當然,也有結構體的嵌套:
struct score
{int n;char ch;
};struct Stu
{char name[20];//姓名int age;//年齡struct score s;//嵌套一個結構體變量
};int main()
{struct Stu s1 = { "zhangsan",24 ,{45,'a'} };return 0;
}
我們也可以將其打印出來:
struct score
{int n;char ch;
};struct Stu
{char name[20];//姓名int age;//年齡struct score s;
};int main()
{struct Stu s1 = { "zhangsan",24 ,{45,'a'} };printf("%s %d %d %c\n", s1.name, s1.age, s1.s.n, s1.s.ch);return 0;
}
打印結果:
1.3 結構的特殊聲明—匿名結構體
在聲明結構的時候,可以不完全聲明:
struct
{char name[20];//姓名int age;//年齡char tele[12];//電話char sex[3];//性別
};
上面的結構在聲明時省略了結構體標簽,我們稱為匿名結構體。
注意:匿名結構體只能通過創建全局變量使用一次!!
# include <stdio.h>struct
{char name[20];//姓名int age;//年齡char tele[12];//電話char sex[3];//性別
}s1;int main()
{return 0;
}
1.4 結構的自引用
在結構體中包含一個類型為該結構體本身的成員是否可以呢?比如,定義一個鏈表的結點:
struct Node
{int data;struct Node next;
};
上述代碼正確嗎?如果正確,那么sizeof(struct Node)是多少呢?
仔細分析,其實是不行的,因為?個結構體中再包含?個類型的結構體變量,這樣結構體變量的大小就會無窮的大,是不合理的。
正確的自引用方式:
struct Node
{int data;struct Node *next;
};
這樣我們就把一個結點分成兩部分,一部分存放數據,叫數據域,另一部分存放下一個結點的地址,叫指針域。
在結構體自引用使用的過程中,夾雜了 typedef 對匿名結體類型重命名,也容易引入問題,看看下面的代碼,可行嗎?
typedef struct
{int data;Node* next;
}Node;
這種寫法語法是不支持的。
因為Node是對前面的匿名結構體類型的重命名產生的,但是在匿名結構體內部提前使用Node類型來創建成員變量,這就產生了"先有雞還是先有蛋"的問題,這是不行的。
解決方案如下:定義結構體不要使用匿名結構體了。
typedef struct Node
{int data;struct Node* next;
}Node;
2.結構體內存對齊(重點!!)
上面我們了解了結構體的基本使用。
接下來我們再討論一個問題 :計算結構體的大小。
2.1 對齊規則
首先要知道結構體的對齊規則:
第一個成員在結構體變量偏移量為0的地址處。
其他成員變量要對齊到某個數字(對齊數)的整數倍的地址處。
注意:
(1) 對齊數=編譯器默認的對齊數與該成員大小的較小值
(1)VS中默認值為8(注意:其他編譯器是沒有默認對齊數的,對齊數就是自身大小。)結構體總大小為最大對齊數(每個成員變量都有一個對齊數)的整數倍。
如果嵌套了結構體的情況,嵌套的結構體對齊到自己的最大對齊的整數倍處。
結構體的整體大小就是所有最大對齊數的(含嵌套結構體的對齊數)的整數倍。
2.2 例題講解
例1:
#include <stdio.h>struct S1
{char c1;int i;char c2;
};int main()
{struct S1 s1;printf("%d\n", sizeof(struct S1));return 0;
}
輸出結果:
畫圖解釋:
例2:
#include <stdio.h>struct S2
{char c1;char c2;int i;
};int main()
{struct S2 s1;printf("%d\n", sizeof(struct S2));return 0;
}
輸出結果:
畫圖解釋:
例3:
#include <stdio.h>struct S3
{double d;char c;int i;
};int main()
{struct S3 s3;printf("%d\n", sizeof(struct S3));return 0;
}
輸出結果:
畫圖解釋:
例4:結構體嵌套問題
#include <stdio.h>struct S4
{char c1;struct S3 s3;double d;
};int main()
{struct S4 s4;printf("%d\n", sizeof(struct S4));return 0;
}
輸出結果:
畫圖解釋:
2.3 為什么存在內存對齊?
大部分的參考資料都是這樣說的:
- 平臺原因(移植原因):
不是所有的硬件平臺都能訪問任意地址上的任意數據的;某些硬件平臺只能在某些地址處取某些特定類型的數據,否則拋出硬件異常。 - 性能原因:
數據結構(尤其是棧)應該盡可能地在自然邊界上對齊。原因在于,為了訪問未對齊的內存,處理器需要作兩次內存訪問;而對齊的內存訪問僅需要?次訪問。假設?個處理器總是從內存中取8個字節,則地址必須是8的倍數。如果我們能保證將所有的double類型的數據的地址都對齊成8的倍數,那么就可以用?個內存操作來讀或者寫值了。否則,我們可能需要執行兩次內存訪問,因為對象可能被分放在兩個8字節內存塊中。
總體來說:結構體的內存對齊是拿空間來換取時間的做法。
那在設計結構體的時候,我們既要滿足對齊,又要節省空間,如何做到:
讓占用空間小的成員盡量集中在?起。
如例1和例2,s1和s2的成員一模一樣,但兩者所占空間大小卻不同。
2.4 修改默認對齊數
- #pragma : 預處理指令,可以改變編譯器的默認對齊數。
- 使用時要引用頭文件<stddef.h>
- 注意:一般修改的對齊數都為2的n次方。不會修改為1,3,5……或負數。
例如還是用例1來說明:
#include <stdio.h>
#include <stddef.h>#pragma pack(1)//設置默認對齊數為1
struct S1
{char c1;int i;char c2;
};
#pragma()//取消設置的對齊數,還原為默認int main()
{printf("%d\n", sizeof(struct S1));
}
輸出結果:
修改之前大小是12個字節,修改后變成了6字節。
3. 結構體傳參
方式1:傳值調用。訪問結構體時用點(.)操作符。
#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);
}int main()
{struct S s = { {1,2,3},200 };print1(s); //傳值調用,直接傳變量名return 0;
}
方式2:傳址調用。訪問結構體時用箭頭(->)操作符。
#include <stdio.h>struct S
{int data[1000];int num;
};void print2(const 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},200 };print2(&s); //傳址調用,傳變量名的地址return 0;
輸出結果都為:
上面的兩種方式哪個更好呢?
答案是:傳址調用更好。
原因:
函數傳參的時候,參數是需要壓棧,會有時間和空間上的系統開銷。
如果傳遞?個結構體對象的時候,結構體過大,參數壓棧的的系統開銷比較大,所以會導致性能的下降。
結論:
結構體傳參的時候,要傳結構體的地址。