5.4 二重指针

二重指针

二重指针的水太深了,视频中介绍的不过冰山一角……
初学阶段,只要将视频中提及的代码都理解即可,后续再逐步扩展有关二重指针的知识点……

5.4.1 二重指针.mp4 (51.47MB)

notes

简述

  • 二重指针是 指向指针的指针
  • 二重指针也叫 指针的指针
  • 值是 另一个指针变量的地址
  • 常见应用场景:
    • 函数参数传递:在函数参数传递中,可以通过传递指向指针的指针,使函数能够修改指针指向的内存地址,从而达到修改函数外部变量的目的
    • 动态内存分配:在动态内存分配中,二重指针可以用来动态分配二维数组内存
  1. int x = 10; // 定义 int 类型变量 x,赋值为 10
  2. int *p = &x; // 定义指向 int 类型变量的指针 p,使得 p 指向 x
  3. int **pp = &p; // 定义指向指针的指针 pp,使得 pp 指向 p

demo | PPT | 体验二重指针

  1. #include <stdio.h>
  2. int main() {
  3. int v = 120, *q = &v, **p = &q;
  4. printf("v = %d\n", v);
  5. printf("*q = %d\n", *q);
  6. printf("**p = %d\n", **p);
  7. return 0;
  8. }
  9. /* 运行结果
  10. v = 120
  11. *q = 120
  12. **p = 120
  13. */

image.png

  1. #include <stdio.h>
  2. int main() {
  3. char* pc[] = {"abc", "def", "hig"}; // pc 是一个二重指针
  4. char** ppc; // 定义 ppc 为二重指针
  5. ppc = pc;
  6. printf("%s\n", *ppc); // => abc
  7. printf("%s\n", *(ppc + 1)); // => def
  8. printf("%s\n", *(ppc + 2)); // => hig
  9. return 0;
  10. }
  1. #include <stdio.h>
  2. int main() {
  3. int a[5] = {1, 3, 5, 7, 9};
  4. int* num[5] = {&a[0], &a[1], &a[2], &a[3], &a[4]};
  5. int **p, i;
  6. p = num;
  7. for (i = 0; i < 5; i++) {
  8. printf("%d\t", **p);
  9. p++;
  10. }
  11. printf("\n");
  12. return 0;
  13. }
  14. /* 运行结果:
  15. 1 3 5 7 9
  16. */

字符指针数组和二重指针之间的关系

问:char* pc[] = {"abc", "def", "hig"}; 其中的 pc 是二重指针吗?
答:回答“是”或者“不是”其实都对

  • 回答“是”:二重指针是指向指针的指针,pc 的每一项都是一个字符串,字符串其实就是一个字符数组,这就意味着 pc 指针数组中的每一个指针都指向一个地址
  • 回答“不是”:字符指针数组 pc 的数据类型是 char *[],它的数据类型,本质上是一个指针数组,而非指针。

补充: 课件中说 pc 是一个二重指针,但网上也有说不是的。其实没必要深究字符指针数组到底是不是二重指针,我们只要知道如何使用即可,这种细节上的概念区分着实没必要,简单了解一下大家给出的答复的背后的原因即可。

5.5 字符指针

字符指针

理解字符串指针(字符串常量)和字符数组之间的差异。

5.5.1 字符指针.mp4 (57.6MB)

notes

字符数组不一定是字符串,但字符串一定是字符数组

  • 字符数组是一段连续的用于存储字符类型数据的内存空间
  • 字符串是由字符组成的字符数组,并且以 NULL **\0** 作为字符串的结束标志
  • 如果字符数组的结尾字符是 **\0** 那么该字符数组可以视作一个字符串,否则就不是一个字符串

使用 %s 作为格式控制字符打印字符数组可能会出现异常:

  1. #include <stdio.h>
  2. int main() {
  3. char buffer1[] = {'h', 'e', 'l', 'l', 'o'};
  4. char buffer2[] = "hello";
  5. printf("buffer1 = %s\n", buffer1);
  6. printf("buffer2 = %s\n", buffer2);
  7. }
  8. /* 运行结果:
  9. buffer1 = hello烫烫烫烫烫烫烫烫烫烫烫烫烫蘦ello
  10. buffer2 = hello
  11. */

上述代码中,buffer1 是一个字符数组,但是它并非字符串,所以当我们使用 %s 进行打印时,得到的结果会存在异常。

  • 分析原因:
    • %s 格式控制字符用于打印以空字符 **'\0'** 结尾的字符串
    • 不是所有的字符数组都是以空字符结尾的字符串
  • 导致的结果:
    • 如果一个字符数组没有以空字符结尾,使用 %s 格式控制字符来打印它将导致未定义的行为
    • 比如上述的 buffer1 打印的内容就是错误的

加入我们已经知道字符数组 buffer1 的结尾字符不是 '\0',那么又希望能够使用 %s 将 buffer1 这个字符数组正常打印出来,又啥解决办法呢?

  • 方案 1:
    • 改变 buffer1
    • 手动在结尾处加上 '\0' 空字符串作为字符串的结束符,以确保 buffer1 可以“正常”打印。
  • 方案 2:
    • 不改变 buffer1
    • 在打印字符数组时,应该使用 %.*s 格式控制字符
    • 传递字符数组的长度作为参数,让 printf 知道需要打印多长的内容
  1. #include <stdio.h>
  2. int main() {
  3. char buffer1[] = { 'h', 'e', 'l', 'l', 'o', '\0'};
  4. char buffer2[] = "hello";
  5. printf("buffer1 = %s\n", buffer1);
  6. printf("buffer2 = %s\n", buffer2);
  7. }
  8. /* 运行结果:
  9. buffer1 = hello
  10. buffer2 = hello
  11. */
  1. #include <stdio.h>
  2. int main() {
  3. char buffer1[] = {'h', 'e', 'l', 'l', 'o'};
  4. char buffer2[] = "hello";
  5. const int buffer1_len = sizeof(buffer1) / sizeof(*buffer1);
  6. printf("buffer1_len = %d\n", buffer1_len);
  7. printf("buffer1 = %.*s\n", buffer1_len, buffer1);
  8. printf("buffer2 = %s\n", buffer2);
  9. }
  10. /* 运行结果:
  11. buffer1_len = 5
  12. buffer1 = hello
  13. buffer2 = hello
  14. */

字符串常量

  • 字符串常量是使用一对双引号 " 括起来的字符序列,其中可以包含任何字符,包括空格和特殊字符。
    • "Hello, World!"
    • "This is a string constant."
    • "1234"
    • "!@#$%^&*()"
  • 字符串常量是只读的,因此不能通过指向字符串的指针来更改字符串的内容
  • 字符串常量通常存储在只读存储器中,尝试修改只读存储器会导致段错误
  • 字符串常量是以字符数组的形式存在的
  • 不同字符串常量:
    • 每个 不同的 字符串常量都是独立存在的,它们占用的内存空间是不同的
    • 每个 不同的 字符串常量都有其唯一的地址
  • 相同字符串常量:
    • 相同的字符串常量的地址也不一定就是相同的,得看我们在定义的时候是怎么写的
    • 编译器会优化字符串常量的存储,使得相同的字符串常量在内存中只有一份
    • 这种优化只是针对字符串常量的,对于数组或者其他变量,在内存中都会分配独立的空间,指向这些变量的指针指向的是它们所分配的内存空间的首地址。
  1. #include <stdio.h>
  2. int main() {
  3. const char* p1 = "Hello";
  4. const char* p2 = "Hello";
  5. printf("p1 address = %p\n", p1);
  6. printf("p2 address = %p\n", p2);
  7. printf("p1 - p2 = %d\n", p1 - p2);
  8. }
  9. /* 运行结果:
  10. p1 address = 00007FF77AB79CF0
  11. p2 address = 00007FF77AB79CF0
  12. p1 - p2 = 0
  13. */
  1. #include <stdio.h>
  2. int main() {
  3. const char* p1 = "Hello1";
  4. const char* p2 = "Hello2";
  5. printf("p1 address = %p\n", p1);
  6. printf("p2 address = %p\n", p2);
  7. printf("p1 - p2 = %d\n", p1 - p2);
  8. }
  9. /* 运行结果:
  10. p1 address = 00007FF6D2EF9CF0
  11. p2 address = 00007FF6D2EF9C38
  12. p1 - p2 = 184
  13. */
  1. #include <stdio.h>
  2. int main() {
  3. char buffer1[] = "Hello"; // 数组
  4. const char* p1 = buffer1;
  5. const char* p2 = "Hello"; // 字符串常量
  6. printf("p1 address = %p\n", p1);
  7. printf("p2 address = %p\n", p2);
  8. }
  9. /* 运行结果:
  10. p1 address = 0000008284B1F9D4
  11. p2 address = 00007FF799B19CF0
  12. */

字符串常量是不可更改的?

这句话不全对,得看怎么理解了,可以说“是”,也可以说“不是”。

  1. #include <stdio.h>
  2. int main() {
  3. char buffer1[] = "Hello"; // 数组
  4. const char* p1 = buffer1;
  5. const char* p2 = "Hello"; // 字符串常量
  6. printf("p1 address = %p\n", p1);
  7. printf("p2 address = %p\n", p2);
  8. }
  9. /* 运行结果:
  10. p1 address = 0000008284B1F9D4
  11. p2 address = 00007FF799B19CF0
  12. */

明确一点:char buffer1[] = "Hello";const char* p2 = "Hello"; 这两种写法中出现的 "Hello" 都是字符串常量。但是由于前者在初始化时,数据类型是 char [],而后者是 char *,所以此时编译器会给它们分配不同的存储地址。

  • 可以改变 p1 指向的字符串,因为它是一个数组
  • 无法改变 p2 指向的字符串,因为它是一个字符串常量
  1. #include <stdio.h>
  2. int main() {
  3. char buffer1[] = "Hello";
  4. const char* p1 = buffer1;
  5. const char* p2 = "Hello";
  6. printf("修改之前:\n");
  7. printf("buffer1: %s\taddress = %p\n", buffer1, buffer1);
  8. printf("p1: %s\taddress = %p\n", p1, p1);
  9. printf("p2: %s\taddress = %p\n", p2, p2);
  10. buffer1[0] = 'h'; // 错误的做法 *p1 = 'h'
  11. printf("修改之后:\n");
  12. printf("buffer1: %s\taddress = %p\n", buffer1, buffer1);
  13. printf("p1: %s\taddress = %p\n", p1, p1);
  14. printf("p2: %s\taddress = %p\n", p2, p2);
  15. // 将 p1 指向的字符串强制转换为非 const 类型指针,然后通过指针修改字符串
  16. *((char*)p1) = 'w';
  17. *((char*)p1 + 1) = 'o';
  18. *((char*)p1 + 2) = 'r';
  19. *((char*)p1 + 3) = 'l';
  20. *((char*)p1 + 4) = 'd';
  21. printf("修改之后:\n");
  22. printf("buffer1: %s\taddress = %p\n", buffer1, buffer1);
  23. printf("p1: %s\taddress = %p\n", p1, p1);
  24. printf("p2: %s\taddress = %p\n", p2, p2);
  25. return 0;
  26. }
  27. /* 运行结果:
  28. 修改之前:
  29. buffer1: Hello address = 00000001000FF614
  30. p1: Hello address = 00000001000FF614
  31. p2: Hello address = 00007FF78CCA9CF0
  32. 修改之后:
  33. buffer1: hello address = 00000001000FF614
  34. p1: hello address = 00000001000FF614
  35. p2: Hello address = 00007FF78CCA9CF0
  36. 修改之后:
  37. buffer1: world address = 00000001000FF614
  38. p1: world address = 00000001000FF614
  39. p2: Hello address = 00007FF78CCA9CF0
  40. */

buffer1[0] = 'h';

  • 字符串数组 buffer1,我们可以直接通过 buffer1[0] = 'h' 来修改数组成员的值
  • 但是无法通过 *p1 = 'h'; 来修改数组成员,这是因为 p1 被 const 关键字修饰,如果给它重新赋值,编译器会报错

如果我们非要改变被 const 修饰的 p1 指针所指向的字符串常量的值,可以使用这样的写法 *((char*)p1) = 'w';

  • 将 p1 指向的字符串强制转换为非 const 类型指针
  • 然后再通过指针修改字符串

但是,如果我们试图去修改 p2 所指向的字符串 "Hello",那么程序会崩溃,并报错 Segmentation fault (core dumped)

🤔 字符串常量是不可更改的嘛?

  • 如果回答“是”
    • 被 const 修饰的,指向字符串的指针,是不允许被重新赋值的
    • 所以我们没办法直接使用传统的做法,去修改指针所指向的内容
  • 如果回答“不是”
    • 虽然被 const 修饰的内容无法被重新赋值,但是可以先将 const 类型的字符指针强制转为 char* 类型的指针
    • char* 是可以被重新赋值的,这时候再去修改“字符串常量”的值,就是被允许的做法

可以这么理解:

  • char buffer1[] = "Hello"; 当我们看到同事这么写时,就意味着同事在告诉我们,这个字符串是一个可修改的内容,如果要改,你随意就好
  • const char* p2 = "Hello"; 当我们看到同事这么写时,就意味着同事不希望我们去改变这个字符串,这东西是一个字符串常量

5.6 动态内存

动态内存

5.6.1 动态内存.mp4 (169.54MB)

notes

内存映像

  • 内存映像是 程序在运行时在计算机内存中的存储结构
  • 内存映像是 将进程在内存中的运行状态(包括代码、数据、堆栈等)以文件的形式保存在硬盘上,以便在需要时重新加载到内存中
  • 内存映像是 程序在内存中的运行状态,包括程序代码、全局变量、堆、栈等,以及它们在内存中的分布和状态
  • 在内存中,程序被分割为多个不同的段,每个段都有其自己的特定的作用和属性,如代码段、数据段、堆栈段等
    • 代码段:包含了程序的 可执行代码
    • 数据段:包含了程序中 已初始化的全局变量和静态变量
    • 堆段:包含了 动态分配的内存
    • 栈段:包含了 函数调用的参数、局部变量等信息
  • 在程序运行时,操作系统会将程序所需要的资源,如内存空间、CPU 时间等,分配给程序并进行动态调度,以保证程序能够正常运行。
  • 在程序运行的同时,操作系统还会监控程序的运行状态,如内存使用情况、CPU 占用率等,并根据需要进行资源调整和分配,以保证系统的稳定和安全。

5. 指针(2) - 图5

栈(stack)分配内存、堆(heap)分配内存

栈分配:

  • 栈段内存由 操作系统 自动 管理
  • 在函数内部声明的局部变量通常从栈上分配内存,并在函数返回时自动释放
  • 在函数调用过程中,每个函数都有自己的栈帧,用于存储该函数的参数、局部变量、返回地址等信息
  • 变量所在的栈帧在代码块结束时被销毁,分配的内存空间也就被释放了
  • 栈段内存则由操作系统自动分配和释放
  • 栈段内存则是在函数调用时自动分配,函数返回时自动释放
  • 存放函数参数、局部变量值等
  • 在执行函数调用时,系统在栈上为函数内的局部变量及形参分配内存
  • 函数执行结束时,自动释放这些内存

堆分配:

  • 堆段内存需要 程序员 手动 管理,需要程序员手动申请和释放
  • 程序员需要确保在使用堆段内存时进行正确的申请、使用和释放,避免出现内存泄漏、悬垂指针、非法访问的情况
  • 申请:堆段内存是通过 malloc()、calloc() 等函数来申请的
  • 释放:堆段内存需要手动释放,否则会导致内存泄漏,可以 使用 **free** 函数进行释放
  • 分配的内存空间可以在程序的任意位置进行访问和修改
  • 在程序运行期间,用来申请的内存都是从堆上分配的
  • 动态内存的生存期由程序员自己来决定

静态内存

  • 静态分配是在程序编译时就分配好了内存空间,变量的内存空间在程序运行时就一直存在,直到程序结束时才会被释放
  • 静态分配的变量包括全局变量和局部静态变量(local static variable)

函数体内声明的非静态变量一定是局部变量(local variable),也称为自动变量(automatic variable)

局部变量的生命周期由编译器自动管理,无需程序员手动管理:

  • 局部变量是在函数执行时创建的,当函数返回时自动销毁
  • 局部变量通常存储在栈上,每次函数调用时,将为其分配一段新的空间,当函数返回时,该空间将被释放

局部静态变量:

  • 局部静态变量的生命周期从函数第一次被调用开始,直到程序结束才结束,而不是在函数调用结束时结束
  • 局部静态变量的内存空间在程序整个运行过程中都存在,而不是在函数调用结束后被释放
  1. #include <stdio.h>
  2. int count() {
  3. static int num = 0;
  4. num++;
  5. return num;
  6. }
  7. int main() {
  8. for (int i = 0; i < 5; i++) {
  9. printf("num = %d\n", count());
  10. }
  11. return 0;
  12. }
  13. /* 运行结果:
  14. num = 1
  15. num = 2
  16. num = 3
  17. num = 4
  18. num = 5*/

static int num = 0; 其中 num 就是一个局部静态变量。

