知识点
结构体指针
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结构体所需的总字节数。
赋值给
empsmalloc返回的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 + Nemps是数组首地址,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或者指针算术,你可以任意访问、修改该对象乃至紧邻的那些同类型对象。它让我们可以在动态内存、函数调用和数据遍历中,都以同一种“指针+大小” 的模式来高效操作结构化数据。