Skip to content

📖 ​C 语言快速入门 (下)


六、指针


6.1 指针の定义


  • 内存区域中的每字节都对应一个编号,这个编号就是 “地址” 。如果在程序中定义了一个变量,那么在对程序进行

    编译时,系统就会给这个变量分配内存单元,通过变量名直接访问变量的值称为 "直接访问" , 如 printf("%d",i)

    "scanf("%d",&i)" 等;另一种通过指针来访问变量的值称为 "间接访问" 。即将变量 i 的地址存放到另一个

    变量中,在 C 语言中,指针变量是一种特殊的变量,它用来存放变量地址。


  • 指针变量的定义格式如下:
c
基类型 *指针变量名
  • 例如:
c
int *i_pointer

  • 指针与指针变量是两个概念,一个变量的 地址 称为该变量的 "指针" 。例如,地址 2000 是变量 i

    的指针。如果有一个变量专门用来存放另一变量的地址(即指针),那么称它为 "指针变量"

    例如,下图中的 i_pointer 就是一个指针变量。

微信截图_20251007003517

📌 说明:

那么指针变量 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 个字节,但是指定了类型,它会按照基类型去访问特定大小空间,从而获取值。


  • 例:直接访问&间接访问
c
#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;
}

  • 运行效果:
微信截图_20251007010622

📌 注意:

  • 1). 指针变量前面的 "*" 表示声明这个变量是指针变量,不要想什么 “这个不是取值操作符吗???”
c
float *pointer_1

注意指针变量名是 pointer_1 , 在声明完指针变量后 *pointer_1 是按这个地址去取值


  • 2). 在定义指针变量时必须指定其类型。需要注意的是,只有整型变量的地址才能放到指向

    整型变量的指针变量中。例如:下面的赋值是错误的:

c
float a;
int *pointer_1;
pointer_1 = &a;  // 毫无意义而且会出错,完全没有必要浪费时间

  • 3). 如果已执行了语句
c
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 标识起作用,但其余两个变量只是普通的整型变量。要声明三个指针

    变量正确的语句应该是下面这样 :

c
int *a,*b,*c;

6.3 指针の应用场景


  • 很多初学者不喜欢使用指针,觉得使用指针容易出错,其实这只是因为没有掌握指针的使用场景。

    一般来说,指针的使用场景通常只有两个,即 传递偏移 , 读者应时刻记得只有在这两种场景

    下使用指针,才能准确地使用指针。


6.3.1 指针の传递


  • 【例1】:指针的传递使用场景:
c
#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 的地址 :
微信截图_20251007132907
  • 进入子函数再添加一个监视看看 j 的地址:
微信截图_20251007132927
  • 显然两个地址空间不一样,那么子函数改变的值是另一块地址空间的值,自然就跟 i 无关了。
  • 接下来看一下上面示例的原理图:
微信截图_20251007133430
  • 说明:如上图所示,程序的执行过程其实就是内存的变化过程,我们需要关注的是栈空间的变化。

    main 函数开始执行时,系统会为 main 函数开辟一片函数栈空间,当程序走到 int i 时,

    main 函数的栈空间就会为变量 i 分配 4 字节大小的空间。调用 change 函数时,系统会为

    change 函数重新分配一片新的函数栈空间,并为形参变量 j 分配 4 字节大小的空间。

    在调用 change(i) 时,实际上是将 i 的值赋值给 j,我们把这种效果称为 值传递

    (C 语言的函数调用均为值传递)。因此,当我们在 change 函数的函数栈空间内修改

    变量 j 的值后, change 函数执行结束,其栈空间就会被释放,j 就不再存在,i 的值不会改变


  • ❓ 那么难道子函数就修改 main 函数内某个变量的值了吗 答案是可以的,这就需要到指针了!

  • 【例2】:在子函数中修改 main 函数中某个变量的值

c
#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*aa+1*(a+1)

(a 是一个数组名,直接在它前面加一个取值运算符 *)

微信截图_20251008151846
  • a : 单独一个数组名,前面说过了,这代表这个 数组的起始地址

    (只不过在上面监视里要是只敲 a 看不出地址,所以只能显式的 &a , 但我们自己要知道 a 就是起始地址)

  • *a : 根据数组的起始地址,往后取值,取的位置是 数组起始地址走一个基类型的大小 , 这里说的基类型

    其实就是 int , 那么基类型的大小其实就是 sizeof(int) , 也就是 4 字节 。 那么从上图可以得知

    a 的起始地址是 0x61fe30 那么走一个基类型的大小,取值的范围就是 0x61fe30 - 0x61fe33 这个

    范围,就可以取到一个整型值 1

  • a+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]


  • 【例】:指针偏移使用场景
c
#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;
}

  • 运行效果:
微信截图_20251008154141