函数内部的变量啥的,应该会在函数章节,也就是第 6 章函数章节会介绍到,这里先简单有个印象就好。

动态内存

  • 动态内存是:程序在运行时根据需要在堆上动态分配的内存空间
  • 动态内存分配函数:malloc、calloc 和 realloc
  • 动态内存的常见应用场景:动态分配大块内存、在函数间传递指针或返回指针、创建动态数据结构、缓存或临时存储大量数据……
  • 动态分配的变量主要是指针和动态分配的数组
  • 动态分配的变量需要程序员手动释放,否则会出现内存泄漏的问题

malloc

  • malloc 全称 memory allocation 意为 “内存分配”
  • malloc() 是 C 语言中的一个用于 动态地分配内存 的函数
  • 作用:从 中分配一块指定大小的内存,并返回该内存块的首地址。
  • 函数原型:void* malloc(size_t size);
  • 参数:size 表示需要分配的内存大小(单位是字节),向系统申请大小为 size 的内存块
  • 返回值:
    • 一个 void 类型的指针 void*,指向分配的内存块的首地址
    • 如果分配失败,则返回 NULL

使用 malloc() 函数分配内存的一般步骤是:

  1. 调用 malloc() 函数分配内存
  2. 用指针变量保存分配的内存地址
  3. 使用分配的内存(读取、写入等操作)
  4. free() 函数释放内存
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. int main() {
  4. int n;
  5. printf("请输入要申请多少个整型空间:");
  6. scanf("%d", &n);
  7. int* p = (int*)malloc(n * sizeof(int));
  8. if (p == NULL) {
  9. printf("内存申请失败\n");
  10. return 1;
  11. }
  12. printf("内存申请成功\n");
  13. for (int i = 0; i < n; i++) {
  14. printf("请输入第 %d 个整数:", i + 1);
  15. scanf("%d", &p[i]);
  16. }
  17. printf("输入的数字为:");
  18. for (int i = 0; i < n; i++) {
  19. printf("%d\t", *(p + i));
  20. }
  21. free(p);
  22. p = NULL;
  23. printf("\n内存成功释放\n");
  24. return 0;
  25. }
  26. /* 运行结果:
  27. 请输入要申请多少个整型空间:3
  28. 内存申请成功
  29. 请输入第 1 个整数:11
  30. 请输入第 2 个整数:22
  31. 请输入第 3 个整数:33
  32. 输入的数字为:11 22 33
  33. 内存成功释放
  34. */

calloc

  • calloc 是 C 语言标准库中的函数,用于在堆内存中分配指定数量的连续内存块,并将分配的内存全部初始化为 0
  • calloc 全称 contiguous allocation 意为 “连续分配”
  • 函数原型:void *calloc(size_t nmemb, size_t size);
  • 参数
    • nmemb 参数表示需要分配的元素数量
    • size 参数表示每个元素的大小(以字节为单位)
  • 返回值:
    • calloc 函数返回一个指向分配内存块的指针
    • 如果分配失败,则返回空指针 NULL
  • 对比 malloc:
    • 与 malloc 函数不同,calloc 函数会自动将分配的内存全部初始化为 0
    • 在一些特定场景下使用 calloc 函数比使用 malloc 函数更为方便和安全
      • 例如:分配字符串、数组等情况
    • calloc 函数需要额外的初始化操作
    • 执行效率通常比 malloc 函数略低

free

  • free 是 C 标准库中的函数,用于释放先前通过 malloc、calloc 或 realloc 分配的内存
  • 函数原型为:void free(void *ptr);
  • 参数:ptr 是指向需要释放的内存块的指针
    • 指针 ptr 必须是之前通过 malloc、calloc 或 realloc 分配的内存块的地址
    • 如果 ptr 不是这些函数分配的内存块的地址,或者已经被释放过了,或者是一个空指针(NULL),则 free 的行为是未定义的。
  • free 时,系统标记此块内存为未占用,可以被重新分配
  • free 的使用可以有效避免内存泄漏问题
    • 当我们调用 free 函数时,相当于在告诉操作系统这段内存不再需要使用
    • 操作系统会将这段内存标记为空闲状态,然后将其放入空闲内存池中,以便下次需要分配内存时使用
    • free 时系统标记此块内存为未占用,可被重新分配
  • 防止野指针:free(p); 之后,加上 p = NULL;
    • 虽然 free(p); 执行完之后,p 指针所指向的内存归还给了系统,但是 p 指针依旧指向那块内存空间
    • 由于 p 指针所指向的内存空间已经归还给系统了,也就是被释放掉了,若我们再使用 p 去访问那块内存空间,则这种行为是未定义的
    • 养成习惯:在释放某个指针所指向的空间的同时,记得将指针的指向也置空,以免出现野指针
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. int main() {
  4. int *p = NULL, n, i;
  5. double aver, sum;
  6. printf("How many students?\n");
  7. scanf("%d", &n);
  8. p = (int *) malloc(n * sizeof(int));
  9. if (p == NULL) {
  10. printf("No enough memory!\n");
  11. exit(1);
  12. }
  13. for (int i = 0; i < n; ++i) {
  14. scanf("%d", &p[i]);
  15. sum += p[i];
  16. }
  17. aver = sum / n;
  18. printf("aver = %.1f\n", aver);
  19. free(p);
  20. p = NULL; // 防止野指针
  21. return 0;
  22. }
  23. /* 运行结果:
  24. How many students?
  25. 3
  26. 11
  27. 22
  28. 33
  29. aver = 22.0
  30. */

int *p = NULL;
养成习惯,如果一个指针,我们在定义它的时候,没法明确它的指向,那么将其初始化为空指针。

p = NULL;
养成习惯,每当我们使用 free 释放一块内存空间后,如果这块空间中有指针指向它,那么将这些指针都给置空,以免出现野指针问题。

