当前位置: 首页 > news >正文

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

在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'结尾符的,所以是6int 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来实现安全拷贝字符串的功能。

http://www.xdnf.cn/news/1279531.html

相关文章:

  • 云原生环境 Prometheus 企业级监控实战
  • Centos 用http ftp搭建本地yum源 保姆级教程
  • QML开发:动画元素
  • 企业高性能web服务器Nginx的详细部署(实战篇)
  • [4.2-2] NCCL新版本的register如何实现的?
  • ResponseBodyAdvice是什么?
  • ChatML vs Harmony:深度解析OpenAI全新对话结构格式的变化
  • ARM基础概念 day51
  • Redis应⽤-缓存与分布式锁
  • Vue3从入门到精通:3.1 性能优化策略深度解析
  • 基于SpringBoot+Uniapp的血压监控小程序(Echarts图形化分析)
  • OV5640 相机开发流程
  • Apollo平台下相机和激光雷达手眼联合标定
  • 游戏引擎(Unreal Engine、Unity、Godot等)大对比:选择最适合你的工具
  • 2025世界机器人大会,多形态机器人开启商业化落地浪潮
  • ubuntu24.04设置登陆背景图片
  • 工业相机与智能相机的区别
  • word的正则替换
  • 《解锁 C++ 进阶密码:引用补充与内联函数、nullptr 核心用法》
  • 【测试报告】SoundWave(Java+Selenium+Jmeter自动化测试)
  • 2025 年国内可用 Docker 镜像加速器地址
  • 前端组件库双雄对决:Bootstrap vs Element UI 完全指南
  • Flink TableAPI 按分钟统计数据量
  • Spring AI赋能图像识别:大数据模型驱动下的智能化变革
  • SAE J2716多协议网关的硬件架构与实时协议转换机制解析
  • calamine读取xlsx文件的方法比较
  • 华为虚拟防火墙配置案例详解
  • 未来物联网大模型:物联网硬件+底层驱动+AI 自动生成和调优LUA脚本,
  • 数据备份与进程管理
  • TikTok登录时显示“访问太频繁,请稍后再试”该怎么办?