5.4 二重指针
二重指针
二重指针的水太深了,视频中介绍的不过冰山一角……
初学阶段,只要将视频中提及的代码都理解即可,后续再逐步扩展有关二重指针的知识点……
notes
简述
- 二重指针是 指向指针的指针
- 二重指针也叫 指针的指针
- 值是 另一个指针变量的地址
- 常见应用场景:
- 函数参数传递:在函数参数传递中,可以通过传递指向指针的指针,使函数能够修改指针指向的内存地址,从而达到修改函数外部变量的目的
- 动态内存分配:在动态内存分配中,二重指针可以用来动态分配二维数组内存
int x = 10; // 定义 int 类型变量 x,赋值为 10
int *p = &x; // 定义指向 int 类型变量的指针 p,使得 p 指向 x
int **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); // => abc
printf("%s\n", *(ppc + 1)); // => def
printf("%s\n", *(ppc + 2)); // => hig
return 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烫烫烫烫烫烫烫烫烫烫烫烫烫蘦ello
buffer2 = 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 = hello
buffer2 = 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 = 5
buffer1 = hello
buffer2 = 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 = 00007FF77AB79CF0
p2 address = 00007FF77AB79CF0
p1 - 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 = 00007FF6D2EF9CF0
p2 address = 00007FF6D2EF9C38
p1 - 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 = 0000008284B1F9D4
p2 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 = 0000008284B1F9D4
p2 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 = 00000001000FF614
p1: Hello address = 00000001000FF614
p2: Hello address = 00007FF78CCA9CF0
修改之后:
buffer1: hello address = 00000001000FF614
p1: hello address = 00000001000FF614
p2: Hello address = 00007FF78CCA9CF0
修改之后:
buffer1: world address = 00000001000FF614
p1: world address = 00000001000FF614
p2: 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 = 1
num = 2
num = 3
num = 4
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() 函数分配内存的一般步骤是:
- 调用 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?
3
11
22
33
aver = 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 10
int 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 100000
int 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 + 1
int 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 10
int 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:the
len1 = 26
len2 = 3
出现 3 次,起始位置分别是:0 8 18
请输入 string1:the day the month the year
请输入 string2:year
len1 = 26
len2 = 4
出现 1 次,起始位置分别是:22
请输入 string1:the day the month the year
请输入 string2:months
len1 = 26
len2 = 6
NO
请输入 string1:aaabacad
请输入 string2:a
len1 = 8
len2 = 1
出现 5 次,起始位置分别是:0 1 2 4 6
请输入 string1:aababaa
请输入 string2:aba
len1 = 7
len2 = 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 开始计算
6
145 23 1 0 234 99
max:234,position:5
min: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;
}
/* 运行结果:
6
145 23 1 0 234 99
max:234,position:5
min: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;
}
/* 运行结果:
6
145 23 1 0 234 99
max:234,position:5
min: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) { // 分配空间失败,返回 NULL
printf("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) { // 分配空间失败,清空已分配的结点,返回 NULL
printf("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;
}
/* 运行结果:
3
33 28 93
33 28 93 */