5.4 二重指针
二重指针
二重指针的水太深了,视频中介绍的不过冰山一角……
初学阶段,只要将视频中提及的代码都理解即可,后续再逐步扩展有关二重指针的知识点……
notes
简述
- 二重指针是 指向指针的指针
- 二重指针也叫 指针的指针
- 值是 另一个指针变量的地址
- 常见应用场景:
- 函数参数传递:在函数参数传递中,可以通过传递指向指针的指针,使函数能够修改指针指向的内存地址,从而达到修改函数外部变量的目的
- 动态内存分配:在动态内存分配中,二重指针可以用来动态分配二维数组内存
int x = 10; // 定义 int 类型变量 x,赋值为 10int *p = &x; // 定义指向 int 类型变量的指针 p,使得 p 指向 xint **pp = &p; // 定义指向指针的指针 pp,使得 pp 指向 p
demo | PPT | 体验二重指针
#include <stdio.h>int main() {int v = 120, *q = &v, **p = &q;printf("v = %d\n", v);printf("*q = %d\n", *q);printf("**p = %d\n", **p);return 0;}/* 运行结果v = 120*q = 120**p = 120*/

#include <stdio.h>int main() {char* pc[] = {"abc", "def", "hig"}; // pc 是一个二重指针char** ppc; // 定义 ppc 为二重指针ppc = pc;printf("%s\n", *ppc); // => abcprintf("%s\n", *(ppc + 1)); // => defprintf("%s\n", *(ppc + 2)); // => higreturn 0;}
#include <stdio.h>int main() {int a[5] = {1, 3, 5, 7, 9};int* num[5] = {&a[0], &a[1], &a[2], &a[3], &a[4]};int **p, i;p = num;for (i = 0; i < 5; i++) {printf("%d\t", **p);p++;}printf("\n");return 0;}/* 运行结果:1 3 5 7 9*/
字符指针数组和二重指针之间的关系
问:char* pc[] = {"abc", "def", "hig"}; 其中的 pc 是二重指针吗?
答:回答“是”或者“不是”其实都对
- 回答“是”:二重指针是指向指针的指针,pc 的每一项都是一个字符串,字符串其实就是一个字符数组,这就意味着 pc 指针数组中的每一个指针都指向一个地址
- 回答“不是”:字符指针数组 pc 的数据类型是
char *[],它的数据类型,本质上是一个指针数组,而非指针。
补充: 课件中说 pc 是一个二重指针,但网上也有说不是的。其实没必要深究字符指针数组到底是不是二重指针,我们只要知道如何使用即可,这种细节上的概念区分着实没必要,简单了解一下大家给出的答复的背后的原因即可。
5.5 字符指针
字符指针
理解字符串指针(字符串常量)和字符数组之间的差异。
notes
字符数组不一定是字符串,但字符串一定是字符数组
- 字符数组是一段连续的用于存储字符类型数据的内存空间
- 字符串是由字符组成的字符数组,并且以 NULL
**\0**作为字符串的结束标志 - 如果字符数组的结尾字符是
**\0**那么该字符数组可以视作一个字符串,否则就不是一个字符串。
使用 %s 作为格式控制字符打印字符数组可能会出现异常:
#include <stdio.h>int main() {char buffer1[] = {'h', 'e', 'l', 'l', 'o'};char buffer2[] = "hello";printf("buffer1 = %s\n", buffer1);printf("buffer2 = %s\n", buffer2);}/* 运行结果:buffer1 = hello烫烫烫烫烫烫烫烫烫烫烫烫烫蘦ellobuffer2 = hello*/
上述代码中,buffer1 是一个字符数组,但是它并非字符串,所以当我们使用 %s 进行打印时,得到的结果会存在异常。
- 分析原因:
- %s 格式控制字符用于打印以空字符
**'\0'**结尾的字符串 - 不是所有的字符数组都是以空字符结尾的字符串
- %s 格式控制字符用于打印以空字符
- 导致的结果:
- 如果一个字符数组没有以空字符结尾,使用 %s 格式控制字符来打印它将导致未定义的行为
- 比如上述的 buffer1 打印的内容就是错误的
加入我们已经知道字符数组 buffer1 的结尾字符不是 '\0',那么又希望能够使用 %s 将 buffer1 这个字符数组正常打印出来,又啥解决办法呢?
- 方案 1:
- 改变 buffer1
- 手动在结尾处加上
'\0'空字符串作为字符串的结束符,以确保 buffer1 可以“正常”打印。
- 方案 2:
- 不改变 buffer1
- 在打印字符数组时,应该使用
%.*s格式控制字符 - 传递字符数组的长度作为参数,让 printf 知道需要打印多长的内容
#include <stdio.h>int main() {char buffer1[] = { 'h', 'e', 'l', 'l', 'o', '\0'};char buffer2[] = "hello";printf("buffer1 = %s\n", buffer1);printf("buffer2 = %s\n", buffer2);}/* 运行结果:buffer1 = hellobuffer2 = hello*/
#include <stdio.h>int main() {char buffer1[] = {'h', 'e', 'l', 'l', 'o'};char buffer2[] = "hello";const int buffer1_len = sizeof(buffer1) / sizeof(*buffer1);printf("buffer1_len = %d\n", buffer1_len);printf("buffer1 = %.*s\n", buffer1_len, buffer1);printf("buffer2 = %s\n", buffer2);}/* 运行结果:buffer1_len = 5buffer1 = hellobuffer2 = hello*/
字符串常量
- 字符串常量是使用一对双引号
"括起来的字符序列,其中可以包含任何字符,包括空格和特殊字符。"Hello, World!""This is a string constant.""1234""!@#$%^&*()"
- 字符串常量是只读的,因此不能通过指向字符串的指针来更改字符串的内容
- 字符串常量通常存储在只读存储器中,尝试修改只读存储器会导致段错误
- 字符串常量是以字符数组的形式存在的
- 不同字符串常量:
- 每个 不同的 字符串常量都是独立存在的,它们占用的内存空间是不同的
- 每个 不同的 字符串常量都有其唯一的地址
- 相同字符串常量:
- 相同的字符串常量的地址也不一定就是相同的,得看我们在定义的时候是怎么写的
- 编译器会优化字符串常量的存储,使得相同的字符串常量在内存中只有一份
- 这种优化只是针对字符串常量的,对于数组或者其他变量,在内存中都会分配独立的空间,指向这些变量的指针指向的是它们所分配的内存空间的首地址。
#include <stdio.h>int main() {const char* p1 = "Hello";const char* p2 = "Hello";printf("p1 address = %p\n", p1);printf("p2 address = %p\n", p2);printf("p1 - p2 = %d\n", p1 - p2);}/* 运行结果:p1 address = 00007FF77AB79CF0p2 address = 00007FF77AB79CF0p1 - p2 = 0*/
#include <stdio.h>int main() {const char* p1 = "Hello1";const char* p2 = "Hello2";printf("p1 address = %p\n", p1);printf("p2 address = %p\n", p2);printf("p1 - p2 = %d\n", p1 - p2);}/* 运行结果:p1 address = 00007FF6D2EF9CF0p2 address = 00007FF6D2EF9C38p1 - p2 = 184*/
#include <stdio.h>int main() {char buffer1[] = "Hello"; // 数组const char* p1 = buffer1;const char* p2 = "Hello"; // 字符串常量printf("p1 address = %p\n", p1);printf("p2 address = %p\n", p2);}/* 运行结果:p1 address = 0000008284B1F9D4p2 address = 00007FF799B19CF0*/
字符串常量是不可更改的?
这句话不全对,得看怎么理解了,可以说“是”,也可以说“不是”。
#include <stdio.h>int main() {char buffer1[] = "Hello"; // 数组const char* p1 = buffer1;const char* p2 = "Hello"; // 字符串常量printf("p1 address = %p\n", p1);printf("p2 address = %p\n", p2);}/* 运行结果:p1 address = 0000008284B1F9D4p2 address = 00007FF799B19CF0*/
明确一点:char buffer1[] = "Hello";、const char* p2 = "Hello"; 这两种写法中出现的 "Hello" 都是字符串常量。但是由于前者在初始化时,数据类型是 char [],而后者是 char *,所以此时编译器会给它们分配不同的存储地址。
- 可以改变 p1 指向的字符串,因为它是一个数组
- 无法改变 p2 指向的字符串,因为它是一个字符串常量
#include <stdio.h>int main() {char buffer1[] = "Hello";const char* p1 = buffer1;const char* p2 = "Hello";printf("修改之前:\n");printf("buffer1: %s\taddress = %p\n", buffer1, buffer1);printf("p1: %s\taddress = %p\n", p1, p1);printf("p2: %s\taddress = %p\n", p2, p2);buffer1[0] = 'h'; // 错误的做法 *p1 = 'h'printf("修改之后:\n");printf("buffer1: %s\taddress = %p\n", buffer1, buffer1);printf("p1: %s\taddress = %p\n", p1, p1);printf("p2: %s\taddress = %p\n", p2, p2);// 将 p1 指向的字符串强制转换为非 const 类型指针,然后通过指针修改字符串*((char*)p1) = 'w';*((char*)p1 + 1) = 'o';*((char*)p1 + 2) = 'r';*((char*)p1 + 3) = 'l';*((char*)p1 + 4) = 'd';printf("修改之后:\n");printf("buffer1: %s\taddress = %p\n", buffer1, buffer1);printf("p1: %s\taddress = %p\n", p1, p1);printf("p2: %s\taddress = %p\n", p2, p2);return 0;}/* 运行结果:修改之前:buffer1: Hello address = 00000001000FF614p1: Hello address = 00000001000FF614p2: Hello address = 00007FF78CCA9CF0修改之后:buffer1: hello address = 00000001000FF614p1: hello address = 00000001000FF614p2: Hello address = 00007FF78CCA9CF0修改之后:buffer1: world address = 00000001000FF614p1: world address = 00000001000FF614p2: Hello address = 00007FF78CCA9CF0*/
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*是可以被重新赋值的,这时候再去修改“字符串常量”的值,就是被允许的做法
- 虽然被 const 修饰的内容无法被重新赋值,但是可以先将 const 类型的字符指针强制转为
可以这么理解:
char buffer1[] = "Hello";当我们看到同事这么写时,就意味着同事在告诉我们,这个字符串是一个可修改的内容,如果要改,你随意就好const char* p2 = "Hello";当我们看到同事这么写时,就意味着同事不希望我们去改变这个字符串,这东西是一个字符串常量
5.6 动态内存
动态内存
notes
内存映像
- 内存映像是 程序在运行时在计算机内存中的存储结构
- 内存映像是 将进程在内存中的运行状态(包括代码、数据、堆栈等)以文件的形式保存在硬盘上,以便在需要时重新加载到内存中
- 内存映像是 程序在内存中的运行状态,包括程序代码、全局变量、堆、栈等,以及它们在内存中的分布和状态
- 在内存中,程序被分割为多个不同的段,每个段都有其自己的特定的作用和属性,如代码段、数据段、堆栈段等
- 代码段:包含了程序的 可执行代码
- 数据段:包含了程序中 已初始化的全局变量和静态变量
- 堆段:包含了 动态分配的内存
- 栈段:包含了 函数调用的参数、局部变量等信息
- 在程序运行时,操作系统会将程序所需要的资源,如内存空间、CPU 时间等,分配给程序并进行动态调度,以保证程序能够正常运行。
- 在程序运行的同时,操作系统还会监控程序的运行状态,如内存使用情况、CPU 占用率等,并根据需要进行资源调整和分配,以保证系统的稳定和安全。

