C 语言字符串:问与答

问:字面串可以有多长?

答:按照 C89 标准,编译器必须最少支持 509 个字符长的字面串。(没错,就是 509。不要怀疑。)C99 把最小长度增加到了 4095 个字符。

问:为什么不把字面串称为“字符串常量”?

答:这是因为它们并不一定是常量。因为字面串是通过指针访问的,所以没有办法避免程序修改字面串中的字符。

问:如果 "\xfcber" 的写法无效,如何书写代表“über”的字面串呢?

答:秘诀是书写两个相邻的字面串,让编译器把它们拼接成一个。在上面的例子中,书写 "\xfc" "ber" 可以得到代表“über”的字面串。

问:改变字面串似乎没有什么危险。为什么会导致未定义的行为呢?

答:一些编译器试图通过只为相同的字面串存储一份副本来节约内存。考虑下面的例子:

1
char *p = "abc", *q = "abc";

编译器可能只存储 "abc" 一次,并且把 pq 都指向此字面串。如果试图通过指针 p 改变 "abc",那么 q 所指向的字符串也会受到影响。毫无疑问,这可能会导致一些非常讨厌的错误。另一个潜在的问题是,字面串可能存储在内存中的“只读”区域,试图修改这种字面串的程序会崩溃。

问:是否每个字符数组都应该包含空字符的空间呢?

答:这不是必需的,因为不是所有的字符数组都作为字符串使用。仅当我们打算把字符数组传递给一个需要以空字符结尾的字符串作为参数的函数时,才需要为空字符预留空间(并实际在数组中存储空字符)。

如果只对单个的字符进行处理,就需要空字符。例如,字符数组可能用于从一个字符集到另一个字符集的翻译:

1
char translation_table[128];

对这个数组唯一可以执行的操作就是取下标。(translation_table[ch] 中存储的是字符 ch 翻译后的值。)这里不会把 translation_table 看作字符串:它不需要包含空字符,而且我们也不会对它执行任何字符串操作。

问:如果 printf 函数和 scanf 函数需要 char * 类型的变量作为它们的第一个实际参数,那么是否意味着可以用字符串变量代替字面串作为实际参数呢?

答:可以,如下例所示:

1
2
3
4
char fmt[] = "%d\n";
int i;
...
printf(fmt, i);

这种能力为一些有趣的实现提供了可能。例如,把格式串作为输入读取。

问:如果想让 printf 函数输出字符串 str,是否可以如下例所示那样仅仅把 str 用作格式串?

1
printf(str);

答:可以,但是很危险。如果 str 包含字符 %,那么就不会获得预期的结果,因为 printf 函数会把 % 认定为转换说明的开始。

问:read_line 函数如何检测 getchar 函数读入字符是否失败?

答:如果因为错误或到达文件尾而不能读入字符,getchar 函数会返回 int 类型的值 EOF。下面是改进后的 read_line 函数,此函数用来检测 getchar 函数的返回值是否为 EOF。改动部分用粗体标记:

int read_line(char str[], int n)
{
  int ch, i = 0;

  while ((ch = getchar()) != '\n' && ch != EOF)
    if (i < n)
      str[i++] = ch;
  str[i] = '\0';
  return i;
}

问:为什么 strcmp 函数会返回一个小于、等于或大于 0 的数?返回值有什么意义吗?

答:strcmp 函数的返回值可能是源于函数的传统编写方式。看一下 Kernightan 和 Ritchie 的《C 程序设计语言》一书中的写法:

1
2
3
4
5
6
7
8
9
int strcmp(char *s, char *t)
{
  int i;

  for (i = 0; s[i] == t[i]; i++)
    if (s[i] == '\0')
      return 0;
  return s[i] - t[i];
}

函数的返回值是字符串 s 和字符串 t 中第一个“不匹配”字符的差。如果 s 指向的字符串“小于” t 指向的,那么结果为负数。如果 s 指向的字符串“大于” t 指向的,则结果为正数。但是,不能保证 strcmp 函数就是按照这种方法编写的,所以最好不要假设返回值有什么特殊的意义。

问:在尝试编译 strcat 函数中的 while 语句时,我的编译器给出警告消息。哪里出错了?

1
2
while (*p++ = *s2++)
  ;

答:没有错。如果在通常需要用 == 的地方使用了 =,许多编译器都会给出警告,但不是所有的编译器都会这样做。这条警告消息 95% 的情况下是正确的,而且如果留意到它会节约大量的调试时间。可惜的是,此消息在这个特殊的示例中是无效的。我们确实打算使用 =,而不是 ==。为了除去警告,可以按如下方式重写 while 语句:

1
2
while ((*p++ = *s2++) != 0)
  ;

因为 while 语句通常测试 *p++ = *s2++ 是否不为 0,所以这样做没有改变 while 语句的意思。但是警告消息没有了,原因是 while 语句现在测试的是条件,而不是赋值。对于 GCC,在赋值的外层加一对圆括号也可以避免警告消息的出现:

1
2
while ((*p++ = *s2++))
  ;

问:strlen 函数和 strcat 函数是否真的像 C 语言处理字符串常用的方式 中所示的那样编写?

问:strlen 函数和 strcat 函数是否真的像 C 语言处理字符串常用的方式 中所示的那样编写?

答:有可能。但是对编译器供应商来说,用汇编语言代替 C 语言来编写这些函数和许多其他字符串函数是很普遍的做法。字符串函数的处理速度越快越好,因为它们很常用并且必须能处理任意长度的字符串。利用 CPU 可能提供的专门的字符串处理指令,用汇编语言编写的这些函数能够获得很高的效率。

问:为什么 C 标准采用术语“程序参数”而不是“命令行参数”?

答:程序不总是在命令行中运行的。例如,在常见的图形用户界面下,程序是通过点击鼠标来启动的。在这类环境中,虽然可能有给程序传递信息的其他方式,但是没有传统意义上的命令行了。术语“程序参数”适用于这样的环境。

问:是否必须使用 argcargv 作为 main 函数的参数名?

答:不是的。使用 argcargv 作为参数名仅仅是一种习惯,而不是语言本身的要求。

问:我曾见过把 argv 声明为 **argv 而不是 *argv[] 的做法。这是否合法?

答:当然合法。在声明形式参数时,不管 a 的元素类型是什么,*a 的写法和 a[] 的写法总是一样的。

问:我们已经见过如何创建其元素是指向字面串的指针的数组。指针数组是否还有其他应用?

答:有的。虽然到目前为止主要讨论其元素是指向字符串的指针的数组,但这不是指针数组的唯一应用。我们可以同样简单地创建其元素是指向任何数据类型的指针的数组,无论该数据是否以数组的形式组织。指针数组与动态存储分配一起使用是特别有用的。

请参阅

(完)

comments powered by Disqus