在C语言编程中,字符串是通过定义char数组来保存,并通过指针以及字符串拷贝函数(如strcpy)来实现字符串的拷贝,无法方便的像C++中使用等号直接对std::string类型的字符串赋值。
使用strcpy这类接口,在字符串长度不太明确的情况下,可能处出现拷贝越界的情况,给程序引入了不安全的因素。本篇就来讨论下,在C语言中,如何安全的使用strcpy这类函数进行字符串的拷贝。
1 strcpy
strcpy是C语言标准库中的一个最基础的字符串处理函数,可以把源字符串复制到目标字符串。
1.1 函数原型
char *strcpy(char *dest, const char *src);
将 src
所指向的以空字符('\0'
)结尾的字符串复制到 dest
所指向的数组中,同时会复制终止空字符
参数:
dest
是目标字符串的指针src
是源字符串的指针
返回值:
- 返回目标字符串的指针
1.2 使用示例
一个基础的strcpy使用示例,需要确保目标数组足够大
// gcc strcpy1.c -o strcpy
#include <stdio.h>
#include <string.h>
int main()
{
char src[] = "Hello";
char dest[10] = {0}; // 确保目标数组足够大
strcpy(dest, src);
printf("复制后的字符串: %s\n", dest);
for (int i=0; i<sizeof(dest); i++)
{
printf("dest[%d]:%x(%c)\n", i, dest[i], dest[i]);
}
return 0;
}
运行结果如下:
注意事项
- 目标数组大小要足够:
strcpy
不会检查目标数组的大小,一旦目标数组的空间不足以容纳源字符串,就会导致缓冲区溢出,这是非常危险的,可能会引发程序崩溃或者产生安全漏洞。 - 源字符串必须以空字符结尾:如果源字符串没有以
'\0'
结尾,strcpy
会一直复制内存中的数据,直到遇到空字符为止,这会造成未定义行为。 - 避免自我复制:不要将字符串复制到自身,否则会导致数据被破坏。
1.3 拷贝越界情况举例
测试一下,如果要拷贝的字符串长度大于目标存储空间,会是什么结果。
// gcc strcpy2.c -o strcpy2
#include <stdio.h>
#include <string.h>
int main()
{
char src[] = "Hello, World"; //原字符串11个字符,再加上结尾'\0'则占12个字符
char dest[10] = {0}; //目标存储空间只有10个
char other[10] = {0}; //随后再定一个10个大小的other来验证是否被越界拷贝了
strcpy(dest, src);
printf("复制后的字符串: %s\n", dest);
printf("dest addr:%p\n", dest);
for (int i=0; i<sizeof(dest); i++)
{
printf("dest[%d]:%x(%c)\n", i, dest[i], dest[i]);
}
printf("other addr:%p\n", other);
size_t bytes = (char*)other - (char*)dest;
printf("other (other-dest):%zu\n", bytes);
for (int i=0; i<sizeof(other); i++)
{
printf("other[%d]:%x(%c)\n", i, other[i], other[i]);
}
return 0;
}
运行结果如下:
需要注意的是,虽然dest字符串通过printf正常输出了,但实际是拷贝字符时,越界拷贝,虽然这里暂时没有问题,但other中的内容被篡改,如果后续需要使用other中的内容,可能就会出现不符合预期的结果。
1.4关于是否会加上结尾符的验证
// gcc strcpy3.c -o strcpy3
#include <stdio.h>
#include <string.h>
int main()
{
char src[] = "Hello";
char dest[10] = {0}; // 确保目标数组足够大
strcpy(dest, "12345abcd");
printf("复制后的字符串: %s\n", dest);
strcpy(dest, src);
printf("复制后的字符串: %s\n", dest);
for (int i=0; i<sizeof(dest); i++)
{
printf("dest[%d]:%x(%c)\n", i, dest[i], dest[i]);
}
//再测试一下char数组逐个赋值后的拷贝(确保src2当做字符串时最后没有‘\0’)
char src2[2];
src2[0] = 'm';
src2[1] = 'n';
strcpy(dest, src2);
printf("复制后的字符串: %s\n", dest);
for (int i=0; i<sizeof(dest); i++)
{
printf("dest[%d]:%x(%c)\n", i, dest[i], dest[i]);
}
return 0;
}
运行结果如下:
可以看到:
拷贝src字符串时,"Hello"被正常拷贝,后面一个’\0’也拷贝了,dest中剩下的则保持原样
拷贝char src2[2]时,由于src2没有自动被赋予的’\0’字符串结尾符,在strcpy赋值时,就只是复制了2个char数据本身,所有在printf打印时,就和后面的数据一起打印出来了,直到遇到了’\0’字符。
另外有点特殊的是,dest原有的内容,被整体向后移动了。
2 strncpy
strncpy是 strcpy的安全版本,用于复制指定长度的字符串。
2.1 函数原型
char *strncpy(char *dest, const char *src, size_t n);
将 src
的前 n
个字符复制到 dest
,不自动添加终止符 '\0'
(除非 src
的长度小于 n
)。
参数:
dest
:目标字符串指针(需提前分配足够空间)。src
:源字符串指针(必须以'\0'
结尾)。n
:最多复制的字符数。
返回值:
- 返回目标字符串的指针,也就是
dest
。
它会复制最多 n
个字符,能够防止缓冲区溢出。
不过要注意,如果源字符串的长度超过 n
,dest
数组将不会以空字符结尾。
2.2 使用示例
// gcc strncpy1.c -o strncpy1
#include <stdio.h>
#include <string.h>
int main()
{
char src[] = "Hello";
char dest[10] = {0}; // 确保目标数组足够大
int destSize = sizeof(dest);
strncpy(dest, src, destSize-1); //最多只能复制destSize-1, 因为要加上结尾符
dest[destSize] = '\0'; //手动加上结尾符
printf("复制后的字符串: %s\n", dest);
for (int i=0; i<sizeof(dest); i++)
{
printf("dest[%d]:%x(%c)\n", i, dest[i], dest[i]);
}
return 0;
}
运行结果如下:
关键注意事项
是否自动添加终止符:
- 如果
src
的长度 小于n
,strncpy
会在复制完src
后,在dest
中填充'\0'
直到n
个字符。 - 如果
src
的长度 大于等于n
,dest
不会自动添加终止符,此时需要手动添加dest[n-1] = '\0'
,否则可能导致字符串处理异常。
- 如果
避免缓冲区溢出
n
应不超过dest
的大小(包括终止符空间)。例如:
char dest[5]; strncpy(dest, src, 5);
可能导致溢出(因为dest
的有效空间只有 4 个字符 + 1 个终止符)
2.3 拷贝越界被截断的情况举例
// gcc strncpy2.c -o strncpy2
#include <stdio.h>
#include <string.h>
int main()
{
char src[] = "Hello, World"; //原字符串11个字符,再加上结尾'\0'则占12个字符
char dest[10] = {0}; //目标存储空间只有10个
char other[10] = {0}; //随后再定一个10个大小的other来验证是否被越界拷贝了
int destSize = sizeof(dest);
strncpy(dest, src, destSize-1);
dest[destSize] = '\0';
printf("复制后的字符串: %s\n", dest);
printf("dest addr:%p\n", dest);
for (int i=0; i<sizeof(dest); i++)
{
printf("dest[%d]:%x(%c)\n", i, dest[i], dest[i]);
}
printf("other addr:%p\n", other);
size_t bytes = (char*)other - (char*)dest;
printf("other (other-dest):%zu\n", bytes);
for (int i=0; i<sizeof(other); i++)
{
printf("other[%d]:%x(%c)\n", i, other[i], other[i]);
}
return 0;
}
运行结果如下:
可以看到,使用strnpy,通过指定拷贝的长度:
- 数据如果超出长度,则被截断,并且有结尾符
- 确保了其它数据不被越界访问
2.4 不规范的使用举例
// gcc strncpy3.c -o strncpy3
#include <stdio.h>
#include <string.h>
int main()
{
char src[] = "Hello";
char dest[10] = {0}; //拷贝的目标位置
char other[10] = "xy"; //随后一个位置用于测试
printf("other[0]: %c\n", other[0]);
printf("dest addr:%p\n", dest);
printf("other addr:%p\n", other);
size_t bytes = (char*)other - (char*)dest;
printf("other (other-dest):%zu\n", bytes); //确认other就是在dest之后
int destSize = sizeof(dest);
//不规范1:这里的原字符串长度大于destSize,并且没有手动添加字'\0'结尾符
strncpy(dest, "12345abcdefg", destSize);
printf("复制后的字符串: %s\n", dest); //dest与后面的other连成一个字符串了!
printf("other[0]: %c\n", other[0]); //由于strncpy拷贝了destSize,后面的other没有影响
//不规范2:这次拷贝的字符串长度小于destSize, 并且没有手动添加字'\0'结尾符
strncpy(dest, src, 5);
printf("复制后的字符串: %s\n", dest);
for (int i=0; i<sizeof(dest); i++)
{
printf("dest[%d]:%x(%c)\n", i, dest[i], dest[i]);
}
int srcSize = sizeof(src); //注意src的sizeof是包含了之后的'\0'结尾符的,所以是6
int srcLen = strlen(src);
printf("srcSize:%d, srcLen:%d\n", srcSize, srcLen);
strncpy(dest, src, srcSize);
printf("复制后的字符串: %s\n", dest);
for (int i=0; i<sizeof(dest); i++)
{
printf("dest[%d]:%x(%c)\n", i, dest[i], dest[i]);
}
return 0;
}
运行结果如下:
strncpy的安全性只是在于拷贝的长度可控,避免越界访问。
但当要拷贝的数据长度大于目标空间时,数据被截断,但没有提示,如果直接使用截断的字符串,也会出现意想不到的的问题。
3 strlcpy
strlcpy是一个更安全的字符串复制函数,并可以返回实际拷贝的长度。
不过需要注意的是,strlcpy
并非标准 C 库函数,而是在 BSD 系统(如 macOS)中存在,
Linux 系统需要通过 #include <bsd/string.h>
引入,并额外链接 libbsd
,另外也要安装一下这个库:
sudo apt-get install libbsd-dev
3.1 函数原型
size_t strlcpy(char *dest, const char *src, size_t size);
将 src
的内容复制到 dest
,确保 dest
以 '\0'
结尾,并返回 src
的原始长度(不包含终止符)。
参数:
dest
:目标字符串指针(需提前分配空间)。src
:源字符串指针(必须以'\0'
结尾)。size
:dest
的最大容量(包括终止符'\0'
)。
返回值:
src
的实际长度(不包含'\0'
)。若返回值 ≥size
,说明复制时发生了截断。
3.2 使用示例
// gcc strlcpy.c -o strlcpy -lbsd
#include <stdio.h>
#include <string.h>
#include <bsd/string.h> // Linux需要显式包含
int main()
{
char src[] = "Hello, World";
char dest[10] = {0};
char other[10] = {0};
size_t len = strlcpy(dest, src, sizeof(dest));
printf("strlcpy ret:%zu\n", len);
if (len > sizeof(dest))
{
printf("copy oversize!\n");
}
printf("复制后的字符串: %s\n", dest);
printf("dest addr:%p\n", dest);
for (int i=0; i<sizeof(dest); i++)
{
printf("dest[%d]:%x(%c)\n", i, dest[i], dest[i]);
}
printf("other addr:%p\n", other);
size_t bytes = (char*)other - (char*)dest;
printf("other (other-dest):%zu\n", bytes);
for (int i=0; i<sizeof(other); i++)
{
printf("other[%d]:%x(%c)\n", i, other[i], other[i]);
}
return 0;
}
运行结果:
strlcpy的特性
自动处理终止符
strlcpy
会确保dest
以'\0'
结尾,即使src
被截断。- 最多复制
size - 1
个字符到dest
,并在末尾添加'\0'
。
安全避免溢出
- 无论
src
多长,dest
都不会溢出。 - 若
src
长度 ≥size
,函数会截断src
,仅复制size - 1
个字符。
- 无论
返回值的用途
返回src的实际长度,可用于检测截断情况:
if (strlcpy(dest, src, size) >= size) { printf("警告:源字符串被截断!\n"); }
4 上述3种函数对比
函数 | 是否自动添加终止符 | 溢出风险 | 截断处理 |
---|---|---|---|
strcpy |
✅ | ❌ | 不截断,可能溢出 |
strncpy |
❌ | ❌ | 截断但不补终止符 |
strlcpy |
✅ | ✅ | 截断并补终止符 |
- strcpy使用简单,在数据长度不确定的情况下使用会有风险
- strnpy指定了拷贝长度吗,但仍有数据被截断的风险
- strlcpy可以通过返回值检测是否被截断,但需要额外库的支持
5 自定义strlcpy
基于上述分析,可以对strnpy进行自定义封装改造,实现类似strlcpy,这样也不需要额外库的支持。
5.1 对strnpy进行封装实现自定义检查
int my_strlcpy(char *dst, const char *src, size_t dstSize)
{
size_t srcLen = strlen(src);
if (NULL == dst || NULL == src || 0 == dstSize)
{
return -1; //参数错误
}
size_t maxCopyLen = dstSize - 1;
strnpy(dst, src, maxCopyLen);
dst[len] = '\0';
if (srclen > maxCopyLen)
{
printf("%d > %d, copy oversize! only copy:%s\n", srclen, maxCopyLen, dst);
return -2; //数据被截断
}
retun 0; //正常拷贝
}
5.2 测试验证
// gcc my_strlcpy.c -o my_strlcpy
#include <stdio.h>
#include <string.h>
int my_strlcpy(char *dst, const char *src, size_t dstSize)
{
size_t srcLen = strlen(src);
if (NULL == dst || NULL == src || 0 == dstSize)
{
return -1; //参数错误
}
size_t maxCopyLen = dstSize - 1;
strncpy(dst, src, maxCopyLen);
dst[maxCopyLen] = '\0';
if (srcLen > maxCopyLen)
{
printf("%zu > %zu, copy oversize! only copy:%s\n", srcLen, maxCopyLen, dst);
return -2; //数据被截断
}
return 0; //正常拷贝
}
int main()
{
char src[] = "Hello, World";
char dest[10] = {0};
char other[10] = {0};
int ret = my_strlcpy(dest, src, sizeof(dest));
if (ret)
{
printf("err! my_strlcpy ret:%d\n", ret);
}
printf("复制后的字符串: %s\n", dest);
printf("dest addr:%p\n", dest);
for (int i=0; i<sizeof(dest); i++)
{
printf("dest[%d]:%x(%c)\n", i, dest[i], dest[i]);
}
printf("other addr:%p\n", other);
size_t bytes = (char*)other - (char*)dest;
printf("other (other-dest):%zu\n", bytes);
for (int i=0; i<sizeof(other); i++)
{
printf("other[%d]:%x(%c)\n", i, other[i], other[i]);
}
return 0;
}
运行结果:
6 总结
本篇介绍了C语言中如何安全的进行字符串拷贝,首先测试了在使用strcpy、strncpy、strlcpy进行字符串拷贝时可能遇到的问题,然后对比这两种方式的基础差别,最后通过自定义封装strncpy来实现安全拷贝字符串的功能。