exit(1);
终止程序的执行并返回退出码(状态码) 1

exit

exit 函数是 C/C++ 标准库中的一个函数,用于终止程序的执行,并在程序退出时进行清理操作,如调用 atexit 注册的函数、关闭文件、释放分配的内存等。

  • 作用:
    • 立即终止程序的执行
    • 清除程序的内存和其他资源
    • 将状态码返回给操作系统
  • 函数原型:void exit(int status);
  • 参数:status 是一个整数,表示程序结束时的状态码
    • 在程序正常结束时,一般返回 0 作为状态码
    • 如果程序异常结束,可以返回其他非零值作为状态码
  • 注意:
    • 通常情况下,程序的结束都应该通过调用 exit() 函数来实现,以便在程序结束时进行必要的资源清理和状态报告
    • 在 exit() 函数调用之前,所有的缓冲区数据都应该被刷新并写入文件,以免数据丢失
    • exit() 函数还可以调用注册的 atexit() 函数,用于在程序结束时执行一些必要的清理操作
      • 关闭文件
      • 关闭数据库连接
      • ……

使用 malloc、calloc 分配 10 个整数空间,并把 1 到 10 存储在这个空间中

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. int main() {
  4. int *ptr = (int*)malloc(10 * sizeof(int));
  5. // int *ptr = (int*)calloc(10, sizeof(int));
  6. if (ptr == NULL) {
  7. printf("内存分配失败");
  8. return 1;
  9. }
  10. for (int i = 0; i < 10; i++) {
  11. *(ptr + i) = i + 1;
  12. }
  13. for (int i = 0; i < 10; i++) {
  14. printf("%d ", *(ptr + i));
  15. }
  16. free(ptr);
  17. ptr = nullptr;
  18. return 0;
  19. }
  20. /* 运行结果:
  21. 1 2 3 4 5 6 7 8 9 10
  22. */

null 指针、void *

null 指针:

  • NULL 指针是 空指针,是一个特殊的指针
  • 在 C++11 及以上标准中,可以使用 nullptr 关键字来表示 NULL 指针
  • 使用 NULL 指针时需要注意,它并不指向任何有效的内存地址,所以解引用 NULL 指针会导致未定义的行为
  • 使用指针前最好先进行 NULL 指针的判断
  • NULL 指针是一个宏定义,值为 0
    • 表示指针不指向任何合法地址
    • 表示指针不指向任何一种地方的状态
    • 虽然 null 指针的值是 0,但是不要试图使用指针数据类型和整型进行比较运算,这样的语句在最新版的 visual studio 中默认是没法编译通过的

void* 指针:

  • void 表示一个无类型指针,是 C++ 中的一种 通用 指针类型,*可以指向任何类型的数据
  • void* 指针所指向的数据类型是未知的
  • 使用 void 指针需要进行 *类型转换 才能访问指针所指向的数据
  • 通用性:
    • void* 指针常用于实现泛型算法,例如 STL 中的容器类和算法库,因为它可以泛指任何类型的数据,具有一定的通用性
    • 比如:在动态内存分配时,malloc 函数返回的是 void* 类型的指针,因为它不知道要分配的数据类型是什么,所以返回一个无类型指针,需要根据实际需要将其转换为特定类型的指针后再使用。
  • 无法进行指针运算:因为 void* 指针并不知道指向的是哪种类型的数据,所以不能通过它进行指针运算,也不能用它来访问指针指向的值,必须先将其转换为具体类型的指针才能进行操作

源码

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. int main() {
  4. int *p, i = 0;
  5. p = (int*)malloc(sizeof(int));
  6. if (p) {
  7. *p = 5;
  8. printf("%d", *p);
  9. }
  10. return 0;
  11. }
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. int main() {
  4. int *p, i = 0;
  5. p = (int*)malloc(sizeof(int));
  6. if (p) {
  7. *p = 5;
  8. printf("%d", *p);
  9. }
  10. free(p); // 释放动态内存分配的空间
  11. return 0;
  12. }
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #define N 10
  4. int main() {
  5. int *p, i = 0;
  6. p = (int*)malloc(N * sizeof(int));
  7. if (p) {
  8. for (i = 0; i <= N; ++i, ++p) {
  9. *p = i + 1;
  10. printf("%d ", *p); // => 1 2 3 4 5 6 7 8 9 10 11
  11. }
  12. }
  13. return 0;
  14. }
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #define N 100000
  4. int main() {
  5. int *p, i = 0;
  6. while(1) {
  7. p = (int *)malloc(N * sizeof(int));
  8. printf("%p", p);
  9. *p = i++;
  10. if (p == NULL) break;
  11. }
  12. return 0;
  13. }

上述这种写法,会不断地瓜分内存,最终可能会导致内存空间不足,从而导致程序崩溃。因此,当我们需要多次调用 malloc 分配内存空间时,需要及时 free,以免造成内存空间不足,程序崩溃的问题。

⚠️ 上述这种写法,建议不要尝试丢到类似 lightly 这样的 cloudIDE 中去跑,远程的云主机会直接跑死掉…… 我们只要知道在使用 malloc 申请完内存后,记得 free 即可。

常见的内存错误

  • 内存分配未成功便使用
  • 内存分配成功,但没有初始化便开始使用
  • 内存分配成功并初始化,但发生越界使用
  • 申请内存后没有及时释放内存
  • 释放内存后仍继续使用

关于内存分配编程的建议原则

  • 仅在需要时才使用 malloc
  • malloc 和 free 要配对使用,malloc 在函数入口,free 在函数出口
  • 使用 malloc 时要检查函数返回值
  • 使用 free 函数后,将指针设置为 NULL
  • 不要把局部变量的地址作为函数返回值返回

5.7 编程实战

读程序与改错

5.7.1 读程序与改错.mp4 (84.05MB)

搜索子串

