4

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

 3 years ago
source link: https://blog.popkx.com/c-language-traps-and-techniques-section-16-processing-strings/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

发表于 2019-04-28 08:04:05   |   已被 访问: 350 次   |   分类于:   C语言   |   暂无评论

在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;
}
486aee162460063475f80d0bb6cdbb62.png

通常,对于计算机来说,上面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语言中几种常用的字符串拼接技巧。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK