📖 C 语言快速入门 (下)
六、指针
6.1 指针の定义
内存区域中的每字节都对应一个编号,这个编号就是 “地址” 。如果在程序中定义了一个变量,那么在对程序进行
编译时,系统就会给这个变量分配内存单元,通过变量名直接访问变量的值称为
"直接访问", 如printf("%d",i)、
"scanf("%d",&i)"等;另一种通过指针来访问变量的值称为"间接访问"。即将变量 i 的地址存放到另一个变量中,在 C 语言中,指针变量是一种特殊的变量,它用来存放变量地址。
- 指针变量的定义格式如下:
基类型 *指针变量名- 例如:
int *i_pointer指针与指针变量是两个概念,一个变量的
地址称为该变量的"指针"。例如,地址 2000 是变量 i的指针。如果有一个变量专门用来存放另一变量的地址(即指针),那么称它为
"指针变量"。例如,下图中的
i_pointer就是一个指针变量。

📌 说明:
那么指针变量 i_pointer 本身占多大的内存空间呢?
本章中编写的程序都是 64 位的应用程序,寻址范围为 64 位 即 8 字节,所以对于
本章来说
sizeof(i_pointer)=8。但是如果编写的程序是 32 位的,那么寻址范围就是 4 字节
(考研中往往会强调程序是 32 位的程序)。
6.2 取地址操作符&取值操作符
取地址操作符为
&, 也称引用, 通过该操作符我们可以获取一个变量的地址值;取值操作符为
*, 也称解引用, 通过该操作符我们可以得到一个地址对应的数据。如下例所示,我们通过
&i获取整型变量i的地址值,然后对整型指针变量p进行初始化,p中存储的是整型变量i的地址值,所以通过*p(printf函数中的*p) 就可以获取到整型变量
i的值。p中存储的是一个绝对地址值❓ 那为什么取值时会获取 4 字节大小的空间呢
这是因为
p为整型变量指针,每个int型数据占用 4 字节大小的空间,所以p在解引用时会访问 4 字节大小的空间。并且这也是为什么指针变量需要指定类型的原因,虽然它们在 64 位程序里
都是占用 8 个字节,但是指定了类型,它会按照基类型去访问特定大小空间,从而获取值。
- 例:直接访问&间接访问
#include <stdio.h>
int main() {
int i = 5;
//定义了一个指针变量,i_pointer 就是指针变量名
//指针变量的初始化是某个变量取地址来赋值,不能随便写个数
int *i_pointer=&i;
printf("i=%d\n",i); //直接访问
printf("i_pointer=%d\n",*i_pointer); //间接访问
return 0;
}- 运行效果:

📌 注意:
- 1). 指针变量前面的
"*"表示声明这个变量是指针变量,不要想什么 “这个不是取值操作符吗???”
float *pointer_1注意指针变量名是 pointer_1 , 在声明完指针变量后 *pointer_1 是按这个地址去取值
2). 在定义指针变量时必须指定其类型。需要注意的是,只有整型变量的地址才能放到指向
整型变量的指针变量中。例如:下面的赋值是错误的:
float a;
int *pointer_1;
pointer_1 = &a; // 毫无意义而且会出错,完全没有必要浪费时间- 3). 如果已执行了语句
pointer_1 = &a;那么 &*pointer_1 的含义是什么呢?
答案:
"&"和"*"两个运算符的优先级别相同,但要自右向左的方向结合。因此,&*pointer_1与&a相同,都表示变量 a 的地址,也就是pointer_1
类似地,
*&a的含义是什么呢?首先进行
&a运算,得到 a 的地址,再进行*运算。*&a和*pointer_1的作用是一样的,他们都等价于变量 a , 即
*&a与 a 等价
4). C 语言本质上是一种自由形式的语言,这很容易诱使我们把
"*"写在靠近类型的一侧,如
int* a这个声明与int *a这个声明具有相同的意思,而且看上去更清晰, a 被声明成类型为
int*的指针。但是,这并不是一个好习惯,因为类似int *a,b,c的语句会使人们很自然地认为这条语句把所有三个变量声明为指向整型的指针,但事实上并非如此,
*实际上是
*a的一部分,只对 a 标识起作用,但其余两个变量只是普通的整型变量。要声明三个指针变量正确的语句应该是下面这样 :
int *a,*b,*c;6.3 指针の应用场景
很多初学者不喜欢使用指针,觉得使用指针容易出错,其实这只是因为没有掌握指针的使用场景。
一般来说,指针的使用场景通常只有两个,即
传递&偏移, 读者应时刻记得只有在这两种场景下使用指针,才能准确地使用指针。
6.3.1 指针の传递
- 【例1】:指针的传递使用场景:
#include <stdio.h>
void change(int j){
j = 5;
}
int main() {
int i = 10;
printf("before change i=%d\n",i); //这里打断点
change(i); //在这一步按 向下箭头,进入 change 函数
printf("after change i=%d\n",i);
return 0;
}在上面的 例1 中,定义了整型变量 i ,其初始化为 10 ,然后通过子函数试图修改整型变量 i 的值。但是
我们发现执行语句
printf("after change i=%d\n",i);后,打印 i 的值仍为 10 。
- 在主函数中添加一个监视看看 i 的地址 :

- 进入子函数再添加一个监视看看 j 的地址:

- 显然两个地址空间不一样,那么子函数改变的值是另一块地址空间的值,自然就跟 i 无关了。
- 接下来看一下上面示例的原理图:

说明:如上图所示,程序的执行过程其实就是内存的变化过程,我们需要关注的是栈空间的变化。
当
main函数开始执行时,系统会为main函数开辟一片函数栈空间,当程序走到int i时,main函数的栈空间就会为变量 i 分配 4 字节大小的空间。调用change函数时,系统会为change函数重新分配一片新的函数栈空间,并为形参变量 j 分配 4 字节大小的空间。在调用
change(i)时,实际上是将 i 的值赋值给 j,我们把这种效果称为值传递(C 语言的函数调用均为值传递)。因此,当我们在
change函数的函数栈空间内修改变量 j 的值后,
change函数执行结束,其栈空间就会被释放,j 就不再存在,i 的值不会改变
❓ 那么难道子函数就修改
main函数内某个变量的值了吗 答案是可以的,这就需要到指针了!【例2】:在子函数中修改
main函数中某个变量的值
#include <stdio.h>
void change(int *j){ //形参位置的 * 意思是声明指针变量
*j = 5; //这里的 * 是取值运算符,按传过来的地址取值
}
//指针的传递
int main() {
int i = 10;
printf("before change i=%d\n",i);
change(&i); //传递变量 i 的地址
printf("after change i=%d\n",i);
return 0;
}我们可以看到程序执行后,语句
printf("after change i=%d\n",i);打印的 i 的值为 5,难道 C 语言函数调用值传递的原理变了❓ 并非,我们这里是将变量 i 的地址传递
给
change函数,也就是实参位置写的&i, 实际效果是j=&i,依然是值传递,只是这时我们的 j 是一个指针变量,内部存储的是变量 i 的地址,所以通过
*j就间接访问到了与变量 i 相同的区域,通过
*j=5就实现了对变量 i 的值的改变。通过单步调试,我们依然可以看到变量 j 自身的地址是与变量 i 的地址不相等的。
6.3.2 指针の偏移
前面介绍了指针的传递。指针即地址,就像我们找到了一栋楼,这栋楼的楼号是
B,那么往前就是
A, 往后就是C, 所以应用指针的另一个场景就是对其进行加减,但对指针进行乘除是没有意义的,就像家庭住址乘以 5 没有意义那样。在工作中,
我们把对
指针的加减称为指针的偏移,加就是向后偏移,减就是向前偏移。
📌 说明:关于 a 、*a、 a+1 & *(a+1)
(a 是一个数组名,直接在它前面加一个取值运算符 *)