5.7.2 搜索子串.mp4 (124.87MB)

计算稿酬

5.7.3 计算稿酬.mp4 (105.86MB)

notes

简述

独立完成 3 个 demo,题目描述见视频。

5.7.1-1 以下程序的输出结果是什么?

PPT

  1. #include <stdio.h>
  2. int main() {
  3. const char* point[] = {"one", "two", "three", "four"};
  4. while (*point[2] != '\0')
  5. printf("%c", *point[2]++); // => three
  6. }

5.7.1-2 指出并更正以下程序的错误

PPT

需求分析:借助指针,将 data 字符串中的内容丢到 array 数组中。

  1. #include <stdio.h>
  2. int main() {
  3. char data[] = "There are some mistakes in the program";
  4. char* point;
  5. char array[30];
  6. int i, length;
  7. length = 0;
  8. while (data[length] != '\0')
  9. length++;
  10. for (i = 0; i < length; i++, point++)
  11. *point = data[i];
  12. array = point;
  13. printf("%s\n", array);
  14. }
  1. array = point; 数组名是指针常量哦,不能被重新赋值的,如果想要让两者指向的地址是一样的,应该这么写 point = array;
  2. for 循环的作用是为了将 data 字符串丢给 point 指向的空间,但是运算完毕后,point 的指向不再指向字符串开头了,所以 point = array; 这件事儿需要在 for 循环之前做
  3. "There are some mistakes in the program" 长度超过了 30,array 的容量不够
  1. #include <stdio.h>
  2. int main() {
  3. char data[] = "There are some mistakes in the program";
  4. char* point;
  5. char array[100]; // 定义一个数组
  6. int i, length;
  7. length = 0;
  8. while (data[length] != '\0') // 求字符串的长度
  9. length++;
  10. point = array; // 给指针赋值地址(主要修改部分)
  11. for (i = 0; i < length; i++, point++) // 输出字符串
  12. *point = data[i];
  13. printf("%s\n", array); // => There are some mistakes in the program
  14. }

修改后的代码

5.7.1-3 题目:使用指针,不用库函数实现合并字符串 s1 和 s2(每个字符串长度不超过 20)

  1. #include <stdio.h>
  2. int main() {
  3. char s1[21], s2[21], s3[42];
  4. int i = 0, j = 0;
  5. printf("请输入字符串 s1:");
  6. scanf("%s", s1);
  7. printf("请输入字符串 s2:");
  8. scanf("%s", s2);
  9. while (s1[i] != '\0') {
  10. s3[i] = s1[i];
  11. i++;
  12. }
  13. while (s2[j] != '\0') {
  14. s3[i] = s2[j];
  15. i++;
  16. j++;
  17. }
  18. s3[i] = '\0';
  19. printf("合并后的字符串为:%s\n", s3);
  20. return 0;
  21. }
  22. /* 运行结果:
  23. 请输入字符串 s1:Hello
  24. 请输入字符串 s2:World
  25. 合并后的字符串为:HelloWorld */
  1. #include <stdio.h>
  2. #define MAX_LEN 41 // 20 + 20 + 1
  3. int main() {
  4. char s1[21], s2[21], s[MAX_LEN];
  5. char *p1 = s1, *p2 = s2, *p = s;
  6. // 输入两个字符串
  7. printf("请输入两个字符串(每个字符串长度不超过20):\n");
  8. scanf("%s%s", s1, s2);
  9. // 将 s1 中的字符复制到 s 中
  10. while (*p1) {
  11. *p++ = *p1++;
  12. }
  13. // 将 s2 中的字符复制到 s 中
  14. while (*p2) {
  15. *p++ = *p2++;
  16. }
  17. // 在 s 的末尾添加一个空字符
  18. *p = '\0';
  19. // 输出合并后的字符串
  20. printf("合并后的字符串为:%s\n", s);
  21. return 0;
  22. }
  23. /* 运行结果:
  24. 请输入两个字符串(每个字符串长度不超过20):
  25. Hello World
  26. 合并后的字符串为:HelloWorld */

老师版

5.7.2 搜索子串

题目描述:
编写一个程序输入两个字符串 string1 和 string2,检查在 string1 中是否包含有 string2。
如果有,则输出 string2 在 string1 中的起始位置
如果没有,则输出 NO
如果 string2 在 string1 中多次出现,则输出在 string1 中出现的次数以及每次出现的起始位置
例如:

  • string1 = "the day the month the year"
  • string2 = "the"

输出结果应为:出现 3 次,起始位置分别是:0,8,18
又如:

  • string1 = "aaabacad"
  • string2 = "a"