6.3.3 指针&数组传递


  • 在前面 5.4 小节已经讲过了 数组传递 ,那么在那节我们就知道,通过形参传过去的地址,或者说指针,

    它存储的是数组的首地址,它在 64 位程序中永远就是 8 个字节,无法从此得到数组的长度。那么在

    5.4 的时候我们通过传了个 len 参数来告诉子函数对应数组的大小。

  • 那么我们学过了取地址运算符和取值运算符,也可以再次重复一下那个例子:

c
#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】 动态内存申请:
c
#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 函数往对应空间存储字符数据

微信截图_20251008213058

6.5 栈空间&堆空间


  • 【例】栈空间&堆空间的差异
c
#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操作后才会释放,

    否则在进程执行过程中会一直有效。


微信截图_20260214034256

七、函数


7.1 函数的声明与定义


  • 函数的调用关系是:由主函数(main)调用其它函数,其它函数也可以互相调用。

    同一个函数可以被一个或多个函数调用任意次,如下图所示:

微信截图_20260214034808
  • 函数嵌套调用:
c
#include <stdio.h>

int printstar(int i);  //函数声明
void print_message();
c
#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);
}
c
#include "func.h"

int main()
{
    int a=10;
    a=printstar(a);
    print_message();
    printstar(a);
    return 0;
}

  • 上例中:有两个 c 文件,func.c 是子函数 printstarprint_message 的实现,也称定义;

    main.cmain 函数,func.h 中存放的是标准头文件的声明和 main 函数中调用的两个子函数的声明,

    如果不在头文件中对使用的函数进行声明,那么在编译时会出现警告。


【总结】C 语言的编译和执行具有以下特点✨


  • 🧩 (1) 模块化设计

一个 C 程序由一个或多个 程序模块 组成,每个程序模块作为一个 源程序文件

对于较大的程序,通常将程序内容分别放在若干源文件中,再由若干源程序文件组成一个 C 程序。

💡 好处:便于分别编写、分别编译,进而提高调试效率(复试有用哦~)

🔄 一个源程序文件可以为多个C程序共用


  • 📦 (2) 编译单位

一个源程序文件由一个或多个 函数 及其他有关内容(如命令行、数据定义等)组成。

🔑 关键点一个源程序文件是一个编译单位,程序编译时是以 源程序文件 为单位

而不是以 函数 为单位进行编译的!

📝 main.cfunc.c 分别单独编译 ,在链接成为可执行文件时,

main 中调用的函数 printstarprint_message 才会通过 链接 去找到函数定义的位置。


  • 🚀 (3) 程序入口

C 程序的执行是从 main 函数开始的!

  • 如果在 main 函数中调用其他函数,调用后会 返回到 main 函数
  • main 函数中结束 整个程序的运行

  • ⚖️ (4) 函数平等原则

所有函数都是 平行的

特点说明
✅ 可以做的函数间互相调用
❌ 不能做的不能嵌套定义不能调用 main 函数

🎯 main 函数是由 系统调用

上例 的调用链:

main() → 调用 print_message() → 调用 printstar()

我们把这种调用称为 嵌套调用 🪆


  • 📋 函数的声明 vs 定义
函数的定义函数的声明
🎯 作用对函数功能的确立把函数信息通知编译系统
📝 内容函数名、返回值类型、形参及其类型、函数体函数名、函数类型、形参的类型/个数/顺序
🏗️ 本质完整的、独立的函数单位让编译器能正确识别检查合法性

  • ⚠️ 隐式声明(不推荐!)

C语言中有几种声明的类型名可以省略:

🔢 函数如果不显式地声明返回值的类型,那么它默认返回整型

使用旧风格声明函数的形式参数时,如果省略参数的类型,那么编译器默认它们为整型。

🚫 然而,依赖隐式声明并不是好的习惯!

因为隐式声明容易让代码的读者产生疑问:

  • 编写者是否是有意遗漏了类型名?
  • 还是不小心忘记了?

显式声明 能够清楚地表达意图!


7.2 函数的分类&调用


👤从用户角度来看,函数分为如下两种。


  • 📚 (1) 标准函数(库函数)

库函数,这是由系统提供的,用户不必自己定义的函数,可以直接使用它们,如 printf 函数、scanf 函数。

📝 不同的C系统提供的库函数的数量和功能会有一些不同,但许多基本的函数是相同的。


  • ✏️ (2) 用户自定义函数

用以解决用户的专门需要


🔧 从函数的形式看,函数分为如下两类。


  • 🚫 (1) 无参函数

一般用来执行 指定的一组操作 。在调用无参函数时,主调函数 不向 被调用函数 传递数据


无参函数的定义形式如下:

c
类型标识符 函数名()
{
    声明部分
    语句部分
}

💡 在之前的例子中,print_message 就是无参函数。


  • 📤 (2) 有参函数

主调函数在调用被调用函数时,通过 参数 向被调用函数 传递数据


有参函数的定义形式如下:

c
类型标识符 函数名(形式参数表列)
{
    声明部分
    语句部分
}

💡 在之前例子中,printstar 就是有参函数,int i 对应的 i形式参数

主调函数和被调用函数之间存在 数据传递关系


