結構體
結構是一些值的集合,這些值被稱爲成員變量。結構的每個成員可以是不同類型的變量。
-
結構的聲明
struct tag
{
member-list;
}variable-list;
//描述一個學生
struct Stu
{
//成員變量、成員列表
char name[20];//名字
int age;//年齡
char sex[5];//性別
char id[20];//學號
};//分號不能丟
特殊的聲明
在聲明結構的時候,可以不完全的聲明
比如:
匿名結構體類型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
上面的兩個結構體在聲明的時候省略掉了結構體標籤(tag)。
在上面代碼的基礎上,下面的代碼合法嗎?
p = &x;
警告:
編譯器會把上面的兩個聲明當成兩個完全不同的兩個類型。
所以是非法的
-
結構的成員
結構的成員可以是標量、數組、指針,甚至是其他的結構體。
結構體成員的訪問
1.結構體變量的成員是通過點操作(.)訪問的。點操作符接受兩個操作數
例如:
我們看到s有成員name和age;
訪問s的成員
struct S s;
strcpy(s.name, "zhangsan");//使用.訪問name成員
s.age = 20;//使用.訪問age成員
結構體訪問指向變量的成員,有時候我們得到的不是一個結構體變量,而是指向一個結構體的指針,那該如何訪問成員,如下:
struct S
{
char name[20];
int age;
}s;
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);
}
-
結構的自引用
在結構體中包含一個類型爲該結構本身的成員
struct Node
{
int data;
struct Node* next;
};
注意:
typedef struct Node
{
int data;
struct Node* next;
}Node;
- 結構體的不完整聲明
struct B;
struct A
{
int _a;
struct B* pb;
};
struct B
{
int _b;
struct A* pa;
};
-
結構體變量的定義和初始化
struct Node
{
int data;
struct Node* next;
};
注意:
typedef struct Node
{
int data;
struct Node* next;
}Node;
結構體的不完整聲明
struct B;
struct A
{
int _a;
struct B* pb;
};
struct B
{
int _b;
struct A* pa;
};
結構體變量的定義和初始化
struct Point
{
int x;
int y;
}p1; //聲明類型的同時定義變量p1
struct Point p2; //定義結構體變量p2
//初始化:定義變量的同時賦值。
struct Point p3 = { x, y };
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 };//結構體嵌套初始化
-
結構體內存對齊
//練習1
struct S1
{
char c1;
int i;
char c2;
};
printf("%d\n", sizeof(struct S1));
//練習2
struct S2
{
char c1;
char c2;
int i;
};
printf("%d\n", sizeof(struct S2));
//練習3
struct S3
{
double d;
char c;
int i;
};
printf("%d\n", sizeof(struct S3));
//練習4-結構體嵌套問題
struct S4
{
char c1;
struct S3 s3;
double d;
};
printf("%d\n", sizeof(struct S4));
如何計算
首先掌握結構體的對齊規則:
1.第一個成員在與結構體變量偏移量爲0的地址處。
2.其他成員變量要對齊到某個數字(對齊數)的整數倍的地址處。對齊數=編譯器默認的對齊數與該成員大小的較小值。 VS中默認的值是8 Linux中的默認值爲4
3.結構體總大小爲其最大對齊數(每個成員變量都有一個對齊數)的整數倍。
4.如果嵌套了結構體的情況,嵌套的結構體對齊到自己的最大對齊數的整數倍處,結構體的整體大小就是所有最大對齊數(含嵌套結構體的對齊數)的整數倍。
爲什麼存在內存對齊?
大部分參考資料都如下所說:
1.平臺原因(移植原因):
不是所有的硬件平臺都能訪問任意地址上的任意數據的;某些硬件平臺只能在某些地址處取某些特定類型的數據,否則拋出硬件異常。
2.性能原因:
數據結構(尤其是棧)應該儘可能地在自然邊上對齊。
原因在於,爲了訪問未對齊的內存,處理器需要作兩次內存訪問;而對齊的內存訪問僅需要一次訪問。
- 總體來說:
結構體的內存對齊是拿空間來換取時間的做法。
那在設計結構體的時候,我們既要滿足對齊,又要節省空間,如何做到:
讓佔用空間小的成員儘量集中在一起。
//例如:
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
S1和S2類型的成員一樣,但是S1和S2所佔用空間的大小有了一些區別。
修改默認對齊數
#include <stdio.h>
#include <stdlib.h>
#pragma pack(8) //設置默認對齊數爲8
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack() //取消設置的默認對齊數,還原爲默認
#pragma pack(1) //設置默認對齊數爲8
struct S2
{
char c1;
int i;
char c2;
};
#pragma pack() //取消設置的默認對齊數,還原爲默認
int main()
{
//輸出的結果是什麼?
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
system("pause");
return 0;
}
-
結構體傳參
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.位段的成員必須是int、unsigned int或signed int 。
2.位段的成員名後邊有一個冒號和一個數字。
比如:
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
A就是一個位段類型
那位段A的大小是多少呢?
#include <stdio.h>
#include <stdlib.h>
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
int main()
{
printf("%d\n", sizeof(struct A));
system("pause");
return 0;
}
位段的內存分配
1.位段的成員可以是int、unsigned int、signed int 或者是char(屬於整形家族)類型
2.位段的空間,上是按照需要以4個字節(int)或者1個字節(char) 的方式來開闢的。
3.位段涉及很多不確定因素,位段是不跨平臺的,注重可移植的程序應該避免使用位段。
位段的跨平臺問題:
1. int位段被當成有符號數還是無符號數是不確定的。
2.位段中最大位的數目不能確定。(16位機器 最大16,32位機器最大32,寫成27, 在16位機器會出問題。)
3.位段中的成員在內存中從左向右分配,還是從右向左分配標準尚未定義。
4.當一個結構包含兩個位段,第二個位段成員比較大,無法容納於第 - 個位段剩餘的位時,是捨棄剩餘的位還是利用,這是不確定的。
- 總結:
跟結構相比,位段可以達到同樣的效果,可以很好的節省空間,但是有跨平臺的問題存在。
枚舉
枚舉顧名思義就是一一例舉。
把可能的取值一一例舉。
-
枚舉類型的定義
enum Day//星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性別
{
MALE,
FEMALE,
SECRET
};
enum Color//顏色
{
RED,
GREEN,
BLUE
};
以上定義的enum Day , enum Sex, enum Color都是枚舉類型。
{}中的內容是枚舉類型的可能取值,也叫枚舉常量。這些可能取值都是有值的,默認從0開始,一次遞增1, 當然在定義的時候也可以賦初值。
例如 :
enum Color//顏色
{
RED = 1,
GREEN = 2,
BLUE = 4
};
枚舉的優點
1.增加代碼的可讀性和可維護性
2.和#define定義的標識符比較枚舉有類型檢查,更加嚴謹。
3.防止了命名污染(封裝)
4.便於調試
5.使用方便,一次可以定義多個常量
枚舉的使用
enum Color//顏色
{
RED = 1,
GREEN = 2,
BLUE = 4
};
enum Color clr = GREEN;//只能拿枚舉常量給枚舉變量賦值,纔不會出現類型的差異。
clr = 5;
聯合(共用體)
聯合類型的定義
聯合也是一種特殊的自定義類型
這種類型定義的變量也包含一系列的成員,特徵是這些成員公用同一塊空間(所以聯合也叫共用體)。
比如:
//聯合類型的聲明
union Un
{
char c;
int i;
};
//聯合變量的定義
union Un un;
//計算連個變量的大小
printf("%d\n", sizeof(un));
聯合的特點
聯合的成員是共用同一塊內存空間的,這樣一個聯合變量的大小,至少是最大成員的大小(因爲聯合至少得有能力保存最大的那個成員)。
-
判斷當前計算機的大小端存儲:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a = 1;
char* p = (char*)&a;
if (*p == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
system("pause");
return 0;
}
#include <stdio.h>
#include <stdlib.h>
int CheckSys()
{
int a = 1;
return *(char *)&a;
}
int main()
{
if (CheckSys() == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
system("pause");
return 0;
}
聯合大小的計算
聯合的大小至少是最大成員的大小
當最大成員的大小不是最大對齊數的整數倍的時候,就要對齊到最大對齊數的整數倍
例如:
#include <stdio.h>
#include <stdlib.h>
union Un1
{
char c[5];
int i;
};
union Un2
{
short c[7];
int i;
};
int main()
{
//下面輸出的結果是什麼?
printf("%d\n", sizeof(union Un1));
printf("%d\n", sizeof(union Un2));
system("pause");
return 0;
}
聯合和結構體的巧妙使用:
#include <stdio.h>
#include <stdlib.h>
union ip_addr
{
unsigned long addr;
struct
{
unsigned char c1;
unsigned char c2;
unsigned char c3;
unsigned char c4;
}ip;
};
int main()
{
union ip_addr my_ip;
my_ip.addr = 176238749;
printf("%d.%d.%d.%d\n", my_ip.ip.c4, my_ip.ip.c3, my_ip.ip.c2, my_ip.ip.c1);
system("pause");
return 0;
}