程序基础关于C语言字符串函数的思考

C语言并不是一种很方便的语言,它的字符串就是一例。按照C语言的定义,“字符串就是一段内存空间,里面包含ASCII字符,并且,以”/0“结尾,总共能存放n-1个字符。”按照这个描述,字符串处理确实很麻烦,还很容易出错。 为了方便用户,C语言标准库向用户提供了一些字符串函数,如字符串拷贝、构造、清空等函数,在一定程度上方便了用户的使用。但是,我无意中发现,这些函数还是有些隐患的。 事情很简单,我注意到我写的一些程序,老是有内存读写错误,但是,经过仔细检查我所有的数据Buffer,以及相关的处理函数,又没有找到什么错误。于是我把怀疑的目光投向我常用的一些字符串处理函数上,如strcpy、sprintf等。在经过几次仔细地跟踪之后,我发现内存错误出自于此。于是,我开始研究如何安全地使用字符串这个话题。 1.字符串拷贝函数 1.1 不安全的strcpy 首先,我写了这样一个测试函数: void strcpyTest0() { int i; char szBuf[128]; for(i=0;i<128;i++) szBuf[i]='*'; szBuf[127]='/0'; //构造一个全部是*的字符串 char szBuf2[256]; for(i=0;i<256;i++) szBuf2[i]='#'; szBuf2[255]='/0'; //构造一个全部是#的字符串 strcpy(szBuf,szBuf2); printf("%s/n",szBuf); } 很简单,把一个字符串拷贝到另外一个空间,但是,很不幸,源字符串比目标地址要长,因此,程序很悲惨地死去了。 1.2 还是不安全的strncpy 通过上例,我发现我需要在拷贝时多输入一个参数,来标明目的地址有多长,检查C语言的库函数说明,有一个strncpy可以达到这个目的,这个函数的原型如下: char *strncpy( char *strDest, const char *strSource, size_t count ); 好了,这下我们的问题解决了,我写出了如下代码: void strcpyTest1() { int i; char szBuf[128]; for(i=0;i<128;i++) szBuf[i]='*'; szBuf[127]='/0'; char szBuf2[256]; for(i=0;i<256;i++) szBuf2[i]='#'; szBuf2[255]='/0'; strncpy(szBuf,szBuf2,128); printf("%s/n",szBuf); } 一切都显得很好,但是,当我输出结果的时候,发现了问题,字符串后面有时会跟几个奇怪的字符,好像没有用“/0”结束,于是我把上面的拷贝语句改成“strncpy(szBuf,szBuf2,8);”,只拷贝8个字符,问题出现了,程序输出如下: ########*********************************************************************************************************************** 果然,当请求的目标地址空间比源字符串空间要小的时候,strncpy将不再用“/0”来结束字符串。巨大的隐患。 1.3 安全地字符串拷贝函数 我仔细想了想,我认为我需要如下一个字符串拷贝函数: 1、允许用一个整数界定目标地址空间尺寸。 2、当目标地址空间nD小于源字符串长度nS时,应该只拷贝nD个字节。 3、任何情况下,目标地址空间均应该以“/0”结束,保持一个合法的字符串身份。因此,得到的字符串最大长度为nD-1. 于是,我写了这么一个字符串拷贝函数: void xg_strncpy1(char *pD, char *pS,int nDestSize) { memcpy(pD,pS,nDestSize); *(pD+nDestSize-1)='/0'; } 很EASY是不,将这个拷贝函数代入上面的例子,只输出7个“#”, 结果正确。 1.4 内存读错误的思考 本来以为可以就此打住了,不过,没多久,我就发现一个奇怪的现象,这个函数在VC的Debug模式下有错误,但是Release模式下却一切正常。 我奇怪了很久,终于有一天我忍不住了,决定解决这个问题,我把上面的memcpy用自己的一个复制循环代替,单步跟踪,想看看究竟怎么回事? 原因找到了,我希望拷贝一个256字节长的字符串,但是,拷贝到第33字节时出错,检查程序,发现我的源字符串空间只有32 Bytes,原来,我上面的代码只是防止了内存写出界,但没有针对读出界进行检查,在VC的Debug模式下,内存读出界也是一种非法错误,因此被报错。 知道了原因,解决就很简单了,我把上面的拷贝函数改成如下形状: void xg_strncpy2(char *pD, char *pS,int nDestSize) { int nLen=strlen(pS)+1; if(nLen>nDestSize) nLen=nDestSize; memcpy(pD,pS,nLen); *(pD+nLen-1)='/0'; } 一切OK. 2.字符串构造函数 2.1 不安全的sprintf 如同上例,我在修改拷贝函数的同时,我也想到了另外一个我常用的字符串构造函数sprintf,显然,这个函数没有界定目标地址空间的尺寸,也是不安全的,下面的代码将会造成崩溃: void sprintfTest0() { int i; char szBuf[128]; for(i=0;i<128;i++) szBuf[i]='*'; szBuf[127]='/0'; char szBuf2[256]; for(i=0;i<256;i++) szBuf2[i]='#'; szBuf2[255]='/0'; sprintf(szBuf,szBuf2); printf("%s/n",szBuf); } 2.2 还是不安全的_snprintf 查阅库函数手册,找到这么一个函数_snprintf,其函数原型如下: int _snprintf( char *buffer, size_t count, const char *format [, argument] …… ); 这个函数允许界定目标地址尺寸,但是,由于研究拷贝函数的经验,我怀疑它也有strncpy相同的问题,因此,我写了这么一段代码测试: void sprintfTest1() { int i; char szBuf[128]; for(i=0;i<128;i++) szBuf[i]='*'; szBuf[127]='/0'; char szBuf2[256]; for(i=0;i<256;i++) szBuf2[i]='#'; szBuf2[255]='/0'; _snprintf(szBuf,8,szBuf2); printf("%s/n",szBuf); } 果然,程序输出如下: ########*********************************************************************************************************************** 同样的错误,没有用“/0”结束,我必须另外想方法。 另外,还发现了另外一个不足,就是这个时候,_snprintf函数返回-1,不再返回打印的字符数,那么,我们如果使用如下代码将会造成逻辑错误,甚至可能崩溃: char szBuf[256]; int nCount=0; while(1) //这里表示循环构造 { nCount+=_snprintf(szBuf+nCount,256-nCount,”... ...”); //多个字符串构造成一个字符串 } 注意,代码利用_snprintf返回的值,来确定下一个起始点,这很常用,但是,当_snprintf返回-1的时候,有可能会写到*(szBuf-1)的位置上,典型的内存写出界。 2.3 安全地字符串构造函数 经过仔细思考,我构造了如下一个函数: int xg_printf(char* szBuf,int nDestSize,char *szFormat, ...) { int nListCount=0; va_list pArgList; va_start (pArgList,szFormat); nListCount+=_vsnprintf(szBuf+nListCount, nDestSize-nListCount,szFormat,pArgList); va_end(pArgList); *(szBuf+nDestSize-1)='/0'; return strlen(szBuf); } 注意,这里我采用了变参函数设计,为的是和sprintf一样方便,另外,最后一个return也非常重要,因为很多场合,我们需要知道究竟打印了多少字符。将这段函数代入上面的例子后一切正常。 总结:C语言字符串库函数可能是出于提高性能目的,在一旦条件不够的时候,往往直接返回,忘了采用“/0”结束字符串。这会造成下一次读取字符串时,数据边界不可控。格式化打印函数,返回值设计不合理,不永远是一个正整数,会造成逻辑隐患。因此,建议大家有兴趣可以参考一下我提供的两个函数。 另外,以上仅为我个人测试之作,限于本人水平所限,肯定还有没考虑到的地方,欢迎大家展开讨论。如果大家需要上面的源代码,请和我联系。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值