知識點
結構體指針
Employee *p;
保存結構體的地址;p->member
用箭頭運算符訪問或修改成員。
數組與指針
Employee *emps = malloc(N * sizeof *emps);
動態創建結構體數組;p < emps + N
與p++
配合遍歷。
scanf
與數組退化p->name
是char name[50]
的首地址,無需&
;%49s
限制最大讀取字符數,防止溢出。
函數參數傳遞
give_raise(Employee *e, ...)
傳入指針,不拷貝整個結構體;在函數內部用
->
修改原變量。
動態內存管理
malloc
申請、free
釋放,避免內存泄露。
通過本練習,你將深入理解如何用結構體指針靈活高效地訪問和修改結構體成員。
題目描述
本題要求你熟練使用 結構體指針 來訪問和修改結構體成員,并將其與數組和動態內存結合:
定義一個
Employee
結構體,包含員工姓名、工號和工資;在
main
中使用malloc
動態分配一個Employee
數組,長度為 3;通過 結構體指針 和 箭頭運算符(
->
)讀取用戶輸入并打印初始信息;實現函數
void give_raise(Employee *e, double pct);
,通過傳入結構體指針給指定員工加薪;在
main
中用指針遍歷全體員工,統一加薪 10%,然后再次打印更新后的信息;最后釋放動態內存并退出。
參考代碼:
#include <stdio.h>
#include <stdlib.h>#define N 3
#define NAME_LEN 50//1.結構體定義:保存員工信息
typedef struct
{char name[NAME_LEN];//員工姓名int id;//員工號double salary;//工資
} Employee;/**給單個員工加薪*@param e 指向Employee的指針*@param pct 加薪百分比,列入加10表示加10%*/
void give_raise(Employee *e ,double pct)
{//e->salary等價于(*e).salarye->salary *=(1.0 + pct / 100.0);
}int main(void)
{Employee *emps = malloc(N * sizeof *emps);if(!emps){fprintf(stderr,"內存分配失敗!\n");return EXIT_FAILURE;}//2.讀取初始信息for(int i = 0;i<N;i++){Employee *p = &emps[i];//指向第i個結構體printf("請輸入員工%d的 姓名 工號 工資:",i+1);//p->name自動退化為char*if(scanf("%49s %d %lf",p->name,&p->id,&p->salary) != 3){fprintf(stderr,"輸入格式錯誤!\n");free(emps);return EXIT_FAILURE;}}//3.打印初始信息printf("\n======初始員工信息==========\n");for(Employee *p = emps; p < emps+N; p++){//p是Employee*,用->訪問成員printf("姓名:%-10s 工號:%4d 工資%.2f\n",p->name,p->id,p->salary);}//4.為所有員工加薪10%for(Employee *p = emps; p < emps + N ; p++){give_raise(p,10.0);}//5.打印加薪后的信息printf("\n=======加薪后員工信息(10%)=========\n");for(Employee *p = emps ; p < emps + N;p++){printf("姓名:%-10s 工號:%4d 工資%.2f\n",p->name,p->id,p->salary);}free(emps);return EXIT_SUCCESS;
}
代碼逐行分析
結構體定義
typedef struct { char name[NAME_LEN]; int id; double salary; } Employee;
name
是字符數組,存放 C 字符串;id
是員工工號;salary
是工資。
動態分配
Employee *emps = malloc(N * sizeof *emps);
malloc
申請N
個Employee
大小的連續內存;sizeof *emps
等同sizeof(Employee)
;若返回
NULL
,則分配失敗。
讀取輸入
Employee *p = &emps[i]; scanf("%49s %d %lf", p->name, &p->id, &p->salary);
p = &emps[i]
:p
指向第i
個結構體;p->name
(無&
):數組名在表達式中退化為char *
;&p->id
、&p->salary
:取基本類型變量的地址。
打印初始信息
for (Employee *p = emps; p < emps + N; p++) { printf("%s %d %.2f\n", p->name, p->id, p->salary); }
指針
p
從emps
(首地址)向后移動,直到末尾。
加薪函數
void give_raise(Employee *e, double pct) { e->salary *= (1 + pct/100); }
e->salary
等價(*e).salary
;直接修改了原內存中的
salary
。
加薪后打印
同上,只是數據已被give_raise
更新。釋放內存
free(emps);
Employee *emps = malloc(N * sizeof *emps);
這一行的目的是在堆上動態分配一塊足夠存放 N
個 Employee
結構體的連續內存,并讓指針 emps
指向它。分解來看:
?sizeof(Employee *)
——也就是指針本身的大小,通常是 8 字節,表示“*emps
的類型大小”,即 sizeof(Employee)
。)
Employee *emps
聲明了一個指針變量emps
,它將用來保存那塊新分配內存的起始地址。malloc(...)
從堆上申請一段未初始化的內存,返回一個void *
,隨后被賦值給emps
。N * sizeof *emps
*emps
的類型是Employee
,所以sizeof *emps
等價于sizeof(Employee)
——也就是一個員工記錄所占的字節數。將它乘以
N
,就得到存放N
個Employee
結構體所需的總字節數。
賦值給
emps
malloc
返回的void *
自動轉為Employee *
(在 C 中不需要顯式 cast),于是emps
就指向了這塊能容下N
個Employee
的內存。
之后你就可以像操作數組一樣,用 emps[0]
…emps[N-1]
來讀寫這些動態分配的 Employee
結構了。記得在最后用 free(emps);
釋放這段內存,避免泄露。
for (Employee *p = emps; p < emps + N; p++) {/* 循環體:用 p->… 訪問或修改當前 Employee */
}
初始化:
Employee *p = emps;
聲明了一個指針變量
p
,類型是 “指向Employee
的指針” (Employee *
)。把它初始化為
emps
,也就是指向剛剛用malloc
分配的結構體數組的第 0 個元素(emps[0]
)的地址。
循環條件:
p < emps + N
emps
是數組首地址,emps + N
是“跳過 N 個Employee
大小的字節”后的位置,也就是數組末尾之后的地址。只要
p
指向的地址 嚴格小于emps + N
(即還沒走到數組末尾后),就繼續執行循環體。這樣保證
p
會依次指向emps[0]
、emps[1]
…emps[N-1]
,不會越界。
迭代表達式:
p++
這是指針算術,每執行一次
p++
,p
都會向后移動 一個Employee
對象的大小,等價于p = p + 1;
。所以第一次循環
p==emps
(第 0 個),第二次p==emps+1
(第 1 個),……,直到p==emps+N-1
(第 N-1 個)。
整體流程
第一步:
p = emps;
指向第一個員工結構。檢查:
p < emps + N ?
對于 N=3,就檢查p < emps+3
,當p
是emps+2
時依然進入;當p
自增到emps+3
時條件不滿足,循環結束。循環體:在
{ … }
中,你可以寫printf("%s", p->name);
或give_raise(p, 10);
,p->member
就是訪問當前Employee
的成員。迭代:每次循環結束后執行
p++
,跳到下一個元素。
這種“用指針當下標” 的寫法在處理動態分配的數組、或者需要同時傳遞首地址和尾后指針(emps
和 emps+N
)時特別方便,也更貼近 C 底層對內存的操作方式。
Employee *p = &emps[i];
emps
是什么?在前面我們用
malloc
或Employee emps[N];
得到了一塊連續的內存,里面按順序存放了 N 個Employee
結構體對象。emps
在表達式里會退化為指向第 0 個元素的指針,類型是Employee *
。
emps[i]
得到第 i 個結構體通過數組下標
i
,emps[i]
就是第i
個Employee
變量,類型是Employee
。
&emps[i]
取出它的地址前面加上取地址符
&
,&emps[i]
的類型就是Employee *
,表示“指向第 i 個結構體”的指針。
把地址賦給
p
聲明
Employee *p
,就是一個可以保存Employee
對象地址的指針變量。p = &emps[i];
后,p
就指向了emps
數組中的第i
個元素。
后續怎么用?
既然
p
指向了那塊內存,就可以用箭頭運算符訪問或修改它的成員:p->id = 1234; printf("%s\n", p->name);
這等價于對
emps[i]
本身做操作:emps[i].id = 1234; printf("%s\n", emps[i].name);
總結:
Employee *p
是一個指向Employee
的指針;&emps[i]
是取得數組中第i
個結構體的地址;把它們結合,就能“通過指針”來訪問或修改
emps[i]
。
if (scanf("%49s %d %lf", p->name, &p->id, &p->salary) != 3)…
scanf
的格式串%49s
:讀入一個不含空白(空格、Tab、換行)的字符串,最多讀 49 個字符,自動在第 50 個位置寫入
'\0'
。這樣保證不會超出
p->name
的緩沖區(char name[50]
)。
%d
:讀入一個十進制整數,存到后面對應的int *
地址。%lf
:讀入一個雙精度浮點數(double
),存到對應的double *
地址。
參數列表
p->name
:p
是Employee *
,p->name
就是其內部char name[50]
數組名,退化成char *
,正好匹配%s
。不需要再寫
&p->name
,因為數組名已經是地址。
&p->id
:p->id
是一個int
,%d
需要int *
,所以要取地址。
&p->salary
:p->salary
是double
,%lf
需要double *
,同樣取地址。
返回值檢查
scanf
成功讀取并賦值的項數應該正好是 3(字符串、整數、浮點各一項)。如果不等于 3,就意味著輸入格式有誤,通常需要進入
if
塊做錯誤處理(如打印提示并退出)。
printf("姓名:%-10s 工號:%4d 工資:%.2f\n",p->name, p->id, p->salary);
格式串解釋
%-10s
:打印一個字符串,左對齊,占 10 個字符寬度,不足的右側補空格。%4d
:打印一個整數,右對齊,占 4 個字符寬度,左側不足補空格。%.2f
:打印一個浮點數,默認右對齊,保留 小數點后 2 位,小數點和整數部分一并計算寬度(這里未指定最小寬度)。
參數
p->name
:要打印的字符串起始地址。p->id
:要打印的整數學號。p->salary
:要打印的浮點工資。
舉例
如果p->name = "Alice"
、p->id = 42
、p->salary = 5230.5
,則輸出:姓名:Alice 工號: 42 工資:5230.50
"Alice"
占 5 字符,%-10s
會在后面補 5 個空格;" 42"
占 4 字符;"5230.50"
是默認緊湊輸出兩位小數。
總結
這兩行一行負責安全地從輸入流中讀取一個字符串、一個整數和一個雙精度浮點數,并檢查是否都讀對了;
另一行則用格式化對齊的方式,按“左對齊姓名、右對齊工號和保留兩位小數的工資”整齊地打印出來。
結構體指針的原理與作用
1. 內存與指針的關系
結構體對象
當你寫Employee emps[N];
或者用malloc
得到一塊連續內存時,系統會在內存中為每個Employee
分配一段固定大小的空間,按成員順序排列:[ name[50] ][ id (4B) ][ salary (8B) ]
[ name[50] ][ id (4B) ][ salary (8B) ] …
指針(
Employee *p
)p
保存的就是某個Employee
對象在內存中的起始地址(即它第一個成員name
的首字節地址)。p + 1
指針算術:當p
的類型是Employee *
時,p + 1
會自動跳過sizeof(Employee)
字節,指向下一個結構體對象。
2. 訪問與修改成員
點運算符
.
:用于結構體變量本身Employee e; e.id = 1001;
箭頭運算符
->
:用于結構體指針Employee *p = &e; p->id = 1001; // 等價于 (*p).id = 1001;
p->id
先解引用p
(得到一個結構體),再訪問它的id
成員。語法更簡潔,不需要寫
(*p).id
。
3. 作用與優勢
動態管理
用
malloc
/free
可在運行時靈活控制結構體數組的大小;只需存一個指針,不必用固定長度的全局或棧數組。
高效傳參
將指針傳給函數(如
give_raise(Employee *e, …)
),只復制 8 字節地址,不復制整個結構體(可能幾十字節甚至更多)。函數內部直接修改原對象,無需返回修改后的新副本。
遍歷與通用性
結構體數組指針
emps
既能像數組那樣使用下標emps[i]
,也能用指針算術for (Employee *p = emps; p < emps+N; p++)
遍歷;這種“首地址 + 元素大小” 的通用訪問方式,使得代碼更簡潔、可移植。
抽象與封裝
函數只關心“指向某個結構體”的地址,無需知道結構體在棧還是堆,也不關心它前后還有多少元素;
例如
give_raise
只用Employee *e
,對任何單個員工對象都通用。
小結
結構體指針 本質上就是保存了“某個結構體對象首地址”的變量。
通過
p->member
或者指針算術,你可以任意訪問、修改該對象乃至緊鄰的那些同類型對象。它讓我們可以在動態內存、函數調用和數據遍歷中,都以同一種“指針+大小” 的模式來高效操作結構化數據。