結構體的宣告,定義及其初始化,C語言結構體完全攻略

2020-07-16 10:04:23
結構體很重要,初學者一定要掌握。比如儲存一個班級學生的資訊,肯定包括姓名、學號、性別、年齡、成績、家庭地址等項。這些項都是具有內在聯絡的,它們是一個整體,都表示同一個學生的資訊。但如果將它們定義成相互獨立的變數的話,就無法反映它們的內在聯絡:
char name[20];   //姓名
int num;         //學號
char sex;        //性別
int age;         //年齡
float score;     //成績
char addr[30];   //家庭住址
而且問題是這樣寫的話,只是定義了一個學生,如果要定義第二個學生就要再寫一遍。這樣不僅麻煩,而且很容易混淆。要是能定義一個變數,而且這個變數正好包含這六個項,即將它們合併成一個整體的話就好了。

結構體就是為了解決這個問題而產生的。結構體是將不同型別的資料按照一定的功能需求進行整體封裝,封裝的資料型別與大小均可以由使用者指定。

之前講的那些基本資料型別只能滿足一些基本的要求,只能表示具有單一特性的簡單事物。但是對於一些有很多特性的複雜事物,每一個特性就是一個基本型別。這個複雜的事物是由很多基本型別組合在一起而生成的一個比較複雜的型別。這時就需要運用結構體。

宣告結構體型別

宣告一個結構體型別的一般形式為:

struct結構體名
{
    成員列表
};

比如將學生的資訊定義成結構體:
struct STUDENT
{
char name[20];
int num;
char sex;
int age;
float score;
char addr[30];
};  //最後的分號千萬不能省略
說明:
1) 最後的分號千萬不能省略。為了防止最後忘記分號,最好先將框架寫出來,寫的時候直接把分號加上:

struct STUDENT
{};

然後再將大括號開啟:

struct STUDENT
{
};

再在裡面書寫內容。

2) 結構體型別是由一些基本資料型別組合而成的新的資料型別。因為結構體型別中的成員是由程式設計師人為定義的,所以結構體型別是由我們人為定義的資料型別。

3) struct 是宣告結構體型別時必須使用的關鍵字,不能省略。“結構體”這個詞是根據英文單詞 structure 譯出的。

4) struct STUDENT 是定義的資料型別的名字,它向編譯系統宣告這是一個“結構體型別”,包括 name、num、sex、age、score、addr 等不同型別的項。

5) struct STUDENT 與系統提供的 int、char、float、double 等標準型別名一樣,都是資料型別,具有同樣的作用,都是用來定義變數的。

但結構體型別和系統提供的標準型別又有所不同:“結構體型別”不僅要求指定該型別為“結構體型別”,即 struct,而且要求指定該型別為某一“特定的”結構體型別,即“結構體名”。因為只有 struct 才是關鍵字,而“結構體名”是由程式設計人員自己命名的。所以說,“結構體型別”不是由系統提供的,而是由程式設計人員自己指定的。

這也就意味著,根據“結構體名”的不同,可以定義無數種“具體的”、“特定的”結構體型別。所以結構體型別並非是固定的一種型別。而 int 型、char 型、float 型、double 型都是固定的型別。

6) “結構體名”的命名規範是全部使用大寫字母。

7) “結構體名”是結構體型別的標誌。花括號內是該結構體的各個成員,它們共同組成一個整體。對各個成員都要進行型別宣告,如:
char name[20];
int num;
char sex;
int age;
float score;
char addr[30];
成員名的命名規則與變數名相同。

8) 宣告結構體型別僅僅是宣告了一個型別,系統並不為之分配記憶體,就如同系統不會為型別 int 分配記憶體一樣。只有當使用這個型別定義了變數時,系統才會為變數分配記憶體。所以在宣告結構體型別的時候,不可以對裡面的變數進行初始化。

定義結構體變數

以上只是宣告了一個資料型別——“結構體型別”。它只是一個型別,與 int、char、float、double 一樣,並沒有具體的資料,系統也不會給它分配實際的記憶體單元。要想在程式中使用“結構體型別”資料,必須要定義“結構體型別變數”,並在其中存放具體的資料。就比如:
int a;
其中 int 是型別,而 a 是用這個型別定義的變數。結構體也是一樣的,上一節只是宣告了一個型別,而本小節要使用這個型別來定義變數,就這麼簡單。一個是型別,一個是用這個型別定義的變數。

定義結構體型別變數有兩種方法。

第一種方法是先宣告“結構體型別”,再定義“結構體型別變數”。這種方式比較自由!

結構體型別的宣告和函數宣告一樣,如果在所有函數,包括main函數的前面進行宣告,那麼就可以在所有函數中直接用它來定義變數;但如果是在某個函數中進行宣告,那麼只能在該函數中用它來定義變數。

一般我們都是在所有函數前面宣告結構體型別,就同我們希望在所有函數中都可以使用int來定義變數一樣。但是正如前面所講,不建議使用全域性變數,所以同樣我們也不建議使用結構體型別定義的全域性變數。我們都是在所有函數前對結構體型別進行宣告,然後在某個函數中再定義區域性的結構體型別變數。

比如在所有函數前定義了一個結構體型別 struct STUDENT,那麼就可以在所有函數中使用它來定義區域性的結構體型別變數。如:
struct STUDENT stud1, stud2;
stud1 和 stud2 就是我們定義的結構體變數名。定義了結構體變數之後,系統就會為之分配記憶體單元。與前面講的區域性變數一樣,如果 stud1 和 stud2 是在某個函數中定義的區域性變數,那麼就只能在該函數中使用。在其他函數中可以定義重名的結構體變數而不會相互產生影響。

第二種方法是在宣告結構體型別的同時定義結構體變數。這就意味著,如果你在所有函數前宣告結構體型別,那麼定義的變數就是全域性變數;而如果要定義區域性變數,那麼就只能在某個函數中對結構體型別進行宣告,從而導致只能在這個函數中使用這個型別。

那麼宣告的時候是如何定義變數的呢?我們知道,宣告的時候最後有個一分號,就在那個分號前寫上你想定義的變數名就行了,如:
struct STUDENT
{
char name[20];
int num;
char sex;
int age;
float score;
char addr[30];
}stud;
這樣就宣告了一個結構體型別,並用這個型別定義了一個結構體變數 stud。這個變數是一個全域性變數。

“結構體型別”的宣告和使用與函數的定義和使用有所不同,函數的定義可以放在呼叫處的後面,只需在前面宣告一下即可。但是“結構體型別”的宣告必須放在“使用結構體型別定義結構體變數”的前面。

如果程式規模比較大,往往會將結構體型別的宣告集中放到一個以 .h 為字尾的標頭檔案中。哪個原始檔需要用到此結構體型別,只要用 #include 命令將該標頭檔案包含到該檔案中即可,這樣做便於修改和使用。

結構體變數可進行哪些運算

結構體變數不能相加、不能相減,也不能相互乘除,但結構體變數可以相互賦值。也就是說,可以將一個結構體變數賦給另一個結構體變數。但前提是這兩個結構體變數的結構體型別必須相同。

結構體變數的參照

定義了結構體變數之後就可以在程式中對它進行參照,但是結構體變數的參照同一般變數的參照不一樣。因為結構體變數中有多個不同型別的成員,所以結構體變數不能整體參照,只能一個成員一個成員地進行參照。

1) 不能將一個結構體變數作為一個整體進行參照,只能分別單獨參照它內部的成員,參照方式為:

結構體變數名.成員名

如果成員名是一個變數名,那麼參照的就是這個變數的內容;如果成員名是一個陣列名,那麼參照的就是這個陣列的首地址。

“.”是“成員運算子”,它在所有運算子中優先順序最高,因此可以將 student1.num 作為一個整體來看待。我們可以直接對變數的成員進行操作,例如:
student1.num = 1207041;
2) 如果結構體型別中的成員也是一個結構體型別,則要用若干個“.”,一級一級地找到最低一級的成員。因為只能對最低階的成員進行操作。

這種“結構體成員也是結構體變數”的形式就有一些 C++ 中“封裝”的味道了。其實結構體本身就是一種封裝,即將不同的資料型別封裝在同一個型別中。當結構體成員也是結構體變數的時候,完全可以將結構體成員釋放出來,比如:
struct AGE
{
    int year;
    int month;
    int day;
};
struct STUDENT
{
    char name[20];
    int num;
    struct AGE birthday;
    float score;
};
完全可以寫成:
struct STUDENT
{
    char name[20];
    int num;
    int year;
    int month;
    int day;
    float score;
};
但這樣看起來很長、很亂。而使用結構體將 year、month、day 封裝起來,程式碼看起來就會好很多。因為 year、month、day 都是生日的組成部分,所以將它們進行進一步的封裝可以使程式碼看起來很整齊,很有層次感,便於操作。

3) 可以參照“結構體變數成員”的地址,也可以參照“結構體變數”的地址。如“&student1.num”和“&student1”,前者表示 student1.num 這個成員在記憶體中的首地址,後者表示結構體變數 student1 在記憶體中的首地址。

在 C 語言中,結構體變數的首地址就是結構體第一個成員的首地址。所以 &student1 就等價於第一個成員 name 的首地址,而 name 是一個陣列,陣列名表示的就是陣列的首地址。所以 &student1 和 student1.name 是等價的。但是要注意的是,它們的等價指的僅僅是“它們表示的是同一個記憶體空間的地址”,但它們的型別是不同的。&student1 是結構體變數的地址 ,是 struct STUDENT* 型的;而 student1.name 是陣列名,所以是 char* 型的。型別的不同導致它們在程式中不能相互替換。

4) 結構體變數的參照方式決定了:
  1. “結構體變數名”可以與“結構體成員名”同名。
  2. “結構體變數名”可以與“結構體名”同名。
  3. “兩個結構體型別定義的結構體變數中的成員可以同名”。就比如定義了一個結構體型別用於存放學生的資訊,裡面有成員“char name[20];”,那麼如果又定義了一個結構體型別用於存放老師的資訊,那麼裡面也可以有成員“char name[20];”。

因為結構體成員在參照時,必須要使用“結構體變數名.成員名”的方式來參照,通過參照就可以區分它們,所以不會產生衝突,因此可以同名!只要不衝突,都可以重名!但是兩個結構體變數名就不可以重名了,因為無法區分它們,就會產生衝突。當然這裡說的是在同一個作用域內,如果在一個函數中定義一個區域性變數a,那麼在另一個函數中當然也可以定義一個區域性變數a。它們互不影響。

下面寫一個程式:
# include <stdio.h>
struct AGE
{
    int year;
    int month;
    int day;
};
struct STUDENT
{
    char name[20];
    int num;
    struct AGE birthday;  //就有點類似於C++中的封裝了
    float score;
};
int main(void)
{
    struct STUDENT student1 = {"小明", 1207041, {1989, 3, 29}, 100};
    printf("name : %sn", student1.name);
    printf("birthday : %d-%d-%dn", student1.birthday.year, student1.birthday.month, student1.birthday.day);
    printf("num : %dn", student1.num);
    printf("score : %.1fn", student1.score);
    return 0;
}
輸出結果是:
name : 小明
birthday : 1989-3-29
num : 1207041
score : 100.0

程式中,雖然我們前面說“&student1 和 student1.name是等價的”,但第 18 行不能像下面這樣寫。
printf("name : %sn", &student1);
原因是 %s 要求輸出引數要麼是字元陣列名,要麼是字元指標變數名,總之是 char* 型的。而 &student1 和 student1.name 在前面講過,雖然它們是等價的,但它們的等價指的僅僅是“它們表示的是同一個記憶體空間的地址”,但它們的型別是不同的。&student1 是 struct STUDENT* 型的,而 student1.name 是 char* 型的,所以只能寫 student1.name。

但是有的編譯器寫 &student1 就可以通過,而有的編譯器則只會產生警告。這種“可錯可不錯”的寫法大家不要使用,按規範書寫可移植性才強。

結構體變數的初始化

結構體白能量的初始化方式有兩種,可以在定義的時候或定義之後對結構體變數進行初始化。

