广告

在C、C++中安全使用指针的最佳实践

2024-09-18 17:46:55 Jacob Beningo 阅读:
C和C++中的指针。它是一把双刃剑,可以带来惊人的内存管理灵活性,但管理不善也会造成严重破坏···

作为一个在嵌入式系统行业工作了二十多年的人,我见证了技术的巨大进步—从8位微控制器到如今复杂的多核系统。然而,有一件事始终不变:C和C++中的指针。它是一把双刃剑,可以带来惊人的内存管理灵活性,但管理不善也会造成严重破坏。TCuednc

最近,一个NULL指针导致系统崩溃的事件,清楚地提醒了我们在代码中安全使用指针是多么重要。TCuednc

在这篇文章中,我们将探讨在C和C++中安全使用指针的最佳实践,确保您的嵌入式系统顺利运行而不会出现意外崩溃。TCuednc

了解指针

指针本质上是存储其他变量内存地址的变量。指针可以实现高效的内存操作和动态内存分配,但也会带来风险,最明显的是,取消引用NULL或未初始化的指针可能会导致灾难性的故障。它们还可能导致安全漏洞、覆盖意外位置和其他问题,因此,了解指针的工作原理是安全使用指针的第一步。TCuednc

声明指针

要声明一个指针,可以使用*运算符:TCuednc

int *ptr; // A pointer to an integerTCuednc

此声明不会为整数分配内存,它只是创建了一个可以指向整数内存位置的指针。在使用指针之前对其进行初始化非常重要,因为使用未初始化的指针可能会导致未定义的行为。(您的编译器可能会将其初始化为0或NULL,或者它可能只是保存分配之前内存的值)。TCuednc

初始化指针

您可以通过多种方式初始化指针:TCuednc

1.分配变量地址:TCuednc

int var = 42;TCuednc

int *ptr = &var; // ptr now points to var TCuednc

2.使用动态内存分配:TCuednc

int *ptr = (int *)malloc(sizeof(int)); // Allocating memory for one integerTCuednc

if (ptr == NULL) {TCuednc

    // Handle memory allocation failureTCuednc

}TCuednc

*ptr = 42; // Assign a value to the allocated memoryTCuednc

使用动态内存分配需要谨慎,尤其是在内存通常受限的嵌入式系统中。由于malloc通常也不是确定性的,因此无法保证运行时的执行,这使得嵌入式系统中的动态内存分配令人生疑。TCuednc

如果您选择使用动态内存分配,请始终通过验证指针是否为NULL来检查内存分配是否成功。malloc函数将返回指向分配块的内存地址。如果为NULL,则表示操作失败!TCuednc

(我曾经想通过在我的软件中大量使用malloc来证明同事的错误。我成功了,但后来我为我的傲慢而后悔!)。TCuednc

安全指针使用的最佳实践

最佳实践1——始终初始化指针

创建指针时,最好立即将其分配给有用的对象。未初始化的指针可能指向随机的内存位置,从而导致未定义的行为。您应该始终初始化指针,即使只是用NULL指针初始化它:TCuednc

int *ptr = NULL; // Initialize to NULLTCuednc

指向NULL的指针比指向随机位置的指针更好。TCuednc

最佳实践#2——解除引用前检查是否为NULL

将指针初始化为NULL的优点在于,我们可以在取消引用之前检查它以确保它已被初始化。如果值为0x08FF001234,我可能会假设此指针已初始化到正确的位置。(假设是不好的!我们或许可以使用MPU和其他链接器技巧来验证它是否指向正确的区域)。TCuednc

在取消引用指针之前,请确保它不为NULL。这个简单的检查可以防止崩溃:TCuednc

if (ptr != NULL) {TCuednc

    // Dereference and do useful work!TCuednc

} else {TCuednc

    // Pointer is NULL, cannot dereference!TCuednc

    // Exception handling!TCuednc

}TCuednc

当我有一个函数指针数组时,我经常使用这个技巧。我可能有这样的表:TCuednc

typedef void (*LedCommand_t)(void);TCuednc

LedCommand_t LedCommands[] = {TCuednc

   turnLedOn,TCuednc

   turnLedOff,TCuednc

   NULLTCuednc

}TCuednc

表中的最后一项为NULL,因为这样可便于检查。我可以循环遍历该表,然后生成一行代码来调用该函数(只要它不为NULL)!TCuednc

最佳实践3——在C++中使用智能指针

如果您使用C++,请考虑使用智能指针,例如std::unique_ptr和std::shared_ptr,它们提供自动内存管理功能:TCuednc

#include TCuednc

std::unique_ptr ptr(new int(42)); // Automatically deallocates memoryTCuednc

智能指针有助于自动管理内存,减少内存泄漏和悬垂指针的可能性。TCuednc

最佳实践#4——谨慎使用指针算法

指针算法非常强大。我曾经编写过一个应用程序,使用它来移动内存并将数据存储在缓冲区中。这是一种高效的解决方案,但如果使用不当,也会很危险。为了确保这些指针保持在正确的内存边界内,进行一些调试和大量测试是绝对必要的。TCuednc

确保您保持在分配的内存范围内:TCuednc

