知识点
结构体指针
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).salary
e->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
或者指针算术,你可以任意访问、修改该对象乃至紧邻的那些同类型对象。它让我们可以在动态内存、函数调用和数据遍历中,都以同一种“指针+大小” 的模式来高效操作结构化数据。