a: 单独一个数组名,前面说过了,这代表这个数组的起始地址(只不过在上面监视里要是只敲 a 看不出地址,所以只能显式的
&a, 但我们自己要知道a就是起始地址)*a: 根据数组的起始地址,往后取值,取的位置是数组起始地址走一个基类型的大小, 这里说的基类型其实就是
int, 那么基类型的大小其实就是sizeof(int), 也就是 4 字节 。 那么从上图可以得知a 的起始地址是
0x61fe30那么走一个基类型的大小,取值的范围就是0x61fe30 - 0x61fe33这个范围,就可以取到一个整型值
1a+1: 这里就涉及到了数组的偏移,前面说单独一个 a 是数组 a 的起始地址,那么a+1的含义就是基于数组的起始地址加一个基类型的大小,也就是
0x61fe30 + 4 => 0x61fe34,那么一般地,
a+i的含义就是 数组的起始地址加i个基类型的大小*(a+1): 前面说取值运算符会往后取一个基类型大小的空间,那么这里a+1知道是0x61fe34,那么
*(a+1)取的范围就是0x61fe34 - 0x61fe37这个范围对应的值,那么取到的就是一个整型值
2。再补充一句:*(a+1)等价于写a[1], 底层编译后的汇编代码其实是一样的。那么我们可以延伸
*(a+i)其实就等价于a[i]
- 【例】:指针偏移使用场景
#include <stdio.h>
//复习使用符号常量
#define N 5
//指针的偏移
int main() {
int a[N] = {1,2,3,4,5};
int *p;
int i;
p = a; //保证等号两边的数值类型一致
for(i=0;i<N;i++){ //正序输出
printf("%3d",*(p+i));
}
printf("\n---------------\n"); //division
p = &a[4]; //让p指向最后一个元素
for(i=0;i<N;i++){
printf("%3d",*(p-i));
}
printf("\n");
return 0;
}- 运行效果:

6.3.3 指针&数组传递
在前面
5.4小节已经讲过了数组传递,那么在那节我们就知道,通过形参传过去的地址,或者说指针,它存储的是数组的首地址,它在 64 位程序中永远就是 8 个字节,无法从此得到数组的长度。那么在
5.4的时候我们通过传了个len参数来告诉子函数对应数组的大小。那么我们学过了取地址运算符和取值运算符,也可以再次重复一下那个例子:
#include <stdio.h>
void change(char *d){ //前面使用 d[] 这种形式,但其实和 *d 是等价的 ,都是声明参数是指针
*d = 'H';
d[1] = 'E';
*(d+2) = 'L';
}
int main() {
char c[10] = "hello";
change(c); //数组名存储的是数组起始地址
puts(c); // output : HELlo
return 0;
}6.4 malloc 动态内存申请
在前面我们学习过 C 语言的数组后,相信不少人都会觉得数组长度固定很不方便,
其实 C 语言的数组长度固定是因为其定义的 整型、浮点型、字符型、数组变量都在
栈空间中,而栈空间的大小在编译时是确定的。如果使用的空间大小不确定,那么就要使用
堆空间, 请看下面示例:
- 【例1】 动态内存申请:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
int size; //size代表我们要申请多大字节的空间
char *p; //void*类型的指针是不能偏移的,因此不会定义无类型指针
scanf("%d",&size); //输入要申请的堆空间大小
//malloc 返回的是 void* 无类型指针
p = (char*)malloc(size);
strcpy(p,"malloc success");
puts(p);
free(p); //释放申请的堆空间
printf("free success\n");
return 0;
}📌 说明:
1).
malloc函数需要导入头文件#include <stdlib.h>2). 对于
malloc函数void* malloc(size_t size)需要传递一个整型变量指定申请的堆空间的大小,返回值为
void*无类型指针。3).
void*类型的指针只能用来存储一个地址而不能偏移,因为malloc向操作系统申请一片堆空间的地址,它并不知道我们申请的空间用来存放什么
类型的数据,所以确定要用来存储什么类型的数据后,都会将
void*强制类型转换为对应的类型,如上例我们转换为
char*4). 需要注意:指针本身大小,和其指向的空间大小,是两码事!!!
如下图所示:定义的整型变量 i 、指针变量 p 均在 main 函数的栈空间中,
通过 malloc 申请的空间会返回一个堆空间的首地址,我们把首地址存入
变量 p 。知道了首地址,就可以通过 strcpy 函数往对应空间存储字符数据

