C语言如何安全的进行字符串拷贝

发布于:2025-08-12 ⋅ 阅读:(17) ⋅ 点赞:(0)

在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;
 }

运行结果如下:

注意事项

  1. 目标数组大小要足够strcpy 不会检查目标数组的大小,一旦目标数组的空间不足以容纳源字符串,就会导致缓冲区溢出,这是非常危险的,可能会引发程序崩溃或者产生安全漏洞。
  2. 源字符串必须以空字符结尾:如果源字符串没有以 '\0' 结尾,strcpy 会一直复制内存中的数据,直到遇到空字符为止,这会造成未定义行为。
  3. 避免自我复制:不要将字符串复制到自身,否则会导致数据被破坏。

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 个字符,能够防止缓冲区溢出。

不过要注意,如果源字符串的长度超过 ndest 数组将不会以空字符结尾。

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 的长度 小于 nstrncpy 会在复制完 src 后,在 dest 中填充 '\0' 直到 n 个字符。
    • 如果 src 的长度 大于等于 ndest 不会自动添加终止符,此时需要手动添加 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' 结尾)。
  • sizedest 的最大容量(包括终止符 '\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来实现安全拷贝字符串的功能。


网站公告

今日签到

点亮在社区的每一天
去签到