C语言陷阱与技巧第16节,处理字符串

在C语言程序开发中处理字符串又是一件非常重要的事。因为虽然对于计算机来说,字符串和其他数据类型没什么两样,都是 0 1 组成的数字流,但是对于人类来说,字符串看起来要容易理解得多。

例如下面这段C语言代码:

#include <stdio.h>
 char str1[] = {
     0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 
     0x77, 0x6F, 0x72, 0x6C, 0x64, 0x00
};  
 char str2[] = "Hello world";
 int main()
{
     printf("str1: %s\n", str1);
     printf("str2: %s\n", str2);

     return 0;
}


通常,对于计算机来说,上面C语言代码中的 str1 和 str2 是等价的,但是对于人类来说,str2 显然要直观的多。遗憾的是,C语言不像C++那样支持运算符的重载,处理字符串非常麻烦,下面这样的代码在C语言中是非法的:

char str[] = "Hello" + " world";

编译相关C语言代码,得到如下错误提示:

# gcc t.c
error: invalid operands to binary + (have ‘char *’ and ‘char *’)
 char str[] = "Hello" + " world";

C语言处理字符串相对比较麻烦,也是其他一些高级语言(如 Java,JavaScript,python)程序员不看好C语言的原因之一,甚至一些程序员都不认可C语言是“高级语言”。不过确实如此,少了一些天然库与运算符重载的支持,C语言甚至在拼接字符串的时候都非常麻烦:

char buf[64];
sprintf(buf, "/path/%s", filename);

上面这段代码是C语言中常使用的字符串拼接方法之一,主要就是借助 sprintf() 函数。可是写出这样的代码就相当于给自己“挖陷阱”:

如果 filename 的长度比较长,最终拼接的字符串超出了 buf 的长度,就会导致程序内存溢出,这种情况下,程序直接崩溃还好。要是程序不崩溃,输出一些错误结果就麻烦了。因为这样的错误非常难调试,它时隐时现,难以捉摸,你甚至可以说这种错误是C语言程序开发中隐蔽最深的错误。

所以,使用 snprintf() 函数要更安全一些:

 int snprintf(char *str, size_t size, const char *format, ...);

将上述拼接字符串的代码改写为:

char buf[64];
snprintf(buf, sizeof(buf), "/path/%s", filename);

这样一来,即使 filename 很长,程序也不会内存溢出了,因为 snprintf() 只会将前面 sizeof(buf) 长度的字符放入 buf。不过这又会带来一个问题,将 filename 截断肯定不会得到正确的结果。所以这种情况下,只能尽量的增加 buf 的长度,例如:

char  buf[128];

可是,多大的空间够用呢?C语言程序开发中,需要处理的字符串长度常常都是不能确定的。那为了安全,只能尽量让 buf 的长度更长一些:

char buf[1024];

但是,可能只有极少数的较长字符串才能用到很多空间,大多数情况下,buf 的空间都是浪费的,这对于C语言程序开发来说是不可接受的。

C语言程序要坚持一个原则:使用更少的资源,更高效率的做事。

C语言的新特性

C语言的特性近些年来也得到了一定的扩展,“变长数组”就是其中之一。顾名思义,C语言的变长数组特性允许我们使用变量作为定义数组的长度,这与大多数C语言教科书强调的“C语言中定义数组时,长度必须是常量表达式”有所不同。请看:

int len1 = strlen("/path/") ;
int len2 = strlen(filename);
char buf[ len1 + len2 ];
snprintf(buf, sizeof(buf), "/path/%s", filename);

这样一来,buf 的长度会随着 filename 的长度动态变化,基本上能够避免C语言程序内存溢出。

值得一提的是,本文为了突出主题,所使用的C语言示例代码没有做太多的错误判断。

使用变长数组的好处是在栈中处理效率比较高,坏处就是可能会降低最终C语言代码的可移植性,因为我们不能确保所有硬件平台的编译器都支持C语言的这种新特性。

所以,如果放弃栈中处理的高效率,我们可以在堆中申请内存给 buf 使用,写出更具有移植性的C语言代码,请看:

int len1 = strlen("/path/") ;
int len2 = strlen(filename);
char *buf = (char *)malloc(len1+len2);
snprintf(buf, len1+len2, "/path/%s", filename);
...
free(buf);

这里应该避免的一个“陷阱”是,snprintf() 的第二个参数不能再使用 sizeof(buf) 了(buf 此时是一个指针)。另外,使用完 buf 之后要及时释放。

其他小技巧

一般来说,在C语言程序开发中,为了代码的可读性和编写的方便,遇到很长的字符串常量时,常常都是分行写,例如:

char str[] = "hello world, i am computer,"
                    "where am i?"

应该注意,其实这里就相当于一次字符串拼接了。所以如果是拼接字符串常量,在C语言中还可以这么做:

#define PATH "/root/test/"
printf( PATH"hello.txt");

编译上述代码段并执行,会得到如下输出:

/root/test/hello.txt

小结

事实上,不仅仅在C语言程序开发中,在各种其他编程语言的程序开发中,字符串的处理都非常重要,本节主要讨论了C语言在字符串处理中的劣势,并在此基础上分析了几个“陷阱”,介绍了C语言中几种常用的字符串拼接技巧。

阅读更多:   C语言
添加新评论

icon_redface.gificon_idea.gificon_cool.gif2016kuk.gificon_mrgreen.gif2016shuai.gif2016tp.gif2016db.gif2016ch.gificon_razz.gif2016zj.gificon_sad.gificon_cry.gif2016zhh.gificon_question.gif2016jk.gif2016bs.gificon_lol.gif2016qiao.gificon_surprised.gif2016fendou.gif2016ll.gif