結構體:是一些值的集合,這些值稱爲成員變量,每一個成員可以是不同類型的變量。
結構體的聲明:
struct tag
{
member_list;
}variable_list;
//例如使用結構體來描述一個學生
struct Stu
{
char name[20];//名字
int age;//年齡
char id[20];//學號
};//分號不能丟
在聲明結構體的時候可以不完全的聲明。(在C語言中,結構體不能爲空)
例如:
//匿名結構體類型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
//這裏的a[20],是一個有20個元素的數組
//數組中的每一個元素就是一個結構體
//以上這兩個結構體在聲明的時候省略了結構體標籤(tag)。
一個問題:p = &x是否合法???
編譯器會把上面的兩個聲明當成完全不同的兩個類型,所以是非法的。
再來看一看:p = &a;
p是一個結構體指針,而&a得到的是數組的地址,因此該表達式不合法
再來看:p = a;
p是一個結構體指針,而a代表的是數組a的首元素的地址,剛好這裏的數組a是包含20個結構體元素的數組,所以p指向了數組的首元素(是一個結構體),因此該表達式合法。
結構體的成員
結構體的成員可以是標量(整形,double型等),數組,指針,甚至是其他結構體。
結構體成員的訪問
1、結構體變量訪問成員,是通過點操作符‘ · ’訪問的,點操作符要接受兩個操作數。
struct Stu
{
char name[20];
int age;
};
struct Stu s;//定義一個結構體變量s
//訪問
strcpy(s.name,"wangtao");//使用點操作符訪問name成員
s.age = 20;//訪問age成員
2、結構體訪問指向變量的成員,有時候我們得到的不是一個結構體變量,而是指向一個結構體的指針。此時我們使用‘->’來訪問成員變量。
void print(struct S* ps)
{
printf("name = %s,age = %d\n", (*ps).name, (*ps).age);
printf("name = %s,age = %d\n", ps->name, ps->age);
}
結構體的自引用
在結構中包含一個類型爲該結構本身的成員是否可以呢?
//代碼1
struct Node
{
int data;
struct Node next;
};
//可行否?(不行)
//如果可以,那sizeof(struct Node)是多少?(無法得出結論)
正確的自引用方式:
//代碼2
struct Node
{
int data;
struct Node* next;
};
注意:
//代碼3
typedef struct
{
int data;
Node* next;
}Node;
//這樣寫代碼,是不可行的
//解決方案:前面加上typedef(類型重定義關鍵字)
typedef struct Node
{
int data;
struct Node* next;
}Node;
結構體的不完整聲明
struct A
{
int _a;
struct B* pb;
};
struct B
{
int _b;
struct A* pa;
};
//注意這樣的代碼時行不通的
//解決方案:結構體互相包含的情況推薦使用結構體的不完整聲明(提前聲明)
struct B; //提前聲明
struct A
{
int _a;
struct B* pb;
};
struct B
{
int _b;
struct A* pa;
};
結構體變量的定義和初始化
1、定義結構體變量
struct Point
{
int x;
int y;
}p1;//聲明一個結構體時順便定義了一個結構體變量p1
struct Point p2;//定義結構體變量p2
2、結構體變量的初始化
//初始化:定義變量的同時賦初值。
struct Point p3 = {3, 6};
struct Stu//類型聲明
{
char name[15];//名字
int age; //年齡
};
struct Stu s = {"zhangsan", 20};//初始化
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = {10, {4,5}, NULL};//結構體嵌套初始化
//聲明一個結構體時順便定義一個變量並且初始化
//該結構體中有變量值結構體變量,初始化時也對其進行初始化
//這叫做結構體嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//結構體嵌套初始化
注意:結構體只允許整體初始化,不允許整體賦值。
結構體傳參
首先說明一點:結構體傳參不能傳結構體變量,而是選擇指針。所有的結構體傳參都統一採用指針傳參。
struct S
{
int data[1000];
int num;
};
struct S s = {{1,2,3,4}, 1000};
//結構體傳參(不會發生降級問題)
void print1(struct S s)
{
printf("%d\n",s.num);
}
//結構體地址傳參
void print2(struct S *ps)
{
printf("%d\n",ps->num);
}
int main()
{
print1(s); //傳結構體
print2(&s); //傳地址
return 0;
}
//print1函數和print2函數哪一個好些呢???
//答案是print2函數
//因爲函數在進行傳參的時候,參數是需要壓棧的。
//如果傳遞一個結構體對象的時候,該結構體過大
//那麼參數壓棧的系統開銷就會比較大,所以就會導致性能的下降
再次強調:結構體傳參的時候,要傳結構體的指針
結構體內存對齊(重點)
1、爲什麼存在內存對齊???
- 平臺原因(移植原因): 不是所有的硬件平臺都能訪問任意地址上的任意數據的;某些硬件平臺只能在某些地址處取某些特定類型的數據,否則拋出硬件異常。
- 性能原因: 數據結構(尤其是棧)應該儘可能地在自然邊界上對齊。
原因在於,爲了訪問未對齊的內存,處理器需要作兩次內存訪問;而對齊的內存訪問僅需要一次訪問。
2、結構體的對齊規則:
- 第一個成員在與結構體變量偏移量爲0的地址處。此處需要注意的是第一個成員也是有對齊數的。
- 其他成員變量要對齊到某個數字(對齊數)的整數倍的地址處。 對齊數 = 編譯器默認的一個對齊數(vs下默認爲8,Linux默認爲4) 與 該成員大小的較小值。
- 結構體總大小爲最大對齊數的整數倍。
- 如果嵌套了結構體的情況,嵌套的結構體對齊到自己的最大對齊數的整數倍處,結構體的整體大小就是所有最大對齊數(含嵌套結構體的對齊數)的整數倍。
- 結構體的對齊數就是結構體內部的最大對齊數
練習一下:默認在Linux環境下,32位平臺
//練習1
struct S1
{
char c1;
int i;
char c2;
};
printf("%d\n", sizeof(struct S1));
//結果爲:12
//解析:有規則1,所以第一個成員變量c1在偏移量爲0的地址處。
//i在32位平臺上爲大小爲4字節,編譯器默認對齊數爲4,
//所以對齊到4(對齊數)的整數倍的地址處,即偏移量爲4的地址處,
//佔據4 、5 、6 、7這4個地址。
//c2在32位平臺上爲大小爲1字節,編譯器默認對齊數爲4,
//所以對齊到1(對齊數)的整數倍的地址處,即偏移量爲8的地址處。
//總共佔據0~8 共9個地址被佔據
//有規則3,結構體總大小爲最大對齊數的整數倍。
//最大對齊數爲4,因此該結構體的大小就爲12
//練習2
struct S2
{
char c1;
char c2;
int i;
};
printf("%d\n", sizeof(struct S2));
//結果爲:8
//解析:有規則1,所以第一個成員變量c1在偏移量爲0的地址處。
//c2在32位平臺上爲大小爲1字節,編譯器默認對齊數爲4,
//所以對齊到1(對齊數)的整數倍的地址處,即偏移量爲1的地址處。
//i在32位平臺上爲大小爲4字節,編譯器默認對齊數爲4,
//所以對齊到4(對齊數)的整數倍的地址處,即偏移量爲4的地址處,
//佔據4 、5 、6 、7這4個地址。
//總共佔據0~7 共8個地址被佔據
//有規則3,結構體總大小爲最大對齊數的整數倍。
//最大對齊數爲4,因此該結構體的大小就爲8
//練習3
struct S3
{
double d;
char c;
int i;
};
printf("%d\n", sizeof(struct S3));
//結果爲:16
//解析:有規則1,所以第一個成員變量d在偏移量爲0的地址處。
//double類型的數據在32爲平臺下大小爲8個字節,佔據0~7共佔據8個地址
//第二個成員c大小爲1個字節,編譯器默認對齊數爲4,
//所以對齊到1(對齊數)的整數倍的地址處,即偏移量爲8的地址處,
//i在32位平臺上爲大小爲4字節,編譯器默認對齊數爲4,
//所以對齊到4(對齊數)的整數倍的地址處,即偏移量爲12的地址處,
//佔據12 、13 、14 、15這4個地址。
//總共佔據0~15 共16個地址被佔據
//有規則3,結構體總大小爲最大對齊數的整數倍。
//最大對齊數爲8,因此該結構體的大小就爲16
//練習4-結構體嵌套問題
struct S4
{
char c1;
struct S3 s3;
double d;
};
printf("%d\n", sizeof(struct S4));
//結果爲:24
//解析:有規則1,所以第一個成員變量d在偏移量爲0的地址處。
//第二個成員是一個結構體大小爲16個字節,
//有規則4和規則5,所以第二個成員(結構體)對齊到4的整數倍的地址處,
//即偏移量爲4的地址處,佔據4~13共佔據16個地址。
//double類型的數據在32爲平臺下大小爲8個字節,編譯器默認對齊數爲4,
//所以對齊到4(對齊數)的整數倍的地址處,即偏移量爲16的地址處,
//佔據16~23共佔據8個地址。
//有規則3,結構體總大小爲最大對齊數的整數倍。
//最大對齊數爲4,因此該結構體的大小就爲24
學習完結構體,我們也順帶了解一下位段。
位段
位段是通過結構體實現的,所以位段的聲明和結構體類似,其中有兩個不同:
1.位段的成員必須是 int 、unsigned int和signed int。
2.位段的成員名後邊有一個冒號和一個數字。
比如:
struct A
{
int a:2;
int b:5;
int c:10;
int d:30;
};
//這裏的A就是一個位段類型
前面我們講解了結構體,結構體中的一大重點就是結構體的內存分配。那麼問題來了,通過結構體實現的位段是否也存在內存對齊的問題呢???答案是一般情況下不需要內存對齊的,因爲位段的內部成員都是一樣的類型,但是在位段與常規的結構體混用的情況下是需要雷村對齊的。那麼,上面的位段A的大小又是多少呢???(答案是:8)(看圖文詳細講解)
struct A
{
int a:2;
int b:5;
int c:10;
int d;
};
//例如這裏的位段就需要內存對齊
位段的內存分配
- 位段的成員可以是 int、unsigned int、 signed int 或者是 char (屬於整形家族)類型
- 位段的空間上是按照需要以4個字節( int )或者1個字節( char )的方式來開闢的。
- 位段涉及很多不確定因素,位段是不跨平臺的,注重可移植的程序應該避免使用位段。
現在我們來看一看剛剛提出的位段A的大小時多少的問題:
位段數據的引用
同結構體成員中的數據引用一樣,但應注意位段的最大取值範圍不要超出二進制位數定的範圍,否則超出部分會丟棄,不會影響到其他元素。
例如:
struct Data
{
int a:2;
int b:5;
int :10; //無名位段
int d:30;
};
struct Data data;
data.a=2;
data.a=10;//超出範圍(a佔2位,最大隻能到3)
位段的跨平臺問題
- int位段被當成有符號數還是無符號數是不確定的。
- 位段中最大位的數目不能確定。(16位機器最大16,32位機器最大32,寫成27,在16 位機器會出問題。)
- 位段中的成員在內存中從左向右分配,還是從右向左分配標準尚未定義。
- 當一個結構包含兩個位段,第二個位段成員比較大,無法容納於第一個位段剩餘的位時,是捨棄剩餘的位還是利用,這是不確定的。
關於位段需要注意的幾點:
- 在某些機器上, 位段總是作爲 unsigned 處理, 而不管它們是否被說明成 unsigned 的。
- 位段不可標明維數; 即, 不能說明位段數組, 例如 flag:l[2]。
- 一個位段必須存儲在同一存儲單元(即字)之中,不能跨兩個單元。如果其單元空間不夠,則剩餘空間不用,從下一個單元起存放該位段。
- 可以通過定義長度爲0的位段的方式使下一位段從下一存儲單元開始。
- 可以定義無名位段。
- 位段的長度不能大於存儲單元的長度,並且大多數C 編譯器都不支持超過一個字長的位段。
- 位段無地址,不能對位段進行取地址運算。
- 位段可以以%d,%o,%x格式輸出。
- 當一個結構位段若出現在表達式中,將被系統自動轉換成整數。