在 C/C++ 编程中,很多传统的字符串和内存操作函数(如 strcpysprintfgets 等)存在缓冲区溢出等安全隐患,容易导致程序崩溃或安全漏洞。为此,C 标准库和编译器厂商提供了一些安全版本的函数,用于替代这些不安全函数。

下面是一些常见的安全函数及其参数说明,以及它们与原始函数的区别。

一、字符串操作函数

原始函数 安全函数 参数说明 说明
strcpy(dest, src) strncpy(dest, src, n)

dest: 目标缓冲区

src: 源字符串

n: 最多复制的字符数

限制复制长度,防止溢出。但不会自动添加 \0,需手动补零。
strcat(dest, src) strncat(dest, src, n)

dest: 目标缓冲区

src: 源字符串

n: 最多追加的字符数

限制追加长度,防止溢出。自动添加 \0
gets(dest) fgets(dest, size, stdin)

dest: 目标缓冲区

size: 缓冲区大小

stdin: 输入流

限制读取长度,防止溢出。推荐使用 fgets 替代 gets
sprintf(dest, format, ...) snprintf(dest, size, format, ...)

dest: 目标缓冲区

size: 缓冲区大小

format: 格式字符串

...: 参数列表

限制写入长度,防止溢出。返回实际需要的字符数(不包括 \0)。
vsprintf(dest, format, args) vsnprintf(dest, size, format, args) 同上,支持 va_list 参数 与 snprintf 类似,支持可变参数。

二、内存操作函数

原始函数 安全函数 参数说明 说明
memcpy(dest, src, n) memcpy_s(dest, destSize, src, n)(C11)

dest: 目标缓冲区

destSize: 目标缓冲区大小

src: 源地址

n: 要复制的字节数

防止缓冲区溢出,返回错误码。仅部分编译器支持(如 MSVC)。
memmove(dest, src, n) memmove_s(dest, destSize, src, n)(C11) 同上 支持重叠内存区域的安全复制。

三、文件操作函数

原始函数 安全函数 参数说明 说明
fopen(filename, mode) fopen_s(fp, filename, mode)(MSVC)

fp: 指向 FILE* 的指针filename: 文件名

mode: 打开模式

避免空指针访问,返回错误码。MSVC 特有。
freopen(filename, mode, stream) freopen_s(fp, filename, mode, stream)(MSVC) 类似 fopen_s 替换现有流的目标文件。

四、输入/输出函数

原始函数 安全函数 参数说明 说明
scanf(format, ...) scanf_s(format, ...)(MSVC)

format: 格式字符串

...: 参数列表

限制字符串长度,防止缓冲区溢出。MSVC 特有。
sscanf(str, format, ...) sscanf_s(str, format, ...)(MSVC) 同上 字符串输入的安全版本。
vscanf(format, args) vscanf_s(format, args)(MSVC) 同上 支持可变参数的安全版本。

五、Windows 平台安全函数(MSVC)

原始函数 安全函数 参数说明 说明
strcpy strcpy_s(dest, size, src)

dest: 目标缓冲区

size: 缓冲区大小

src: 源字符串

检查缓冲区大小,返回错误码。
strcat strcat_s(dest, size, src)

dest: 目标缓冲区

size: 缓冲区大小

src: 源字符串

防止溢出,自动检查长度。
scanf scanf_s("%s", str, (unsigned)_countof(str)) 与 scanf 类似,但需指定缓冲区大小 防止缓冲区溢出。
sprintf sprintf_s(dest, size, format, ...)

dest: 目标缓冲区

size: 缓冲区大小

format: 格式字符串

限制写入长度,防止溢出。

六、C++ 标准库推荐方式(避免使用 C 风格)

在 C++ 中,推荐使用标准库中的安全类和函数代替 C 风格字符串操作:

替代方式 说明
std::string 使用 std::string 替代 char[],避免手动管理缓冲区。
std::array<char, N> 固定大小数组,避免动态分配。
std::vector<char> 动态数组,适用于不确定大小的缓冲区。
std::getline(std::cin, str) 替代 fgets,用于安全地读取一行文本。
std::stringstream 替代 sprintf/sscanf,避免格式化错误。

