在linux C 编程中,我们经常遇到字符串的处理,最多的就是字符串的长度、拷贝字符串、比较字符串等;当然现在的C库中为我们提供了很多字符串处理函数。熟练的运用这些函数,可以减少编程工作量,这里介绍几个常用的字符串函数,并编写一些程序,如果没有这些库函数,我们将如何实现其功能;
1 求字符串长度函数 strlen
头文件:string.h
函数原型:size_t strlen(const char *s)
功能:求字符串长度(不含字符串结束标志'\0')
如果没有这个函数,我们如何实现strlen呢?
程序如下:
#include <stdio.h> #include <string.h> int mystrlen(const char *p) { int i = 0; while(p[i]) i++; return i; } int main() { int len; char str[] = "Helloworld"; len = mystrlen(str); printf("len = %d\n",len); return 0; }
执行结果如下:
fs@ubuntu:~/qiang/string$ gcc -o strlen strlen.c fs@ubuntu:~/qiang/string$ ./strlen len = 10
同样可以实现求字符串长度功能。
既然在讲strlen(),在这里多说明一下,注意strlen()与sizeof()的区别:
sizeof和strlen有以下区别:
sizeof是一个操作符,strlen是库函数。
sizeof的参数可以是数据的类型,也可以是变量,而strlen只能以结尾为‘\0‘的字符串作参数。
编译器在编译时就计算出了sizeof的结果。而strlen函数必须在运行时才能计算出来。并且sizeof计算的是数据类型占内存的大小,而strlen计算的是字符串实际的长度。
数组做sizeof的参数不退化,传递给strlen就退化为指针了。
注意:有些是操作符看起来像是函数,而有些函数名看起来又像操作符,这类容易混淆的名称一定要加以区分,否则遇到数组名这类特殊数据类型作参数时就很容易出错。最容易混淆为函数的操作符就是sizeof。
说明:指针是一种普通的变量,从访问上没有什么不同于其他变量的特性。其保存的数值是个整型数据,和整型变量不同的是,这个整型数据指向的是一段内存地址。
2、字符串拷贝函数strcpy()
头文件:string.h
函数原型:char *strcpy(char *dest,const char *src)
功能: 字符串拷贝
参数:src为源串的起始地址,dest为目标串的起始地址
如果没有这个函数,我们将如何实现呢?程序如下:
#include <stdio.h> char *mystrcpy(char *dest,const char *src) { char *p; p = dest; while(*src) { *dest++ = *src++; } *dest = '\0'; return p; } int main() { const char str1[] = "Helloworld"; char str2[30]; mystrcpy(str2,str1); printf("str2 = %s\n",str2); return 0; }
执行结果如下:
fs@ubuntu:~/qiang/string$ ./strcpy str2 = Helloworld
同样能够得到结果,当然有了strcpy()会很方便;
3、字符串连接接函数strcat
头文件:string.h
函数原型:char *strcat(char *dest,const char *src)
功能:把字符串src连接到字符串dest的后面
实现方法:
#include <stdio.h> char *mystrcat(char *dest,const char *src) { char *p; p = dest; while(*dest) dest++; while(*src) { *dest++ = *src++; } *dest = '\0'; return p; } int main() { char str1[] = "hello"; char str2[] = "world"; mystrcat(str1,str2); printf("str1 = %s\n",str1); return 0; }
执行结果如下:
fs@ubuntu:~/qiang/string$ gcc -o strcat strcat.c fs@ubuntu:~/qiang/string$ ./strcat str1 = helloworld
在使用strcat函数时,需要注意,目标数组应该有足够的空间,连接源串。注意,目标字符串'\0'被删除,然后连接源串。如果越界会发生什么呢?我们可以来验证一下:
#include <stdio.h> #include <string.h> int main() { char str2[6] = "world"; char str1[6] = "hello"; strcat(str1,str2); printf("str1 = %s\n",str1); return 0; }
执行结果如下:
fs@ubuntu:~/qiang/string$ gcc -o strcat1 strcat1.c fs@ubuntu:~/qiang/string$ ./strcat1 str1 = helloworld *** stack smashing detected ***: ./strcat1 terminated ======= Backtrace: ========= /lib/i386-linux-gnu/libc.so.6(__fortify_fail+0x45)[0xb76cbd95] /lib/i386-linux-gnu/libc.so.6(+0x103d4a)[0xb76cbd4a] ./strcat1[0x80484d7] /lib/i386-linux-gnu/libc.so.6(__libc_start_main+0xf3)[0xb75e14d3] ./strcat1[0x80483d1] ======= Memory map: ======== 08048000-08049000 r-xp 00000000 08:01 830810 /home/fs/qiang/string/strcat1
当然,下面还有好多,这里就不展示了,我们来看看结果,helloworld能打印出来,编译时也没错误,但执行时告知溢出了。
我们稍微修改程序,大家注意区别:
#include <stdio.h> #include <string.h> int main() { char str1[6] = "hello"; char str2[6] = "world"; strcat(str1,str2); printf("str1 = %s\n",str1); printf("str2 = %s\n",str2); return 0; }
输出结果如下:
fs@ubuntu:~/qiang/string$ gcc -o strcat1 strcat1.c fs@ubuntu:~/qiang/string$ ./strcat1 str1 = helloworld str2 = orld
并没有溢出!(实际是溢出了!!!!!!!)
是不是很有意思?大家看看两者程序的区别,只是六七行交换了位置,但一个溢出,一个却不溢出,为什么呢?大家应该知道数据存储的方式吧,第一个程序中:
char str2[6] = "world"; char str1[6] = "hello";
先定义了str2[],程序为其分配了一段连续的空间,接着定义了str1[],程序会在刚才为str2[]定义的地址后面接着定义一段连续空间,如果接着将str2 接在str1后面,str1原来只定义了6个字节,需要连接在一起需要11个字节,肯定超出了我们定义的地址空间,后面是一片未知区域,会发生溢出;但为什么程序2却没有溢出呢?
char str1[6] = "hello"; char str2[6] = "world";
这里就比较巧了,因为str2是在str1后面定义的,str2接在str1后面确实会溢出,但溢出后的一片空间,正好是str2的地址空间,区域是可知的,只是helloworld覆盖掉了原来str2的东西。所以不会溢出,这样说,大家明白吧?
继续看,大家有木有发现,我在程序二中对str2的值进行了打印,不再是原来的world,变成了orld,按道理来讲,str2的值不会改变的啊?大家在这里要清楚str2只是一个地址而已,只负责输出当前地址以后的字符串,以'\0'结束;所以这里的orld是strcat(str1,str2)后str1的值,但为什么是“orld”呢?因为目标字符串的'\0'被删除,然后连接串;
此时str1后面的orld覆盖了原world,但str2原来指向的是w的地址,现在原存放'w'的地址处存放的是'o',所以会输出"orld"!大家是否还有疑问,后面好像还有个'd'没有被覆盖啊,为什么输出的不是"orldd"呢?大家应该明白字符串有个结束符'\0'吧,它将'd'覆盖了,如果大家觉得不好理解,可以画一下图,就比较清楚了;
其实这只是个特例,让大家看一下如果数据溢出造成的后果!
4、字符串比较函数strcmp
头文件:string.h
函数原型:int strcmp(const char *s1,const char *s2)
功能:按照ASCII码顺序比较字符串s1和字符串s2的大小
如果没有这个函数,我们如下实现:
#include <stdio.h> int mystrcmp(const char *s1,const char *s2) { int i = 0; while(*s1 || *s2) { if(*s1 > *s2) { return 1; } else if(*s1 < *s2) { return -1; } else { s1++; s2++; } } return 0; } int main() { int n; char str1[] = "hell"; char str2[] = "hello"; n = mystrcmp(str1,str2); printf("n = %d\n",n); return 0; }
执行结果如下:
fs@ubuntu:~/qiang/string$ gcc -o strcmp strcmp.c fs@ubuntu:~/qiang/string$ ./strcmp n = -1
刚才提到的函数功能:比较两字符串的大小,好像比较抽象,我们其实是比较两个字符串是否相等;下面我们看个题目:
题目:计算字符串中子串出现的次数
什么意思呢?就是helloworldhehehehellowo中,比如说子串"hello"在字符串中出现的次数,如果单纯的用getchar()获取每个字符并比较,会很麻烦,在这里我们可以用strcmp来实现,会很方便,大家可以看看strcmp的具体应用,实现程序如下:
#include <stdio.h> #include <string.h> int main() { int i = 0; int count = 0; int len1,len2; char str1[100] = {'\0'}; char str2[20] = {'\0'}; printf("Please input two strings!\n"); scanf("%s%s",str1,str2); len1 = strlen(str1); len2 = strlen(str2); while(i + len2 <= len1) { if(!(strncmp(&str1[i],str2,len2))) { count++; i += len2; } else i++; } printf("count = %d\n",count); return 0; }
执行结果如下:
fs@ubuntu:~/qiang/string$ ./zichuan Please input two strings! xiaoqiangxiqiangxiaoxiaqiang xiao count = 2 fs@ubuntu:~/qiang/string$
大家看看结果是不是正确的。
附:(转载)
strcmp 字符串比较函数,strcpy 字符串拷贝函数, strlen 字符串测长函数, strcat字符串连接函数,sprintf格式化字符串拷贝函数等等。因为字符串就是以‘\0’结束的一段内存,这些函数实质上也就是操作内存的函数,所以避免不了的与指针打交道,使得这些函数充满了陷阱,如果这些函数使用不当,很有可能在程序中埋伏下危险的陷阱,使程序的稳定性遭受重创。下面我就字符串使用中一些常见的问题来进行举例说明。
一. strcpy:极度危险的函数,一不小心就会中招,危险指数:四星
strcpy的原型是这样的: char *strcpy(char *dest, const char *src) 作为常见的字符串复制函数,C库中的实现是不安全的,因为它不做字符串的检查,以至于如果参数传入了非法指针,比如:src不是指向字符串的指针。后果就不堪设想,程序会一直复制,直到遇到‘\0’才结束,这样很有可能就会使得dest指向的内存区域缓冲区溢出,使得导致不程序相干的部分出现错误,这种错误也许就是致命的。所以使用这个函数一定确保第二个参数传入合法的指针。
例子:
#include <string.h> #include <stdlib.h> #include <stdio.h> char dest[5] = {'D'}; char mydata[7] = {'m','y','d','a','t','a','\0'}; int main(void) { char i; char source[5]; char bound[5] = {'&','&','&','&','&'}; for (i = 0; i < 5; i++) source[i] = 'S'; printf("before strcopy, mydata is %s\n", mydata); strcpy(dest, source); printf("dest is %s\n", dest); printf("after strcopy, mydata is %s\n", mydata); }
二. strcat 造成缓冲区溢出的隐形杀手,危险指数 三星
strcat 是将一个字符串连接到另外一个字符串上,其函数原型为char *strcat(char *dest,char *src)。这个函数也很危险,因为C语言的实现也是不安全的,传入非法的指针有可能会造成程序的崩溃。首先保证两个指针都应该指向字符串,其次dest指针指向的空间要足以容的下src指向的字符串,否则会造成缓冲区溢出而破坏其他程序数据。
例子1:
#include <string.h> #include <stdlib.h> #include <stdio.h> char dest[5] = {'D', '\0'}; char mydata[7] = {'m','y','d','a','t','a','\0'}; int main(void) { char i; char source[5]; char bound[5] = {'&','&','&','&','&'}; for (i = 0; i < 4; i++) source[i] = 'S'; source[4] = '\0'; printf("before strcat, mydata is %s\n", mydata); strcat(dest, source); printf("dest is %s\n", dest); printf("after strcat, mydata is %s\n", mydata); }
这个例子因为目标dest只有5个字节大小,并且数据占了一个字节,只剩下四个字节位置,而源数据字符串长度为4个字符加一个‘\0’有五个字节大小,所以会多出一个字节覆盖了mydata的数据,多出的‘\0’成为了mydata的第一个字节,导致调用strcat后输出mydata为空。
例子2 :
#include <string.h> #include <stdlib.h> #include <stdio.h> char dest[5] = {'D', 'D', 'D', 'D', 'D'}; char mydata[7] = {'m','y','d','a','t','a','\0'}; int main(void) { char i; char source[5]; char bound[5] = {'&','&','&','&','&'}; for (i = 0; i < 4; i++) source[i] = 'S'; source[4] = '\0'; printf("before strcat, mydata is %s\n", mydata); strcat(dest, source); printf("dest is %s\n", dest); printf("after strcat, mydata is %s\n", mydata); }
三. strlen 很多malloc函数缓冲区溢出问题的始作俑者 危险指数 二星
strlen是字符串求长函数,但是它求出的长度不包括‘\0’,所以在用malloc分配内存的时候,很容易少分配一个字节,就这小小的一个字节就会造成缓冲区溢出,我们知道malloc分配的内存区域是有一个头的,这样就有可能破坏其他malloc的头使得内存释放失败,带来一系列连锁反映。因为malloc函数的实现与系统有关,这个不好用程序模拟,但是这种情况确实存在。因此如果用strlen求字符串长度用于malloc一定要记住要加1。
四. sprintf 同样可以造成缓冲区溢出,危险指数 一星
sprintf是格式化字符拷贝函数,函数原型是int sprintf( char *buffer, const char *format, … ) 。这个函数的实现也是不安全的,使用这个函数要确保buffer足够大,否则这个函数在不做任何提示的情况下就将buffer溢出,这个函数虽然返回复制的字节数,可以通过这个检查复制了多少个字节,以确定是否缓冲区溢出。但这种亡羊补牢的做法其实没有实际意义。缓冲区溢出的错误已经发生也许会是程序崩溃,检测的时间也许都没有,就算有检测时间,也只是用于提示程序的BUG,在正式的程序中没有多大用处。
例子:
#include <string.h> #include <stdlib.h> #include <stdio.h> char dest[2] = {'D'}; char mydata[7] = {'m','y','d','a','t','a','\0'}; int main(void) { char i; char source[5]; char bound[5] = {'&','&','&','&','&'}; for (i = 0; i < 4; i++) source[i] = 'S'; source[4] = '\0'; printf("before sprintf, mydata is %s\n", mydata); sprintf(dest, "%s", source); printf("dest is %s\n", dest); printf("after sprintf, mydata is %s\n", mydata); }
总上所述,C语言字符串操作函数一般都不对参数做检查,需要调用者确保参数的合法性。如果传入不正确的参数,就会造成缓冲区溢出。轻则数据被修改,重则程序崩溃。最郁闷的是影响到程序中不相关的部分。我前面举的例子都很简单,很容易一眼看出问题的所在,但是大型程序就不会这么简单了,这些错误就是致命的。所以使用C语言的字符串函数时一定要养成良好的习惯,自己检查参数的合法性,然后再调用。目前C语言中这些字符串操作函数都有一些安全的版本就是带n的系列,比如:strncpy,strncat,snprintf。这些函数规定了源字符串的大小,对缓冲区溢出的预防有一定的作用,比如:snprintf,其函数原型是int snprintf(char *str, size_t size, const char *format, ...) 第二个参数size,可以保证复制size个字节,如果要复制的字符串大于size就会截短,从而保证str不会溢出。程序中尽量使用这些安全的版本。良好的习惯是一个程序稳定与健壮的保证,而良好的习惯都是使用这些常用的函数养成的,所以一定要主要这些字符串函数的使用。