6.5 栈空间&堆空间
- 【例】栈空间&堆空间的差异
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 函数栈空间释放后,函数内的所有局部变量消失
char* print_stack(){
char c[20] = "I am print_stack";
puts(c); // 在函数内访问
return c;
}
char* print_malloc(){
char *p;
p = (char*)malloc(20);
strcpy(p, "I am print_malloc");
puts(p); // 在函数内访问
return p;
}
int main()
{
char *p;
p = print_stack(); // 数据放在栈空间
printf("p = %s\n", p);
p = print_malloc(); // 数据放在堆空间
puts(p);
return 0;
}上例 中代码的执行结果如下图所示。为什么第二次打印会有异常?原因是
print_stack()函数中的字符串存放在栈空间中,函数执行结束后,栈空间会被释放,字符数组 c 的原有空间已被分配给其他函数使用,
因此在调用
print_stack()函数后,printf("p=%s\n",p);中的 p 不能获取栈空间的数据。而
print_malloc()函数中的字符串存放在堆空间中,堆空间只有在执行free操作后才会释放,否则在进程执行过程中会一直有效。

七、函数
7.1 函数的声明与定义
函数的调用关系是:由主函数(main)调用其它函数,其它函数也可以互相调用。
同一个函数可以被一个或多个函数调用任意次,如下图所示:

- 函数嵌套调用:
#include <stdio.h>
int printstar(int i); //函数声明
void print_message();#include "func.h"
int printstar(int i) //i即为形式参数
{
printf("************************\n");
printf("printstar %d\n",i);
return i+3;
}
void print_message() //可以调用printstar
{
printf("how do you do\n");
printstar(3);
}#include "func.h"
int main()
{
int a=10;
a=printstar(a);
print_message();
printstar(a);
return 0;
}上例中:有两个
c文件,func.c是子函数printstar和print_message的实现,也称定义;main.c是main函数,func.h中存放的是标准头文件的声明和main函数中调用的两个子函数的声明,如果不在头文件中对使用的函数进行声明,那么在编译时会出现警告。
【总结】C 语言的编译和执行具有以下特点✨
- 🧩 (1) 模块化设计
一个 C 程序由一个或多个 程序模块 组成,每个程序模块作为一个 源程序文件 。
对于较大的程序,通常将程序内容分别放在若干源文件中,再由若干源程序文件组成一个 C 程序。
💡 好处:便于分别编写、分别编译,进而提高调试效率(复试有用哦~)
🔄 一个源程序文件可以为多个C程序共用
- 📦 (2) 编译单位
一个源程序文件由一个或多个 函数 及其他有关内容(如命令行、数据定义等)组成。
🔑 关键点:一个源程序文件是一个编译单位,程序编译时是以 源程序文件 为单位
而不是以 函数 为单位进行编译的!
📝 main.c 和 func.c 分别单独编译 ,在链接成为可执行文件时,
main 中调用的函数 printstar 和 print_message 才会通过 链接 去找到函数定义的位置。
- 🚀 (3) 程序入口
C 程序的执行是从 main 函数开始的!
- 如果在 main 函数中调用其他函数,调用后会 返回到 main 函数
- 在 main 函数中结束 整个程序的运行
- ⚖️ (4) 函数平等原则
所有函数都是 平行的!
| 特点 | 说明 |
|---|---|
| ✅ 可以做的 | 函数间互相调用 |
| ❌ 不能做的 | 不能嵌套定义、不能调用 main 函数 |
🎯
main函数是由 系统调用 的
上例 的调用链:
main() → 调用 print_message() → 调用 printstar()我们把这种调用称为 嵌套调用 🪆
- 📋 函数的声明 vs 定义
| 函数的定义 | 函数的声明 | |
|---|---|---|
| 🎯 作用 | 对函数功能的确立 | 把函数信息通知编译系统 |
| 📝 内容 | 函数名、返回值类型、形参及其类型、函数体 | 函数名、函数类型、形参的类型/个数/顺序 |
| 🏗️ 本质 | 完整的、独立的函数单位 | 让编译器能正确识别并检查合法性 |
- ⚠️ 隐式声明(不推荐!)
C语言中有几种声明的类型名可以省略:
🔢 函数如果不显式地声明返回值的类型,那么它默认返回整型
使用旧风格声明函数的形式参数时,如果省略参数的类型,那么编译器默认它们为整型。
🚫 然而,依赖隐式声明并不是好的习惯!
因为隐式声明容易让代码的读者产生疑问:
- 编写者是否是有意遗漏了类型名?
- 还是不小心忘记了?
✅ 显式声明 能够清楚地表达意图!
7.2 函数的分类&调用
👤从用户角度来看,函数分为如下两种。
- 📚 (1) 标准函数(库函数)
即库函数,这是由系统提供的,用户不必自己定义的函数,可以直接使用它们,如 printf 函数、scanf 函数。
📝 不同的C系统提供的库函数的数量和功能会有一些不同,但许多基本的函数是相同的。
- ✏️ (2) 用户自定义函数
用以解决用户的专门需要。
🔧 从函数的形式看,函数分为如下两类。
- 🚫 (1) 无参函数
一般用来执行 指定的一组操作 。在调用无参函数时,主调函数 不向 被调用函数 传递数据 。
无参函数的定义形式如下:
类型标识符 函数名()
{
声明部分
语句部分
}💡 在之前的例子中,
print_message就是无参函数。
- 📤 (2) 有参函数
主调函数在调用被调用函数时,通过 参数 向被调用函数 传递数据 。
有参函数的定义形式如下:
类型标识符 函数名(形式参数表列)
{
声明部分
语句部分
}💡 在之前例子中,
printstar就是有参函数,int i对应的i为 形式参数,主调函数和被调用函数之间存在 数据传递关系 。
7.3 函数的递归调用
- 我们把 函数自身调用自身 的操作,称为
递归函数,递归函数一定要有结束条件,否则会产生死循环!
假设现在要求读者写一个程序来
求数字 n 的阶乘,读者可能会觉得这很简单,写个for循环就可以实现,然而,使用递归来实现更好一些,因为使用递归在解决一些问题时,可以让问题变得简单,
降低编程的难度。
比如接下来的题目:假如有 n 个台阶,一次只能上 1 个台阶或 2 个台阶,请问走到第 n 个台阶有几种走法?
为便于读者理解题意,这里举例说明如下:假如有 3 个台阶,那么总计就有 3 种走法:第一种为每次上 1 个
台阶,上 3 次;第二种为先上 2 个台阶,再上 1 个台阶;第三种为先上 1 个台阶,再上 2 个台阶。
具体实现如下。
- 【例】求 n 的阶乘 & 走楼梯
#include <stdio.h>
//求 n 的阶乘
int f(int n)
{
if(1==n)
return 1;
return n * f(n-1);
}
//走楼梯
int step(int n)
{
if(1==n)
return 1;
if(2==n)
return 2;
return step(n-1) + step(n-2);
}
int main()
{
int n;
int ret;
// 1. 求 n 的阶乘
scanf("%d", &n); //请输入数字的大小
ret=f(n);
printf("%d\n", ret);
// 2. 走楼梯
scanf("%d", &n); //请输入台阶数
ret=step(n);
printf("%d\n", ret);
return 0;
}7.4 局部变量&全局变量
- 【例】全局变量的使用
#include <stdio.h>
int i = 10; //全局变量
void print(int a)
{
printf("print i = %d\n",i);
}
int main()
{
{
int j = 5;
} //局部变量的有效范围是离自己最近的花括号
printf("main i = %d\n",i);
i = 5;
print(i);
return 0;
}
❓ 全局变量存储在哪
如上例所示,全局变量
i存储在数据段,所以main函数和print函数都是可见的,全局变量不会因为某个函数执行结束而消失,在整个进程的执行过程中始终有效,
因此工作中应尽量避免使用全局变量!在前几章中,我们在函数内定义的变量都称为局部变量,
局部变量存储在自己的函数对应的栈空间内,函数执行结束后,函数内的局部变量所分配的空间将
会得到释放。如果局部变量与全局变量重名,那么将采取就近原则,即实际获取和修改的值是局部变量的值。