栈(stack)分配内存、堆(heap)分配内存
栈分配:
- 栈段内存由 操作系统 自动 管理
- 在函数内部声明的局部变量通常从栈上分配内存,并在函数返回时自动释放
- 在函数调用过程中,每个函数都有自己的栈帧,用于存储该函数的参数、局部变量、返回地址等信息
- 变量所在的栈帧在代码块结束时被销毁,分配的内存空间也就被释放了
- 栈段内存则由操作系统自动分配和释放
- 栈段内存则是在函数调用时自动分配,函数返回时自动释放
- 存放函数参数、局部变量值等
- 在执行函数调用时,系统在栈上为函数内的局部变量及形参分配内存
- 函数执行结束时,自动释放这些内存
堆分配:
- 堆段内存需要 程序员 手动 管理,需要程序员手动申请和释放
- 程序员需要确保在使用堆段内存时进行正确的申请、使用和释放,避免出现内存泄漏、悬垂指针、非法访问的情况
- 申请:堆段内存是通过 malloc()、calloc() 等函数来申请的
- 释放:堆段内存需要手动释放,否则会导致内存泄漏,可以 使用
**free**函数进行释放 - 分配的内存空间可以在程序的任意位置进行访问和修改
- 在程序运行期间,用来申请的内存都是从堆上分配的
- 动态内存的生存期由程序员自己来决定
静态内存
- 静态分配是在程序编译时就分配好了内存空间,变量的内存空间在程序运行时就一直存在,直到程序结束时才会被释放
- 静态分配的变量包括全局变量和局部静态变量(local static variable)
函数体内声明的非静态变量一定是局部变量(local variable),也称为自动变量(automatic variable)
局部变量的生命周期由编译器自动管理,无需程序员手动管理:
- 局部变量是在函数执行时创建的,当函数返回时自动销毁
- 局部变量通常存储在栈上,每次函数调用时,将为其分配一段新的空间,当函数返回时,该空间将被释放
局部静态变量:
- 局部静态变量的生命周期从函数第一次被调用开始,直到程序结束才结束,而不是在函数调用结束时结束
- 局部静态变量的内存空间在程序整个运行过程中都存在,而不是在函数调用结束后被释放
#include <stdio.h>int count() {static int num = 0;num++;return num;}int main() {for (int i = 0; i < 5; i++) {printf("num = %d\n", count());}return 0;}/* 运行结果:num = 1num = 2num = 3num = 4num = 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() 函数分配内存的一般步骤是:
- 调用 malloc() 函数分配内存
- 用指针变量保存分配的内存地址
- 使用分配的内存(读取、写入等操作)
- 用 free() 函数释放内存
#include <stdio.h>#include <stdlib.h>int main() {int n;printf("请输入要申请多少个整型空间:");scanf("%d", &n);int* p = (int*)malloc(n * sizeof(int));if (p == NULL) {printf("内存申请失败\n");return 1;}printf("内存申请成功\n");for (int i = 0; i < n; i++) {printf("请输入第 %d 个整数:", i + 1);scanf("%d", &p[i]);}printf("输入的数字为:");for (int i = 0; i < n; i++) {printf("%d\t", *(p + i));}free(p);p = NULL;printf("\n内存成功释放\n");return 0;}/* 运行结果:请输入要申请多少个整型空间:3内存申请成功请输入第 1 个整数:11请输入第 2 个整数:22请输入第 3 个整数:33输入的数字为:11 22 33内存成功释放*/
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 去访问那块内存空间,则这种行为是未定义的
- 养成习惯:在释放某个指针所指向的空间的同时,记得将指针的指向也置空,以免出现野指针
- 虽然
#include <stdio.h>#include <stdlib.h>int main() {int *p = NULL, n, i;double aver, sum;printf("How many students?\n");scanf("%d", &n);p = (int *) malloc(n * sizeof(int));if (p == NULL) {printf("No enough memory!\n");exit(1);}for (int i = 0; i < n; ++i) {scanf("%d", &p[i]);sum += p[i];}aver = sum / n;printf("aver = %.1f\n", aver);free(p);p = NULL; // 防止野指针return 0;}/* 运行结果:How many students?3112233aver = 22.0*/
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 存储在这个空间中
#include <stdio.h>#include <stdlib.h>int main() {int *ptr = (int*)malloc(10 * sizeof(int));// int *ptr = (int*)calloc(10, sizeof(int));if (ptr == NULL) {printf("内存分配失败");return 1;}for (int i = 0; i < 10; i++) {*(ptr + i) = i + 1;}for (int i = 0; i < 10; i++) {printf("%d ", *(ptr + i));}free(ptr);ptr = nullptr;return 0;}/* 运行结果:1 2 3 4 5 6 7 8 9 10*/
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* 指针并不知道指向的是哪种类型的数据,所以不能通过它进行指针运算,也不能用它来访问指针指向的值,必须先将其转换为具体类型的指针才能进行操作
源码
#include <stdio.h>#include <stdlib.h>int main() {int *p, i = 0;p = (int*)malloc(sizeof(int));if (p) {*p = 5;printf("%d", *p);}return 0;}
#include <stdio.h>#include <stdlib.h>int main() {int *p, i = 0;p = (int*)malloc(sizeof(int));if (p) {*p = 5;printf("%d", *p);}free(p); // 释放动态内存分配的空间return 0;}
#include <stdio.h>#include <stdlib.h>#define N 10int main() {int *p, i = 0;p = (int*)malloc(N * sizeof(int));if (p) {for (i = 0; i <= N; ++i, ++p) {*p = i + 1;printf("%d ", *p); // => 1 2 3 4 5 6 7 8 9 10 11}}return 0;}
#include <stdio.h>#include <stdlib.h>#define N 100000int main() {int *p, i = 0;while(1) {p = (int *)malloc(N * sizeof(int));printf("%p", p);*p = i++;if (p == NULL) break;}return 0;}
上述这种写法,会不断地瓜分内存,最终可能会导致内存空间不足,从而导致程序崩溃。因此,当我们需要多次调用 malloc 分配内存空间时,需要及时 free,以免造成内存空间不足,程序崩溃的问题。
⚠️ 上述这种写法,建议不要尝试丢到类似 lightly 这样的 cloudIDE 中去跑,远程的云主机会直接跑死掉…… 我们只要知道在使用 malloc 申请完内存后,记得 free 即可。
常见的内存错误
- 内存分配未成功便使用
- 内存分配成功,但没有初始化便开始使用
- 内存分配成功并初始化,但发生越界使用
- 申请内存后没有及时释放内存
- 释放内存后仍继续使用
关于内存分配编程的建议原则
- 仅在需要时才使用 malloc
- malloc 和 free 要配对使用,malloc 在函数入口,free 在函数出口
- 使用 malloc 时要检查函数返回值
- 使用 free 函数后,将指针设置为 NULL
- 不要把局部变量的地址作为函数返回值返回
5.7 编程实战
读程序与改错
搜索子串
计算稿酬
notes
简述
独立完成 3 个 demo,题目描述见视频。
5.7.1-1 以下程序的输出结果是什么?

#include <stdio.h>int main() {const char* point[] = {"one", "two", "three", "four"};while (*point[2] != '\0')printf("%c", *point[2]++); // => three}
5.7.1-2 指出并更正以下程序的错误

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

5.7.1-3 题目:使用指针,不用库函数实现合并字符串 s1 和 s2(每个字符串长度不超过 20)
#include <stdio.h>int main() {char s1[21], s2[21], s3[42];int i = 0, j = 0;printf("请输入字符串 s1:");scanf("%s", s1);printf("请输入字符串 s2:");scanf("%s", s2);while (s1[i] != '\0') {s3[i] = s1[i];i++;}while (s2[j] != '\0') {s3[i] = s2[j];i++;j++;}s3[i] = '\0';printf("合并后的字符串为:%s\n", s3);return 0;}/* 运行结果:请输入字符串 s1:Hello请输入字符串 s2:World合并后的字符串为:HelloWorld */
#include <stdio.h>#define MAX_LEN 41 // 20 + 20 + 1int main() {char s1[21], s2[21], s[MAX_LEN];char *p1 = s1, *p2 = s2, *p = s;// 输入两个字符串printf("请输入两个字符串(每个字符串长度不超过20):\n");scanf("%s%s", s1, s2);// 将 s1 中的字符复制到 s 中while (*p1) {*p++ = *p1++;}// 将 s2 中的字符复制到 s 中while (*p2) {*p++ = *p2++;}// 在 s 的末尾添加一个空字符*p = '\0';// 输出合并后的字符串printf("合并后的字符串为:%s\n", s);return 0;}/* 运行结果:请输入两个字符串(每个字符串长度不超过20):Hello World合并后的字符串为: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
#include <stdio.h>#include <string.h>#define S1_MAX 100#define S2_MAX 10int main() {char string1[S1_MAX + 1], string2[S2_MAX + 1];int locat[S1_MAX + 1];int count = 0;int len1, len2;// 输入字符串printf("请输入 string1:");fgets(string1, S1_MAX + 1, stdin); // scanf("%s", string1);printf("请输入 string2:");fgets(string2, S2_MAX + 1, stdin); // scanf("%s", string2);// 计算字符串长度len1 = strlen(string1) - 1;len2 = strlen(string2) - 1;printf("len1 = %d\n", len1);printf("len2 = %d\n", len2);// 判断 string2 是否在 string1 中出现过for (int i = 0; i < len1; i++) {int j = 0;while (string1[i + j] == string2[j] && j < len2) {j++;}if (j == len2) {// string2 在 string1 中出现locat[count] = i;count++;}}if (count == 0) {printf("NO\n");} else {printf("出现 %d 次,起始位置分别是:", count);for (int i = 0; i < count; i++) {printf("%d ", locat[i]);}}return 0;}/* 运行结果:请输入 string1:the day the month the year请输入 string2:thelen1 = 26len2 = 3出现 3 次,起始位置分别是:0 8 18请输入 string1:the day the month the year请输入 string2:yearlen1 = 26len2 = 4出现 1 次,起始位置分别是:22请输入 string1:the day the month the year请输入 string2:monthslen1 = 26len2 = 6NO请输入 string1:aaabacad请输入 string2:alen1 = 8len2 = 1出现 5 次,起始位置分别是:0 1 2 4 6请输入 string1:aababaa请输入 string2:abalen1 = 7len2 = 3出现 2 次,起始位置分别是:1 3*/
PPT






5.7.3 计算稿酬
题目描述:
给稿件文件名称,从文件中读取单词统计单词数量,根据稿费/单词,计算稿酬
算法思想:
- 输入文件名称和稿件单词单价
- 根据文件名称,用文件指针依次读取字符串
- 只要文件没有结束,则单词计数器就加 1
- 根据统计出的单词数量和单价,计算稿酬并显示
#include <stdio.h>#include <stdlib.h>int main() {const int max_filename_len = 50;char name[max_filename_len + 1], str[max_filename_len + 1];float money, total;int count = 0;FILE* fp;printf("\n请输入待处理的稿件名称:");scanf_s("%s", name, max_filename_len);printf("\n请输入稿费单价 / 单词:");scanf_s("%f", &money);fopen_s(&fp, name, "r");if (fp == NULL) {printf("文件打开失败");return -1;}while (!feof(fp)) {fscanf_s(fp, "%s", str, max_filename_len);++count;}total = count * money;printf("稿酬为:%.2f", total);fclose(fp);/*fgets(name, max_filename_len + 1, stdin);*/return 0;}/* 运行结果:请输入待处理的稿件名称:23_04_09.cpp请输入稿费单价 / 单词:1稿酬为:74.00*/
while(!feof(fp)) { 循环体 } 只要没有读取到文件结尾,就不断读取文件内容
- feof 函数用于判断给定的文件流是否已经到达了文件的结尾
- 当函数返回值为真时,说明文件已经到达了文件结尾,不能再读取数据
- 当函数返回值为假时,说明文件还可以继续读取数据
fscanf_s(fp, "%s", str, max_filename_len); 执行一次,读一个词汇
如果格式控制字符串是 %s,则 fscanf_s 会尝试读取一个字符串,直到遇到空白字符为止(空格、制表符、换行符等)

