指针与测试:单元测试中的指针模拟、桩函数与指针、内存泄漏测试、指针的模糊测试

说实话,指针和测试放在一起讲,很多人第一反应是「这有啥好说的?写个测试用例不就行了?」。嗯,我当年也是这么想的。直到有一次,我在一个嵌入式项目中,因为一个野指针导致系统随机死机,排查了整整三天。从那以后,我彻底改变了看法——指针的测试,不是锦上添花,而是保命手段

这一章,我们就来聊聊指针在测试中的几个关键场景。说白了,就是怎么用测试工具和技巧,把指针那些隐藏的坑一个个挖出来。

单元测试中的指针模拟

单元测试里,指针是个麻烦事。为什么?因为指针往往指向外部资源——硬件寄存器、动态内存、文件句柄。你写单元测试时,这些资源可能不存在,或者不方便模拟。

我个人习惯的做法是:用模拟对象(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);

你看,这里的关键是:函数只通过指针操作数据,它不关心指针指向的是真实硬件还是模拟变量。这就是指针模拟的核心思想——隔离外部依赖。

我的经验:在嵌入式项目中,我经常用这种技巧来测试驱动层代码。把硬件寄存器地址替换成模拟变量的地址,就能在PC上跑单元测试。省去了反复烧录固件的痛苦。

桩函数与指针

桩函数(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
}

为什么会这样?因为指针让函数之间的耦合变得灵活。通过替换函数指针或使用链接时替换,我们可以轻松地注入桩函数。说白了,指针就是测试的「后门」

避坑指南:我曾经在替换malloc时,忘记恢复原始函数,导致后续测试全部失败。记住:每个测试用例结束后,一定要把桩函数恢复原状。否则,测试之间会互相干扰。

内存泄漏测试

内存泄漏是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);
    }
}
我的建议:模糊测试不是万能的。它只能发现「崩溃」类问题,不能发现「逻辑错误」。所以,我通常把模糊测试作为补充手段,而不是唯一手段。单元测试、静态分析、模糊测试,三者结合才是王道。

知识体系总览

下面这张图,是我对本章知识体系的总结。你可以看到,指针测试的四个方向各有侧重,但核心都是「控制指针指向的内容」。

指针与测试 指针模拟 替换真实资源 隔离外部依赖 Mock对象 桩函数与指针 函数指针替换 链接时替换 模拟外部调用 内存泄漏测试 Valgrind 自定义分配器 分配/释放计数 指针模糊测试 随机指针值 边界条件 崩溃检测 核心思想:控制指针指向的内容 单元测试 + 静态分析 + 模糊测试 = 全面覆盖

你看,这四个方向其实有一个共同点:都是通过控制指针指向的内容,来验证代码在各种情况下的行为。指针模拟是控制指向「正常」内容,桩函数是控制指向「替代」内容,内存泄漏测试是控制指向「动态」内容,模糊测试是控制指向「随机」内容。

嗯,说到这里,我想起一个项目。当时我们团队写了一个内存池管理器,里面全是指针操作。上线前,我们用这四种方法轮番测试了一遍。结果呢?发现了7个潜在bug,其中3个是空指针解引用,2个是内存泄漏,还有2个是越界访问。要是没做这些测试,这些bug到了生产环境,后果不堪设想。

所以,我的建议是:别把指针测试当成「可选」任务。它应该是你开发流程中的固定环节。哪怕只是写一个简单的自定义分配器,也能帮你省下大量调试时间。

总结一下:
  • 指针模拟:用Mock对象替代真实资源,隔离外部依赖
  • 桩函数:通过函数指针或链接替换,模拟外部调用
  • 内存泄漏测试:用Valgrind或自定义分配器,确保分配和释放匹配
  • 模糊测试:随机生成指针值,发现边界情况和崩溃

公众号:蓝海资料掘金营,微信deep3321