指针与测试:单元测试中的指针模拟、桩函数与指针、内存泄漏测试、指针的模糊测试
说实话,指针和测试放在一起讲,很多人第一反应是「这有啥好说的?写个测试用例不就行了?」。嗯,我当年也是这么想的。直到有一次,我在一个嵌入式项目中,因为一个野指针导致系统随机死机,排查了整整三天。从那以后,我彻底改变了看法——指针的测试,不是锦上添花,而是保命手段。
这一章,我们就来聊聊指针在测试中的几个关键场景。说白了,就是怎么用测试工具和技巧,把指针那些隐藏的坑一个个挖出来。
单元测试中的指针模拟
单元测试里,指针是个麻烦事。为什么?因为指针往往指向外部资源——硬件寄存器、动态内存、文件句柄。你写单元测试时,这些资源可能不存在,或者不方便模拟。
我个人习惯的做法是:用模拟对象(Mock)来替代真实指针指向的资源。举个例子:
// 原始函数:向硬件寄存器写入数据
void write_register(uint32_t *reg, uint32_t value) {
*reg = value;
}
// 单元测试中,我们模拟一个寄存器
uint32_t mock_reg = 0;
write_register(&mock_reg, 0x55);
assert(mock_reg == 0x55);
你看,这里的关键是:函数只通过指针操作数据,它不关心指针指向的是真实硬件还是模拟变量。这就是指针模拟的核心思想——隔离外部依赖。
桩函数与指针
桩函数(Stub)是单元测试中的另一个利器。当被测函数调用外部函数时,我们用桩函数来替代。而指针,恰恰是桩函数和被测函数之间的桥梁。
我遇到过这样一个场景:一个函数调用了 malloc,但单元测试中我不想真的分配内存。怎么办?用桩函数替换 malloc:
// 被测函数
int process_data(int *data, int size) {
int *buffer = (int*)malloc(size * sizeof(int));
if (!buffer) return -1;
// ... 处理数据
free(buffer);
return 0;
}
// 桩函数:模拟malloc失败
void *stub_malloc(size_t size) {
return NULL; // 模拟内存分配失败
}
// 测试用例
void test_malloc_failure() {
// 替换malloc为桩函数
// 这里假设使用某种测试框架支持函数替换
set_malloc_stub(stub_malloc);
int data[] = {1, 2, 3};
int result = process_data(data, 3);
assert(result == -1); // 期望返回-1
}
为什么会这样?因为指针让函数之间的耦合变得灵活。通过替换函数指针或使用链接时替换,我们可以轻松地注入桩函数。说白了,指针就是测试的「后门」。
内存泄漏测试
内存泄漏是C语言的老大难问题。指针用不好,泄漏就找上门。我个人习惯用两种方法来检测内存泄漏:
- 静态分析工具:比如Valgrind、AddressSanitizer。它们能检测出未释放的内存块。
- 自定义内存分配器:在测试中替换malloc/free,记录每次分配和释放。
这里我重点说说第二种方法。它特别适合嵌入式环境,因为很多嵌入式平台跑不了Valgrind。
// 自定义内存分配器
static int alloc_count = 0;
static int free_count = 0;
void *test_malloc(size_t size) {
alloc_count++;
return malloc(size);
}
void test_free(void *ptr) {
free_count++;
free(ptr);
}
// 测试用例
void test_no_leak() {
alloc_count = 0;
free_count = 0;
// 替换malloc/free
set_malloc(test_malloc);
set_free(test_free);
// 执行被测代码
int *p = (int*)test_malloc(100);
test_free(p);
// 断言:分配次数等于释放次数
assert(alloc_count == free_count);
}
指针的模糊测试
模糊测试(Fuzz Testing)说白了就是「乱拳打死老师傅」。给程序输入各种随机数据,看它会不会崩溃。对于指针来说,模糊测试特别有效——因为它能发现那些你没想到的边界情况。
我参与过一个网络协议栈的项目,就是用模糊测试发现了一个空指针解引用的bug。当时测试工具随机生成了一个畸形数据包,导致某个指针为NULL,然后代码直接解引用了它。
模糊测试的核心思路是:让指针指向各种「不可能」的地址。比如:
- NULL指针
- 已释放的内存
- 栈上的局部变量(函数返回后失效)
- 对齐异常的地址
- 超大偏移量
一个简单的模糊测试框架可能长这样:
// 模糊测试:随机生成指针值
void fuzz_test_pointer(void (*func)(int*)) {
// 测试NULL指针
func(NULL);
// 测试已释放的内存
int *p = malloc(sizeof(int));
free(p);
func(p); // 悬空指针
// 测试栈变量
int stack_var = 42;
func(&stack_var); // 函数返回后失效
// 测试随机地址
srand(time(NULL));
for (int i = 0; i < 1000; i++) {
int *random_ptr = (int*)(rand() % 0xFFFFFFFF);
func(random_ptr);
}
}
知识体系总览
下面这张图,是我对本章知识体系的总结。你可以看到,指针测试的四个方向各有侧重,但核心都是「控制指针指向的内容」。
你看,这四个方向其实有一个共同点:都是通过控制指针指向的内容,来验证代码在各种情况下的行为。指针模拟是控制指向「正常」内容,桩函数是控制指向「替代」内容,内存泄漏测试是控制指向「动态」内容,模糊测试是控制指向「随机」内容。
嗯,说到这里,我想起一个项目。当时我们团队写了一个内存池管理器,里面全是指针操作。上线前,我们用这四种方法轮番测试了一遍。结果呢?发现了7个潜在bug,其中3个是空指针解引用,2个是内存泄漏,还有2个是越界访问。要是没做这些测试,这些bug到了生产环境,后果不堪设想。
所以,我的建议是:别把指针测试当成「可选」任务。它应该是你开发流程中的固定环节。哪怕只是写一个简单的自定义分配器,也能帮你省下大量调试时间。
- 指针模拟:用Mock对象替代真实资源,隔离外部依赖
- 桩函数:通过函数指针或链接替换,模拟外部调用
- 内存泄漏测试:用Valgrind或自定义分配器,确保分配和释放匹配
- 模糊测试:随机生成指针值,发现边界情况和崩溃