int arr[5] = {1, 2, 3, 4, 5};TCuednc

int *ptr = arr; // Pointer to the first elementTCuednc

for (int i = 0; i < 5; i++) {TCuednc

    printf("%d\\n", *(ptr + i)); // Accessing elements using pointer arithmeticTCuednc

}TCuednc

从上面的例子中你可以看出,虽然指针可能停留在内存边界内,但它充满了神奇的数字,这使得这段代码很危险!要将数组中的元素从5个变为4个,至少需要修改三个地方的代码,因此,如果修改代码,读取缓冲区外数据的几率很高!TCuednc

最佳实践#5——使用工具链文件进行内存管理

在嵌入式系统中,高效管理内存至关重要。工具链文件可以帮助定义内存布局并确保指针正确对齐。使用CMake等工具链时,您可以指定内存设置,确保指针指向正确的位置:TCuednc

set(MEMORY_START 0x20000000)TCuednc

set(MEMORY_END 0x2001FFFF)TCuednc

通过定义内存区域,您可以更有效地管理指针,确保它们指向有效的内存地址。您还可以使用这些内存区域来检查指针值的完整性!为指针赋值并不意味着指针值是正确的。TCuednc

安全处理动态内存

动态内存分配是指针相关问题的常见来源,这就是为什么在资源受限的系统中我们通常会避免使用它。进入主循环之前使用动态内存可能是安全的,因为这样会使分配对于应用程序来说看起来是静态的。TCuednc

然而,有时动态分配是完成这项工作的最好的工具。在这种情况下,以下是一些安全使用它的策略:TCuednc

策略1–始终释放已分配的内存

无论何时分配内存,请确保在不再需要时释放它,以防止内存泄漏:TCuednc

int *ptr = (int *)malloc(sizeof(int));TCuednc

if (ptr != NULL) {TCuednc

    *ptr = 42;TCuednc

    free(ptr); // Free the allocated memoryTCuednc

}TCuednc

无法释放已分配的内存可能会导致内存耗尽,尤其是在长期运行的嵌入式系统中。TCuednc

策略#2—释放后将指针设置为NULL

释放指针后,将其设置为NULL以避免悬垂指针:TCuednc

free(ptr);TCuednc

ptr = NULL; // Prevents accidental dereferenceTCuednc

这个简单的步骤可以避免因取消引用已释放的内存而导致的潜在崩溃。TCuednc

策略3——使用RAII

资源获取即初始化(RAII)是C++和面向对象编程语言中的常见原则。它用于将动态内存分配封装在类中。类构造函数分配内存,析构函数释放内存,资源能得到妥善管理。TCuednc

class Resource {TCuednc

public:TCuednc

    Resource() {TCuednc

        data = new int[10]; // Allocate memoryTCuednc

    }TCuednc

    ~Resource() {TCuednc

        delete[] data; // Deallocate memoryTCuednc

    }TCuednc

private:TCuednc

    int* data;TCuednc

};TCuednc

避免常见的指针陷阱

陷阱#1——指针强转

在C语言中,强制转换指针可能会隐藏问题并导致未定义的行为。与其这样,不如让编译器处理类型转换:TCuednc

int *ptr = (int *)malloc(sizeof(int)); // Avoid casting; it's unnecessary in CTCuednc

让编译器完成其工作可以帮助发现编译过程中的潜在问题。TCuednc

注意:你必须小心处理这个问题。有些阵营极力推崇显式编程,你必须显式地进行转换。TCuednc

陷阱2——多个指针指向同一内存

当多个指针指向同一内存位置时,通过一个指针更改值可能会导致混乱和错误:TCuednc

int *ptr1 = (int *)malloc(sizeof(int));TCuednc

int *ptr2 = ptr1;TCuednc

*ptr1 = 10; // Both ptr1 and ptr2 point to the same memoryTCuednc

printf("Value through ptr2: %d\\n", *ptr2); // Outputs 10TCuednc

如果您没有留意谁拥有内存,这种情况可能会导致意想不到的行为。TCuednc

结论

指针是C和C++中的一项强大功能,但它们也存在风险,可能导致NULL指针崩溃等严重问题。通过遵循这些最佳实践(初始化指针、在取消引用之前检查是否为NULL、在C++中使用智能指针以及谨慎管理动态内存),您可以安全地驾驭复杂的指针。TCuednc

此外,通过利用工具链文件来管理内存布局,您可以确保指针在嵌入式系统中始终有效且表现良好。在发展使用指针的技能时,请记住安全和谨慎应始终是您的指导原则。TCuednc

(原文刊登于EDN姊妹网站Embedded,参考链接:Best practices to safely navigate pointers in C/C++,由Ricardo Xie编译。)TCuednc

责编:Ricardo
本文为电子技术设计原创文章,未经授权禁止转载。请尊重知识产权,违者本司保留追究责任的权利。
  • 微信扫一扫
    一键转发
  • 最前沿的电子设计资讯
    请关注“电子技术设计微信公众号”
广告
热门推荐
广告
广告
EE直播间
在线研讨会
广告
广告
面包芯语
广告
向右滑动:上一篇 向左滑动:下一篇 我知道了