输出结果应为:出现 5 次,起始位置分别是:0,1,2,4,6
假设:string1 的最大长度是 100,string2 的最大长度是 10

  1. #include <stdio.h>
  2. #include <string.h>
  3. #define S1_MAX 100
  4. #define S2_MAX 10
  5. int main() {
  6. char string1[S1_MAX + 1], string2[S2_MAX + 1];
  7. int locat[S1_MAX + 1];
  8. int count = 0;
  9. int len1, len2;
  10. // 输入字符串
  11. printf("请输入 string1:");
  12. fgets(string1, S1_MAX + 1, stdin); // scanf("%s", string1);
  13. printf("请输入 string2:");
  14. fgets(string2, S2_MAX + 1, stdin); // scanf("%s", string2);
  15. // 计算字符串长度
  16. len1 = strlen(string1) - 1;
  17. len2 = strlen(string2) - 1;
  18. printf("len1 = %d\n", len1);
  19. printf("len2 = %d\n", len2);
  20. // 判断 string2 是否在 string1 中出现过
  21. for (int i = 0; i < len1; i++) {
  22. int j = 0;
  23. while (string1[i + j] == string2[j] && j < len2) {
  24. j++;
  25. }
  26. if (j == len2) {
  27. // string2 在 string1 中出现
  28. locat[count] = i;
  29. count++;
  30. }
  31. }
  32. if (count == 0) {
  33. printf("NO\n");
  34. } else {
  35. printf("出现 %d 次,起始位置分别是:", count);
  36. for (int i = 0; i < count; i++) {
  37. printf("%d ", locat[i]);
  38. }
  39. }
  40. return 0;
  41. }
  42. /* 运行结果:
  43. 请输入 string1:the day the month the year
  44. 请输入 string2:the
  45. len1 = 26
  46. len2 = 3
  47. 出现 3 次,起始位置分别是:0 8 18
  48. 请输入 string1:the day the month the year
  49. 请输入 string2:year
  50. len1 = 26
  51. len2 = 4
  52. 出现 1 次,起始位置分别是:22
  53. 请输入 string1:the day the month the year
  54. 请输入 string2:months
  55. len1 = 26
  56. len2 = 6
  57. NO
  58. 请输入 string1:aaabacad
  59. 请输入 string2:a
  60. len1 = 8
  61. len2 = 1
  62. 出现 5 次,起始位置分别是:0 1 2 4 6
  63. 请输入 string1:aababaa
  64. 请输入 string2:aba
  65. len1 = 7
  66. len2 = 3
  67. 出现 2 次,起始位置分别是:1 3
  68. */

PPT image.png

image.png

image.png
image.png
image.png

image.png

5.7.3 计算稿酬

题目描述:
给稿件文件名称,从文件中读取单词统计单词数量,根据稿费/单词,计算稿酬

算法思想:

  1. 输入文件名称和稿件单词单价
  2. 根据文件名称,用文件指针依次读取字符串
  3. 只要文件没有结束,则单词计数器就加 1
  4. 根据统计出的单词数量和单价,计算稿酬并显示
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. int main() {
  4. const int max_filename_len = 50;
  5. char name[max_filename_len + 1], str[max_filename_len + 1];
  6. float money, total;
  7. int count = 0;
  8. FILE* fp;
  9. printf("\n请输入待处理的稿件名称:");
  10. scanf_s("%s", name, max_filename_len);
  11. printf("\n请输入稿费单价 / 单词:");
  12. scanf_s("%f", &money);
  13. fopen_s(&fp, name, "r");
  14. if (fp == NULL) {
  15. printf("文件打开失败");
  16. return -1;
  17. }
  18. while (!feof(fp)) {
  19. fscanf_s(fp, "%s", str, max_filename_len);
  20. ++count;
  21. }
  22. total = count * money;
  23. printf("稿酬为:%.2f", total);
  24. fclose(fp);
  25. /*fgets(name, max_filename_len + 1, stdin);*/
  26. return 0;
  27. }
  28. /* 运行结果:
  29. 请输入待处理的稿件名称:23_04_09.cpp
  30. 请输入稿费单价 / 单词:1
  31. 稿酬为:74.00
  32. */

while(!feof(fp)) { 循环体 } 只要没有读取到文件结尾,就不断读取文件内容

  • feof 函数用于判断给定的文件流是否已经到达了文件的结尾
  • 当函数返回值为真时,说明文件已经到达了文件结尾,不能再读取数据
  • 当函数返回值为假时,说明文件还可以继续读取数据

fscanf_s(fp, "%s", str, max_filename_len); 执行一次,读一个词汇
如果格式控制字符串是 %s,则 fscanf_s 会尝试读取一个字符串,直到遇到空白字符为止(空格、制表符、换行符等)

image.png

PPT image.png

image.pngimage.png

5.8 华为cloudIDE编程与调试

230-指针求最值

5.8.1 华为cloudIDE编程与调试-230-指针求最值.mp4 (28.41MB)

586-建立单链表实验要求解读

5.8.2 华为cloudIDE-586-建立单链表实验要求解读.mp4 (9.18MB)

notes

简述

两个练习题都蛮重要的,要求能独立撸出来。

练习 1:指针练习之最值问题

利用指针完成一个 C 程序:要求输入 n 个整数构成一个序列,搜索这一序列的最大/最小值及该值在序列中的位置。

约定:

  1. 首先输入元素个数,如果元素个数非法,则输出 error 后退出程序(error 后不接回车或换行等符号)
  2. 然后依次输入各元素,以空格分割
  3. 输出中所有标点符号都为英文符号
  4. 目标元素在序列中的位置从 1 开始计算
  1. 6
  2. 145 23 1 0 234 99
  1. max:234,position:5
  2. min:0,position:4

注意:以上输出后都有换行符,除此之外,任何多余输出皆视为错误。

  1. #include <stdio.h>
  2. #include <stdbool.h>
  3. int main() {
  4. int n;
  5. scanf("%d", &n);
  6. if (n <= 0) {
  7. printf("error");
  8. return 0;
  9. }
  10. int *arr = new int[n];
  11. for (int i = 0; i < n; i++) {
  12. scanf("%d", &arr[i]);
  13. }
  14. int max = arr[0], min = arr[0], max_position = 1, min_position = 1;
  15. for (int i = 1; i < n; i++) {
  16. if (arr[i] > max) {
  17. max = arr[i];
  18. max_position = i + 1;
  19. }
  20. if (arr[i] < min) {
  21. min = arr[i];
  22. min_position = i + 1;
  23. }
  24. }
  25. printf("max:%d,position:%d\n", max, max_position);
  26. printf("min:%d,position:%d\n", min, min_position);
  27. delete[] arr;
  28. return 0;
  29. }
  30. /* 运行结果:
  31. 6
  32. 145 23 1 0 234 99
  33. max:234,position:5
  34. min:0,position:4
  35. */
  1. #include "stdio.h"
  2. #include "stdlib.h"
  3. int main() {
  4. int n, *p;
  5. scanf("%d", &n);
  6. if (n <= 0) {
  7. printf("error");
  8. exit(0);
  9. }
  10. p = (int *)malloc(sizeof(int) * n);
  11. for (int i = 0; i < n; i++) {
  12. scanf("%d", (p + i));
  13. }
  14. int minValue = *p, maxValue = *p;
  15. int minPosition = 1, maxPosition = 1;
  16. for (int i = 1; i < n; i++) {
  17. if (minValue > *(p + i)) {
  18. minValue = *(p + i);
  19. minPosition = i + 1;
  20. }
  21. if (maxValue < *(p + i)) {
  22. maxValue = *(p + i);
  23. maxPosition = i + 1;
  24. }
  25. }
  26. printf("max:%d,position:%d\n", maxValue, maxPosition);
  27. printf("min:%d,position:%d\n", minValue, minPosition);
  28. free(p);
  29. return 0;
  30. }
  31. /* 运行结果:
  32. 6
  33. 145 23 1 0 234 99
  34. max:234,position:5
  35. min:0,position:4
  36. */