7.3 函数的递归调用


  • 我们把 函数自身调用自身 的操作,称为 递归函数 ,递归函数一定要有结束条件,否则会产生死循环!

  • 假设现在要求读者写一个程序来 求数字 n 的阶乘,读者可能会觉得这很简单,写个 for 循环就可以

    实现,然而,使用递归来实现更好一些,因为使用递归在解决一些问题时,可以让问题变得简单,

    降低编程的难度。

  • 比如接下来的题目:假如有 n 个台阶,一次只能上 1 个台阶或 2 个台阶,请问走到第 n 个台阶有几种走法?

    为便于读者理解题意,这里举例说明如下:假如有 3 个台阶,那么总计就有 3 种走法:第一种为每次上 1 个

    台阶,上 3 次;第二种为先上 2 个台阶,再上 1 个台阶;第三种为先上 1 个台阶,再上 2 个台阶。

    具体实现如下。


  • 【例】求 n 的阶乘 & 走楼梯
c
#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 局部变量&全局变量


  • 【例】全局变量的使用
c
#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;
}

微信截图_20260214044859
  • ❓ 全局变量存储在哪

  • 如上例所示,全局变量 i 存储在数据段,所以 main 函数和 print 函数都是可见的,

    全局变量不会因为某个函数执行结束而消失,在整个进程的执行过程中始终有效,

    因此工作中应尽量避免使用全局变量!在前几章中,我们在函数内定义的变量都称为局部变量,

    局部变量存储在自己的函数对应的栈空间内,函数执行结束后,函数内的局部变量所分配的空间将

    会得到释放。如果局部变量与全局变量重名,那么将采取就近原则,即实际获取和修改的值是局部变量的值。

微信截图_20260214045247

八、结构体


  • 在 C 语言中,结构体(struct) 是一种用户 的数据类型,它允许你将 的数据组合在一起。

    结构体是处理复杂数据结构的基础,常用于描述具有多个属性的对象,如学生信息、坐标点等。


8.1 结构体的定义


  • 你可以使用关键字 struct 来定义一个结构体,结构体内部可以包含多个不同类型的成员变量。
c
struct 结构体名 {
    数据类型 成员1;
    数据类型 成员2;
    ...
};

  • 示例:定义一个包含学生信息的结构体
c
#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 定义


  • 如果你需要存储多个相同类型的结构体,可以使用 结构体数组 。这使得管理多个对象变得更加方便。
c
struct 结构体名 数组名[数组大小];

  • 示例:定义一个结构体数组
c
#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 列表初始化 & 指针访问


  • 可以见到前面的赋值语句有点太麻烦了( 一个一个成员赋值 ), 那么可以使用如下:
c
struct Student students[3] = {{"Alice", 18, 90.5}, {"Bob", 19, 88.0}, {"Charlie", 20, 85.0}};
  • 这是 C 语言的 聚合初始化器(Aggregate Initializer),也叫 列表初始化大括号初始化

  • 基本规则:按成员声明顺序一一对应

c
struct Student {
    char name[50];   // ← 第1个:"Alice"
    int age;         // ← 第2个:18
    float grade;     // ← 第3个:90.5
};

  • 此外指针与结构体数组结合使用,可以更加灵活地遍历和操作结构体数组。

    指针可以指向结构体数组中的元素,通过指针递增,可以访问数组中的每一个结构体。


  • 示例:列表初始化 & 通过指针访问结构体数组
c
#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). 原代码:使用 ->
c
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). 使用 . 等价
c
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). 更直观的 . 写法
c
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). 用下标(最简洁)
c
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 对象,所以用 .

  • 常见优先级陷阱:
c
*ptr + i      // ❌ 错误!等价于 (*ptr) + i,指针算术变整数运算
*(ptr + i)    // ✓ 正确!先算指针偏移,再解引用

. 的优先级高于 *,所以必须加括号:

c
*ptr.age      // ❌ 错误!等价于 *(ptr.age),先找 ptr.age 再解引用
(*ptr).age    // ✓ 正确!先解引用 ptr,再取 age

  • 👍 在实际写代码中 :ptr[i].age 最常用,既清晰又简洁!

8.2.4 typedef


  • typedef 用于为已有的数据类型创建一个新的名字 (别名),让代码更简洁、可读性更强。
c
typedef 原类型 新名字;

  • 使用 typedef 修改前面例子的代码:
c
#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;
}

  • 配合结构体指针使用:
c
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;

  • 常用的替换:
c
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 , short2Bint4B
    • 要求二:struct 必须是成员中
    • 要求三:struct 是成员中的

8.3.2 【补充】alignof


  • 【前置补充】: C++11 引入的 alignof 运算符
c++
alignof(类型名)
  • 返回一个 size_t 值,表示该类型需要的 字节对齐数。

  • 简单示例:

c++
#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 示例一


c++
#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;
}

  • 运行结果:
微信截图_20260306053605
  • 内存构造:
微信截图_20260306053615

8.3.3 示例二