這篇解讀 C 語言結構體非常全面
本次分享一篇關於結構體的入門、提高的筆記,文章比較長,前面部分是結構體基礎,已經掌握的童鞋可以跳過,直接看看後半部分的提高實例。
有的時候,我們所遇到的數據結構,不僅僅是一羣數字或者是字符串那麼簡單。比如我們每一個人的學籍信息,學號是一個長整數,名字卻是字符;甚至有更復雜的情況,這種問題在現實生活中並不少見。我們之前學過一種叫數組的數據結構,它可以允許我們把很多同類型的數據集中在一起處理。相對於之前,這已經是一次極大的進步。但是,新的問題,往往又會出現,這個時候,我們就得上更高端的裝備——結構體。
相比於數組,結構體有以下的更強大的優勢:
-
批量存儲數據
-
存儲不同類型的數據
-
支持嵌套
結構體的聲明與定義
聲明
結構體的聲明使用struct
關鍵字,如果我們想要把我們的學籍信息組織一下的話,可以這樣表示:
struct Info
{
unsigned long identifier;//學號,用無符號長整數表示
char name[20];//名字,用字符數組表示
unsigned int year;//入學年份,用無符號整數表示
unsigned int years;//學制,用無符號整數表示
}
這樣,我們就相當於描繪好了一個框架,以後要用的話直接定義一個這種類型的變量就好了。
定義
我們剛剛申請了一個名叫Info
的結構體類型,那麼理論上我們可以像聲明其他變量的操作一樣,去聲明我們的結構體操作,但是 C 語言中規定,聲明結構體變量的時候,struct
關鍵字是不可少的。
struct 結構體類型名 結構體變量名
不過,你可以在某個函數里面定義:
#include <stdio.h>
struct Info
{
unsigned long identifier;//學號,用無符號長整數表示
char name[20];//名字,用字符數組表示
unsigned int year;//入學年份,用無符號整數表示
unsigned int years;//學制,用無符號整數表示
};
int main(void)
{
/**
*在main函數中聲明結構體變量
*結構體變量名叫info
*struct關鍵字不能丟
*/
struct Info info;
...
}
也可以在聲明的時候就把變量名定義下來(此時這個變量是全局變量):
#include <stdio.h>
struct Info
{
unsigned long identifier;//學號,用無符號長整數表示
char name[20];//名字,用字符數組表示
unsigned int year;//入學年份,用無符號整數表示
unsigned int years;//學制,用無符號整數表示
} info;
/**
*此時直接定義了變量
*該變量是全局變量
*變量名叫info
*/
int main(void)
{
...
}
訪問結構體成員
結構體成員的訪問有點不同於以往的任何變量,它是採用點號運算符.
來訪問成員的。比如,info.name
就是引用info
結構體的name
成員,是一個字符數組,而info.year
則可以查到入學年份,是個無符號整型。
比如,下面開始錄入學生的信息:
//Example 01
#include <stdio.h>
struct Info
{
unsigned long identifier;//學號,用無符號長整數表示
char name[20];//名字,用字符數組表示
unsigned int year;//入學年份,用無符號整數表示
unsigned int years;//學制,用無符號整數表示
};
int main(void)
{
struct Info info;
printf("請輸入學生的學號:");
scanf("%d", &info.identifier);
printf("請輸入學生的姓名:");
scanf("%s", info.name);
printf("請輸入學生的入學年份:");
scanf("%d", &info.year);
printf("請輸入學生的學制:");
scanf("%d", &info.years);
printf("\n數據錄入完畢\n\n");
printf("學號:%d\n姓名:%s\n入學年份:%d\n學制:%d\n畢業時間:%d\n", \
info.identifier, info.name, info.year, info.years, info.year + info.years);
return 0;
}
運行結果如下:
//Consequence 01
請輸入學生的學號:20191101
請輸入學生的姓名:Harris
請輸入學生的入學年份:2019
請輸入學生的學制:4
數據錄入完畢
學號:20191101
姓名:Harris
入學年份:2019
學制:4
畢業時間:2023
初始化結構體
像數組一樣,結構體也可以在定義的時候初始化,方法也幾乎一樣:
struct Info info = {
20191101,
"Harris",
2019,
4
};
在 C99 標準中,還支持給指定元素賦值(就像數組一樣):
struct Info info = {
.name = "Harris",
.year = 2019
};
對於沒有被初始化的成員,則**「數值型」**成員初始化爲 0,**「字符型」**成員初始化爲‘\0’。
對齊
下面這個代碼,大家來看看會發生什麼:
//EXample 02 V1
#include <stdio.h>
int main(void)
{
struct A
{
char a;
int b;
char c;
} a = {'a', 10, 'o'};
printf("size of a = %d\n", sizeof(a));
return 0;
}
我們之前學過,char
類型的變量佔 1 字節,int
類型的變量佔 4 字節,那麼這麼一算,一個結構體 A 型的變量應該就是 6 字節了。別急,我們看運行結果:
//COnsequence 02 V1
size of a = 12
怎麼變成 12 了呢?標準更新了?老師教錯了?都不是。我們把代碼改一下:
//EXample 02 V2
#include <stdio.h>
int main(void)
{
struct A
{
char a;
char c;
int b;
} a = {'a', 'o', 10};
printf("size of a = %d\n", sizeof(a));
return 0;
}
結果:
//Consequence 02 V2
size of a = 8
實際上,這是編譯器對我們程序的一種優化——內存對齊。在第一個例子中,第一個和第三個成員是char
類型是 1 個字節,而中間的int
卻有 4 個字節,爲了對齊,兩個char
也佔用了 4 個字節,於是就是 12 個字節。
而在第二個例子裏面,前兩個都是char
,最後一個是int
,那麼前兩個可以一起佔用 4 個字節(實際只用 2 個,第一個例子也同理,只是爲了訪問速度更快,而不是爲了擴展),最後的int
佔用 4 字節,合起來就是 8 個字節。
關於如何聲明結構體來節省內存容量,可以閱讀下面的這篇文章,作者是艾瑞克 · 雷蒙,時尚最具爭議性的黑客之一,被公認爲開源運動的主要領導者之一:
英文原版,中文版
結構體嵌套
在學籍裏面,如果我們的日期想要更加詳細一些,精確到 day,這時候就可以使用結構體嵌套來完成:
#include <stdio.h>
struct Date
{
unsigned int year;
unsigned int month;
unsigned int day;
};
struct Info
{
unsigned long identifier;//學號,用無符號長整數表示
char name[20];//名字,用字符數組表示
struct Date date;/*---入學日期,用結構體Date表示---*/
unsigned int years;//學制,用無符號整數表示
};
int main(void)
{
...
}
如此一來,比我們單獨聲明普通變量快多了。
不過,這樣訪問變量,就必須用點號一層層往下訪問。比如要訪問day
這個成員,那就只能info.date.day
而不能直接info.date
或者info,day
。
//Example 03
#include <stdio.h>
struct Date
{
unsigned int year;
unsigned int month;
unsigned int day;
};
struct Info
{
unsigned long identifier;//學號,用無符號長整數表示
char name[20];//名字,用字符數組表示
struct Date date;/*---入學日期,用結構體Date表示---*/
unsigned int years;//學制,用無符號整數表示
};
int main(void)
{
struct Info info;
printf("請輸入學生的學號:");
scanf("%d", &info.identifier);
printf("請輸入學生的姓名:");
scanf("%s", info.name);
printf("請輸入學生的入學年份:");
scanf("%d", &info.date.year);
printf("請輸入學生的入學月份:");
scanf("%d", &info.date.month);
printf("請輸入學生的入學日期:");
scanf("%d", &info.date.day);
printf("請輸入學生的學制:");
scanf("%d", &info.years);
printf("\n數據錄入完畢\n\n");
printf("學號:%d\n姓名:%s\n入學時間:%d/%d/%d\n學制:%d\n畢業時間:%d\n",\
info.identifier, info.name,\
info.date.year, info.date.month, info.date.day,\
info.years, info.date.year + info.years);
return 0;
}
運行結果如下:
//Consequence 03
請輸入學生的學號:20191101
請輸入學生的姓名:Harris
請輸入學生的入學年份:2019
請輸入學生的入學月份:9
請輸入學生的入學日期:7
請輸入學生的學制:4
數據錄入完畢
學號:20191101
姓名:Harris
入學時間:2019/9/7
學制:4
畢業時間:2023
結構體數組
剛剛我們演示了存儲一個學生的學籍信息的時候,使用結構體的例子。那麼,如果要錄入一批學生,這時候我們就可以沿用之前的思路,使用結構體數組。
我們知道,數組的定義,就是存放一堆相同類型的數據的容器。而結構體一旦被我們聲明,那麼你就可以把它看作一個類型,只不過是你自己定義的罷了。
定義結構體數組也很簡單:
struct 結構體類型
{
成員;
} 數組名[長度];
/****或者這樣****/
struct 結構體類型
{
成員;
};
struct 結構體類型 數組名[長度];
結構體指針
既然我們可以把結構體看作一個類型,那麼也就必然有對應的指針變量。
struct Info* pinfo;
但是在指針這裏,結構體和數組就不一樣了。我們知道,數組名實際上就是指向這個數組第一個元素的地址,所以可以將數組名直接賦值給指針。而結構體的變量名並不是指向該結構體的地址,所以要使用取地址運算符&
才能獲取地址:
pinfo = &info;
通過結構體指針來訪問結構體有以下兩種方法:
-
(*結構體指針).成員名
-
結構體指針->成員名
第一個方法由於點號運算符比指針的取值運算符優先級更高,因此需要加一個小括號來確定優先級,讓指針先解引用變成結構體變量,在使用點號的方法去訪問。
相比之下,第二種方法就直觀許多。
這兩種方法在實現上是完全等價的,但是點號只能用於結構體變量,而箭頭只能夠用於指針。
第一種方法:
#include <stdio.h>
...
int main(void)
{
struct Info *p;
p = &info;
printf("學號:\n", (*p).identifier);
printf("姓名:\n", (*p).name);
printf("入學時間:%d/%d/%d\n", (*p).date.year, (*p).date.month, (*p).date.day);
printf("學制:\n", (*p).years);
return 0;
}
第二種方法:
#include <stdio.h>
...
int main(void)
{
struct Info *p;
p = &info;
printf("學號:\n", p -> identifier);
printf("姓名:\n", p -> name);
printf("入學時間:%d/%d/%d\n", p -> date.year, p -> date.month, p -> date.day);
printf("學制:\n", p -> years);
return 0;
}
傳遞結構體信息
傳遞結構體變量
我們先來看看下面的代碼:
//Example 04
#include <stdio.h>
int main(void)
{
struct Test
{
int x;
int y;
}t1, t2;
t1.x = 3;
t1.y = 4;
t2 = t1;
printf("t2.x = %d, t2.y = %d\n", t2.x, t2.y);
return 0;
}
運行結果如下:
//Consequence 04
t2.x = 3, t2.y = 4
這麼看來,結構體是可以直接賦值的。那麼既然這樣,作爲函數的參數和返回值也自然是沒問題的了。
先來試試作爲參數:
//Example 05
#include <stdio.h>
struct Date
{
unsigned int year;
unsigned int month;
unsigned int day;
};
struct Info
{
unsigned long identifier;
char name[20];
struct Date date;
unsigned int years;
};
struct Info getInput(struct Info info);
void printInfo(struct Info info);
struct Info getInput(struct Info info)
{
printf("請輸入學號:");
scanf("%d", &info.identifier);
printf("請輸入姓名:");
scanf("%s", info.name);
printf("請輸入入學年份:");
scanf("%d", &info.date.year);
printf("請輸入月份:");
scanf("%d", &info.date.month);
printf("請輸入日期:");
scanf("%d", &info.date.day);
printf("請輸入學制:");
scanf("%d", &info.years);
return info;
}
void printInfo(struct Info info)
{
printf("學號:%d\n姓名:%s\n入學時間:%d/%d/%d\n學制:%d\n畢業時間:%d\n", \
info.identifier, info.name, \
info.date.year, info.date.month, info.date.day, \
info.years, info.date.year + info.years);
}
int main(void)
{
struct Info i1 = {};
struct Info i2 = {};
printf("請錄入第一個同學的信息...\n");
i1 = getInput(i1);
putchar('\n');
printf("請錄入第二個學生的信息...\n");
i2 = getInput(i2);
printf("\n錄入完畢,現在開始打印...\n\n");
printf("打印第一個學生的信息...\n");
printInfo(i1);
putchar('\n');
printf("打印第二個學生的信息...\n");
printInfo(i2);
return 0;
}
運行結果如下:
//Consequence 05
請錄入第一個同學的信息...
請輸入學號:20191101
請輸入姓名:Harris
請輸入入學年份:2019
請輸入月份:9
請輸入日期:7
請輸入學制:4
請錄入第二個學生的信息...
請輸入學號:20191102
請輸入姓名:Joy
請輸入入學年份:2019
請輸入月份:9
請輸入日期:8
請輸入學制:5
錄入完畢,現在開始打印...
打印第一個學生的信息...
學號:20191101
姓名:Harris
入學時間:2019/9/7
學制:4
畢業時間:2023
打印第二個學生的信息...
學號:20191102
姓名:Joy
入學時間:2019/9/8
學制:5
畢業時間:2024
傳遞指向結構體變量的指針
早期的 C 語言是不允許直接將結構體作爲參數直接傳遞進去的。主要是考慮到如果結構體的內存佔用太大,那麼整個程序的內存開銷就會爆炸。不過現在的 C 語言已經放開了這方面的限制。
不過,作爲一名合格的開發者,我們應該要去珍惜硬件資源。那麼,傳遞指針就是一個很好的辦法。
將剛纔的代碼修改一下:
//Example 06
#include <stdio.h>
struct Date
{
unsigned int year;
unsigned int month;
unsigned int day;
};
struct Info
{
unsigned long identifier;
char name[20];
struct Date date;
unsigned int years;
};
void getInput(struct Info *info);
void printInfo(struct Info *info);
void getInput(struct Info *info)
{
printf("請輸入學號:");
scanf("%d", &info->identifier);
printf("請輸入姓名:");
scanf("%s", info->name);
printf("請輸入入學年份:");
scanf("%d", &info->date.year);
printf("請輸入月份:");
scanf("%d", &info->date.month);
printf("請輸入日期:");
scanf("%d", &info->date.day);
printf("請輸入學制:");
scanf("%d", &info->years);
}
void printInfo(struct Info *info)
{
printf("學號:%d\n姓名:%s\n入學時間:%d/%d/%d\n學制:%d\n畢業時間:%d\n", \
info->identifier, info->name, \
info->date.year, info->date.month, info->date.day, \
info->years, info->date.year + info->years);
}
int main(void)
{
struct Info i1 = {};
struct Info i2 = {};
printf("請錄入第一個同學的信息...\n");
getInput(&i1);
putchar('\n');
printf("請錄入第二個學生的信息...\n");
getInput(&i2);
printf("\n錄入完畢,現在開始打印...\n\n");
printf("打印第一個學生的信息...\n");
printInfo(&i1);
putchar('\n');
printf("打印第二個學生的信息...\n");
printInfo(&i2);
return 0;
}
此時傳遞的就是一個指針,而不是一個龐大的結構體。
動態申請結構體
結構體也可以在堆裏面動態申請:
//Example 01
#include <stdio.h>
...
int main(void)
{
struct Info *i1;
struct Info *i2;
i1 = (struct Info *)malloc(sizeof(struct Info));
i2 = (struct Info *)malloc(sizeof(struct Info));
if (i1 == NULL || i2 == NULL)
{
printf("內存分配失敗!\n");
exit(1);
}
printf("請錄入第一個同學的信息...\n");
getInput(i1);
putchar('\n');
printf("請錄入第二個學生的信息...\n");
getInput(i2);
printf("\n錄入完畢,現在開始打印...\n\n");
printf("打印第一個學生的信息...\n");
printInfo(i1);
putchar('\n');
printf("打印第二個學生的信息...\n");
printInfo(i2);
free(i1);
free(i2);
return 0;
}
實戰:建立一個圖書館數據庫
實際上,我們建立的數組可以是指向結構體指針的數組。
代碼實現如下:
//Example 02
#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 100
struct Date
{
int year;
int month;
int day;
};
struct Book
{
char title[128];
char author[48];
float price;
struct Date date;
char publisher[48];
};
void getInput(struct Book* book);//錄入數據
void printBook(struct Book* book);//打印數據
void initLibrary(struct Book* lib[]);//初始化結構體
void printLibrary(struct Book* lib[]);//打印單本書數據
void releaseLibrary(struct Book* lib[]);//釋放內存
void getInput(struct Book* book)
{
printf("請輸入書名:");
scanf("%s", book->title);
printf("請輸入作者:");
scanf("%s", book->author);
printf("請輸入售價:");
scanf("%f", &book->price);
printf("請輸入出版日期:");
scanf("%d-%d-%d", &book->date.year, &book->date.month, &book->date.day);
printf("請輸入出版社:");
scanf("%s", book->publisher);
}
void printBook(struct Book* book)
{
printf("書名:%s\n", book->title);
printf("作者:%s\n", book->author);
printf("售價:%.2f\n", book->price);
printf("出版日期:%d-%d-%d\n", book->date.year, book->date.month, book->date.day);
printf("出版社:%s\n", book->publisher);
}
void initLibrary(struct Book* lib[])
{
for (int i = 0; i < MAX_SIZE; i++)
{
lib[i] = NULL;
}
}
void printLibrary(struct Book* lib[])
{
for (int i = 0; i < MAX_SIZE; i++)
{
if (lib[i] != NULL)
{
printBook(lib[i]);
putchar('\n');
}
}
}
void releaseLibrary(struct Book* lib[])
{
for (int i = 0; i < MAX_SIZE; i++)
{
if (lib[i] != NULL)
{
free(lib[i]);
}
}
}
int main(void)
{
struct Book* lib[MAX_SIZE];
struct Book* p = NULL;
int ch, index = 0;
initLibrary(lib);
while (1)
{
printf("請問是否要錄入圖書信息(Y/N):");
do
{
ch = getchar();
} while (ch != 'Y' && ch != 'N');
if (ch == 'Y')
{
if (index < MAX_SIZE)
{
p = (struct Book*)malloc(sizeof(struct Book));
getInput(p);
lib[index] = p;
index++;
putchar('\n');
}
else
{
printf("數據庫已滿!\n");
break;
}
}
else
{
break;
}
}
printf("\n數據錄入完畢,開始打印驗證...\n\n");
printLibrary(lib);
releaseLibrary(lib);
return 0;
}
運行結果如下:
//Consequence 02
請問是否要錄入圖書信息(Y/N):Y
請輸入書名:人類簡史
請輸入作者:尤瓦爾·赫拉利
請輸入售價:32.25
請輸入出版日期:2016-3-4
請輸入出版社:中信出版集團
請問是否要錄入圖書信息(Y/N):N
數據錄入完畢,開始打印驗證...
書名:人類簡史
作者:尤瓦爾·赫拉利
售價:32.25
出版日期:2016-3-4
出版社:中信出版集團
單鏈表
我們知道,數組變量在內存中,是連續的,而且不可拓展。顯然在一些情況下,這種數據結構擁有很大的侷限性。比如移動數據的時候,會牽一髮而動全身,尤其是反轉這種操作更加令人窒息。那麼,需要需要一種數據結構來弄出一種更加靈活的 “數組”,那麼這,就是 「鏈表」 。
本節我們只講講單鏈表。
所謂鏈表,就是由一個個 「結點」 組成的一個數據結構。每個結點都有 「數據域」 和 「指針域」 組成。其中數據域用來存儲你想要存儲的信息,而指針域用來存儲下一個結點的地址。如圖:
單鏈表
當然,鏈表最前面還有一個頭指針,用來存儲頭結點的地址。
這樣一來,鏈表中的每一個結點都可以不用挨個存放,因爲有了指針把他們串起來。因此結點放在哪都無所謂,反正指針總是能夠指向下一個元素。我們只需要知道頭指針,就能夠順藤摸瓜地找到整個鏈表。
因此對於學籍數據庫來說,我們只需要在 Info 結構體中加上一個指向自身類型的成員即可:
struct Info
{
unsigned long identifier;
char name[20];
struct Date date;
unsigned int years;
struct Info* next;
};
在單鏈表中插入元素
頭插法
這種每次都將數據插入單鏈表的頭部(頭指針後面)的插入法就叫頭插法。
如果要把學生信息加入到單鏈表,可以這麼寫:
void addInfo(struct Info** students)//students是頭指針
{
struct Info* info, *temp;
info = (struct Info*)malloc(sizeof(struct Info));
if (info == NULL)
{
printf("內存分配失敗!\n");
exit(1);
}
getInput(info);
if (*students != NULL)
{
temp = *students;
*students = info;
info->next = temp;
}
else
{
*students = info;
info->next = NULL;
}
}
❝
由於 students 存放的是頭指針,因此我們需要傳入它的地址傳遞給函數,才能夠改變它本身的值。而 students 本身又是一個指向 Info 結構體的指針,所以參數的類型應該就是
struct Info**
。❞
往單鏈表裏面添加一個結點,也就是先申請一個結點,然後判斷鏈表是否爲空。如果爲空,那麼直接將頭指針指向它,然後next
成員指向NULL
。若不爲空,那麼先將next
指向頭指針原本指向的結點,然後將頭指針指向新結點即可。
那麼,打印鏈表也變得很簡單:
void printStu(struct Info* students)
{
struct Info* info;
int count = 1;
info = students;
while (book != NULL)
{
printf("Student%d:\n", count);
printf("姓名:%s\n", info->name);
printf("學號:%d\n", info->identifier);
info = info->next;
count++;
}
}
想要讀取單鏈表裏面的數據,只需要迭代單鏈表中的每一個結點,直到next
成員爲NULL
,即表示單鏈表的結束。
最後,當然還是別忘了釋放空間:
void releaseStu(struct Info** students)
{
struct Info* temp;
while (*students != NULL)
{
temp = *students;
*students = (*students)->next;
free(temp);
}
}
尾插法
與頭插法類似,尾插法就是把每一個數據都插入到鏈表的末尾。
void addInfo(struct Info** students)
{
struct Info* info, *temp;
info = (struct Info*)malloc(sizeof(struct Info));
if (info == NULL)
{
printf("內存分配失敗!\n");
exit(1);
}
getInput(info);
if (*students != NULL)
{
temp = *students;
*students = info;
//定位到鏈表的末尾的位置
while (temp->next != NULL)
{
temp = temp->next;
}
//插入數據
temp->next = info;
info->next = temp;
}
else
{
*students = info;
info->next = NULL;
}
}
這麼一來,程序執行的效率難免要降低很多,因爲每次插入數據,都要先遍歷一次鏈表。如果鏈表很長,那麼對於插入數據來說就是一次災難。不過,我們可以給程序添加一個指針,讓它永遠都指向鏈表的尾部,這樣一來,就可以用很少的空間換取很高的程序執行效率。
代碼更改如下:
void addInfo(struct Info** students)
{
struct Info* info, *temp;
static struct Info* tail;//設置靜態指針
info = (struct Info*)malloc(sizeof(struct Info));
if (info == NULL)
{
printf("內存分配失敗!\n");
exit(1);
}
getInput(info);
if (*students != NULL)
{
tail->next = info;
info->next = NULL;
}
else
{
*students = info;
info->next = NULL;
}
}
搜索單鏈表
單鏈表是我們用來存儲數據的一個容器,那麼有時候需要快速查找信息就需要開發相關搜索的功能。比如說輸入學號,查找同學的所有信息。
struct Info *searchInfo(struct Info* students, long* target)
{
struct Info* info;
info = students;
while (info != NULL)
{
if (info->identifier == target)
{
break;
}
info = info->next;
}
return book;
};
void printInfo(struct Info* info)
{
...
}
...
int main(void)
{
...
printf("\n請輸入學生學號:");
scanf("%d", input);
info = searchInfo(students, input);
if (info == NULL)
{
printf("抱歉,未找到相關結果!\n");
}
else
{
do
{
printf("相關結果如下:\n");
printInfo(book);
} while ((info = searchInfo(info->next, input)) != NULL);
}
releaseInfo(...);
return 0;
}
插入結點到指定位置
到了這裏,才體現出鏈表真正的優勢。
設想一下,如果有一個有序數組,現在要求你去插入一個數字,插入完成之後,數組依然保持有序。你會怎麼做?
沒錯,你應該會挨個去比較,然後找到合適的位置(當然這裏也可以使用二分法,比較節省算力),把這個位置後面的所有數都往後移動一個位置,然後將我們要插入的數字放入剛剛我們騰出來的空間裏面。
你會發現,這樣的處理方法,經常需要移動大量的數據,對於程序的執行效率來說,是一個不利因素。那麼鏈表,就無所謂。反正在內存中,鏈表的存儲毫無邏輯,我們只需要改變指針的值就可以實現鏈表的中間插入。
//Example 03
#include <stdio.h>
#include <stdlib.h>
struct Node
{
int value;
struct Node* next;
};
void insNode(struct Node** head, int value)
{
struct Node* pre;
struct Node* cur;
struct Node* New;
cur = *head;
pre = NULL;
while (cur != NULL && cur->value < value)
{
pre = cur;
cur = cur->next;
}
New = (struct Node*)malloc(sizeof(struct Node));
if (New == NULL)
{
printf("內存分配失敗!\n");
exit(1);
}
New->value = value;
New->next = cur;
if (pre == NULL)
{
*head = New;
}
else
{
pre->next = New;
}
}
void printNode(struct Node* head)
{
struct Node* cur;
cur = head;
while (cur != NULL)
{
printf("%d ", cur->value);
cur = cur->next;
}
putchar('\n');
}
int main(void)
{
struct Node* head = NULL;
int input;
printf("開始插入整數...\n");
while (1)
{
printf("請輸入一個整數,輸入-1表示結束:");
scanf("%d", &input);
if (input == -1)
{
break;
}
insNode(&head, input);
printNode(head);
}
return 0;
}
運行結果如下:
//Consequence 03
開始插入整數...
請輸入一個整數,輸入-1表示結束:4
4
請輸入一個整數,輸入-1表示結束:5
4 5
請輸入一個整數,輸入-1表示結束:3
3 4 5
請輸入一個整數,輸入-1表示結束:6
3 4 5 6
請輸入一個整數,輸入-1表示結束:2
2 3 4 5 6
請輸入一個整數,輸入-1表示結束:5
2 3 4 5 5 6
請輸入一個整數,輸入-1表示結束:1
1 2 3 4 5 5 6
請輸入一個整數,輸入-1表示結束:7
1 2 3 4 5 5 6 7
請輸入一個整數,輸入-1表示結束:-1
刪除結點
刪除結點的思路也差不多,首先修改待刪除的結點的上一個結點的指針,將其指向待刪除結點的下一個結點。然後釋放待刪除結點的空間。
...
void delNode(struct Node** head, int value)
{
struct Node* pre;
struct Node* cur;
cur = *head;
pre = NULL;
while (cur != NULL && cur->value != value)
{
pre = cur;
cur = cur->next;
}
if (cur == NULL)
{
printf("未找到匹配項!\n");
return ;
}
else
{
if (pre == NULL)
{
*head = cur->next;
}
else
{
pre->next = cur->next;
}
free(cur);
}
}
內存池
C 語言的內存管理,從來都是一個讓人頭禿的問題。要想更自由地管理內存,就必須去堆中申請,然後還需要考慮何時釋放,萬一釋放不當,或者沒有及時釋放,造成的後果都是難以估量的。
當然如果就這些,那倒也還不算什麼。問題就在於,如果大量地使用malloc
和free
函數來申請內存,首先使要經歷一個從應用層切入系統內核層,調用完成之後,再返回應用層的一系列步驟,實際上使非常浪費時間的。更重要的是,還會產生大量的內存碎片。比如,先申請了一個 1KB 的空間,緊接着又申請了一個 8KB 的空間。而後,這個 1KB 使用完了,被釋放,但是這個空間卻只有等到下一次有剛好 1KB 的空間申請,才能夠被重新調用。這麼一來,極限情況下,整個堆有可能被弄得支離破碎,最終導致大量內存浪費。
那麼這種情況下,我們解決這類問題的思路,就是創建一個內存池。
內存池,實際上就是我們讓程序創建出來的一塊額外的緩存區域,如果有需要釋放內存,先不必使用free
函數,如果內存池有空,那麼直接放入內存池。同樣的道理,下一次程序申請空間的時候,先檢查下內存池裏面有沒有合適的內存,如果有,則直接拿出來調用,如果沒有,那麼再使用malloc
。
其實內存池我們就可以使用單鏈表來進行維護,下面通過一個通訊錄的程序來說明內存池的運用。
普通的版本:
//Example 04 V1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Person
{
char name[40];
char phone[20];
struct Person* next;
};
void getInput(struct Person* person);
void printPerson(struct Person* person);
void addPerson(struct Person** contects);
void changePerson(struct Person* contacts);
void delPerson(struct Person** contacts);
struct Person* findPerson(struct Person* contacts);
void displayContacts(struct Person* contacts);
void releaseContacts(struct Person** contacts);
void getInput(struct Person* person)
{
printf("請輸入姓名:");
scanf("%s", person->name);
printf("請輸入電話:");
scanf("%s", person->phone);
}
void addPerson(struct Person** contacts)
{
struct Person* person;
struct Person* temp;
person = (struct Person*)malloc(sizeof(struct Person));
if (person == NULL)
{
printf("內存分配失敗!\n");
exit(1);
}
getInput(person);
//將person添加到通訊錄中
if (*contacts != NULL)
{
temp = *contacts;
*contacts = person;
person->next = temp;
}
else
{
*contacts = person;
person->next = NULL;
}
}
void printPerson(struct Person* person)
{
printf("聯繫人:%s\n", person->name);
printf("電話:%s\n", person->phone);
}
struct Person* findPerson(struct Person* contacts)
{
struct Person* current;
char input[40];
printf("請輸入聯繫人:");
scanf("%s", input);
current = contacts;
while (current != NULL && strcmp(current->name, input))
{
current = current->next;
}
return current;
}
void changePerson(struct Person* contacts)
{
struct Person* person;
person = findPerson(contacts);
if (person == NULL)
{
printf("找不到聯繫人!\n");
}
else
{
printf("請輸入聯繫電話:");
scanf("%s", person->phone);
}
}
void delPerson(struct Person** contacts)
{
struct Person* person;
struct Person* current;
struct Person* previous;
//先找到待刪除的節點的指針
person = findPerson(*contacts);
if (person == NULL)
{
printf("找不到該聯繫人!\n");
}
else
{
current = *contacts;
previous = NULL;
//將current定位到待刪除的節點
while (current != NULL && current != person)
{
previous = current;
current = current->next;
}
if (previous == NULL)
{
//若待刪除的是第一個節點
*contacts = current->next;
}
else
{
//若待刪除的不是第一個節點
previous->next = current->next;
}
free(person);//將內存空間釋放
}
}
void displayContacts(struct Person* contacts)
{
struct Person* current;
current = contacts;
while (current != NULL)
{
printPerson(current);
current = current->next;
}
}
void releaseContacts(struct Person** contacts)
{
struct Person* temp;
while (*contacts != NULL)
{
temp = *contacts;
*contacts = (*contacts)->next;
free(temp);
}
}
int main(void)
{
int code;
struct Person* contacts = NULL;
struct Person* person;
printf("| 歡迎使用通訊錄管理程序 |\n");
printf("|--- 1:插入新的聯繫人 ---|\n");
printf("|--- 2:查找現有聯繫人 ---|\n");
printf("|--- 3:更改現有聯繫人 ---|\n");
printf("|--- 4:刪除現有聯繫人 ---|\n");
printf("|--- 5:顯示當前通訊錄 ---|\n");
printf("|--- 6:退出通訊錄程序 ---|\n");
while (1)
{
printf("\n請輸入指令代碼:");
scanf("%d", &code);
switch (code)
{
case 1:addPerson(&contacts); break;
case 2:person = findPerson(contacts);
if (person == NULL)
{
printf("找不到該聯繫人!\n");
}
else
{
printPerson(person);
}
break;
case 3:changePerson(contacts); break;
case 4:delPerson(&contacts); break;
case 5:displayContacts(contacts); break;
case 6:goto END;
}
}
END://此處直接跳出恆循環
releaseContacts(&contacts);
return 0;
}
運行結果如下:
//Consequence 04 V1
| 歡迎使用通訊錄管理程序 |
|--- 1:插入新的聯繫人 ---|
|--- 2:查找現有聯繫人 ---|
|--- 3:更改現有聯繫人 ---|
|--- 4:刪除現有聯繫人 ---|
|--- 5:顯示當前通訊錄 ---|
|--- 6:退出通訊錄程序 ---|
請輸入指令代碼:1
請輸入姓名:HarrisWilde
請輸入電話:0101111
請輸入指令代碼:1
請輸入姓名:Jack
請輸入電話:0101112
請輸入指令代碼:1
請輸入姓名:Rose
請輸入電話:0101113
請輸入指令代碼:2
請輸入聯繫人:HarrisWilde
聯繫人:HarrisWilde
電話:0101111
請輸入指令代碼:2
請輸入聯繫人:Mike
找不到該聯繫人!
請輸入指令代碼:5
聯繫人:Rose
電話:0101113
聯繫人:Jack
電話:0101112
聯繫人:HarrisWilde
電話:0101111
請輸入指令代碼:3
請輸入聯繫人:HarrisWilde
請輸入聯繫電話:0101234
請輸入指令代碼:5
聯繫人:Rose
電話:0101113
聯繫人:Jack
電話:0101112
聯繫人:HarrisWilde
電話:0101234
請輸入指令代碼:6
下面加入內存池:
//Example 04 V2
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX 1024
struct Person
{
char name[40];
char phone[20];
struct Person* next;
};
struct Person* pool = NULL;
int count;
void getInput(struct Person* person);
void printPerson(struct Person* person);
void addPerson(struct Person** contects);
void changePerson(struct Person* contacts);
void delPerson(struct Person** contacts);
struct Person* findPerson(struct Person* contacts);
void displayContacts(struct Person* contacts);
void releaseContacts(struct Person** contacts);
void releasePool(void);
void getInput(struct Person* person)
{
printf("請輸入姓名:");
scanf("%s", person->name);
printf("請輸入電話:");
scanf("%s", person->phone);
}
void addPerson(struct Person** contacts)
{
struct Person* person;
struct Person* temp;
//如果內存池不是空的,那麼首先從裏面獲取空間
if (pool != NULL)
{
person = pool;
pool = pool->next;
count--;
}
//內存池爲空,則直接申請
else
{
person = (struct Person*)malloc(sizeof(struct Person));
if (person == NULL)
{
printf("內存分配失敗!\n");
exit(1);
}
}
getInput(person);
//將person添加到通訊錄中
if (*contacts != NULL)
{
temp = *contacts;
*contacts = person;
person->next = temp;
}
else
{
*contacts = person;
person->next = NULL;
}
}
void printPerson(struct Person* person)
{
printf("聯繫人:%s\n", person->name);
printf("電話:%s\n", person->phone);
}
struct Person* findPerson(struct Person* contacts)
{
struct Person* current;
char input[40];
printf("請輸入聯繫人:");
scanf("%s", input);
current = contacts;
while (current != NULL && strcmp(current->name, input))
{
current = current->next;
}
return current;
}
void changePerson(struct Person* contacts)
{
struct Person* person;
person = findPerson(contacts);
if (person == NULL)
{
printf("找不到聯繫人!\n");
}
else
{
printf("請輸入聯繫電話:");
scanf("%s", person->phone);
}
}
void delPerson(struct Person** contacts)
{
struct Person* person;
struct Person* current;
struct Person* previous;
struct Person* temp;
{
};
//先找到待刪除的節點的指針
person = findPerson(*contacts);
if (person == NULL)
{
printf("找不到該聯繫人!\n");
}
else
{
current = *contacts;
previous = NULL;
//將current定位到待刪除的節點
while (current != NULL && current != person)
{
previous = current;
current = current->next;
}
if (previous == NULL)
{
//若待刪除的是第一個節點
*contacts = current->next;
}
else
{
//若待刪除的不是第一個節點
previous->next = current->next;
}
//判斷內存池中有沒有空位
if (count < MAX)
{
//使用頭插法將person指向的空間插入內存池中
if (pool != NULL)
{
temp = pool;
pool = person;
person->next = temp;
}
else
{
pool = person;
person->next = NULL;
}
count++;
}
//沒有空位,直接釋放
else
{
free(person);//將內存空間釋放
}
}
}
void displayContacts(struct Person* contacts)
{
struct Person* current;
current = contacts;
while (current != NULL)
{
printPerson(current);
current = current->next;
}
}
void releaseContacts(struct Person** contacts)
{
struct Person* temp;
while (*contacts != NULL)
{
temp = *contacts;
*contacts = (*contacts)->next;
free(temp);
}
}
void releasePool(void)
{
struct Person* temp;
while (pool != NULL)
{
temp = pool;
pool = pool->next;
free(temp);
}
}
int main(void)
{
int code;
struct Person* contacts = NULL;
struct Person* person;
printf("| 歡迎使用通訊錄管理程序 |\n");
printf("|--- 1:插入新的聯繫人 ---|\n");
printf("|--- 2:查找現有聯繫人 ---|\n");
printf("|--- 3:更改現有聯繫人 ---|\n");
printf("|--- 4:刪除現有聯繫人 ---|\n");
printf("|--- 5:顯示當前通訊錄 ---|\n");
printf("|--- 6:退出通訊錄程序 ---|\n");
while (1)
{
printf("\n請輸入指令代碼:");
scanf("%d", &code);
switch (code)
{
case 1:addPerson(&contacts); break;
case 2:person = findPerson(contacts);
if (person == NULL)
{
printf("找不到該聯繫人!\n");
}
else
{
printPerson(person);
}
break;
case 3:changePerson(contacts); break;
case 4:delPerson(&contacts); break;
case 5:displayContacts(contacts); break;
case 6:goto END;
}
}
END://此處直接跳出恆循環
releaseContacts(&contacts);
releasePool();
return 0;
}
typedef
給數據類型起別名
C 語言是一門古老的語言,它是在 1969 至 1973 年間,由兩位天才丹尼斯 · 裏奇和肯 · 湯普遜在貝爾實驗室以 B 語言爲基礎開發出來的,用於他們的重寫 UNIX 計劃(這也爲後來 UNIX 系統的可移植性打下了基礎,之前的 UNIX 是使用匯編語言編寫的,當然也是這兩位爲了玩一個自己設計的遊戲而編寫的)。天才就是和咱常人不一樣,不過他倆的故事,在這篇裏面不多囉嗦,我們回到話題。
雖然 C 語言誕生的很早,但是卻依舊不是最早的高級編程語言。目前公認的最早的高級編程語言,是 IBM 公司於 1957 年開發的 FORTRAN 語言。C 語言誕生之時,FORTRAN 已經統領行業數十年之久。因此,C 語言要想快速吸納 FORTRAN 中的潛在用戶,就必須做出一些妥協。
我們知道,不同的語言的語法,一般來說是不同的,甚至還有較大的差距。比如:
C:
int a, b, c;
float i, j, k;
而 FORTRAN 語言是這樣的:
integer :: a, b, c;
real :: i, j, k;
如果讓 FORTRAN 用戶使用原來的變量名稱進行使用,那麼就能夠快速遷移到 C 語言上面來,這就是typedef
的用處之一。
我們使用 FORTRAN 語言的類型名,那就這麼辦:
typedef int integer;
typedef float real;
integer a, b, c;
real i, j, k;
結構體的搭檔
雖然結構體的出現能夠讓我們有一個更科學的數據結構來管理數據,但是每次使用結構體都需要struct...
,未免顯得有些冗長和麻煩。有了typedef
的助攻,我們就可以很輕鬆地給結構體類型起一個容易理解的名字:
typedef struct date
{
int year;
int month;
int day;
} DATE;//爲了區分,一般用全大寫
int main(void)
{
DATE* date;
...
}
甚至還可以順便給它的指針也定義一個別名:
typedef struct date
{
int year;
int month;
int day;
} DATE, *PDATE;
進階
我們還可以利用typedef
來簡化一些比較複雜的命令。
比如:
int (*ptr) [5];
我們知道這是一個數組指針,指向一個 5 元素的數組。那麼我們可以改寫成這樣:
typedef int(*PTR_TO_ARRAY)[3];
這樣就可以把很複雜的聲明變得很簡單:
PTR_TO_ARRAY a = &array;
取名的時候要儘量使用容易理解的名字,這樣才能達到使用typedef
的最終目的。
共用體
共用體也稱聯合體。
聲明
和結構體還是有點像:
union 共用體名稱
{
成員1;
成員2;
成員3;
};
但是兩者有本質的不同。共用體的每一個成員共用一段內存,那麼這也就意味着它們不可能同時被正確地訪問。如:
//Example 05
#include <stdio.h>
#include <string.h>
union Test
{
int i;
double pi;
char str[9];
};
int main(void)
{
union Test test;
test.i = 10;
test.pi = 3.14;
strcpy(test.str, "TechZone");
printf("test.i: %d\n", test.i);
printf("test.pi: %.2f\n", test.pi);
printf("test.str: %s\n", test.str);
return 0;
}
執行結果如下:
//Consequence 05
test.i: 1751344468
test.pi: 3946574856045802736197446431383475413237648487838717723111623714247921409395495328582015991082102150186282825269379326297769425957893182570875995348588904500564659454087397032067072.00
test.str: TechZone
可以看到,共用體只能正確地展示出最後一次被賦值的成員。共用體的內存應該要能夠滿足最大的成員能夠正常存儲。但是並不一定等於最大的成員的尺寸,因爲還要考慮內存對齊的問題。
共用體可以類似結構體一樣來定義和聲明,但是共用體還可以允許不帶名字:
union
{
int i;
char ch;
float f;
} a, b;
初始化
共用體不能在同一時間存放多個成員,所以不能批量初始化
union data
{
int i;
char ch;
float f;
};
union data a = {520}; //初始化第一個成員
union data b = a; //直接使用一個共用體初始化另一個共用體
union data c = {.ch = 'C'}; //C99的特性,指定初始化成員
枚舉
枚舉是一個基本的數據類型,它可以讓數據更簡潔。
如果寫一個判斷星期的文章,我們當然可以使用宏定義來使代碼更加易懂,不過:
#define MON 1
#define TUE 2
#define WED 3
#define THU 4
#define FRI 5
#define SAT 6
#define SUN 7
這樣的寫法有點費鍵盤。那麼枚舉就簡單多了:
enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
};
❝
注意: 第一個枚舉成員的默認值爲整型的 0,後續枚舉成員的值在前一個成員上加 1。我們在這個實例中把第一個枚舉成員的值定義爲 1,第二個就爲 2,以此類推。
❞
枚舉變量的定義和聲明方法和共用體一樣,也可以省略枚舉名,直接聲明變量名。
//Example 06
#include <stdio.h>
#include <stdlib.h>
int main()
{
enum color { red = 1, green, blue };
enum color favorite_color;
printf("請輸入你喜歡的顏色: (1. red, 2. green, 3. blue): ");
scanf("%d", &favorite_color);
//輸出結果
switch (favorite_color)
{
case red:
printf("你喜歡的顏色是紅色");
break;
case green:
printf("你喜歡的顏色是綠色");
break;
case blue:
printf("你喜歡的顏色是藍色");
break;
default:
printf("你沒有選擇你喜歡的顏色");
}
return 0;
}
執行結果如下:
//Consequence 06
請輸入你喜歡的顏色: (1. red, 2. green, 3. blue): 3
你喜歡的顏色是藍色
也可以把整數轉換爲枚舉類型:
//Example 07
#include <stdio.h>
#include <stdlib.h>
int main()
{
enum day
{
saturday,
sunday,
monday,
tuesday,
wednesday,
thursday,
friday
} workday;
int a = 1;
enum day weekend;
weekend = (enum day) a; //使用強制類型轉換
//weekend = a; //錯誤
printf("weekend:%d", weekend);
return 0;
}
運行結果如下:
//Consequence 07
weekend:1
位域
C 語言除了開發桌面應用等,還有一個很重要的領域,那就是 「單片機」 開發。單片機上的硬件資源十分有限,容不得我們去肆意揮灑。單片機使一種集成電路芯片,使採用超大規模集成電路技術把具有數據處理能力的 CPU、RAM、ROM、I/O、中斷系統、定時器 / 計數器等功能(有的還包括顯示驅動電路、脈寬調製電路、模擬多路轉換器、A/D 轉換器等電路)集成到一塊硅片上構成的一個小而完善的微型計算機系統,在工控領域使用廣泛。
對於這樣的設備,通常內存只有 256B,那麼能夠給我們利用的資源就十分珍貴了。在這種情況下,如果我們只需要定義一個變量來存放布爾值,一般就申請一個整型變量,通過 1 和 0 來間接存儲。但是,顯然 1 和 0 只用 1 個 bit 就能夠放完,而一個整型卻是 4 個字節,也就是 32bit。這就造成了內存的浪費。
好在,C 語言爲我們提供了一種數據結構,稱爲 「位域」 (也叫位端、位字段)。也就是把一個字節中的二進制位劃分,並且你能夠指定每個區域的位數。每個域有一個域名,並允許程序中按域名進行單獨操作。
使用位域的做法是在結構體定義的時候,在結構體成員後面使用冒號(:)和數字來表示該成員所佔的位數。
//Example 08
#include <stdio.h>
int main(void)
{
struct Test
{
unsigned int a : 1;
unsigned int b : 1;
unsigned int c : 2;
} test;
test.a = 0;
test.b = 1;
test.c = 2;
printf("a = %d, b = %d, c = %d\n", test.a, test.b, test.c);
printf("size of test = %d\n", sizeof(test));
return 0;
}
運行結果如下:
//Consequence 08
a = 0, b = 1, c = 2
size of test = 4
如此一來,結構體test
只用了 4bit,卻存放下了 0、1、2 三個整數。但是由於 2 在二進制中是 10,因此佔了 2 個 bit。如果把test.b
賦值爲 2,那麼:
//Consequence 08 V2
a = 0, b = 0, c = 2
size of test = 4
可以看到,b 中的 10 溢出了,只剩下 0。
當然,位域的寬度不能夠超過本身類型的長度,比如:
unsigned int a : 100;
那麼就會報錯:
錯誤 C2034 “main::test::a”: 位域類型對位數太小
位域成員也可以沒有名稱,只要給出類型和寬度即可:
struct Test
{
unsigned int x : 1;
unsigned int y : 2;
unsigned int z : 3;
unsigned int : 26;
};
無名位域一般用來作爲填充或者調整成員的位置,因爲沒有名稱,所以無名位域並不能夠拿來使用。
❝
C 語言的標準只說明 unsigned int 和 signed int 支持位域,然後 C99 增加了_Bool 類型也支持位域,其他數據類型理論上是不支持的。不過大多數編譯器在具體實現時都進行了擴展,額外支持了 signed char、unsigned char 以及枚舉類型,所以如果對 char 類型的結構體成員使用位域,基本上也沒什麼問題。但如果考慮到程序的可移植性,就需要謹慎對待了。另外,由於內存的基本單位是字節,而位域只是字節的一部分,所以並不能對位域進行取地址運算。
❞
雖然科技發展日新月異,但是秉承着節約成本這個放之四海而皆準的原則,還是要注意使用!畢竟 5 毛錢可能是小錢,但是乘以 5000 萬呢?
原文來源於: 間接來源 嵌入式大雜燴,來源:TechZone 作者:HarrisWilde
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/oAcy0D-gtmWYZ95pMDcJzg