关于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&lt;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&lt;128;i++) szBuf[i]='*';

       szBuf[127]='\0';

       char szBuf2[256];

       for(i=0;i&lt;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&lt;128;i++) szBuf[i]='*';

       szBuf[127]='\0';

       char szBuf2[256];

       for(i=0;i&lt;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&lt;128;i++) szBuf[i]='*';

       szBuf[127]='\0';

       char szBuf2[256];

       for(i=0;i&lt;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”结束字符串。这会造成下一次读取字符串时,数据边界不可控。格式化打印函数,返回值设计不合理,不永远是一个正整数,会造成逻辑隐患。因此,建议大家有兴趣可以参考一下我提供的两个函数。

另外,以上仅为我个人测试之作,限于本人水平所限,肯定还有没考虑到的地方,欢迎大家展开讨论。如果大家需要上面的源代码,请和我联系

本文转自tonyxiaohome 51CTO博客,原文链接:http://blog.51cto.com/tonyxiaohome/198463,如需转载请自行联系原作者

相关文章
|
2月前
|
安全 C语言
C语言中的字符、字符串及内存操作函数详细讲解
通过这些函数的正确使用,可以有效管理字符串和内存操作,它们是C语言编程中不可或缺的工具。
233 15
|
9月前
|
存储 C语言 开发者
【C语言】字符串操作函数详解
这些字符串操作函数在C语言中提供了强大的功能,帮助开发者有效地处理字符串数据。通过对每个函数的详细讲解、示例代码和表格说明,可以更好地理解如何使用这些函数进行各种字符串操作。如果在实际编程中遇到特定的字符串处理需求,可以参考这些函数和示例,灵活运用。
339 10
|
10月前
|
存储 算法 C语言
C语言中常见的字符串处理技巧,包括字符串的定义、初始化、输入输出、长度计算、比较、查找与替换、拼接、截取、转换、遍历及注意事项
本文深入探讨了C语言中常见的字符串处理技巧,包括字符串的定义、初始化、输入输出、长度计算、比较、查找与替换、拼接、截取、转换、遍历及注意事项,并通过案例分析展示了实际应用,旨在帮助读者提高编程效率和代码质量。
517 4
|
11月前
|
C语言 C++
【C语言】解决不同场景字符串问题:巧妙运用字符串函数
【C语言】解决不同场景字符串问题:巧妙运用字符串函数
106 2
|
存储 C语言
【C语言基础考研向】10 字符数组初始化及传递和scanf 读取字符串
本文介绍了C语言中字符数组的初始化方法及其在函数间传递的注意事项。字符数组初始化有两种方式:逐个字符赋值或整体初始化字符串。实际工作中常用后者,如`char c[10]=&quot;hello&quot;`。示例代码展示了如何初始化及传递字符数组,并解释了为何未正确添加结束符`\0`会导致乱码。此外,还讨论了`scanf`函数读取字符串时忽略空格和回车的特点。
344 8
|
存储 Serverless C语言
【C语言基础考研向】11 gets函数与puts函数及str系列字符串操作函数
本文介绍了C语言中的`gets`和`puts`函数,`gets`用于从标准输入读取字符串直至换行符,并自动添加字符串结束标志`\0`。`puts`则用于向标准输出打印字符串并自动换行。此外,文章还详细讲解了`str`系列字符串操作函数,包括统计字符串长度的`strlen`、复制字符串的`strcpy`、比较字符串的`strcmp`以及拼接字符串的`strcat`。通过示例代码展示了这些函数的具体应用及注意事项。
563 7
|
11月前
|
存储 C语言
【c语言】字符串函数和内存函数
本文介绍了C语言中常用的字符串函数和内存函数,包括`strlen`、`strcpy`、`strcat`、`strcmp`、`strstr`、`strncpy`、`strncat`、`strncmp`、`strtok`、`memcpy`、`memmove`和`memset`等函数的使用方法及模拟实现。文章详细讲解了每个函数的功能、参数、返回值,并提供了具体的代码示例,帮助读者更好地理解和掌握这些函数的应用。
165 0
|
存储 人工智能 C语言
C语言程序设计核心详解 第八章 指针超详细讲解_指针变量_二维数组指针_指向字符串指针
本文详细讲解了C语言中的指针,包括指针变量的定义与引用、指向数组及字符串的指针变量等。首先介绍了指针变量的基本概念和定义格式,随后通过多个示例展示了如何使用指针变量来操作普通变量、数组和字符串。文章还深入探讨了指向函数的指针变量以及指针数组的概念,并解释了空指针的意义和使用场景。通过丰富的代码示例和图形化展示,帮助读者更好地理解和掌握C语言中的指针知识。
427 4
|
C语言
C语言 字符串操作函数
本文档详细介绍了多个常用的字符串操作函数,包括 `strlen`、`strcpy`、`strncpy`、`strcat`、`strncat`、`strcmp`、`strncpy`、`sprintf`、`itoa`、`strchr`、`strspn`、`strcspn`、`strstr` 和 `strtok`。每个函数均提供了语法说明、参数解释、返回值描述及示例代码。此外,还给出了部分函数的自实现版本,帮助读者深入理解其工作原理。通过这些函数,可以轻松地进行字符串长度计算、复制、连接、比较等操作。
|
11月前
|
存储 安全 编译器
深入C语言库:字符与字符串函数模拟实现
深入C语言库:字符与字符串函数模拟实现
107 0