两种写法在功能上是等效的,但它们采用了不同的内存分配方法:

  • 写法 1:使用 C++ 的 newdelete[] 动态分配和释放内存。这种写法在 C++ 中更常见,同时支持构造和析构函数的调用,适用于需要构造和析构的对象。
  • 写法 2:使用 C 语言的 malloc()free() 函数动态分配和释放内存。这种写法在 C 语言中更常见,对于简单的数据类型和结构体非常实用,但不会自动调用构造和析构函数。

练习 2:建立单链表

建立带头结点的单链表,结点结构如下定义:

  1. struct node {
  2. int data;
  3. struct node *next;
  4. }

struct node* createList(int data[], int n) 函数实现建立单链表的功能,具体说明如下:

  • 输入参数:data 是一个长度为 n 的数组,里面存储的建立单链表所需的数据
  • 返回值:带头结点的单链表的首地址
  • 注意:单链表存储的数据和 data 里面数据顺序一致。
    • 比如:n = 3,data 存放的数据是 1 2 3,则建立的单链表 header 所指的数据结点依次为 1,2,3
    • 如果出现错误,则输出 “error”,并返回 NULL

思路:

  1. 函数形式已指定:struct node* createList(int data[], int n); 不需要再接收其它输入
  2. 判断元素个数是否为 n,否则输出“error”,返回 NULL。
  3. header 指针指向的单链表数据和 data 里面数据顺序一致:尾插法。
  4. 返回时需要 return header;

建立带头结点的单链表:带有头结点的单链表有两类结点,头结点元素结点

  • 头结点通常不存储数据
  • 元素结点存储数据

你可以用下面的这些函数测试 createList 得到的链表是否正确建立

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. struct node {
  4. int data;
  5. struct node* next;
  6. }
  7. int main() {
  8. struct node* header = NULL, *p;
  9. int* data, n, i;
  10. scanf("%d", &n);
  11. data = (int*)malloc(n * sizeof(int));
  12. if (!data) return 0;
  13. for (i = 0; i < n; ++i) scanf("%d", data + i);
  14. header = (struct node*)createList(data, n);
  15. p = header;
  16. printlst(header);
  17. freelst(header);
  18. free(data);
  19. return 0;
  20. }
  21. // 辅助函数
  22. void freelst(struct node* h) {
  23. struct node* p = h->next;
  24. while(p) {
  25. h->next = p->next;
  26. free(p);
  27. p = h->next;
  28. }
  29. free(h);
  30. }
  31. void printlst(struct node* h) {
  32. struct node* p = h->next;
  33. while(p) {
  34. printf("%d", p->data);
  35. p = p->next;
  36. }
  37. }
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. struct node {
  4. int data;
  5. struct node *next;
  6. };
  7. struct node *createList(int data[], int n) {
  8. struct node *header = (struct node *)malloc(sizeof(struct node)); // 创建头结点
  9. if (!header) { // 分配空间失败,返回 NULL
  10. printf("error\n");
  11. return NULL;
  12. }
  13. header->next = NULL; // 设置头结点的后继指针为空
  14. struct node *tail = header; // 定义一个指向尾结点的指针
  15. for (int i = 0; i < n; i++) {
  16. struct node *p = (struct node *)malloc(sizeof(struct node));
  17. if (!p) { // 分配空间失败,清空已分配的结点,返回 NULL
  18. printf("error\n");
  19. while (header->next) {
  20. struct node *tmp = header->next;
  21. header->next = tmp->next;
  22. free(tmp);
  23. }
  24. free(header);
  25. return NULL;
  26. }
  27. p->data = data[i];
  28. p->next = NULL;
  29. tail->next = p; // 将新建结点插入链表尾部
  30. tail = p; // 更新尾结点指针
  31. }
  32. return header; // 返回头结点指针
  33. }
  34. // 上面提供的是 createList 函数的实现,下面是测试用的辅助函数
  35. void freelst(struct node *h) {
  36. struct node *p = h->next;
  37. while (p) {
  38. h->next = p->next;
  39. free(p);
  40. p = h->next;
  41. }
  42. free(h);
  43. }
  44. void printlst(struct node *h) {
  45. struct node *p = h->next;
  46. while (p) {
  47. printf("%d ", p->data);
  48. p = p->next;
  49. }
  50. printf("\n");
  51. }
  52. int main() {
  53. struct node *header = NULL;
  54. int *data, n, i;
  55. scanf("%d", &n);
  56. data = (int *)malloc(n * sizeof(int));
  57. if (!data) return 0;
  58. for (i = 0; i < n; ++i) scanf("%d", data + i);
  59. header = createList(data, n);
  60. if (header) printlst(header);
  61. freelst(header);
  62. free(data);
  63. return 0;
  64. }
  65. /* 运行结果:
  66. 3
  67. 33 28 93
  68. 33 28 93 */