PPT



5.8 华为cloudIDE编程与调试
230-指针求最值
586-建立单链表实验要求解读
notes
简述
两个练习题都蛮重要的,要求能独立撸出来。
练习 1:指针练习之最值问题
利用指针完成一个 C 程序:要求输入 n 个整数构成一个序列,搜索这一序列的最大/最小值及该值在序列中的位置。
约定:
- 首先输入元素个数,如果元素个数非法,则输出 error 后退出程序(error 后不接回车或换行等符号)
- 然后依次输入各元素,以空格分割
- 输出中所有标点符号都为英文符号
- 目标元素在序列中的位置从 1 开始计算
6145 23 1 0 234 99
max:234,position:5min:0,position:4
注意:以上输出后都有换行符,除此之外,任何多余输出皆视为错误。
#include <stdio.h>#include <stdbool.h>int main() {int n;scanf("%d", &n);if (n <= 0) {printf("error");return 0;}int *arr = new int[n];for (int i = 0; i < n; i++) {scanf("%d", &arr[i]);}int max = arr[0], min = arr[0], max_position = 1, min_position = 1;for (int i = 1; i < n; i++) {if (arr[i] > max) {max = arr[i];max_position = i + 1;}if (arr[i] < min) {min = arr[i];min_position = i + 1;}}printf("max:%d,position:%d\n", max, max_position);printf("min:%d,position:%d\n", min, min_position);delete[] arr;return 0;}/* 运行结果:6145 23 1 0 234 99max:234,position:5min:0,position:4*/
#include "stdio.h"#include "stdlib.h"int main() {int n, *p;scanf("%d", &n);if (n <= 0) {printf("error");exit(0);}p = (int *)malloc(sizeof(int) * n);for (int i = 0; i < n; i++) {scanf("%d", (p + i));}int minValue = *p, maxValue = *p;int minPosition = 1, maxPosition = 1;for (int i = 1; i < n; i++) {if (minValue > *(p + i)) {minValue = *(p + i);minPosition = i + 1;}if (maxValue < *(p + i)) {maxValue = *(p + i);maxPosition = i + 1;}}printf("max:%d,position:%d\n", maxValue, maxPosition);printf("min:%d,position:%d\n", minValue, minPosition);free(p);return 0;}/* 运行结果:6145 23 1 0 234 99max:234,position:5min:0,position:4*/
两种写法在功能上是等效的,但它们采用了不同的内存分配方法:
- 写法 1:使用 C++ 的
new和delete[]动态分配和释放内存。这种写法在 C++ 中更常见,同时支持构造和析构函数的调用,适用于需要构造和析构的对象。 - 写法 2:使用 C 语言的
malloc()和free()函数动态分配和释放内存。这种写法在 C 语言中更常见,对于简单的数据类型和结构体非常实用,但不会自动调用构造和析构函数。
练习 2:建立单链表
建立带头结点的单链表,结点结构如下定义:
struct node {int data;struct node *next;}
struct node* createList(int data[], int n) 函数实现建立单链表的功能,具体说明如下:
- 输入参数:data 是一个长度为 n 的数组,里面存储的建立单链表所需的数据
- 返回值:带头结点的单链表的首地址
- 注意:单链表存储的数据和 data 里面数据顺序一致。
- 比如:n = 3,data 存放的数据是 1 2 3,则建立的单链表 header 所指的数据结点依次为 1,2,3
- 如果出现错误,则输出 “error”,并返回 NULL
思路:
- 函数形式已指定:
struct node* createList(int data[], int n);不需要再接收其它输入 - 判断元素个数是否为 n,否则输出“error”,返回 NULL。
- header 指针指向的单链表数据和 data 里面数据顺序一致:尾插法。
- 返回时需要
return header;
建立带头结点的单链表:带有头结点的单链表有两类结点,头结点 和 元素结点
- 头结点通常不存储数据
- 元素结点存储数据
你可以用下面的这些函数测试 createList 得到的链表是否正确建立
#include <stdio.h>#include <stdlib.h>struct node {int data;struct node* next;}int main() {struct node* header = NULL, *p;int* data, n, i;scanf("%d", &n);data = (int*)malloc(n * sizeof(int));if (!data) return 0;for (i = 0; i < n; ++i) scanf("%d", data + i);header = (struct node*)createList(data, n);p = header;printlst(header);freelst(header);free(data);return 0;}// 辅助函数void freelst(struct node* h) {struct node* p = h->next;while(p) {h->next = p->next;free(p);p = h->next;}free(h);}void printlst(struct node* h) {struct node* p = h->next;while(p) {printf("%d", p->data);p = p->next;}}
#include <stdio.h>#include <stdlib.h>struct node {int data;struct node *next;};struct node *createList(int data[], int n) {struct node *header = (struct node *)malloc(sizeof(struct node)); // 创建头结点if (!header) { // 分配空间失败,返回 NULLprintf("error\n");return NULL;}header->next = NULL; // 设置头结点的后继指针为空struct node *tail = header; // 定义一个指向尾结点的指针for (int i = 0; i < n; i++) {struct node *p = (struct node *)malloc(sizeof(struct node));if (!p) { // 分配空间失败,清空已分配的结点,返回 NULLprintf("error\n");while (header->next) {struct node *tmp = header->next;header->next = tmp->next;free(tmp);}free(header);return NULL;}p->data = data[i];p->next = NULL;tail->next = p; // 将新建结点插入链表尾部tail = p; // 更新尾结点指针}return header; // 返回头结点指针}// 上面提供的是 createList 函数的实现,下面是测试用的辅助函数void freelst(struct node *h) {struct node *p = h->next;while (p) {h->next = p->next;free(p);p = h->next;}free(h);}void printlst(struct node *h) {struct node *p = h->next;while (p) {printf("%d ", p->data);p = p->next;}printf("\n");}int main() {struct node *header = NULL;int *data, n, i;scanf("%d", &n);data = (int *)malloc(n * sizeof(int));if (!data) return 0;for (i = 0; i < n; ++i) scanf("%d", data + i);header = createList(data, n);if (header) printlst(header);freelst(header);free(data);return 0;}/* 运行结果:333 28 9333 28 93 */

