简介

避免高危函数的最佳实践

  • 使用带有长度限制的函数版本(如 strncpy、snprintf)。
  • 在使用动态内存管理时,确保正确处理分配失败和释放内存。
  • 使用安全库(如 C++ 标准库的 std::string、std::vector 等)来避免手动管理内存。

内存管理函数

malloc(size_t size)

从堆分配指定大小的内存块。malloc 通常调用操作系统的内存管理接口(如 brk 或 mmap)来分配内存。它维护一个内部的堆管理数据结构来追踪已分配和未分配的内存块。简化的伪代码可能如下:

void* malloc(size_t size) {
    if (size == 0) {
        return nullptr;
    }
    // 查找足够大的未分配块
    Block* block = find_free_block(size);
    if (!block) {
        // 分配新的内存块
        block = request_memory_from_os(size);
    }
    block->allocated = true;
    return (void*)(block + 1); // 跳过块的元数据,返回数据区指针
}

calloc(size_t num, size_t size):分配一块内存并将其初始化为 0。 realloc(void *ptr, size_t size):重新调整之前分配的内存块大小。 free(void *ptr):释放动态分配的内存。

字符串处理函数

strcpy(char *dest, const char *src)

复制字符串。缓冲区溢出:如果目标缓冲区的大小不足以容纳源字符串及其终止符,可能会导致数据被写入非法内存区域,从而引发崩溃或安全漏洞。

char* strcpy(char* dest, const char* src) {
    char* temp = dest;
    while ((*dest++ = *src++) != '\0');
    return temp;
}

strncpy(char *dest, const char *src, size_t n)

复制指定长度的字符串。

strcat(char *dest, const char *src)

将源字符串追加到目标字符串后。

strncat(char *dest, const char *src, size_t n)

将最多 n 个字符的源字符串追加到目标字符串后。

strlen(const char *str)

计算字符串长度,不包括终止符 \0。无直接风险,但需要确保传入的字符串以 \0 结束,否则可能会访问非法内存

strcmp(const char *str1, const char *str2)

比较两个字符串。它按字符逐一比较,返回结果表示比较大小。一般情况下,strcmp 本身不会造成缓冲区溢出等问题,但如果传递的参数是未终止的字符串,可能会导致访问非法内存区域。

int strcmp(const char* str1, const char* str2) {
    while (*str1 && (*str1 == *str2)) {
        str1++;
        str2++;
    }
    return *(unsigned char*)str1 - *(unsigned char*)str2;
}

strncmp(const char *str1, const char *str2, size_t n):比较前 n 个字符的字符串。 strchr(const char *str, int c):在字符串中查找字符首次出现的位置。 strrchr(const char *str, int c):在字符串中查找字符最后一次出现的位置。 strstr(const char *haystack, const char *needle):查找子字符串。

sprintf(char *str, const char *format, ...)

格式化字符串。它会将格式化数据写入目标缓冲区,但不检查缓冲区是否有足够的空间,容易导致缓冲区溢出。 替代方案:使用 snprintf。

snprintf(char *str, size_t size, const char *format, ...):格式化字符串并指定缓冲区大小。

文件操作函数

fopen(const char *filename, const char *mode):打开文件。 fclose(FILE *stream):关闭文件。 fread(void *ptr, size_t size, size_t count, FILE *stream):从文件中读取数据。 fwrite(const void *ptr, size_t size, size_t count, FILE *stream):向文件中写入数据。 fgets(char *str, int n, FILE *stream):从文件中读取一行数据。 fputs(const char *str, FILE *stream):将字符串写入文件。 fprintf(FILE *stream, const char *format, ...):格式化输出到文件。 fscanf(FILE *stream, const char *format, ...):从文件中读取格式化输入。 ftell(FILE *stream):获取当前文件指针的位置。 fseek(FILE *stream, long offset, int whence):调整文件指针位置。 rewind(FILE *stream):将文件指针重新定位到文件开头。

数学函数

abs(int n):计算整数的绝对值。 fabs(double x):计算浮点数的绝对值。 pow(double x, double y):计算 x 的 y 次幂。 sqrt(double x):计算平方根。 sin(double x):计算正弦值。 cos(double x):计算余弦值。 tan(double x):计算正切值。 log(double x):计算自然对数。 exp(double x):计算 e 的 x 次幂。 ceil(double x):向上取整。 floor(double x):向下取整。 rand():生成随机数。 srand(unsigned int seed):设置随机数种子。

