课件

5.pdf

5.1 指针基础

notes

简述

指针是非常重要的概念,玩 C/C++ 主要就是玩指针、玩内存……

指针

  • 指针(Pointer)变量是一种特殊的变量,变量中存放的不是某种数据类型的数据,而是另一个变量或常量的地址。
  • 作用:可以通过指针来访问对应地址空间的变量的值
  • 指针非常灵活,和数组,函数结合起来功能特别强大
  • 灵活性使得初学者觉得很难
  • 指针变量可以和其他数据类型的变量一起定义
  • 指针是分类型的
  • 指针是可以参与运算的

使用指针前,必须明确指针的指向

  • 如果没有进行初始化,指针变量中存储的是一个 随机的地址值,这样就会出现 不可预测的问题
  • 在进行指针运算时,需要保证指针变量指向的内存地址确实存在,否则就会产生一些不可预期的结果,例如悬空指针、野指针等
  • 如果在定义指针的时候,我们没法明确指针的具体指向,此时可以将指针初始化为 空指针(NULL)或一个有效的地址值

忠告:

  • 永远清楚每个指针指向了哪里,指针必须指向一块有意义的内存
  • 永远清楚每个指针指向的内容是什么
  • 永远不要使用未经初始化的指针变量

指针存储的值

  • 存储着另一个变量的内存地址
  • 可以使用 & 取地址运算符获取另一个变量的地址并将其赋值给指针变量
  • 指针变量存储的是地址值,要访问该地址上的值,需要使用 ***** 取内容运算符 进行 间接访问
  • 需要注意访问的地址是否合法,如果访问非法的地址,会导致程序崩溃或出现不可预测的结果

指针的类型

  • 指针是分类型的
  • 指针的类型是指针所指向的数据的数据类型
  • 指针变量的运算是以其所指向的数据类型为基础的
  • 在进行指针运算时,编译器会根据指针变量的类型来确定运算的步长
  • 指针变量的类型决定了:
    • 指针所指向的内存块的大小
    • 指针可以指向哪种类型的数据
    • 应该如何使用指针来访问和操作数据
  • 指针的类型用于表示该指针变量所指向的数据类型,指针变量的类型和指针所指向的数据类型必须匹配,不能乱指
  • 同种数据类型的指针变量之间可以互相赋值

指针的各种运算与指针所指向的基类型之间的关系

指针变量的类型 决定了指针所指向的内存块的大小

例 1:一个 int 类型的指针变量指向的内存块大小为 4 个字节
对于一个 int 类型的指针变量 p,p+1 表示指针向前移动了 4 个字节的距离,因为 int 类型的数据占用 4 个字节

例 2:一个 char 类型的指针变量指向的内存块大小为 1 个字节
对于一个 char 类型的指针变量 p,p+1 表示指针向前移动了 1 个字节的距离,因为 char 类型的数据只占用 1 个字节

在进行指针运算时,编译器会根据指针变量的类型来确定运算的步长。如果对指针进行错误的运算,可能会导致指针移动的距离不正确,进而访问到错误的内存地址,导致程序出错或崩溃。

指针的生命周期

指针的生命周期:指针在程序中存在的时间段

  • 指针变量只有在其所在的函数或作用域内有效
  • 离开函数或作用域后,指针所指向的内存可能被释放或重用
  • 不能在指针变量作用域外使用该指针

指针的运算

指针运算 是对指针变量所指向的地址进行运算

  • 指针可以进行加、减等运算
  • 可以对指针进行比较运算,但是需要注意指针的类型和指向的内存是否相同,不能对指向不同数组元素的指针进行比较,因为它们所指向的内存不同
  • 同种数据类型的指针变量之间可以相互赋值
  • 指针的各种运算与其所指向的基类型有关
  • 指针的移动字节数以其基类型所占有的字节数为基本单位
  • 指针运算的优势:
    • 指针存放地址,访问速度更快
    • 指针访问数组更加快速灵活
    • 指针增加了访问内存空间的方法

不同位置的 * 号的作用

* 号在不同的语义环境下作用有所不同

  • 定义指针:* 号可以用于 定义 指针变量,* 号表示此时定义的变量是一个指针类型的变量
  • 使用指针:* 号可以用于 使用 指针变量,* 号是 解引用 操作符,用于访问指针指向的内存地址上存储的数据

定义指针类型的变量:

  1. int *p;
  2. // 定义指向整型变量的指针

取指针变量所指向的地址上存储的数据

  1. int a = 10;
  2. int *p = &a;
  3. printf("a = %d\n", *p);
  4. // 在使用指针变量时,* 号用于访问指针指向的内存地址上存储的数据

解引用

解引用(dereference)是指针运算中的一种操作,通过指针变量所存储的地址来访问该地址上存储的数据

假设 ptr 是一个指针变量,使用 *ptr 可以访问指针变量指向的内存地址上存储的数据,即 解析(dereference)指针

取地址运算符 &、取内容运算符 * 之间的差异

取地址运算符 & 和取内容运算符 * 是两个相反的运算符

  • & 运算符用于取变量地址
  • * 运算符用于获取指针所指向的内存地址上存储的值

& 运算符用于获取变量的地址,可以将一个变量的地址赋值给指针变量。例如:

  1. int a = 10;
  2. int *p = &a; // 将变量 a 的地址赋值给指针变量 p

* 运算符用于访问指针所指向的内存地址上存储的值。例如:

  1. int a = 10;
  2. int *p = &a;
  3. printf("%d", *p); // 输出变量 a 的值

取地址 &、取内容 * | 两者是互逆的
image.png

变量名和指针之间的关系

  • 变量名:可以看做是内存位置的符号名
  • 指针:实际上是一个变量,它存储了一个值,这个值表示 某个变量在内存中的地址


  1. #include <stdio.h>
  2. int main() {
  3. int var = 100; // 定义一个变量 var
  4. int* ptr; // 定义一个指向整型变量的指针变量 ptr
  5. ptr = &var; // ptr 指向 var 变量的地址
  6. printf("var的地址:%p\n", &var);
  7. printf("ptr指向的地址:%p\n", ptr);
  8. printf("var的值:%d\n", var);
  9. printf("ptr所指向的值:%d\n", *ptr);
  10. return 0;
  11. }
  12. /* 运行结果:
  13. var的地址:0x7ffc620cbcd8
  14. ptr指向的地址:0x7ffc620cbcd8
  15. var的值:100
  16. ptr所指向的值:100
  17. */
  • var*ptr 是完全等效的。
  • &varptr 是完全等效的。

对比通过指针访问内存和通过变量名访问内存:

  • 功能和灵活性:
    • 可以通过指针运算遍历内存中的连续数据块,而使用变量名则无法做到这一点。
    • 使用指针访问内存可以灵活地控制内存的访问与修改,可以动态地分配和释放内存空间,并且可以在程序运行时修改内存中的数据,具有较高的灵活性和功能性。
    • 使用变量名访问内存的数据是静态的,只能在编译时确定,不具备动态性和灵活性。
    • 使用变量名需要在变量定义时确定内存的大小与类型,并且只能访问该变量的内存。
  • 场景:
    • 变量名:对于访问数据量较小的变量,可以使用变量名访问内存
    • 指针:对于访问较大数据块或者需要遍历内存的情况,可以使用指针访问内存

让指针指向一个整型变量,并使用指针打印这个整型变量的值

  1. #include <stdio.h>
  2. int main() {
  3. int a = 10;
  4. int* ptr = &a; // 定义一个指向整型变量 a 的指针变量 ptr,并将其初始化为 &a
  5. printf("a的值为:%d\n", a);
  6. printf("指针变量ptr的值为:%p\n", ptr);
  7. printf("指针变量ptr所指向的值为:%d\n", *ptr);
  8. return 0;
  9. }
  10. /* 运行结果:
  11. a的值为:10
  12. 指针变量ptr的值为:0x7ffc5d11f9b8
  13. 指针变量ptr所指向的值为:10
  14. */
  • 定义指针变量:int* ptr = &a;
  • 取地址:使用 & 运算符获取变量 a 的地址
  • 打印地址:使用 %p 控制字符输出指针变量的值
  • 解引用:使用 * 运算符获取指针变量 ptr 所指向的地址上存储的数据(即变量 a 的值)

使用指针遍历一维数组

  1. #include <stdio.h>
  2. int main() {
  3. int arr[] = {10, 20, 30, 40, 50};
  4. int* ptr = arr; // 定义一个指向整型数组 arr 的指针变量 ptr,指向 arr 的第一个元素
  5. // 输出 arr 数组中的元素值
  6. for (int i = 0; i < 5; i++) {
  7. printf("arr[%d] = %d\n", i, *(ptr + i));
  8. }
  9. return 0;
  10. }
  11. /* 运行结果:
  12. arr[0] = 10
  13. arr[1] = 20
  14. arr[2] = 30
  15. arr[3] = 40
  16. arr[4] = 50
  17. */
  1. #include <stdio.h>
  2. int main() {
  3. int arr[] = {10, 20, 30, 40, 50};
  4. int* ptr = arr;
  5. for (int i = 0; i < 5; i++) {
  6. printf("arr[%d] = %d\n", i, *(ptr + i));
  7. // printf("arr[%d] = %d\n", i, ptr[i]);
  8. // printf("arr[%d] = %d\n", i, arr[i]);
  9. }
  10. return 0;
  11. }

*(ptr + i) 等效写法:

  • ptr[i]
  • arr[i]
  1. #include <stdio.h>
  2. int main() {
  3. int arr[] = {10, 20, 30, 40, 50};
  4. int(*ptr)[5] = &arr;
  5. for (int i = 0; i < 5; i++) {
  6. printf("arr[%d] = %d\n", i, (*ptr)[i]);
  7. }
  8. return 0;
  9. }

给指针赋值的多种方式

  1. int a = 10;
  2. int *ptr1 = &a; // 定义一个指向整型变量 a 的指针变量 ptr1
  3. int *ptr2 = ptr1; // 直接将指针变量 ptr1 赋给指针变量 ptr2
  1. int a = 10;
  2. int *ptr = &a; // 定义一个指向整型变量 a 的指针变量 ptr
  3. ptr += 1; // 使用指针运算将 ptr 指向另一个地址
  1. int *func() {
  2. int a = 10;
  3. return &a;
  4. }
  5. int main() {
  6. int *ptr = func(); // 调用函数返回指针类型变量,并将其赋给指针变量 ptr
  7. return 0;
  8. }
  1. int *ptr = (int *)malloc(sizeof(int)); // 动态分配一块内存空间


注意:

  • 确保是指针指向的是合法内存:在对指针变量进行赋值时,一定要确保指针变量指向的内存空间是合法的,否则会导致程序运行时出现访问非法内存的错误。
  • 确保指针指向的数据类型和指针的类型是匹配的:在赋值时还要注意指针变量的类型,确保被赋值的指针变量与赋值变量的类型一致或兼容。如果不一致,则需要进行类型转换。

5.1.3 | 使用指针递增输出数据

需求:输入两个整数,分别存放到 a、b 中,递增输出数据
前提:

  • 不定义新的整型变量
  • 变量 a、b 不能被重新赋值
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. int main() {
  4. int a, b;
  5. scanf("%d%d", &a, &b);
  6. int *p_max = &a, *p_min = &b, *p;
  7. if (*p_max < *p_min) {
  8. p = p_max;
  9. p_max = p_min;
  10. p_min = p;
  11. }
  12. printf("p_min:%d p_max:%d\n", *p_min, *p_max);
  13. printf("a:%d b:%d\n", a, b);
  14. return 0;
  15. }
  16. /* 运行结果:
  17. 20 10
  18. p_min:10 p_max:20
  19. a:20 b:10
  20. */

不修改原来 a b 变量的数据,也可以达到递增输出的目的!

判断空指针

在 C 语言中,空指针(Null Pointer)是一个特殊的指针,它并不指向任何有效的内存地址

判空指针:在使用指针变量时,我们需要判断它是否为空指针,以避免访问未指向有效内存的地址而导致程序出错。

  1. int *ptr = NULL;
  2. if (ptr == NULL) {
  3. // ptr 是空指针
  4. }
  5. int *ptr = NULL;
  6. if (!ptr) {
  7. // ptr 是空指针
  8. }

体验指针的减法运算

  1. #include <stdio.h>
  2. int main() {
  3. int arr[] = {1, 2, 3, 4, 5};
  4. int *p1 = arr;
  5. int *p2 = &arr[2];
  6. // 计算相邻元素之间的距离
  7. int distance = p2 - p1;
  8. printf("p1 所指向的成员:%d\n", *p1);
  9. printf("p2 所指向的成员:%d\n", *p2);
  10. printf("这两个成员之间的距离:%d\n", distance);
  11. printf("这两个成员之间的距离:%ld\n", p2 - p1);
  12. printf("p1:%p\n", p1);
  13. printf("p2:%p\n", p2);
  14. return 0;
  15. }
  16. /*
  17. 运行结果:
  18. p1 所指向的成员:1
  19. p2 所指向的成员:3
  20. 这两个成员之间的距离:2
  21. 这两个成员之间的距离:2
  22. p1:0x7ffc59dfa490
  23. p2:0x7ffc59dfa498
  24. */

从 p2 和 p1 之间的地址值来看,会发现它们之间差了 8 个字节的大小,但是当我们将其视作整型值输出时,结果是 2。这是因为指针指向的类型是 int 类型,int 类型占 4 个字节。

5. 指针(1) - 图2

p1、p2 都是 int 类型,所以基类型的字节数是 4,综上最终会得到的结果是 2。

指针关系运算

  1. #include <stdio.h>
  2. int main() {
  3. int a = 10, b = 20;
  4. int *p1 = &a, *p2 = &b;
  5. printf("p1 == p2: %d\n", p1 == p2);
  6. printf("p1 != p2: %d\n", p1 != p2);
  7. printf("p1 > p2: %d\n", p1 > p2);
  8. printf("p1 < p2: %d\n", p1 < p2);
  9. return 0;
  10. }
  11. /* 运行结果:
  12. p1 == p2: 0
  13. p1 != p2: 1
  14. p1 > p2: 1
  15. p1 < p2: 0
  16. */

注意:

  • 通过相等比较 == != 我们可以知道两个指针指向的内存空间是否相同
  • 大小比较 > < 比较的是变量的内存地址的大小
    • 判断的是这两个指针在内存中的相对位置
    • 和该指针所指向的变量的内存中存放的值没有关系

5.2 指针与数组

notes

简述

二维指针已经开始有些吃不消了,行、列指针在写法上非常类似,但是它们的运算是不一样的,需要弄清楚行、列指针。

引用:

  • 理解数组名 👉🏻 4.1 数组
  • strcmp 👉🏻 4.1 数组
  • 冒泡排序 👉🏻 4.3 编程实战

数组名

在 C 语言中,对于数组名,我们需要知道以下几点:

  • 数组名表示数组首元素的地址
  • 数组名是一个指向数组第一个元素的指针
  • 数组名是指针常量,不是指针变量,因此不能被重新赋值
    • 由于数组名是常量,无法直接赋值,所以 无法通过直接给数组名重新赋值实现数组的拷贝
  • 可以将数组名看做是一个“常量”指针(不能修改其指向)
  • 数组元素可以使用下标来访问,也可以使用指针来访问
  • 指针也可以当成一个数组名来使用
  • 访问数组元素可以用指针也可以用数组名混搭访问

获取数组首地址

可以使用 arr&arr[0] 访问数组首地址,这两种做法是没有任何差异的。

  1. arr 通过数组名获取
  2. &arr[0] 通过数组的首元素地址获取
  1. #include <stdio.h>
  2. int main() {
  3. int arr[5] = {1, 2, 3, 4, 5};
  4. int* p1 = arr; // 使用 arr 访问数组首地址
  5. int* p2 = &arr[0]; // 使用 &arr[0] 访问数组首地址
  6. printf("arr = %p\n", arr);
  7. printf("&arr[0] = %p\n", &arr[0]);
  8. printf("p1 = %p\n", p1);
  9. printf("p2 = %p\n", p2);
  10. printf("arr == &arr[0] 结果是:%d\n", arr == &arr[0]);
  11. printf("arr == p1 结果是:%d\n", arr == p1);
  12. printf("arr == p2 结果是:%d\n", arr == p2);
  13. return 0;
  14. }
  15. /* 运行结果:
  16. arr = 0x7ffd8c189270
  17. &arr[0] = 0x7ffd8c189270
  18. p1 = 0x7ffd8c189270
  19. p2 = 0x7ffd8c189270
  20. arr == &arr[0] 结果是:1
  21. arr == p1 结果是:1
  22. arr == p2 结果是:1
  23. */

arr&arr[0]p1p2 都指向数组 arr 的首地址。

验证指针的加减运算和基类型之间的关系

指针加减运算会根据指针所指向的基类型的大小来确定指针应该移动多少个字节

如果指针指向的基类型大小为 T,则:

  • 指针加 1 会使指针指向下一个 T 类型的位置
  • 指针减 1 会使指针指向上一个 T 类型的位置
  1. #include <stdio.h>
  2. int main() {
  3. int arr[5] = {1, 2, 3, 4, 5};
  4. int* p = arr; // p 指向数组 arr 的第一个元素
  5. printf("*p = %d\n", *p);
  6. p = p + 1; // p 现在指向数组 arr 的第二个元素
  7. printf("*p = %d\n", *p);
  8. p = p - 1; // p 现在又指向数组 arr 的第一个元素
  9. printf("*p = %d\n", *p);
  10. return 0;
  11. }
  12. /* 运行结果:
  13. *p = 1
  14. *p = 2
  15. *p = 1
  16. */

p = p + 1;p = p - 1;
由于 p 是一个整型指针,每走一步,其实就相当于是移动了一个整型的字节数。

  • +1 增加 4 个字节的偏移量
  • -1 减少 4 个字节的偏移量

使用 sizeof 计算数组的长度

计算公式:
5. 指针(1) - 图3%22%20aria-hidden%3D%22true%22%3E%0A%3Ctext%20font-family%3D%22monospace%22%20stroke%3D%22none%22%20transform%3D%22scale(71.759)%20matrix(1%200%200%20-1%200%200)%22%3E%E6%95%B0%3C%2Ftext%3E%0A%3Cg%20transform%3D%22translate(932%2C0)%22%3E%0A%3Ctext%20font-family%3D%22monospace%22%20stroke%3D%22none%22%20transform%3D%22scale(71.759)%20matrix(1%200%200%20-1%200%200)%22%3E%E7%BB%84%3C%2Ftext%3E%0A%3C%2Fg%3E%0A%3Cg%20transform%3D%22translate(1865%2C0)%22%3E%0A%3Ctext%20font-family%3D%22monospace%22%20stroke%3D%22none%22%20transform%3D%22scale(71.759)%20matrix(1%200%200%20-1%200%200)%22%3E%E5%85%83%3C%2Ftext%3E%0A%3C%2Fg%3E%0A%3Cg%20transform%3D%22translate(2798%2C0)%22%3E%0A%3Ctext%20font-family%3D%22monospace%22%20stroke%3D%22none%22%20transform%3D%22scale(71.759)%20matrix(1%200%200%20-1%200%200)%22%3E%E7%B4%A0%3C%2Ftext%3E%0A%3C%2Fg%3E%0A%3Cg%20transform%3D%22translate(3731%2C0)%22%3E%0A%3Ctext%20font-family%3D%22monospace%22%20stroke%3D%22none%22%20transform%3D%22scale(71.759)%20matrix(1%200%200%20-1%200%200)%22%3E%E4%B8%AA%3C%2Ftext%3E%0A%3C%2Fg%3E%0A%3Cg%20transform%3D%22translate(4664%2C0)%22%3E%0A%3Ctext%20font-family%3D%22monospace%22%20stroke%3D%22none%22%20transform%3D%22scale(71.759)%20matrix(1%200%200%20-1%200%200)%22%3E%E6%95%B0%3C%2Ftext%3E%0A%3C%2Fg%3E%0A%20%3Cuse%20xlink%3Ahref%3D%22%23E1-MJMAIN-2217%22%20x%3D%225819%22%20y%3D%220%22%3E%3C%2Fuse%3E%0A%3Cg%20transform%3D%22translate(6542%2C0)%22%3E%0A%3Ctext%20font-family%3D%22monospace%22%20stroke%3D%22none%22%20transform%3D%22scale(71.759)%20matrix(1%200%200%20-1%200%200)%22%3E%E5%8D%95%3C%2Ftext%3E%0A%3Cg%20transform%3D%22translate(932%2C0)%22%3E%0A%3Ctext%20font-family%3D%22monospace%22%20stroke%3D%22none%22%20transform%3D%22scale(71.759)%20matrix(1%200%200%20-1%200%200)%22%3E%E4%B8%AA%3C%2Ftext%3E%0A%3C%2Fg%3E%0A%3Cg%20transform%3D%22translate(1865%2C0)%22%3E%0A%3Ctext%20font-family%3D%22monospace%22%20stroke%3D%22none%22%20transform%3D%22scale(71.759)%20matrix(1%200%200%20-1%200%200)%22%3E%E5%85%83%3C%2Ftext%3E%0A%3C%2Fg%3E%0A%3Cg%20transform%3D%22translate(2798%2C0)%22%3E%0A%3Ctext%20font-family%3D%22monospace%22%20stroke%3D%22none%22%20transform%3D%22scale(71.759)%20matrix(1%200%200%20-1%200%200)%22%3E%E7%B4%A0%3C%2Ftext%3E%0A%3C%2Fg%3E%0A%3Cg%20transform%3D%22translate(3731%2C0)%22%3E%0A%3Ctext%20font-family%3D%22monospace%22%20stroke%3D%22none%22%20transform%3D%22scale(71.759)%20matrix(1%200%200%20-1%200%200)%22%3E%E5%A4%A7%3C%2Ftext%3E%0A%3C%2Fg%3E%0A%3Cg%20transform%3D%22translate(4664%2C0)%22%3E%0A%3Ctext%20font-family%3D%22monospace%22%20stroke%3D%22none%22%20transform%3D%22scale(71.759)%20matrix(1%200%200%20-1%200%200)%22%3E%E5%B0%8F%3C%2Ftext%3E%0A%3C%2Fg%3E%0A%3C%2Fg%3E%0A%20%3Cuse%20xlink%3Ahref%3D%22%23E1-MJMAIN-3D%22%20x%3D%2212417%22%20y%3D%220%22%3E%3C%2Fuse%3E%0A%3Cg%20transform%3D%22translate(13473%2C0)%22%3E%0A%3Ctext%20font-family%3D%22monospace%22%20stroke%3D%22none%22%20transform%3D%22scale(71.759)%20matrix(1%200%200%20-1%200%200)%22%3E%E6%80%BB%3C%2Ftext%3E%0A%3Cg%20transform%3D%22translate(932%2C0)%22%3E%0A%3Ctext%20font-family%3D%22monospace%22%20stroke%3D%22none%22%20transform%3D%22scale(71.759)%20matrix(1%200%200%20-1%200%200)%22%3E%E5%AD%98%3C%2Ftext%3E%0A%3C%2Fg%3E%0A%3Cg%20transform%3D%22translate(1865%2C0)%22%3E%0A%3Ctext%20font-family%3D%22monospace%22%20stroke%3D%22none%22%20transform%3D%22scale(71.759)%20matrix(1%200%200%20-1%200%200)%22%3E%E5%82%A8%3C%2Ftext%3E%0A%3C%2Fg%3E%0A%3Cg%20transform%3D%22translate(2798%2C0)%22%3E%0A%3Ctext%20font-family%3D%22monospace%22%20stroke%3D%22none%22%20transform%3D%22scale(71.759)%20matrix(1%200%200%20-1%200%200)%22%3E%E7%A9%BA%3C%2Ftext%3E%0A%3C%2Fg%3E%0A%3Cg%20transform%3D%22translate(3731%2C0)%22%3E%0A%3Ctext%20font-family%3D%22monospace%22%20stroke%3D%22none%22%20transform%3D%22scale(71.759)%20matrix(1%200%200%20-1%200%200)%22%3E%E9%97%B4%3C%2Ftext%3E%0A%3C%2Fg%3E%0A%3C%2Fg%3E%0A%3C%2Fg%3E%0A%3C%2Fsvg%3E#card=math&code=%E6%95%B0%E7%BB%84%E5%85%83%E7%B4%A0%E4%B8%AA%E6%95%B0%20%2A%20%E5%8D%95%E4%B8%AA%E5%85%83%E7%B4%A0%E5%A4%A7%E5%B0%8F%20%3D%20%E6%80%BB%E5%AD%98%E5%82%A8%E7%A9%BA%E9%97%B4&id=FjwHL)

  1. #include <stdio.h>
  2. int main() {
  3. int arr[5] = {1, 2, 3, 4, 5};
  4. int size = sizeof(arr);
  5. int length = sizeof(arr) / sizeof(*arr);
  6. printf("数组 arr 占用的存储空间为 %d 字节\n", size);
  7. printf("数组 arr 的长度为 %d\n", length);
  8. return 0;
  9. }
  10. /* 运行结果:
  11. 数组 arr 占用的存储空间为 20 字节
  12. 数组 arr 的长度为 5
  13. */

arr_size = sizeof(arr) / sizeof(*arr) 是一种 计算数组元素个数的常用方法

  • sizeof(arr) 计算数组 arr 的总字节数
  • sizeof(*arr) 每个数组元素的字节数

访问数组元素的多种方法

image.png

  1. #include <stdio.h>
  2. int main() {
  3. int arr[] = {10, 20, 30, 40, 50}, *ptr = arr;
  4. printf("方法 1:\n");
  5. for (int i = 0; i < 5; i++) {
  6. printf("arr[%d] = %d\n", i, arr[i]);
  7. }
  8. printf("\n方法 3:\n");
  9. for (int i = 0; i < 5; i++) {
  10. printf("*(ptr + %d) = %d\n", i, *(ptr + i));
  11. }
  12. printf("\n方法 4:\n");
  13. for (int i = 0; i < 5; i++) {
  14. printf("ptr[%d] = %d\n", i, ptr[i]);
  15. }
  16. printf("\n方法 5:\n");
  17. for (int i = 0; i < 5; i++) {
  18. printf("*(arr + %d) = %d\n", i, *(arr + i));
  19. }
  20. printf("\n方法 2:\n");
  21. for (int i = 0; i < 5; i++) {
  22. printf("*ptr++ = %d\n", *ptr++);
  23. }
  24. return 0;
  25. }
  26. /* 运行结果:
  27. 方法 1:
  28. arr[0] = 10
  29. arr[1] = 20
  30. arr[2] = 30
  31. arr[3] = 40
  32. arr[4] = 50
  33. 方法 3:
  34. *(ptr + 0) = 10
  35. *(ptr + 1) = 20
  36. *(ptr + 2) = 30
  37. *(ptr + 3) = 40
  38. *(ptr + 4) = 50
  39. 方法 4:
  40. ptr[0] = 10
  41. ptr[1] = 20
  42. ptr[2] = 30
  43. ptr[3] = 40
  44. ptr[4] = 50
  45. 方法 5:
  46. *(arr + 0) = 10
  47. *(arr + 1) = 20
  48. *(arr + 2) = 30
  49. *(arr + 3) = 40
  50. *(arr + 4) = 50
  51. 方法 2:
  52. *ptr++ = 10
  53. *ptr++ = 20
  54. *ptr++ = 30
  55. *ptr++ = 40
  56. *ptr++ = 50
  57. */

使用指针实现数组求和功能

  1. #include <stdio.h>
  2. int array_sum(int* ptr, int size);
  3. int main() {
  4. int arr[] = {10, 20, 30, 40, 50};
  5. int size = 5;
  6. int sum = array_sum(arr, size);
  7. printf("数组元素之和为:%d\n", sum);
  8. return 0;
  9. }
  10. int array_sum(int* ptr, int size) {
  11. int sum = 0;
  12. for (int i = 0; i < size; i++) {
  13. sum += *(ptr + i);
  14. }
  15. return sum;
  16. }
  17. /* 运行结果:
  18. 数组元素之和为:150
  19. */

二维数组首地址

  • 二维数组在内存中是 按行存储
  • 二维数组的首地址是 行地址
  • 需要使用 行指针 指向二维数组首地址
  • 行指针加减 1,就相当于移动了一行
  • 使用取内容符 * 进行解引用,可以将行地址转为列地址,相当于转为一维数组来处理
  • 每一行的数据是连续存储的
  • 每一行的大小是相等的
  • 二维数组的首地址和第 0 行的首地址是相等的
  1. #include <stdio.h>
  2. int main() {
  3. int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
  4. printf("二维数组首地址 arr:%p\n", arr);
  5. printf("第一行的首地址 arr[0]:%p\n", arr[0]);
  6. return 0;
  7. }
  8. /* 运行结果:
  9. 二维数组首地址 arr:0x7fffb20bbb70
  10. 第一行的首地址 arr[0]:0x7fffb20bbb70
  11. */

遍历二维数组的指定行

  1. #include <stdio.h>
  2. int main() {
  3. int arr[3][4] = {
  4. {1, 2, 3, 4},
  5. {5, 6, 7, 8},
  6. {9, 10, 11, 12}
  7. };
  8. int(*p)[4] = arr; // 行指针 p
  9. int n;
  10. printf("请输入要查看二维数组的第___行?【可输入:0、1、2】\n");
  11. scanf("%d", &n);
  12. int* row2 = *(p + n); // 偏移 n 行
  13. for (int i = 0; i < 4; i++) {
  14. printf("%d ", *(row2 + i)); // 第 n 行的第 i 个成员
  15. }
  16. printf("\n");
  17. return 0;
  18. }
  19. /* 运行结果:
  20. 请输入要查看二维数组的第___行?【可输入:0、1、2】
  21. 0
  22. 1 2 3 4
  23. 请输入要查看二维数组的第___行?【可输入:0、1、2】
  24. 1
  25. 5 6 7 8
  26. 请输入要查看二维数组的第___行?【可输入:0、1、2】
  27. 2
  28. 9 10 11 12
  29. */

int(*p)[4] = arr;

  • 可以使用使用二维数组名 arr 给 p 直接赋值
  • 因为 p 是一个行指针,要求每一行有 4 个成员
  • arr 指向的就是二维数组的第 0 行的首地址
  • arr 这个二维数组的列数满足一行 4 个成员的要求

遍历二维数组的指定列

  1. #include <stdio.h>
  2. int main() {
  3. int arr[3][4] = {
  4. {1, 2, 3, 4},
  5. {5, 6, 7, 8},
  6. {9, 10, 11, 12}
  7. };
  8. int n;
  9. printf("请输入要查看二维数组的第___列?【可输入:0、1、2、3】\n");
  10. scanf("%d", &n);
  11. // 使用指针数组访问二维数组
  12. int* col[4];
  13. for (int i = 0; i < 4; i++) {
  14. col[i] = arr[0] + i; // 第 i 列的首地址
  15. }
  16. for (int row = 0; row < 3; row++) {
  17. printf("%d ", *(col[n] + row * 4));
  18. }
  19. return 0;
  20. }
  21. /* 运行结果:
  22. 请输入要查看二维数组的第___列?【可输入:0、1、2、3】
  23. 0
  24. 1 5 9
  25. 请输入要查看二维数组的第___列?【可输入:0、1、2、3】
  26. 1
  27. 2 6 10
  28. 请输入要查看二维数组的第___列?【可输入:0、1、2、3】
  29. 2
  30. 3 7 11
  31. 请输入要查看二维数组的第___列?【可输入:0、1、2、3】
  32. 3
  33. 4 8 12
  34. */

int *p[4]; int (*p)[4]; 这两者之间的区别

  • int *p[4] 表示 p 是一个数组,它有 4 个元素,每个元素都是一个指向整型的指针。
  • int (*p)[4] 表示 p 是一个指向整型数组的指针。

简单来说,前者是一个指向指针数组的指针,后者是一个指向数组的指针。例如,可以使用第一种方式来表示一个包含 4 个指向整型的指针的数组,而使用第二种方式来表示一个包含 4 个整型元素的数组的指针。

行地址、列地址、行指针、列指针

image.png

问:指针怎么能够判断是访问的行还是列呢?

  • 对于用指针快速访问二维数组,到底增 1 是增行还是增列,由指针类型决定
  • 行指针:改变的是行
  • 列指针:改变的是列

行指针、列指针自增偏移。比如:int a[2][4] 这个二维数组,一共有两行,四列:

  • 行指针加一(减一):相当于向下(向上)偏移 4 个成员
  • 列指针加一(减一):相当于向左(向右)偏移 1 个成员

问:怎么区分行指针和列指针呢?
可以在定义的时候区分:

  • 行指针 int (*p)[4]; 表示一个指向包含 4 个 int 元素的一维数组的指针。
    • 此时 p 是一个行指针,每一行有 4 个成员
    • 行指针是一个指向一维数组的指针
    • 行指针的类型是 **int (*)[n]**
      • n 表示一维数组的长度(二维数组的列数)
    • 通过行指针,可以按行访问二维数组
    • 每次访问一个包含 n 个元素的一维数组
    • 通过指针运算,增 1 或减 1,将行指针向上一行或下一行移动
    • 行指针是一个指针
  • 列指针 int *p[4]; 表示一个包含 4 个指针的数组,每个指针都指向一个 int 类型的变量。这种类型的数组通常被称为指针数组。
    • 此时 p 是一个列指针,一共有 4 列
    • 列指针是包含若干个指向 int 类型指针数组
    • 列指针的类型是 **int *[]**
    • 通过列指针,可以按列访问二维数组
    • 每次访问一个包含所有行的某一列的元素
    • 通过每次偏移 总列数的倍数 来控制指针在不同行之间的上下移动
    • 列指针是一组指针

image.png

行指针,访问第 i 行,第 j 列,下面的写法都是等效的:

  • *(p[i] + j)
  • *(*(p + i)+j)
  • (*(p + i))[j]

image.png

列指针:直接视作一维数组来处理即可
image.png

image.png

  1. #include <stdio.h>
  2. int main() {
  3. int arr[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
  4. printf("使用行指针遍历二维数组\n");
  5. int(*p1)[4] = arr; // 定义指向含有 4 个 int 元素的数组的指针
  6. for (int i = 0; i < 3; i++) {
  7. for (int j = 0; j < 4; j++) {
  8. printf("%d ", p1[i][j]);
  9. // printf("%d ", *(*(p1 + i) + j));
  10. }
  11. printf("\n");
  12. }
  13. printf("使用列指针遍历二维数组\n");
  14. int* p2 = *arr;
  15. for (int i = 0; i < 3; i++) {
  16. for (int j = 0; j < 4; j++) {
  17. printf("%d ", *(p2 + i * 4 + j));
  18. }
  19. printf("\n");
  20. }
  21. printf("二维数组首地址:%p\n", arr);
  22. printf("二维数组首地址:%p\n", *arr);
  23. printf("二维数组首地址:%p\n", p1);
  24. printf("二维数组首地址:%p\n", p2);
  25. printf("行地址偏移:%p\n", p1 + 1);
  26. printf("列地址偏移:%p\n", p2 + 1);
  27. printf("列地址偏移:%p\n", p2 + 4);
  28. return 0;
  29. }
  30. /*
  31. 使用行指针遍历二维数组
  32. 1 2 3 4
  33. 5 6 7 8
  34. 9 10 11 12
  35. 使用列指针遍历二维数组
  36. 1 2 3 4
  37. 5 6 7 8
  38. 9 10 11 12
  39. 二维数组首地址:0x7ffc968648d0
  40. 二维数组首地址:0x7ffc968648d0
  41. 二维数组首地址:0x7ffc968648d0
  42. 二维数组首地址:0x7ffc968648d0
  43. 行地址偏移:0x7ffc968648e0
  44. 列地址偏移:0x7ffc968648d4
  45. 列地址偏移:0x7ffc968648e0
  46. */

指针数组和数组指针

指针数组和数组指针是两个不同的概念。

指针数组:

  • 指针数组是一个 数组
  • 指针数组是由基类型相同的指针所构成的数组
  • 指针数组的每个元素都是一个指针
  • 指针数组 常用于对多个字符串进行处理
  • 列指针就是一个指针数组
  1. int *p[3];
  2. // 定义了一个指针数组,其中包含 3 个整型指针

数组指针:

  • 数组指针是一个 指针
  • 数组指针指向一个 数组
  • 数组指针是一个指向数组的指针,可以像普通指针一样进行操作
  • 行指针就是一个数组指针
  • 数组指针需要使用括号将指针符号和数组符号括起来,以明确指针是指向数组的而不是数组的一个元素
  1. int a[3];
  2. int (*p)[3] = &a;
  3. // &a 是一个指向数组 a 的指针,而 (*p) 是一个指向 a 数组的指针。

字符指针数组比字符二维数组更适合用来处理多字符串

当需要处理多个字符串时,使用字符指针数组可以:

  • 提高内存使用效率
    • 字符指针数组元素指向的字符串可以是不规则的长度。
    • 字符二维数组的每个元素的长度必须相同,在定义时已确定。
  • 更容易实现字符串的动态增删改查等操作
    • 字符指针数组中字符串地址 可以不连续
  1. #include <stdio.h>
  2. int main() {
  3. // const char *strs[] = {"hello", "world", "!!!", "123", "abc"}; // 字符指针数组
  4. char strs[][20] = {"hello", "world", "!!!", "123", "abc"}; // 字符串数组
  5. printf("第 0 个成员 \"%s\"\t的地址是:%p\n", strs[0], strs[0]);
  6. printf("第 1 个成员 \"%s\"\t的地址是:%p\t相较于 0 偏移的字节数:%d\n", strs[1], strs[1], int(strs[1] - strs[0]));
  7. printf("第 2 个成员 \"%s\"\t的地址是:%p\t相较于 0 偏移的字节数:%d\n", strs[2], strs[2], int(strs[2] - strs[0]));
  8. printf("第 3 个成员 \"%s\"\t的地址是:%p\t相较于 0 偏移的字节数:%d\n", strs[3], strs[3], int(strs[3] - strs[0]));
  9. printf("第 4 个成员 \"%s\"\t的地址是:%p\t相较于 0 偏移的字节数:%d\n", strs[4], strs[4], int(strs[4] - strs[0]));
  10. return 0;
  11. }
  12. /* 运行结果
  13. 字符指针数组
  14. 第 0 个成员 "hello" 的地址是:0x402004
  15. 第 1 个成员 "world" 的地址是:0x40200a 相较于 0 偏移的字节数:6
  16. 第 2 个成员 "!!!" 的地址是:0x402010 相较于 0 偏移的字节数:12
  17. 第 3 个成员 "123" 的地址是:0x402014 相较于 0 偏移的字节数:16
  18. 第 4 个成员 "abc" 的地址是:0x402018 相较于 0 偏移的字节数:20
  19. 字符串数组
  20. 第 0 个成员 "hello" 的地址是:0x7ffd4784c980
  21. 第 1 个成员 "world" 的地址是:0x7ffd4784c994 相较于 0 偏移的字节数:20
  22. 第 2 个成员 "!!!" 的地址是:0x7ffd4784c9a8 相较于 0 偏移的字节数:40
  23. 第 3 个成员 "123" 的地址是:0x7ffd4784c9bc 相较于 0 偏移的字节数:60
  24. 第 4 个成员 "abc" 的地址是:0x7ffd4784c9d0 相较于 0 偏移的字节数:80
  25. */

qsort

qsort 是 C 语言标准库中的一个用于对数组进行快速排序的函数

  1. void qsort(
  2. void *base,
  3. size_t nmemb,
  4. size_t size,
  5. int (*compar)(const void *, const void *)
  6. );

参数说明:

  • base:指向要排序数组的指针。
  • nmemb:要排序的元素个数。
  • size:每个元素的大小(以字节为单位)。
  • compar:比较函数的指针,用于确定元素之间的相对顺序。
    • compar 函数声明:int compar(const void *p1, const void *p2);
    • 参数说明:函数的参数分别为要比较的两个元素的指针,函数应该返回一个整数值,表示两个元素之间的相对顺序
    • 返回值:通过回调函数 compar 的返回值决定 p1 和 p2 的位置
      • 返回大于 0 的整数:p1 丢到 p2 后面
      • 返回小于 0 的整数:p1 丢到 p2 前面
      • 返回 0:不改变 p1、p2 之间的位置
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. // 比较函数,用于确定两个元素的相对顺序
  4. int compare(const void* p1, const void* p2) {
  5. int num1 = *(const int*)p1;
  6. int num2 = *(const int*)p2;
  7. if (num1 < num2) {
  8. return -1;
  9. } else if (num1 > num2) {
  10. return 1;
  11. } else {
  12. return 0;
  13. }
  14. }
  15. int main() {
  16. int arr[] = {4, 2, 8, 3, 1, 9, 6, 5, 7};
  17. int n = sizeof(arr) / sizeof(arr[0]);
  18. printf("排序前 array: ");
  19. for (int i = 0; i < n; i++) {
  20. printf("%d ", arr[i]);
  21. }
  22. printf("\n");
  23. // 使用 qsort 函数进行排序
  24. qsort(arr, n, sizeof(int), compare);
  25. printf("排序后 array: ");
  26. for (int i = 0; i < n; i++) {
  27. printf("%d ", arr[i]);
  28. }
  29. printf("\n");
  30. return 0;
  31. }
  32. /* 运行结果
  33. 排序前 array: 4 2 8 3 1 9 6 5 7
  34. 排序后 array: 1 2 3 4 5 6 7 8 9
  35. */

国名排序

需求:使用字符指针数组对 n 个国家的国名进行按照字母顺序排序。

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. int main() {
  5. // 定义存放国名的字符指针数组 countries
  6. const char* countries[] = {"China", "Russia", "Japan", "United States",
  7. "Germany", "India", "Brazil"};
  8. // 计算一共有多少个国家
  9. int n = sizeof(countries) / sizeof(countries[0]);
  10. printf("排序之前 countries:\n");
  11. for (int i = 0; i < n; i++) {
  12. printf("%s\n", countries[i]);
  13. }
  14. // 对国家列表进行排序(冒泡排序)
  15. for (int i = 0; i < n; i++) {
  16. for (int j = 0; j < n - i - 1; j++) {
  17. if (strcmp(countries[j], countries[j + 1]) > 0) {
  18. const char *country = countries[j];
  19. countries[j] = countries[j + 1];
  20. countries[j + 1] = country;
  21. }
  22. }
  23. }
  24. printf("\n排序之后 countries:\n");
  25. for (int i = 0; i < n; i++) {
  26. printf("%s\n", countries[i]);
  27. }
  28. return 0;
  29. }
  30. /* 运行结果
  31. 排序之前 countries:
  32. China
  33. Russia
  34. Japan
  35. United States
  36. Germany
  37. India
  38. Brazil
  39. 排序之后 countries:
  40. Brazil
  41. China
  42. Germany
  43. India
  44. Japan
  45. Russia
  46. United States
  47. */
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. int compare(const void* p1, const void* p2) {
  5. return strcmp(*(const char**)p1, *(const char**)p2);
  6. }
  7. int main() {
  8. // 定义存放国名的字符指针数组 countries
  9. const char* countries[] = {"China", "Russia", "Japan", "United States",
  10. "Germany", "India", "Brazil"};
  11. // 计算一共有多少个国家
  12. int n = sizeof(countries) / sizeof(countries[0]);
  13. printf("排序之前 countries:\n");
  14. for (int i = 0; i < n; i++) {
  15. printf("%s\n", countries[i]);
  16. }
  17. // 对国家列表进行排序
  18. qsort(countries, n, sizeof(countries[0]), compare);
  19. printf("\n排序之后 countries:\n");
  20. for (int i = 0; i < n; i++) {
  21. printf("%s\n", countries[i]);
  22. }
  23. return 0;
  24. }
  25. /* 运行结果
  26. 排序之前 countries:
  27. China
  28. Russia
  29. Japan
  30. United States
  31. Germany
  32. India
  33. Brazil
  34. 排序之后 countries:
  35. Brazil
  36. China
  37. Germany
  38. India
  39. Japan
  40. Russia
  41. United States
  42. */

5.3 指针与结构

notes

sprintf

  • sprintf 是 C 语言中的一个字符串处理函数,可以将格式化的数据写入字符串中
  • 函数声明:int sprintf(char *str, const char *format, ...);
    • 第一个参数是目标字符串指针
    • 第二个参数是格式化字符串
    • 剩下的参数根据格式化字符串中的占位符进行相应的类型转换
  • sprintf 的工作方式与 printf 类似,不过它将输出结果写入到字符串中,而不是输出到屏幕
  1. #include <stdio.h>
  2. #include <time.h>
  3. int main() {
  4. char str[100];
  5. time_t t = time(NULL);
  6. struct tm* tm_info = localtime(&t);
  7. sprintf(str, "%02d-%02d-%d %02d:%02d:%02d",
  8. tm_info->tm_mday,
  9. tm_info->tm_mon + 1,
  10. tm_info->tm_year + 1900,
  11. tm_info->tm_hour,
  12. tm_info->tm_min,
  13. tm_info->tm_sec
  14. );
  15. printf("当前时间: %s\n", str);
  16. return 0;
  17. }
  18. /* 运行结果
  19. 当前时间: 23-03-2023 13:18:03
  20. */

访问结构成员的多种方式

  • 通过 结构变量名 访问
  • 通过 指向结构的指针间接运算符 *****(取内容运算符)
  • 通过 指向结构的指针指向成员运算符 ->
  1. #include <stdio.h>
  2. struct my_info {
  3. char name[20];
  4. int age;
  5. };
  6. int main() {
  7. // 定义一个结构体变量
  8. struct my_info info = {
  9. .name = "dahuyou",
  10. .age = 23,
  11. };
  12. // 1. 通过 结构变量名 访问
  13. printf("info.name\t名称: %s\n", info.name);
  14. printf("info.age\t年龄: %d\n", info.age);
  15. // 定义指向结构体的指针,并将其指向结构体变量
  16. struct my_info* p = &info;
  17. (*p).age = 24;
  18. // 2. 通过 指向结构的指针 和 间接运算符 *(取内容运算符)
  19. printf("(*p).name\t名称: %s\n", (*p).name);
  20. printf("(*p).age\t年龄: %d\n", (*p).age);
  21. sprintf(p->name, "Tdahuyou");
  22. // strcpy(p->name, "Tdahuyou");
  23. // 3. 通过 指向结构的指针 和 指向成员运算符 ->
  24. printf("p->name\t\t名称: %s\n", p->name);
  25. printf("p->age\t\t年龄: %d\n", p->age);
  26. return 0;
  27. }
  28. /* 运行结果
  29. info.name 名称: dahuyou
  30. info.age 年龄: 23
  31. (*p).name 名称: dahuyou
  32. (*p).age 年龄: 24
  33. p->name 名称: Tdahuyou
  34. p->age 年龄: 24
  35. */

指针之间拷贝、共享结构类型的变量

  1. #include <cstring>
  2. #include <stdio.h>
  3. struct my_info {
  4. char name[20];
  5. };
  6. int main() {
  7. // 定义一个结构体变量
  8. struct my_info info1 = {
  9. .name = "dahuyou",
  10. }, info2 = {
  11. .name = "xiaohuyou",
  12. }, info3 = {
  13. .name = "huyou",
  14. };
  15. struct my_info* p1 = &info1;
  16. struct my_info* p2 = &info2;
  17. struct my_info* p3 = &info3;
  18. printf("1. 初始化\n");
  19. printf("p1->name\t名称: %s\n", p1->name);
  20. printf("p2->name\t名称: %s\n", p2->name);
  21. printf("p3->name\t名称: %s\n", p3->name);
  22. p2 = p1; // p2 和 p1 共享一个 info1 结构体
  23. *p3 = *p1; // p1 结构体 info1 中的信息拷贝了一份到 p3 结构体中
  24. printf("2. 此时 p1 和 p2 共享 info1 结构体;p3 拷贝了一份 p1 指向的结构体 info1 中的数据\n");
  25. printf("p1->name\t名称: %s\n", p1->name);
  26. printf("p2->name\t名称: %s\n", p2->name);
  27. printf("p3->name\t名称: %s\n", p3->name);
  28. strcpy(info1.name, "Tdahuyou");
  29. printf("3. 修改 info1.name 为 Tdahuyou 之后\n");
  30. printf("p1->name\t名称: %s\n", p1->name);
  31. printf("p2->name\t名称: %s\n", p2->name);
  32. printf("p3->name\t名称: %s\n", p3->name);
  33. return 0;
  34. }
  35. /* 运行结果
  36. 1. 初始化
  37. p1->name 名称: dahuyou
  38. p2->name 名称: xiaohuyou
  39. p3->name 名称: huyou
  40. 2. 此时 p1 和 p2 共享 info1 结构体;p3 拷贝了一份 p1 指向的结构体 info1 中的数据
  41. p1->name 名称: dahuyou
  42. p2->name 名称: dahuyou
  43. p3->name 名称: dahuyou
  44. 3. 修改 info1.name 为 Tdahuyou 之后
  45. p1->name 名称: Tdahuyou
  46. p2->name 名称: Tdahuyou
  47. p3->name 名称: dahuyou
  48. */

5. 指针(1) - 图10