自定義類型(一):結構體和位段

結構體:是一些值的集合,這些值稱爲成員變量,每一個成員可以是不同類型的變量。

結構體的聲明:

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、爲什麼存在內存對齊???

  1. 平臺原因(移植原因): 不是所有的硬件平臺都能訪問任意地址上的任意數據的;某些硬件平臺只能在某些地址處取某些特定類型的數據,否則拋出硬件異常。
  2. 性能原因: 數據結構(尤其是棧)應該儘可能地在自然邊界上對齊。
    原因在於,爲了訪問未對齊的內存,處理器需要作兩次內存訪問;而對齊的內存訪問僅需要一次訪問。

2、結構體的對齊規則:

  1. 第一個成員在與結構體變量偏移量爲0的地址處。此處需要注意的是第一個成員也是有對齊數的。
  2. 其他成員變量要對齊到某個數字(對齊數)的整數倍的地址處。 對齊數 = 編譯器默認的一個對齊數(vs下默認爲8,Linux默認爲4) 與 該成員大小的較小值。
  3. 結構體總大小爲最大對齊數的整數倍。
  4. 如果嵌套了結構體的情況,嵌套的結構體對齊到自己的最大對齊數的整數倍處,結構體的整體大小就是所有最大對齊數(含嵌套結構體的對齊數)的整數倍。
  5. 結構體的對齊數就是結構體內部的最大對齊數

練習一下:默認在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;
};
//例如這裏的位段就需要內存對齊

位段的內存分配

  1. 位段的成員可以是 int、unsigned int、 signed int 或者是 char (屬於整形家族)類型
  2. 位段的空間上是按照需要以4個字節( int )或者1個字節( char )的方式來開闢的。
  3. 位段涉及很多不確定因素,位段是不跨平臺的,注重可移植的程序應該避免使用位段。

現在我們來看一看剛剛提出的位段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)

位段的跨平臺問題

  1. int位段被當成有符號數還是無符號數是不確定的。
  2. 位段中最大位的數目不能確定。(16位機器最大16,32位機器最大32,寫成27,在16 位機器會出問題。)
  3. 位段中的成員在內存中從左向右分配,還是從右向左分配標準尚未定義。
  4. 當一個結構包含兩個位段,第二個位段成員比較大,無法容納於第一個位段剩餘的位時,是捨棄剩餘的位還是利用,這是不確定的。

關於位段需要注意的幾點:

  1. 在某些機器上, 位段總是作爲 unsigned 處理, 而不管它們是否被說明成 unsigned 的。
  2. 位段不可標明維數; 即, 不能說明位段數組, 例如 flag:l[2]。
  3. 一個位段必須存儲在同一存儲單元(即字)之中,不能跨兩個單元。如果其單元空間不夠,則剩餘空間不用,從下一個單元起存放該位段。
  4. 可以通過定義長度爲0的位段的方式使下一位段從下一存儲單元開始。
  5. 可以定義無名位段。
  6. 位段的長度不能大於存儲單元的長度,並且大多數C 編譯器都不支持超過一個字長的位段。
  7. 位段無地址,不能對位段進行取地址運算。
  8. 位段可以以%d,%o,%x格式輸出。
  9. 當一個結構位段若出現在表達式中,將被系統自動轉換成整數。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章