一般情況下我們都是在定義的時候對它進行初始化,因為那樣比較方便。如果定義之後再進行初始化,那就只能一個一個成員進行賦值,就同陣列一樣。

下面先介紹如何在定義的時候進行初始化。在定義結構體變數時對其進行初始化,只要用大括號“{}”括起來,然後按結構體型別宣告時各項的順序進行初始化即可。各項之間用逗號分隔。如果結構體型別中的成員也是一個結構體型別,則要使用若干個“{}”一級一級地找到成員,然後對其進行初始化。
# include <stdio.h>
struct AGE
{
    int year;
    int month;
    int day;
};
struct STUDENT
{
    char name[20];
    int num;
    struct AGE birthday;
    float score;
};
int main(void)
{
    struct STUDENT student1 = {"小明", 1207041, {1989, 3, 29}, 100};
    return 0;
}
注意,同字元、字元陣列的初始化一樣,如果是字元那麼就用單引號括起來,如果是字串就用雙引號括起來。

第二種方式是定義後再初始化,我們將上面的程式改一下即可:
# include <stdio.h>
# include <string.h>
struct AGE
{
    int year;
    int month;
    int day;
};
struct STUDENT
{
    char name[20];  //姓名
    int num;  //學號
    struct AGE birthday;  /*用struct AGE結構體型別定義結構體變數birthday, 即生日*/
    float score;  //分數
};
int main(void)
{
    struct STUDENT student1;  /*用struct STUDENT結構體型別定義結構體變數student1*/
    strcpy(student1.name, "小明");  //不能寫成&student1
    student1.num = 1207041;
    student1.birthday.year = 1989;
    student1.birthday.month = 3;
    student1.birthday.day = 29;
    student1.score = 100;
    printf("name : %sn", student1.name);  //不能寫成&student1
    printf("num : %dn", student1.num);
    printf("birthday : %d-%d-%dn", student1.birthday.year, student1.birthday.month, student1.birthday.day);
    printf("score : %.1fn", student1.score);
    return 0;
}
輸出結果是:
name : 小明
num : 1207041
birthday : 1989-3-29
score : 100.0

除此之外,我們還可以使用 scanf() 從鍵盤輸入對結構體變數進行初始化:
# include <stdio.h>
struct AGE
{
    int year;
    int month;
    int day;
};
struct STUDENT
{
    char name[20];
    int num;
    struct AGE birthday;
    float score;
};  //分號不能省
int main(void)
{
    struct STUDENT student1;  /*用struct STUDENT結構體型別定義結構體變數student1*/
    printf("請輸入姓名:");
    scanf("%s", student1.name);  //不能寫成&student1
    printf("請輸入學號:");
    scanf("%d", &student1.num);
    printf("請輸入生日:");
    scanf("%d", &student1.birthday.year);
    scanf("%d", &student1.birthday.month);
    scanf("%d", &student1.birthday.day);
    printf("請輸入成績:");
    scanf("%f", &student1.score);
    printf("name: %sn", student1.name);  //不能寫成&student1
    printf("num: %dn", student1.num);
    printf("birthday: %d-%d-%dn", student1.birthday.year, student1.birthday.month, student1.birthday.day);
    printf("score: %.1fn", student1.score);
    return 0;
}
輸出結果是:
請輸入姓名:小明請輸入學號:1207041請輸入生日:1989 3 29請輸入成績:100
name: 小明
num: 1207041
birthday: 1989-3-29
score: 100.0

假如有一個陣列,陣列名是 a。我們知道 a 表示的就是這個陣列的首地址,但是有些編譯器會對陣列名 a 取地址,即 &a 也等同於陣列的首地址。雖然這麼寫從語法的角度是沒有意義的,但程式卻是正確的。所以上面程式中
scanf("%s",student1.name);
也可以這麼寫:
scanf("%s", &student1.name);
雖然在編譯器中,&student1.name 代表的也是 name 的首地址,但是不建議初學者這麼寫,原因如下:
  1. 這麼寫沒有語法意義。
  2. 它只是編譯器自己規定的,並不是所有的編譯器都會這樣定義,所以這麼寫不具備通用性。
  3. 這麼寫可讀性很差,讓人感到困惑且鬱悶。