八、结构体
在 C 语言中,结构体(struct) 是一种用户 的数据类型,它允许你将 的数据组合在一起。
结构体是处理复杂数据结构的基础,常用于描述具有多个属性的对象,如学生信息、坐标点等。
8.1 结构体的定义
- 你可以使用关键字
struct来定义一个结构体,结构体内部可以包含多个不同类型的成员变量。
struct 结构体名 {
数据类型 成员1;
数据类型 成员2;
...
};- 示例:定义一个包含学生信息的结构体
#include<stdio.h>
// 定义一个结构体来存储学生信息
struct Student {
char name[50];
int age;
float grade;
};
int main() {
struct Student s1; // 定义结构体变量
// 给结构体成员赋值
s1.age = 20;
s1.grade = 85.5;
// 输出学生信息
printf("学生的年龄: %d\n", s1.age);
printf("学生的成绩: %.2f\n", s1.grade);
return 0;
}在这个例子中,定义了一个名为
Student的结构体,它包含了name(学生名字)、age(年龄)和
grade(成绩)三个成员。结构体变量s1通过.成员名访问和修改各个成员。
8.2 结构体数组
8.2.1 定义
- 如果你需要存储多个相同类型的结构体,可以使用 结构体数组 。这使得管理多个对象变得更加方便。
struct 结构体名 数组名[数组大小];- 示例:定义一个结构体数组
#include<stdio.h>
// 定义一个结构体来存储学生信息
struct Student {
char name[50];
int age;
float grade;
};
int main() {
struct Student students[2]; // 定义一个包含两个学生信息的结构体数组
// 给第一个学生赋值
students[0].age = 20;
students[0].grade = 88.5;
// 给第二个学生赋值
students[1].age = 22;
students[1].grade = 90.0;
// 输出第一个学生的信息
printf("第一个学生的年龄: %d\n", students[0].age);
printf("第一个学生的成绩: %.2f\n", students[0].grade);
// 输出第二个学生的信息
printf("第二个学生的年龄: %d\n", students[1].age);
printf("第二个学生的成绩: %.2f\n", students[1].grade);
return 0;
}在这个例子中,定义了一个包含两个学生信息的结构体数组
students。通过访问数组中的元素,可以分别设置每个学生的属性。
8.2.2 列表初始化 & 指针访问
- 可以见到前面的赋值语句有点太麻烦了( 一个一个成员赋值 ), 那么可以使用如下:
struct Student students[3] = {{"Alice", 18, 90.5}, {"Bob", 19, 88.0}, {"Charlie", 20, 85.0}};这是 C 语言的 聚合初始化器(Aggregate Initializer),也叫 列表初始化 或 大括号初始化 。
基本规则:按成员声明顺序一一对应
struct Student {
char name[50]; // ← 第1个:"Alice"
int age; // ← 第2个:18
float grade; // ← 第3个:90.5
};此外指针与结构体数组结合使用,可以更加灵活地遍历和操作结构体数组。
指针可以指向结构体数组中的元素,通过指针递增,可以访问数组中的每一个结构体。
- 示例:列表初始化 & 通过指针访问结构体数组
#include<stdio.h>
// 定义一个结构体存储学生信息
struct Student {
char name[50];
int age;
float grade;
};
int main() {
struct Student students[3] = {{"Alice", 18, 90.5}, {"Bob", 19, 88.0}, {"Charlie", 20, 85.0}};
struct Student *ptr = students; // 定义指向结构体数组的指针
// 使用指针遍历结构体数组
for (int i = 0; i < 3; i++) {
printf("学生 %d 的年龄: %d\n", i + 1, (ptr + i) -> age);
printf("学生 %d 的成绩: %.2f\n", i + 1, (ptr + i) -> grade);
}
return 0;
}在这个例子中,定义了指针
ptr指向结构体数组students。通过指针递增和箭头操作符
->,我们可以访问结构体数组中的每个成员。
8.2.3 -> & . 的区别
❗ 通过本小节可以极大加深对 8.2.2 小节示例代码的理解 !!!
| 运算符 | 使用场景 | 含义 |
|---|---|---|
-> | 指针 访问成员 | p->member 等价于 (*p).member |
. | 对象/变量 访问成员 | 直接访问 |
- 在
8.2.2中 我们使用的是->, 那么可以等价出不同写法:
- 1). 原代码:使用
->
for (int i = 0; i < 3; i++) {
printf("学生 %d 的年龄: %d\n", i + 1, (ptr + i) -> age);
printf("学生 %d 的成绩: %.2f\n", i + 1, (ptr + i) -> grade);
}- 2). 使用
.等价
for (int i = 0; i < 3; i++) {
printf("学生 %d 的年龄: %d\n", i + 1, (*(ptr + i)).age);
printf("学生 %d 的成绩: %.2f\n", i + 1, (*(ptr + i)).grade);
}- 解释:
ptr + i→ 指向第i个结构体的指针(地址)*(ptr + i)→ 解引用,得到第i个结构体本身(对象)(*(ptr + i)).age→ 用.访问成员
- 3). 更直观的
.写法
for (int i = 0; i < 3; i++) {
struct Student s = *(ptr + i); // 先解引用取出结构体
printf("学生 %d 的年龄: %d\n", i + 1, s.age); // 直接用 .
printf("学生 %d 的成绩: %.2f\n", i + 1, s.grade);
}- 4). 用下标(最简洁)
for (int i = 0; i < 3; i++) {
printf("学生 %d 的年龄: %d\n", i + 1, ptr[i].age); // ptr[i] 等价于 *(ptr+i)
printf("学生 %d 的成绩: %.2f\n", i + 1, ptr[i].grade);
}- 本质:
ptr[i]是*(ptr + i)的语法糖,结果是一个struct Student对象,所以用.
- 常见优先级陷阱:
*ptr + i // ❌ 错误!等价于 (*ptr) + i,指针算术变整数运算
*(ptr + i) // ✓ 正确!先算指针偏移,再解引用
.的优先级高于*,所以必须加括号:
*ptr.age // ❌ 错误!等价于 *(ptr.age),先找 ptr.age 再解引用
(*ptr).age // ✓ 正确!先解引用 ptr,再取 age- 👍 在实际写代码中 :
ptr[i].age最常用,既清晰又简洁!
8.2.4 typedef
typedef用于为已有的数据类型创建一个新的名字 (别名),让代码更简洁、可读性更强。
typedef 原类型 新名字;- 使用
typedef修改前面例子的代码:
#include<stdio.h>
// typedef struct Student {
// char name[50];
// int age;
// float grade;
// };
// 可以是有 Student(上面那样) 也能没有(有别名就用别名了) --> 匿名结构体
typedef struct {
char name[50];
int age;
float grade;
}stu;
int main() {
// 使用别名声明的结构体数组
stu stuArr[3] = {{"Alice", 18, 90.5}, {"Bob", 19, 88.0}, {"Charlie", 20, 85.0}};
// 遍历结构体数组
for (int i = 0; i < 3; i++) {
// 使用别名声明当前遍历到的学生
stu temp = stuArr[i];
printf("学生 %d 的年龄: %d\n", i + 1, temp.age);
printf("学生 %d 的成绩: %.2f\n", i + 1, temp.grade);
}
return 0;
}- 配合结构体指针使用:
typedef struct Node {
int data;
struct Node* next; // 这里不能用 Node*,因为 typedef 还没完成
} Node, *NodePtr; // Node = struct Node, NodePtr = struct Node*
Node n1; // 结构体变量
NodePtr p = &n1; // 结构体指针,等价于 Node *p;
p->data = 100;- 常用的替换:
typedef long long ll; // ll = long long
typedef pair<int, int> PII; // PII = pair<int,int>8.3 边界对齐【408 重点】
8.3.1 三个要求
- 在 C 语言的
struct类型中, “ 边界对齐 ” 一般有如下要求:- 要求一 :,
char类型对齐值为1B,short为2B,int为4B - 要求二:
struct的 必须是成员中 - 要求三:
struct的 是成员中的
- 要求一 :,
8.3.2 【补充】alignof
- 【前置补充】:
C++11引入的alignof运算符
alignof(类型名)返回一个
size_t值,表示该类型需要的 字节对齐数。简单示例:
#include <iostream>
using namespace std;
int main() {
cout << "char: " << alignof(char) << endl; // 1
cout << "short: " << alignof(short) << endl; // 2
cout << "int: " << alignof(int) << endl; // 4
cout << "double: " << alignof(double) << endl; // 8
cout << "pointer: " << alignof(int*) << endl; // 4 / 8
return 0;
}8.3.2 示例一
#include <iostream>
using namespace std;
struct Example {
char a; // 对齐值为 1, 占用 1 个字节
short b; // 对齐值为 2, 占用 2 个字节
int c; // 对齐值为 4, 占用 4 个字节
};
int main() {
struct Example example;
// 使用 alignof 获取结构体 Example 的对齐要求
size_t alignof_val = alignof(struct Example);
printf("a 的地址: %p\n", &example.a);
printf("b 的地址: %p\n", &example.b);
printf("c 的地址: %p\n", &example.c);
printf("Size of struct Example: %d bytes\n", sizeof(example));
printf("alignof_val: %d\n", alignof_val);
return 0;
}- 运行结果:

- 内存构造:

