课程目标:掌握数组越界的核心陷阱、野指针与空指针的产生原因,能识别实战中的典型错误,熟练运用规避技巧与修正方法,为后续内存操作类知识点奠定基础。
关键词
数组越界、下标校验、野指针、空指针、悬空指针、指针初始化、SAFE_FREE宏、解引用校验
一、课程导入
在C语言实战开发中,内存操作错误是最致命、最高频的bug来源,其中数组越界、野指针/空指针解引用,更是新手到进阶开发者都容易踩坑的重点。
本节课将聚焦这两个核心陷阱,从"错误表现→根源分析→规避方法→实操修正"四个维度,结合真实实战案例,帮大家彻底吃透问题本质,避免在开发中踩坑。
二、核心知识点详解
知识点1:数组越界访问
1.1 错误表现
数组越界是指访问了超出数组实际长度的下标,常见表现有3种:
-
程序直接崩溃,提示"Segmentation Fault(段错误)";
-
数据被篡改,比如相邻变量的值莫名变化,导致逻辑异常;
-
缓冲区溢出,极端情况下可能被恶意利用,引发安全漏洞。
1.2 根本原因
C语言的设计特性决定了它不检查数组下标的合法性,这是数组越界的核心根源。
比如定义int arr[5],数组下标范围是0~4(共5个元素),但C语言不会阻止我们访问arr[5]、arr[-1]这类非法下标------访问非法下标时,会读取/修改内存中随机地址的数据,进而引发异常。
补充:数组在内存中是连续存储的,越界访问会破坏相邻内存的内容,这也是"数据篡改"的核心原因。
1.3 典型场景(高频坑点)
-
循环遍历数组时,终止条件错误(如i <= len而非i < len);
-
手动指定下标时,忽略"下标从0开始"的规则(如数组长度为5,下标写到5);
-
数组作为函数参数传递后,误用sizeof计算长度,导致遍历越界(后续第4课详细讲解)。
知识点2:野指针与空指针
2.1 概念区分
野指针和空指针都属于"无效指针",但本质不同,需严格区分:
-
空指针:明确指向NULL(系统定义的无效地址,值为0),比如int *p = NULL;
-
野指针:指针未初始化、或指向的内存已释放,最终指向随机的无效地址(无法预判)。
2.2 错误表现
无论是野指针还是空指针,直接解引用(*p)都会导致:
-
程序崩溃(Segmentation Fault);
-
内存访问异常,篡改随机内存的数据(比数组越界更难排查)。
2.3 根本原因(高频场景)
(1)野指针的3种常见产生场景
-
指针声明时未初始化:int *p; (未赋值,指向随机内存,直接解引用必出问题);
-
指针指向的内存被释放后,未置NULL:free§后,p仍指向原内存地址(该地址已被系统回收,成为无效地址);
-
指针指向局部变量:函数执行完毕后,局部变量被销毁,指向它的指针成为野指针(后续第7课详细讲解)。
(2)空指针的常见产生场景
-
指针初始化时明确赋值为NULL,但未判断就直接解引用;
-
函数返回NULL(如malloc分配内存失败时返回NULL),未校验就解引用。
三、实战案例与修正
结合高频场景,拆解错误代码,给出标准修正方案,配套实操步骤。
案例1:数组越界(循环遍历场景)
错误代码
c
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5}; // 数组长度5,下标0~4
// 错误:终止条件i <= 5,会访问arr[5](越界)
for (int i = 0; i <= 5; i++) {
printf("arr[%d] = %d\n", i, arr[i]);
}
return 0;
}
错误分析
循环终止条件设置错误,i从0开始,当i=5时,arr[5]超出数组实际下标范围(0~4),属于越界访问,运行时可能崩溃,或输出随机垃圾值。
正确代码(修正方案)
c
#include <stdio.h>
#define ARR_LEN 5 // 用宏定义数组长度,避免硬编码
int main() {
int arr[ARR_LEN] = {1, 2, 3, 4, 5};
// 修正:终止条件i < ARR_LEN,遵循"左闭右开"原则
for (int i = 0; i < ARR_LEN; i++) {
printf("arr[%d] = %d\n", i, arr[i]);
}
return 0;
}
实操要点
-
用宏定义数组长度,避免硬编码(后续修改长度时,只需修改宏定义,减少错误);
-
循环遍历数组时,始终用"i < 数组长度"作为终止条件,牢记"下标从0开始"。
案例2:野指针(未初始化场景)
错误代码
c
#include <stdio.h>
int main() {
int *p; // 指针未初始化,野指针
*p = 10; // 解引用野指针,程序崩溃
printf("%d\n", *p);
return 0;
}
错误分析
指针p声明时未初始化,指向内存中随机的无效地址,解引用(*p)会试图修改该随机地址的数据,触发段错误,程序直接崩溃。
正确代码(修正方案)
c
#include <stdio.h>
int main() {
int *p = NULL; // 指针初始化时置NULL
int a = 10;
// 解引用前先校验指针非NULL
if (p != NULL) {
*p = 10;
} else {
// 指针无效时,给出提示,避免崩溃
printf("指针p为NULL,无法解引用\n");
p = &a; // 给指针赋值有效地址
}
printf("%d\n", *p); // 正确输出10
return 0;
}
实操要点
-
所有指针声明时,必须初始化(优先置NULL);
-
解引用指针前,必须校验指针 != NULL,避免空指针/野指针解引用。
案例3:悬空指针(内存释放后未置NULL场景)
错误代码
c
#include <stdlib.h>
int main() {
int *p = (int*)malloc(4); // 分配堆内存
if (p != NULL) {
*p = 20;
free(p); // 释放内存,但未置NULL
}
*p = 30; // 解引用已释放的悬空指针,行为不可预测
return 0;
}
错误分析
free§后,p指向的堆内存被系统回收,此时p成为悬空指针(仍指向原地址,但地址已无效),再解引用*p会修改随机内存的数据,程序可能崩溃,或出现逻辑异常。
正确代码(修正方案)
c
#include <stdlib.h>
// 封装安全释放宏,避免重复写校验逻辑
#define SAFE_FREE(p) {if(p != NULL){free(p); p = NULL;}}
int main() {
int *p = (int*)malloc(4);
if (p != NULL) {
*p = 20;
SAFE_FREE(p); // 释放后自动置NULL
}
// 再次解引用前校验,避免错误
if (p != NULL) {
*p = 30;
} else {
printf("指针p已释放,无法解引用\n");
}
return 0;
}
实操要点
-
内存释放(free)后,必须将指针置NULL,避免成为悬空指针;
-
封装SAFE_FREE宏,简化释放逻辑,减少遗漏置NULL的错误。
四、课堂作业
作业要求:找出错误代码中的问题,写出错误原因,给出修正代码,标注关键修正点。
- 作业1(数组越界):
c
#include <stdio.h>
int main() {
char str[5] = "hello"; // 错误点:数组长度不足,未预留\0空间
for (int i = 0; i <= 5; i++) {
printf("%c", str[i]);
}
return 0;
}
- 作业2(野指针/空指针):
c
#include <stdlib.h>
int main() {
int *p = (int*)malloc(10 * sizeof(int));
// 未校验malloc是否成功,直接解引用
for (int i = 0; i < 10; i++) {
p[i] = i;
}
free(p);
// 重复释放指针p
free(p);
return 0;
}
- 作业3(综合实操):
编写一个程序,定义一个长度为10的int数组,用循环给数组赋值,遍历输出数组所有元素,要求避免数组越界;同时定义一个指针,指向数组首元素,解引用指针输出数组前3个元素,要求避免野指针/空指针错误。
五、课程总结
本节课核心掌握2个核心陷阱、1套规避逻辑:
-
数组越界:根源是C语言不校验下标,规避关键是"用宏定义长度+循环终止条件校验+下标不越界";
-
野指针/空指针:根源是指针未初始化、释放后未置NULL、解引用前未校验,规避关键是"初始化置NULL+解引用前校验+释放后置NULL";
-
核心原则:所有数组访问必校验下标,所有指针操作必做初始化和非NULL校验,从源头避免内存操作错误。