C++中,复制C风格字符串的方法有4种:strcpy, strncpy, strcpy_s, strncpy_s。它们有什么区别和联系了?

1. strcpy

strcpy和strncpy是早期C库函数,头文件string.h。现在已经发布对应safe版本,也就是strcpy_s, strncpy_s。

strcpy 函数将 strSource(包括终止 null 字符)复制到 strDestination 指定的位置。 如果源和目标字符串重叠,则 strcpy 的行为是不确定的。

注意:strcpy不安全的原因

由于 strcpy 在复制 strSource 之前不检查 strDestination 中是否有足够的空间,因此它可能会导致缓冲区溢出。 因此,我们建议使用 strcpy_s。

参考微软官方文档strcpy、wcscpy、_mbscpy

下面的程序,编译器将产生C4996警告/错误提示

char p1[] = "hello";
char* ps = new char[100];

// 如果想忽略C4996,VS2019下面,需要用编译器指令
// #pragma warning(suppress : 4996)
strcpy(ps, p1); // C4996

2. strncpy

strncpy 函数将 strSource 的初始 计数 字符复制到 strDest 并返回 strDest。 如果 count 小于或等于 strSource 的长度,则不会自动向复制的字符串追加 null 字符。 如果 count 大于 strSource 的长度,则目标字符串将用空字符填充,最大长度为 count。 如果源和目标字符串重叠,则 strncpy 的行为是不确定的。

同strcpy一样,strncpy也是不安全的

strncpy 不检查 strDest 中是否有足够的空间;这会导致缓冲区溢出的潜在原因。 Count 参数限制复制的字符数;它不是 strDest 的大小限制。

strncpy和strcpy的主要区别:strncpy多了一个参数count(第3个参数),可以指定要从strSource(源字符串)拷贝的字符个数。

参考微软官方文档strncpy、_strncpy_l、wcsncpy、_wcsncpy_l、_mbsncpy、_mbsncpy_l

char s[20];

strcpy_s(s, sizeof(s), "AA BB CC"); // "AA BB CC"

// 2 <= "tt"字符串长度, 将"tt"拷贝并覆盖s[0..1], 不影响后面的元素
#pragma warning(suppress : 4996)
strncpy(s, "tt", 2); // "tt BB CC" C4996

// 3 > "rr"字符串长度, 自动添加null字符(\0)
#pragma warning(suppress : 4996)
strncpy(s, "rr", 3); // "rr" C4996

printf("%s\n", s);

3. strcpy_s

strcpy_s是strcpy的安全版本,通过第二个参数dest_size限制使用目的缓存大小,对缓存大小、源字符串长度、要使用的缓存大小都做了安全检查,避免溢出。

strcpy_s 函数将 src 地址中的内容(包括终止 null 字符)复制到 dest 指定的位置。 目标字符串必须足够大以保存源字符串及其结尾的 null 字符。 如果源和目标字符串重叠,则 strcpy_s 的行为不确定。

如果 dest 或 src 为空指针,或者如果目标字符串的大小 dest_size 太小,则调用无效参数处理程序,如 参数验证中所述。 如果允许执行继续,则当 dest 或 src 为 null 指针时,这些函数将返回 EINVAL ,并将 errno 设置为 EINVAL ; 当目标字符串过小时,它们将返回 ERANGE 并将 errno 设置为 ERANGE 。
成功执行时,目标字符串始终以 null 结尾。

参考微软官方文档strcpy_s、wcscpy_s、_mbscpy_s、_mbscpy_s_l

要正常使用strcpy_s进行字符串拷贝,必须要求sizeof(s1)(缓存大小) >= dest_size(限制使用目的缓存大小) > strlen(src)(源字符串长度(不包括null字符))。另外dest_size长度,理论上不能超过RSIZE_MAX。

char s1[5];
//  sizeof(s1) >= dest_size > strlen(src)
strcpy_s(s1, 4, "AA"); // "AA"

//  sizeof(s1) >= dest_size <= strlen(src)
strcpy_s(s1, 1, "AA"); // 程序异常退出 

//  sizeof(s1) >= dest_size > strlen(src)
strcpy_s(s1, sizeof(s1), "AA"); // "AA"
strcpy_s(s1, sizeof(s1), "AA B"); // "AA B"

//  dest_size <= strlen(src)
strcpy_s(s1, sizeof(s1), "AA BB"); // s1缓存大小 <= 源字符串长度(不包括null字符), 程序异常退出

// sizeof(s1) < dest_size
strcpy_s(s1, 6, "A"); // dest_size > sizeof(s1), 程序异常退出  C6386

printf("%s\n", s1);

4. strncpy_s

strcpy_s与strncpy相比较,多了第4个参数指定要拷贝的源字符串字符数。必须为null字符预留目的缓存空间。

函数尝试将 strSource 的前 D 个字符复制到 strDest,其中 D 是 count 和 strSource 的长度。 如果这些 D 字符将放在 strDest (其大小被指定为 numberOfElements) 并且仍为 null 终止符留下空间,则会复制这些字符,并追加一个终止 null。否则, strDest[0] 设置为 null 字符,并调用无效参数处理程序,如 参数验证中所述。

参加微软官方文档strncpy_s、_strncpy_s_l、wcsncpy_s、_wcsncpy_s_l、_mbsncpy_s、_mbsncpy_s_l

char dst[5];
//strncpy_s(dst, 5, "a long string", 5); // 将源字符串5个字符拷贝到目的缓存5byte长度, 没有预留null字符空间, 程序异常退出

strncpy_s(dst, 5, "a long string", _TRUNCATE); // 截断 "a lo" , 等价于下面的语句
strncpy_s(dst, 5, "a long string", 4); // "a lo"

printf("%s\n", dst);

总结

  • 4个函数有个共同弱点,就是如果源和目标字符串存在重叠,行为未定义;
  • 使用安全版本库函数(_s后缀)进行字符串拷贝,不要使用不安全版本;
  • 带_s版本字符串拷贝,总能确保拷贝后字符串以null字符结尾;
  • 需要预留null字符空间;