七、使用建议

  1. 优先使用标准库:如 std::stringstd::vector,避免手动管理缓冲区。
  2. 启用编译器警告:如 -Wdeprecated-declarations-Wformat-security 等,帮助发现不安全函数。
  3. 使用安全函数:如 strncpysnprintffgets 等,替代 strcpysprintfgets
  4. 注意边界检查:即使使用安全函数,也应确保传入的缓冲区大小正确。
  5. 使用静态分析工具:如 Clang-Tidy、Coverity、Valgrind 等,帮助发现潜在问题。

八、示例代码对比

1. strncpy 替代 strcpy

不安全写法:
char dest[10];
strcpy(dest, "This is a long string");  // 潜在缓冲区溢出
  • 问题strcpy 不检查目标缓冲区大小,若源字符串长度超过 dest 容量(10 字节),会导致缓冲区溢出,覆盖栈上其他数据(如返回地址),可能引发程序崩溃或安全漏洞。
安全写法:
char dest[10];
strncpy(dest, "This is a long string", sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0';  // 手动补空字符
  • 参数说明
    • dest: 目标缓冲区。
    • "This is a long string": 源字符串。
    • sizeof(dest) - 1: 最多复制的字符数(保留 \0 空间)。
  • 原因
    • strncpy 的第三个参数限制复制长度,防止溢出。
    • 注意strncpy 不会自动添加 \0,需手动补零以确保字符串完整性。

2. strncat 替代 strcat

不安全写法:
char dest[10] = "Hello";
strcat(dest, " World!");  // 潜在缓冲区溢出
  • 问题strcat 不检查目标缓冲区容量,若拼接后的字符串长度超过 dest 容量(10 字节),会导致缓冲区溢出。
安全写法:
char dest[10] = "Hello";
strncat(dest, " World!", sizeof(dest) - strlen(dest) - 1);
  • 参数说明
    • dest: 目标缓冲区。
    • " World!": 源字符串。
    • sizeof(dest) - strlen(dest) - 1: 最多追加的字符数(保留 \0 空间)。
  • 原因
    • strncat 的第三个参数限制追加长度,防止溢出。
    • 自动添加 \0,确保字符串完整性。

3. memcpy_s 替代 memcpy

不安全写法:
char src[100] = "Hello World";
char dest[50];
memcpy(dest, src, strlen(src) + 1);  // 潜在缓冲区溢出
  • 问题memcpy 不检查目标缓冲区大小,若复制长度超过 dest 容量(50 字节),会导致缓冲区溢出。
安全写法:
#include <string.h>  // C11 标准

char src[100] = "Hello World";
char dest[50];
errno_t err = memcpy_s(dest, sizeof(dest), src, strlen(src) + 1);
if (err != 0) {
    // 处理错误(如缓冲区不足)
}
  • 参数说明
    • dest: 目标缓冲区。
    • sizeof(dest): 目标缓冲区大小。
    • src: 源地址。
    • strlen(src) + 1: 要复制的字节数。
  • 原因
    • memcpy_s 的第二个参数 destSize 显式指定目标缓冲区大小。
    • 若复制长度超过目标容量,函数返回错误码而非直接溢出。
    • 注意memcpy_s 是 C11 标准的一部分,部分编译器(如 MSVC)支持,GCC/Clang 可能需要启用 _FORTIFY_SOURCE

4. memmove_s 替代 memmove

不安全写法:
char src[100] = "Hello World";
char dest[50];
memmove(dest, src, strlen(src) + 1);  // 潜在缓冲区溢出
  • 问题memmove 不检查目标缓冲区大小,若复制长度超过 dest 容量(50 字节),会导致缓冲区溢出。
安全写法:
#include <string.h>  // C11 标准

char src[100] = "Hello World";
char dest[50];
errno_t err = memmove_s(dest, sizeof(dest), src, strlen(src) + 1);
if (err != 0) {
    // 处理错误
}
  • 参数说明:与 memcpy_s 相同。
  • 原因
    • memmove_s 支持重叠内存区域的复制,防止缓冲区溢出。
    • 返回错误码而非直接溢出。

5. fopen_s 替代 fopen

不安全写法:
FILE* fp = fopen("data.txt", "r");
if (fp == NULL) {
    // 错误处理
}
  • 问题fopen 不检查 fp 是否为 NULL,且在多线程环境下可能引发竞争条件(TOCTOU 攻击)。
安全写法:
FILE* fp;
errno_t err = fopen_s(&fp, "data.txt", "r");
if (err != 0 || fp == NULL) {
    // 处理错误
}
  • 参数说明
    • &fp: 指向 FILE* 的指针。
    • "data.txt": 文件名。
    • "r": 打开模式。
  • 原因
    • fopen_s 的第一个参数是 FILE**,直接传递指针地址,避免空指针访问。
    • 返回错误码而非依赖 errno,提高可读性和安全性。
    • 注意fopen_s 是 Microsoft 特有扩展,GCC/Clang 推荐使用 fopen 加强错误检查。

6. freopen_s 替代 freopen

不安全写法:
FILE* fp = freopen("data.txt", "w", stdout);
if (fp == NULL) {
    // 错误处理
}
  • 问题freopen 不检查 fp 是否为 NULL,且在多线程环境下可能引发竞争条件。
安全写法:
FILE* fp;
errno_t err = freopen_s(&fp, "data.txt", "w", stdout);
if (err != 0 || fp == NULL) {
    // 处理错误
}
  • 参数说明:与 fopen_s 类似。
  • 原因
    • freopen_s 替换现有流的目标文件,增强错误处理。

7. scanf_s 替代 scanf

不安全写法:
char name[10];
scanf("%s", name);  // 潜在缓冲区溢出
  • 问题scanf 不限制输入长度,若用户输入超过 name 容量(10 字节),会导致缓冲区溢出。
安全写法:
char name[10];
scanf_s("%s", name, (unsigned)_countof(name));  // 限制最大输入长度
  • 参数说明
    • name: 目标缓冲区。
    • (unsigned)_countof(name): 缓冲区大小。
  • 原因
    • scanf_s 的第三个参数指定缓冲区大小,防止溢出。
    • 注意scanf_s 是 Microsoft 特有扩展,GCC/Clang 推荐使用 fgets

8. fgets 替代 gets

不安全写法:
char buffer[10];
gets(buffer);  // 无边界检查,绝对禁止使用!
  • 问题gets 不检查输入长度,用户输入任意长度都可能溢出缓冲区。
安全写法:
char buffer[10];
fgets(buffer, sizeof(buffer), stdin);  // 限制最大读取长度
buffer[strcspn(buffer, "\n")] = '\0';  // 去除换行符
  • 参数说明
    • buffer: 目标缓冲区。
    • sizeof(buffer): 缓冲区大小。
    • stdin: 输入流。
  • 原因
    • fgets 的第二个参数指定最大读取字节数,防止溢出。
    • 保留换行符需手动去除(通过 strcspn 查找 \n 位置)。

9. snprintf 替代 sprintf

不安全写法:
char buffer[10];
sprintf(buffer, "%s", "This is a long string");  // 缓冲区溢出
  • 问题sprintf 不限制输出长度,若格式化结果超过缓冲区容量(10 字节),会导致溢出。
安全写法:
char buffer[10];
snprintf(buffer, sizeof(buffer), "%s", "This is a long string");
  • 参数说明
    • buffer: 目标缓冲区。
    • sizeof(buffer): 缓冲区大小。
    • "%s": 格式字符串。
    • "This is a long string": 参数列表。
  • 原因
    • snprintf 的第二个参数指定缓冲区大小,限制写入长度。
    • 自动添加 \0,确保字符串完整性。
    • 返回值为实际需要的字符数(不包括 \0),可用于判断是否被截断。

10. vsnprintf 替代 vsprintf

不安全写法:
char buffer[10];
va_list args;
va_start(args, format);
vsprintf(buffer, format, args);  // 潜在缓冲区溢出
va_end(args);
  • 问题vsprintf 不限制输出长度,若格式化结果超过缓冲区容量(10 字节),会导致溢出。
安全写法:
char buffer[10];
va_list args;
va_start(args, format);
vsnprintf(buffer, sizeof(buffer), format, args);  // 限制写入长度
va_end(args);
  • 参数说明:与 snprintf 类似,支持 va_list 参数。
  • 原因
    • vsnprintf 的第二个参数指定缓冲区大小,防止溢出。

11. std::string 替代 char[]

不安全写法(C 风格):
char buffer[100];
strcpy(buffer, "Hello");  // 手动管理缓冲区
安全写法(C++):
#include <string>

std::string str = "Hello";  // 自动管理内存
str += " World";            // 动态扩展
  • 原因
    • std::string 自动处理内存分配和释放,避免缓冲区溢出。
    • 支持动态扩展,无需手动计算长度。
    • 优势:结合 std::vectorstd::array 等容器,进一步提升安全性
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