时间函数

time(time_t *timer):获取当前时间。 clock():获取程序运行时间。 difftime(time_t end, time_t begin):计算两个时间点的差值。 gmtime(const time_t *timer):将时间转换为 UTC 时间。 localtime(const time_t *timer):将时间转换为本地时间。 strftime(char *str, size_t maxsize, const char *format, const struct tm *timeptr):格式化时间输出。

内存操作函数

memcpy(void *dest, const void *src, size_t n)

复制内存块,用于将一块内存复制到另一块内存。由于不检查源和目标内存的大小,可能会导致缓冲区溢出。

现代实现可能会优化为按字(4 字节)或双字(8 字节)复制,以提高效率。简化版的实现可以如下:·

void* memcpy(void* dest, const void* src, size_t n) {
    char* d = (char*)dest;
    const char* s = (const char*)src;
    
    // 按字节复制 n 个字节
    while (n--) {
        *d++ = *s++;
    }
    return dest;
}

替代方案:确保源和目标内存区域大小适合。

memmove(void *dest, const void *src, size_t n)

安全地复制内存块,允许重叠。memmove 与 memcpy 类似,也是用于内存复制,但它能处理源地址和目标地址重叠的情况。它确保正确处理重叠内存区域中的数据,避免数据覆盖问题。

void* memmove(void* dest, const void* src, size_t n) {
    char* d = (char*)dest;
    const char* s = (const char*)src;
    
    if (d < s) {
        // 如果目标地址小于源地址,从前向后复制(类似于 memcpy)
        while (n--) {
            *d++ = *s++;
        }
    } else {
        // 如果目标地址大于源地址,从后向前复制,避免覆盖
        d += n;
        s += n;
        while (n--) {
            *(--d) = *(--s);
        }
    }
    
    return dest;
}

memset(void *str, int c, size_t n)

将内存块设置为指定的值。

如果设置的值可以扩展为多个字节(例如,所有字节都为 0 或 0xFF),可以按字、双字甚至更大的数据块进行设置,以提高速度。

现代硬件可能会支持矢量化操作,可以将多个字节的设置指令打包成一个操作,进一步提升性能。

void* memset(void* str, int c, size_t n) {
    unsigned char* p = (unsigned char*)str;
    
    while (n--) {
        *p++ = (unsigned char)c;
    }
    
    return str;
}

memcmp(const void *str1, const void *str2, size_t n)

用于逐字节比较两块内存区域的内容。它返回一个整数,用于表示两块内存是否相同及其相对大小。

int memcmp(const void* ptr1, const void* ptr2, size_t n) {
    const unsigned char* p1 = (const unsigned char*)ptr1;
    const unsigned char* p2 = (const unsigned char*)ptr2;
    
    while (n--) {
        if (*p1 != *p2) {
            return *p1 - *p2;
        }
        p1++;
        p2++;
    }
    return 0; // 如果所有字节都相同,返回 0
}

动态库加载

dlopen(const char *filename, int flag):动态加载共享库。 dlsym(void *handle, const char *symbol):获取共享库中的符号地址。 dlclose(void *handle):关闭动态加载的共享库。 dlerror():获取动态库相关的错误信息。

C++ 标准库

std::string:C++ 字符串类,提供动态管理字符串的功能。 std::vector:动态数组类,支持自动扩展和缩小。 std::map:关联容器,用于存储键值对。 std::unordered_map:哈希表实现的关联容器。 std::sort:排序算法。 std::find:查找算法。 std::unique_ptr:独占所有权的智能指针。 std::shared_ptr:共享所有权的智能指针。 std::mutex:互斥锁,用于多线程编程。

输入输出函数

printf(const char *format, ...):格式化输出到标准输出。 scanf(const char *format, ...):格式化输入。 putchar(int char):输出单个字符。 getchar():读取单个字符。 puts(const char *str):输出字符串并换行。

gets(char *str)

读取一行输入(高危函数,建议使用 fgets)。该函数从标准输入读取字符串,不做任何缓冲区大小检查,极易造成缓冲区溢出。 替代方案:使用 fgets 来指定读取的缓冲区大小。

信号处理

signal(int signum, void (*handler)(int)):设置信号处理函数。 raise(int sig):发送信号。 abort():异常终止程序。 exit(int status):正常终止程序。