C现代方法(第13章)笔记——字符串

文章目录

  • 第13章 字符串
    • 13.1 字面串
      • 13.1.1 字面串的转义序列
      • 13.1.2 延续字面串
      • 13.1.3 如何存储字面串
      • 13.1.4 字面串的操作
      • 13.1.5 字面串与字符常量
    • 13.2 字符串变量
      • 13.2.1 初始化字符串变量
      • 13.2.2 字符数组与字符指针(※重点※)
    • 13.3 字符串的读和写
      • 13.3.1 用printf函数和puts函数写字符串
      • 13.3.2 用scanf函数读字符串
      • 13.3.3 逐个字符读字符串
    • 13.4 访问字符串中的字符
    • 13.5 使用C语言的字符串库
      • 13.5.1 strcpy函数
      • 13.5.2 strlen函数
      • 13.5.3 strcat函数
      • 13.5.4 strcmp函数
      • 13.5.5 程序——显示一个月的提醒列表
    • 13.6 字符串惯用法(※重点※)
      • 13.6.1 搜索字符串的结尾
      • 13.6.2 复制字符串
    • 13.7 字符串数组
      • 13.7.1 命令行参数
      • 13.7.2 程序——核对行星的名字
    • 问与答
    • 写在最后

第13章 字符串

——字符串总是“板着一副面孔”,但它是我们唯一能指望的交流纽带。

前几章虽然使用过char类型变量和char类型数组,但我们始终没有谈到处理字符序列(C语言的术语是字符串)的便捷方法。本章就来补上这一课,并将介绍字符串常量(在C标准中称为字面串)和字符串变量。其中,字符串变量可以在程序运行过程中发生改变。

13.1节介绍有关字面串的规则,包括如何在字面串中嵌入转义序列,以及如何分割较长的字面串。13.2节讲解声明字符串变量的方法,字符串变量其实就是字符数组,不过末尾要加上一个特殊的空字符来标示字符串的末尾。13.3节描述了读/写字符串的方法。13.4节讨论用来处理字符串的函数的编写方法。13.5节涵盖了一些C语言函数库中处理字符串的函数。13.6节介绍处理字符串时经常会采用的惯用法。13.7节描述如何创建这样的数组:其元素是指向不同长度字符串的指针。这一节还会说明C语言如何使用这种数组为程序提供命令行支持。

13.1 字面串

字面串(string literal)是用一对双引号括起来的字符序列:

"When you come to a fork in the road, take it." 

我们是在第2章中首次遇到字面串的。字面串常常作为格式串出现在printf函数和scanf函数的调用中。在标准中,字面串和字符串是彼此紧密相关但又不同的

  • 字面串是源文件的组成部分,是程序中的一串用引号围起来的文本,仅仅是一个字面意义上的字符串,所以叫字面串。
  • 字面串经程序编译后生成字符串,而字符串是指位于系统存储器里的、以空字符终止的字符序列。

13.1.1 字面串的转义序列

字面串可以像字符常量一样包含转义序列(7.3节)。我们在printf函数和scanf函数的格式串中已经使用过转义字符。例如,字符串

"Candy\nIs dandy\nBut liquor\nIs quicker.\n  --Ogden Nash\n" 

中每一个字符\n都会导致光标移到下一行:

Candy 
Is dandy 
But liquor 
Is quicker. 
  --Ogden Nash

虽然字面串中的八进制数和十六进制数的转义序列也是合法的,但是它们不像字符转义序列那样常见。

请注意!!在字面串中小心使用八进制数十六进制数的转义序列。

  • 八进制数的转义序列在3个数字之后结束,或者在第一个非八进制数字符处结束。例如,字符串"\1234"包含两个字符(\1234),而字符串"\189"包含3个字符(\189)。

  • 而十六进制数的转义序列则不限于3个数字,而是直到第一个非十六进制数字符截止。思考一下,如果字符串包含转义序列\xfc,那么会出现什么情况。\xfc代表Latin1字符集中的字符üLatin1ASCII的常见扩展。字符串"Z\xfcrich"(“Zürich”)6个字符(Z\xfcrich),但是字符串"\xfcber"(不是“über”)只有2个字符(\xfcber)。大部分编译器会拒绝接收后面那种字符串,因为十六进制数的转义序列范围通常限制在\x0~\xff

13.1.2 延续字面串

如果发现字面串太长而无法放置在单独一行以内,只要把第一行用字符\结尾,那么C语言就允许在下一行延续字面串。除了(看不到的)末尾的换行符,在同一行不可以有其他字符跟在\后面:

printf("When you come to a fork in the road, take it. \ 
--Yogi Berra");  

一般说来,字符\可以用来把两行或更多行的代码连接成一行[在C标准中这一过程称为“拼接(splicing)”]。

使用\有一个缺陷:字面串必须从下一行的起始位置继续。因此,这就破坏了程序的缩进结构。根据下面的规则,处理长字面串有一种更好的方法:当两条或更多条字面串相邻时(仅用空白字符分割),编译器会把它们合并成一条字符串。这条规则允许把字符串分割放在两行或者更多行中:

printf("When you come to a fork in the road, take it. "  
       "--Yogi Berra");

13.1.3 如何存储字面串

我们经常在printf函数调用和scanf函数调用中用到字面串。但是,当调用printf函数并且用字面串作为参数时,究竟传递了什么呢?为了回答这个问题,需要明白字面串是如何存储的。

就本质而言,C语言把字面串作为字符数组来处理。当C语言编译器在程序中遇到长度为n的字面串时,它会为字面串分配长度为n+1的内存空间。这块内存空间将用来存储字面串中的字符,以及一个用来标志字符串末尾的额外字符(空字符)。空字符是一个所有位都为0的字节,因此用转义序列\0来表示。

请注意!!不要混淆空字符('\0')零字符('0')。空字符的码值为0,而零字符则有不同的码值(ASCII中为48)。

例如,字面串"abc"是作为有4个字符的数组来存储的(abc\0)。字面串可以为空。字符串""作为单独一个空字符来存储。

既然字面串是作为数组来存储的,那么编译器会把它看作char *类型的指针。例如,printf函数和scanf函数都接收char *类型的值作为它们的第一个参数。思考下面的例子:

printf("abc");

当调用printf函数时,会传递"abc"的地址(即指向存储字母a的内存单元的指针)。

13.1.4 字面串的操作

通常情况下可以在任何C语言允许使用char *指针的地方使用字面串。例如,字面串可以出现在赋值运算符的右边:

char *p;
p = "abc"

这个赋值操作不是复制"abc"中的字符,而是使p指向字符串的第一个字符。

C语言允许对指针取下标,因此可以对字面串取下标:

char ch; 

ch = "abc"[1]; 

ch的新值将是字母b。其他可能的下标是0(这将选择字母a)、2(字母c)和3(空字符)。字面串的这种特性并不常用,但有时也比较方便。思考下面的函数,这个函数把0~15的数转换成等价的十六进制的字符形式:

char digit_to_hex_char(int digit) 
{ 
    return "0123456789ABCDEF"[digit]; 
}

请注意!!试图改变字面串会导致未定义的行为:

char *p = "abc"; 

*p = 'd';   /*** WRONG ***/
//改变字面串可能会导致程序崩溃或运行不稳定。

13.1.5 字面串与字符常量

只包含一个字符的字面串不同于字符常量。字面串"a"是用指针来表示的,这个指针指向存放字符"a"(后面紧跟空字符)的内存单元。字符常量'a'是用整数(字符集的数值码)来表示的。

请注意!!不要在需要字符串的时候使用字符(反之亦然)。函数调用

printf("\n");  /*** right ***/

//因为printf函数期望指针作为它的第一个参数。然而,下面的调用却是非法的:

printf('\n');    /*** WRONG ***/

13.2 字符串变量

一些编程语言为声明字符串变量提供了专门的string类型。

C语言采取了不同的方式:只要保证字符串是以空字符结尾的,任何一维的字符数组都可以用来存储字符串。这种方法很简单,但使用起来有很大难度。有时很难辨别是否把字符数组作为字符串来使用。如果编写自己的字符串处理函数,请千万注意要正确地处理空字符。而且,要确定字符串长度没有比逐个字符地搜索空字符更快捷的方法了

假设需要用一个变量来存储最多80个字符的字符串。由于字符串在末尾处需要有空字符,我们把变量声明为含有81个字符的数组:

#define STR_LEN 80 
...  
char str[STR_LEN+1];    

这里把STR_LEN定义为80而不是81,强调的是str可以存储最多80个字符的字符串,然后才在str的声明中对STR_LEN1。这是C程序员常用的方式。

请注意!!当声明用于存放字符串的字符数组时,要始终保证数组的长度比字符串的长度多一个字符。这是因为C语言规定每个字符串都要以空字符结尾。如果没有给空字符预留位置,可能会导致程序运行时出现不可预知的结果,因为C函数库中的函数假设字符串都是以空字符结束的。

声明长度为STR_LEN+1的字符数组并不意味着它总是用于存放长度为STR_LEN的字符串。字符串的长度取决于空字符的位置,而不是取决于用于存放字符串的字符数组的长度。有STR_LEN+1个字符的数组可以存放多种长度的字符串,范围是从空字符串到长度为STR_LEN的字符串。

13.2.1 初始化字符串变量

字符串变量可以在声明时进行初始化:

char date1[8] = "June 14";

编译器将把字符串"June 14"中的字符复制到数组date1中,然后追加一个空字符从而使date1可以作为字符串使用。

"June 14"是一个字面串初始化器,用来初始化数组。实际上,我们可以写成:

char date1[8] = {'J', 'u', 'n', 'e', ' ', '1', '4', '\0'};

如果初始化器太短以致不能填满字符串变量,会如何呢?在这种情况下,编译器会添加空字符。因此,在声明

char date2[9] = "June 14";

之后,date2将如下所示:

0 1 2 3 4 5 6 7 8
J u n e 1 4 \0 \0

大体上来说,这种行为与C语言处理数组初始化器(8.1节)的方法一致。当数组的初始化器比数组本身短时,余下的数组元素会被初始化为0。在把字符数组额外的元素初始化为\0这点上,编译器对字符串和数组遵循相同的规则。

如果初始化器比字符串变量长又会怎样呢?这对字符串而言是非法的,就如同对数组而言是非法的一样。然而,C语言允许初始化器(不包括空字符)与变量有完全相同的长度

char date3[7] = "June 14";

因为没有给空字符留空间,所以编译器不会试图存储空字符:

0 1 2 3 4 5 6
J u n e 1 4

请注意!!如果正在计划对用来放置字符串的字符数组进行初始化,一定要确保数组的长度长于初始化器的长度,否则编译器将忽略空字符,这将使得数组无法作为字符串使用!!。

字符串变量的声明中可以省略它的长度。这种情况下,编译器会自动计算长度:

char date4[] = "June 14";

编译器为date4分配8个字符的空间,这足够存储"June 14"中的字符和一个空字符。(不指定date4的长度并不意味着以后可以改变数组的长度。一旦编译了程序,date4的长度就固定是8了。)如果初始化器很长,那么省略字符串变量的长度是特别有效的,因为人工计算长度很容易出错。

13.2.2 字符数组与字符指针(※重点※)

一起来比较下面这两个看起来很相似的声明:

char date[] = "June 14"; //字符数组
char *date = "June 14";  //字符指针

前者声明date是一个数组,后者声明date是一个指针。正因为有了数组和指针之间的紧密关系,才使上面这两个声明中的date都可以用作字符串。具体来说,任何期望传递字符数组或字符指针的函数都能够接收这两种声明的date作为参数。

然而,需要注意!!两者之间有很大的差异,不能错误地认为上面这两种date可以互换:

  • 数组字符可以修改,而指针指向字面串不能修改。在声明为数组时,就像任意数组元素一样,可以修改存储在date中的字符。在声明为指针时,date指向字面串,在13.1节我们已经看到字面串是不可以修改的
  • 一个是数组名,一个是变量名。在声明为数组时,date是数组名。在声明为指针时,date是变量,这个变量可以在程序执行期间指向其他字符串。

如果希望可以修改字符串,那么就要建立字符数组来存储字符串,仅仅声明指针变量是不够的

下面的声明使编译器为指针变量分配了足够的内存空间:

char *p;

可惜的是,它不能为字符串分配空间。(怎么会这样呢?因为我们没有指明字符串的长度。)在使用p作为字符串之前,必须把p指向字符数组。一种可能是把p指向已经存在的字符串变量:

char str[STR_LEN+1], *p; 
p = str;

现在p指向了str的第一个字符,所以可以把p作为字符串使用了。

另一种可能是让p指向一个动态分配的字符串17.2节)。

请注意!!使用未初始化的指针变量作为字符串是非常严重的错误。考虑下面的例子,它试图创建字符串"abc"

char *p; 

p[0] = 'a';   /*** WRONG ***/ 
p[1] = 'b';   /*** WRONG ***/ 
p[2] = 'c';   /*** WRONG ***/ 
p[3] = '\0';  /*** WRONG ***/ 

因为p没有被初始化,所以我们不知道它指向哪里。用指针p把字符a、b、c\0写入内存会导致未定义的行为。

13.3 字符串的读和写

使用printf函数或puts函数来写字符串是很容易的读字符串却有点麻烦,主要是因为输入的字符串可能比用来存储它的字符串变量长。为了一次性读入字符串,可以使用scanf函数,也可以每次读入一个字符。

13.3.1 用printf函数和puts函数写字符串

转换说明%s允许printf函数写字符串。考虑下面的例子:

char str[] = "Are we having fun yet?"; 
printf("%s\n", str);

输出会是

Are we having fun yet? 

printf函数会逐个写字符串中的字符,直到遇到空字符才停止。(如果空字符丢失,printf函数会越过字符串的末尾继续写,直到最终在内存的某个地方找到空字符为止。)

如果只想显示字符串的一部分,可以使用转换说明%.ps,这里p是要显示的字符数量。语句

printf("%.6s\n", str); //显示 Are we

字符串跟数一样,可以在指定的栏内显示。转换说明%ms会在大小为m的栏内显示字符串。(对于超过m个字符的字符串,printf函数会显示出整个字符串,而不会截断。)如果字符串少于m个字符,则会在栏内右对齐输出。如果要强制左对齐,可以在m前加一个减号。m值和p值可以组合使用:转换说明%m.ps会使字符串的前p个字符在大小为m的栏内显示。

printf函数不是唯一一个字符串输出函数。C函数库还提供了puts函数,此函数可以按如下方式使用:

puts(str);

puts函数只有一个参数,即需要显示的字符串。在写完字符串后,puts函数总会添加一个额外的换行符,从而前进到下一个输出行的开始处。

13.3.2 用scanf函数读字符串

转换说明%s允许scanf函数把字符串读入字符数组:

scanf("%s", str);

scanf函数调用中,不需要在str前添加运算符&,因为str是数组名,编译器在把它传递给函数时会把它当作指针来处理。

调用时,scanf函数会跳过空白字符(3.2节),然后读入字符并存储到str中,直到遇到空白字符为止。scanf函数始终会在字符串末尾存储一个空字符。

scanf函数读入字符串永远不会包含空白字符。因此,scanf函数通常不会读入一整行输入。换行符会使scanf函数停止读入,空格符或制表符也会产生同样的结果

冷门小知识(书外补充):为了让scanf一次读入一整行输入,可以这样做:

char buffer[100];
scanf("%99[^\n]", buffer);
  • %[^\n]:这部分告诉scanf读取字符序列,直到遇到换行符为止。%[^...]是一个字符集的扫描集合,其中^表示不包含的字符,而\n是要排除的字符,因此%[^\n]表示读取除换行符之外的所有字符。
  • 99:这表示scanf可以读取的最大字符数(包括终止的空字符\0)。在上面的例子中,最多可以读取99个字符,如果输入的行超过这个长度,scanf会将其截断,这是为了防止缓冲区溢出。

除了上面介绍的scanf隐藏用法,历史上我们一直使用gets函数读入一整行输入,但是由于安全方面的原因(容易导致缓冲区溢出),从C11开始已经将它废除。因为它毕竟存在过,所以还是应该加以介绍。

类似于scanf函数,gets函数把读入的字符放到数组中,然后存储一个空字符。然而,在其他方面gets函数有些不同于scanf函数。

  • gets函数不会在开始读字符串之前跳过空白字符(scanf函数会跳过)。
  • gets函数会持续读入,直到找到换行符才停止(scanf函数会在任意空白字符处停止)。此外,gets函数会忽略换行符,不会把它存储到数组中,并用空字符代替换行符

为了领会scanf函数与gets函数之间的差异,考虑下面的程序片段:

char sentence[SENT_LEN+1]; 

printf("Enter a sentence: \n"); 
scanf("%s", sentence);

假定用户在提示信息Enter a sentence: 的后面输入信息

Enter a sentence: 
To C, or not to C: that is the question. 

scanf函数会把字符串"To"存储到sentence中。下一次scanf函数调用将从单词To后面的空格处继续读入这行。

现在假设用gets函数替换scanf函数:

char sentence[SENT_LEN+1]; 

printf("Enter a sentence: \n");
gets(sentence);   //替换scanf函数

当用户输入和先前相同的信息时,gets函数会把字符串"To C, or not to C: that is the question."一整句存储到sentence中。

请注意!!在把字符读入数组时,scanf函数和gets函数都无法检测数组何时被填满。因此,它们存储字符时可能越过数组的边界,这会导致未定义的行为。通过用转换说明%ns代替%s可以使scanf函数更安全。这里的数字n指出可以存储的最多字符数。可惜的是,gets函数天生就是不安全的,所以新标准将它废除。相比之下,fgets函数(22.5节)则是一种好得多的选择。

13.3.3 逐个字符读字符串

因为对许多程序而言,scanf函数和gets函数都有风险且不够灵活,C程序员经常会自己编写输入函数。通过每次一个字符的方式来读入字符串,这类函数可以提供比标准输入函数更大程度的控制。

如果决定设计自己的输入函数,那么就需要考虑下面这些问题:

  • 在开始存储字符串之前,函数应该跳过空白字符吗?
  • 什么字符会导致函数停止读取:换行符、任意空白字符还是其他某种字符?需要存储这类字符还是将其忽略?
  • 如果输入的字符串太长以致无法存储,那么函数应该做些什么:忽略额外的字符还是把它们留给下一次输入操作?

假定我们所需要的函数不会跳过空白字符,在第一个换行符(不存储到字符串中)处停止读取,并且忽略额外的字符。函数将有如下原型:

int read_line(char str[], int n); 

str表示用来存储输入的数组,而n是读入字符的最大数量。如果输入行包含多于n个的字符,read_line函数将忽略多余的字符。read_line函数会返回实际存储在str中的字符数量(0~n范围内的任意数)。我们不可能总是需要read_line函数的返回值,但是有这个返回值也没问题。

read_line函数主要由一个循环构成。只要str中还有空间,此循环就会调用getchar函数(7.3节)逐个读入字符并把它们存储到str中。在读入换行符时循环终止。(严格地说,如果getchar函数读入字符失败,也应该终止循环,但是这里暂时忽略这种复杂情况。)下面是read_line函数的完整定义:

int read_line(char str[],  int n) 
{  
    int ch, i = 0; 
  
    while ((ch = getchar()) != '\n'){
        if (i < n) 
            str[i++] = ch;
    }
    str[i] = '\0';        /* terminates string */ 
    return i;             /* number of characters stored */ 
}

注意,ch的类型为int而不是char,这是因为getchar把它读取的字符作为int类型的值返回。

返回之前,read_line函数在字符串的末尾放置一个空字符。scanfgets等标准函数会自动在输入字符串的末尾放置一个空字符;然而,如果要自己写输入函数,必须人工加上空字符。

13.4 访问字符串中的字符

字符串是以数组的方式存储的,因此可以使用下标来访问字符串中的字符。例如,为了对字符串s中的每个字符进行处理,可以设定一个循环来对计数器i进行自增操作,并通过表达式s[i]来选择字符。

假定需要一个函数来统计字符串中空格的数量。利用数组取下标操作可以写出如下函数:

int count_spaces(const char s[]) 
{ 
    int count = 0, i; 
  
    for (i = 0; s[i] != '\0';  i++) 
        if (s[i] == ' ') 
            count++; 
    return count;
}

s的声明中加上const表明count_spaces函数不会改变数组。如果s不是字符串,count_spaces将需要第2个参数来指明数组的长度。然而,因为s是字符串,所以count_spaces可以通过测试空字符来定位s的末尾。


许多C程序员不会像例子中那样编写count_spaces函数,他们更愿意使用指针来跟踪字符串中的当前位置。就像在12.2节中见到的那样,这种方法对于处理数组来说一直有效,但在处理字符串方面尤其方便。

下面用指针算术运算代替数组取下标来重新编写count_spaces函数。这次不再需要变量i,而是利用s自身来跟踪字符串中的位置。通过对s反复进行自增操作,count_spaces函数可以逐个访问字符串中的字符。下面是count_spaces函数的新版本:

 
int count_spaces(const char *s) 
{ 
    int count = 0; 
  
    for (; *s != '\0'; s++) 
        if (*s == ' ') 
            count++; 
    return count; 
}

注意!!const没有阻止count_spaces函数对s的修改,它的作用是阻止函数改变s所指向的字符。而且,因为s是传递给count_spaces函数的指针的副本,所以对s进行自增操作不会影响原始的指针。

count_spaces函数示例引出了一些关于如何编写字符串函数的问题:

  • 用数组操作或指针操作访问字符串中的字符,哪种方法更好一些呢?只要使用方便,可以使用任意一种方法,甚至可以混合使用两种方法。在count_spaces函数的第2种写法中,不再需要变量i,而是把s作为指针来对函数进行一些简化。从传统意义上来说,C程序员更倾向于使用指针操作来处理字符串。
  • 字符串形式参数应该声明为数组还是指针呢count_spaces函数的两种写法说明了这两种选择:第1种写法把s声明为数组,第2种写法把s声明为指针。实际上,这两种声明之间没有任何差异。回顾12.3节的内容就知道,编译器会把数组型的形式参数视为指针
  • 形式参数的形式(s[]或者*s)是否会对实际参数产生影响呢?不会的。当调用count_spaces函数时,实际参数可以是数组名、指针变量或者字面串。count_spaces函数无法说明差异。

13.5 使用C语言的字符串库

一些编程语言提供的运算符可以对字符串进行复制、比较、拼接、选择子串等操作,但C语言的运算符根本无法操作字符串。在C语言中把字符串当作数组来处理,因此对字符串的限制方式和对数组的一样,特别是它们都不能用C语言的运算符进行复制和比较操作。

请注意!!直接复制或比较字符串会失败。例如,假定str1str2有如下声明:

char str1[10], str2[10]; 

利用=运算符来把字符串复制到字符数组中是不可能的:

str1 = "abc";     /*** WRONG ***/ 
str2 = str1;      /*** WRONG ***/
//我的理解是,当程序走到这里时,str1和“abc”都在内存中开辟了自己的空间,如果这时
//让str1指向“abc”第一个字符,那它原来开辟的空间就内存泄漏了,所以这是非法的。

12.3节可知,把数组名用作=的左操作数是非法的。但是,使用=初始化字符数组是合法的

char str1[10] = "abc"; //在声明中,=不是赋值运算符

试图使用关系运算符或判等运算符来比较字符串是合法的,但不会产生预期的结果:

if (str1 == str2) ...    /*** WRONG ***/

这条语句把str1str2作为指针来进行比较,而不是比较两个数组的内容。因为str1str2有不同的地址,所以表达式str1 == str2的值一定为0

幸运的是,字符串的所有操作功能都没有丢失:C语言的函数库为完成对字符串的操作提供了丰富的函数集。这些函数的原型驻留在头(23.6节)中,所以需要字符串操作的程序应该包含下列内容:

#include 

中声明的每个函数至少需要一个字符串作为实际参数。字符串形式参数声明为char *类型,这使得实际参数可以是字符数组、char *类型的变量或者字面串——上述这些都适合作为字符串。然而,要注意那些没有声明为const的字符串形式参数。这些形式参数可能会在调用函数时发生改变,所以对应的实际参数不应该是字面串。

中有许多函数,这里将介绍几种最基本的。在后续的例子中,假设str1str2都是用作字符串的字符数组

13.5.1 strcpy函数

strcpy(字符串复制)函数在中的原型如下:

char *strcpy(char *s1, const char *s2);

strcpy函数把字符串s2复制给字符串s1。(准确地讲,应该说是“strcpy函数把s2指向的字符串复制到s1指向的数组中”。)也就是说,strcpy函数把s2中的字符复制到s1中,直到遇到s2中的第一个空字符为止(该空字符也需要复制)。strcpy函数返回s1(即指向目标字符串的指针)。这一过程不会改变s2指向的字符串,因此将其声明为const

strcpy函数的存在弥补了不能使用赋值运算符复制字符串的不足。例如,假设我们想把字符串"abcd"存储到str2中,不能使用下面的赋值:

str2 = "abcd";   /*** WRONG ***/ 

这是因为str2是数组名,不能出现在赋值运算的左侧。但是,这时可以调用strcpy函数:

strcpy(str2, "abcd");    /* str2 now contains "abcd" */ 

类似地,不能直接把str2赋值给str1,但是可以调用strcpy

strcpy(str1, str2);     /* str1 now contains "abcd" */ 

大多数情况下我们会忽略strcpy函数的返回值,但有时候strcpy函数调用是一个更大的表达式的一部分,这时其返回值就比较有用了。例如,可以把一系列strcpy函数调用连起来:

strcpy(str1, strcpy(str2, "abcd"));    
/* both str1 and str2 now contain "abcd" */ 

请注意!!在strcpy(str1, str2)的调用中,strcpy函数无法检查str2指向的字符串的大小是否真的适合str1指向的数组

假设str1指向的字符串长度为n,如果str2指向的字符串中的字符数不超过n-1,那么复制操作可以完成。但是,如果str2指向更长的字符串,那么结果就无法预料了。(因为strcpy函数会一直复制到第一个空字符为止,所以它会越过str1指向的数组的边界继续复制。)


尽管执行会慢一点,但是调用strncpy函数(23.6节)仍是一种更安全的复制字符串的方法。strncpy类似于strcpy,但它还有第三个参数可以用于限制所复制的字符数(包括空字符)。为了将str2复制到str1,可以使用如下的strncpy调用:

strncpy(str1, str2, sizeof(str1)); 

只要str1足够装下存储在str2中的字符串(包括空字符),复制就能正确完成。当然,strncpy本身也不是没有风险。如果str2中存储的字符串的长度大于str1数组的长度,strncpy会导致str1中的字符串没有终止的空字符。下面是一种更安全的用法:

strncpy(str1, str2, sizeof(str1) - 1); 
str1[sizeof(str1)-1] = '\0'; 

第二条语句确保str1总是以空字符结束,即使strncpy没能从str2中复制到空字符。

13.5.2 strlen函数

strlen(求字符串长度)函数的原型如下:

size_t strlen (const char *s); 

定义在C函数库中的size_t类型(7.6节)是一个typedef名字,表示C语言中的一种无符号整型。除非处理极长的字符串,否则不需要关心其技术细节。我们可以简单地把strlen的返回值作为整数处理

strlen函数返回字符串s的长度:s中第一个空字符之前的字符个数(不包括空字符)。下面是几个示例:

int len; 

len = strlen("abc");      /* len is now 3 */ 
len = strlen("");          /* len is now 0 */ 
strcpy(strl, "abc"); 
len = strlen(strl);       /* len is now 3 */ 

最后一个例子说明了很重要的一点:当用数组作为实际参数时,strlen不会测量数组本身的长度,而是返回存储在数组中的字符串的长度

13.5.3 strcat函数

strcat(字符串拼接)函数的原型如下:

char *strcat(char *s1, const char *s2);

strcat函数把字符串s2的内容追加到字符串s1的末尾,并且返回字符串s1(指向结果字符串的指针)。

下面列举了一些使用strcat函数的例子:

strcpy(str1, "abc"); 
strcat(str1, "def");   /* str1 now contains "abcdef" */ 
strcpy(str1, "abc"); 
strcpy(str2, "def"); 
strcat(str1, str2);    /* str1 now contains "abcdef" */ 

同使用strcpy函数一样,通常忽略strcat函数的返回值。下面的例子说明了可能使用返回值的方法:

strcpy(str1, "abc"); 
strcpy(str2, "def"); 
strcat(str1, strcat(str2, "ghi")); 
/* str1 now contains "abcdefghi"; str2 contains "defghi"  */ 

请注意!!如果str1指向的数组没有大到足以容纳str2指向的字符串中的字符,那么调用strcat(str1, str2)的结果将是不可预测的。考虑下面的例子:

char str1[6] = "abc"; 
strcat(str1, "def");    /*** WRONG ***/ 

strcat函数会试图把字符def\0添加到str1中已存储的字符串的末尾。不幸的是,str1仅限6个字符,这导致strcat函数写到了数组末尾的后面。

strncat函数(23.6节)函数比strcat更安全,但速度也慢一些。与strncpy一样,它有第三个参数来限制所复制的字符数。下面是调用的形式:

strncat(str1, str2, sizeof(str1)strlen(str1) - 1) ; 

strncat函数会在遇到空字符时终止str1,第三个参数(待复制的字符数)没有考虑该空字符。在上面的例子中,第三个参数计算str1中的剩余空间(由表达式sizeof(str1) – strlen(str1)给出),然后减去1以确保为空字符留下空间。

13.5.4 strcmp函数

strcmp(字符串比较)函数的原型如下:

int strcmp(const char *s1, const char *s2);

strcmp函数比较字符串s1和字符串s2,然后根据s1是小于、等于或大于s2函数返回一个小于、等于或大于0的值。例如,为了检查str1是否小于str2,可以写

if (strcmp(str1, str2) < 0)    /* is str1 < str2? */ 
    ... 

为了检查str1是否小于或等于str2,可以写

if (strcmp(str1, str2) <= 0)   /* is str1 <= str2? */ 
    ... 

通过选择适当的关系运算符(<、<=、>、>=)判等运算符(==、!=),可以测试str1str2之间任何可能的关系。

类似于字典中单词的编排方式,strcmp函数利用字典顺序进行字符串比较。更精确地说,只要满足下列两个条件之一,那么strcmp函数就认为s1是小于s2的:

  • s1s2的前i个字符一致,但是s1的第i+1个字符小于s2的第i+1个字符。例如,"abc"小于"bcd""abd"小于"abe"
  • s1的所有字符与s2的字符一致,但是s1s2短。例如,"abc"小于"abcd"

当比较两个字符串中的字符时,strcmp函数会查看字符对应的数值码。一些底层字符集的知识可以帮助预测strcmp函数的结果。例如,下面是ASCII字符集的一些重要性质。

  • A~Za~z0~9这几组字符的数值码是连续的。
  • 所有的大写字母都小于小写字母。(在ASCII码中,65~90的编码表示大写字母97~122的编码表示小写字母。)
  • 数字小于字母。(48~57的编码表示数字。)
  • 空格符小于所有打印字符。(ASCII码中空格符的值是32。)

13.5.5 程序——显示一个月的提醒列表

为了说明C语言字符串函数库的用法,现在来看一个程序。这个程序会显示一个月的每日提醒列表。用户需要输入一系列提醒,每条提醒都要有一个前缀来说明是一个月中的哪一天。当用户输入的是0而不是有效的日期时,程序会显示出输入的全部提醒的列表,并按日期排序。
下面是与程序的会话示例:

Enter day and reminder:  24 Susan’s birthday 
Enter day and reminder:  5 6:00 - Dinner with Marge and Russ 
Enter day and reminder:  26 Movie - "Chinatown" 
Enter day and reminder:  7 10:30 - Dental appointment 
Enter day and reminder:  12 Movie - "Dazed and Confused" 
Enter day and reminder:  5 Saturday class 
Enter day and reminder:  12 Saturday class 
Enter day and reminder:  0 
Day Reminder 
  5 Saturday class 
  5 6:00 - Dinner with Marge and Russ
  7 10:30 - Dental appointment 
 12 Saturday class 
 12 Movie - "Dazed and Confused" 
 24 Susan’s birthday 
 26 Movie - "Chinatown"

总体策略不是很复杂:程序需要读入一系列日期和提醒的组合,并且按照顺序进行存储(按日期排序),然后显示出来。为了读入日期,会用到scanf函数。为了读入提醒,会用到13.3节介绍的read_line函数。

把字符串存储在二维的字符数组中,数组的每一行包含一个字符串。在程序读入日期以及相关的提醒后,通过使用strcmp函数进行比较来查找数组从而确定这一天所在的位置。然后,程序会使用strcpy函数把此位置之后的所有字符串往后移动一个位置。最后,程序会把这一天复制到数组中,并且调用strcat函数来把提醒附加到这一天后面。(日期和提醒在此之前是分开存放的。)

当然,总会有少量略微复杂的地方。例如,希望日期在两个字符的栏中右对齐以便它们的个位可以对齐。有很多种方法可以解决这个问题。这里选择用scanf函数把日期读入到整型变量中,然后调用sprintf函数(22.8节)把日期转换成字符串格式。sprintf是个类似于printf的库函数,不同之处在于它会把输出写到字符串中。函数调用

sprintf(day_str, "%2d", day);

day的值写到day_str中。因为sprintf在写完后会自动添加一个空字符,所以day_str会包含一个由空字符结尾的字符串。

另一个复杂的地方是确保用户没有输入两位以上的数字,为此将使用下列scanf函数调用:

scanf("%2d", &day);

即使输入有更多的数字,在%d之间的数2也会通知scanf函数在读入两个数字后停止。


解决了上述细节问题之后,程序编写如下:

/*
remind.c
--Prints a one-month reminder list 
*/
#include  
#include  

#define MAX_REMIND 50 /* maximum number of reminders */ 
#define MSG_LEN 60 /* max length of reminder message */ 

int read_line(char str[], int n); 

int main(void) 
{ 
    char reminders[MAX_REMIND][MSG_LEN+3]; 
    char day_str[3], msg_str[MSG_LEN+1]; 
    int day, i, j, num_remind = 0; 

    for (;;) { 
        if (num_remind == MAX_REMIND) { 
            printf("-- No space left --\n"); 
            break; 
        } 
        printf("Enter day and reminder: "); 
        scanf("%2d", &day); 
        if (day == 0) 
            break; 
        sprintf(day_str, "%2d", day); 
        read_line(msg_str, MSG_LEN); 
    
        for (i = 0; i < num_remind; i++) 
            if (strcmp(day_str, reminders[i]) < 0) 
                break; 
        for (j = num_remind; j > i; j--) 
            strcpy(reminders[j], reminders[j-1]); 
    
        strcpy(reminders[i], day_str); 
        strcat(reminders[i], msg_str); 
    
        num_remind++; 
    } 
 
    printf("\nDay Reminder\n"); 
    for (i = 0; i < num_remind; i++) 
        printf(" %s\n", reminders[i]); 
    
    return 0; 
} 

int read_line(char str[], int n) 
{ 
    int ch, i = 0; 
    
    while ((ch = getchar()) != '\n') 
        if (i < n) 
            str[i++] = ch; 
    str[i] = '\0'; 
    return i; 
}

虽然程序remind.c很好地说明了strcpy函数、strcat函数和strcmp函数,但是作为实际的提醒程序,它还缺少一些东西。该程序显然有许多需要完善的地方,从小调整到大改进都有(例如,当程序终止时把提醒保存到文件中)。本章和后续各章末尾的编程题将讨论一些改进。

13.6 字符串惯用法(※重点※)

处理字符串的函数是特别丰富的惯用法资源。本节将探索几种最著名的惯用法,并利用它们编写strlen函数和strcat函数。当然,我们可能永远都不需要编写这两个函数,因为它们是标准函数库的一部分,但类似的函数还是有可能需要编写的。

本节使用的简洁风格是在许多C程序员中流行的风格。即使不准备在自己的程序中使用,也应该掌握这种风格,因为很可能会在其他程序员编写的程序中遇到。

在开始之前最后再说一点。如果你想在本节尝试自己编写strlenstrcat,请修改函数的名字(比如,把strlen改成my_strlen)。如2.1.1节中解释的那样,我们不可以编写与库函数同名的函数,即使不包含该函数所属的头也不行。事实上,所有以str和一个小写字母开头的名字都是保留的(以便在未来的C标准版本中往头里加入函数)。

13.6.1 搜索字符串的结尾

许多字符串操作需要搜索字符串的结尾。strlen函数就是一个重要的例子。下面的strlen函数搜索字符串参数的结尾,并且使用一个变量来跟踪字符串的长度:

size_t strlen(const char *s) 
{ 
    size_t n; 
    
    for (n = 0; *s != '\0'; s++) 
        n++; 
    return n; 
} 

指针s从左至右扫描整个字符串,变量n记录当前已经扫描的字符数量。当s最终指向一个空字符时,n所包含的值就是字符串的长度。

现在看看是否能精简strlen函数的定义。首先,把n的初始化移到它的声明中:


size_t strlen(const char *s) 
{ 
    size_t n = 0; 
    
    for (; *s != '\0'; s++) 
        n++; 
    return n; 
}

接下来注意到,条件*s != '\0'*s != 0是一样的,因为空字符的整数值就是0。而测试*s != 0与测试*s是一样的,两者都在*s不为0时结果为真。这些发现引出strlen函数的又一个版本:

size_t strlen(const char *s) 
{ 
    size_t n = 0; 
    
    for (; *s; s++) 
        n++; 
    return n; 
} 

然而,就如同在12.2节中见到的那样,在同一个表达式中对s进行自增操作并且测试*s是可行的:


size_t strlen(const char *s) 
{ 
    size_t n = 0; 
    
    for (; *s++;) 
        n++; 
    return n; 
}

while语句替换for语句,可以得到如下版本的strlen函数:

size_t strlen(const char *s) 
{ 
    size_t n = 0; 
    
    while (*s++) 
        n++; 
    return n; 
} 

然前面已经对strlen函数进行了相当大的精简,但是可能仍没有提高它的运行速度。至少对于一些编译器来说下面的版本确实会运行得更快一些:

size_t strlen(const char *s) 
{ 
    const char *p = s; 
    
    while (*s) 
        s++; 
    return s - p; 
} 

这个版本的strlen函数通过定位空字符位置的方式来计算字符串的长度,然后用空字符的地址减去字符串中第一个字符的地址。运行速度的提升得益于不需要在while循环内部对n进行自增操作。请注意,在p的声明中出现了单词const,如果没有它,编译器会注意到把s赋值给p会给s指向的字符串造成一定风险。

//惯用法1
while(*s)
   s++;

//惯用法2
while(*s++)
   ;

上面两种惯用法都是“搜索字符串结尾的空字符”的惯用法。第一个版本最终使s指向了空字符。第二个版本更加简洁,但是最后使s正好指向空字符后面的位置。

13.6.2 复制字符串

复制字符串是另一种常见操作。为了介绍C语言中的“字符串复制”惯用法,这里将开发strcat函数的两个版本。首先从直接但有些冗长的strcat函数写法开始:

char *strcat(char *s1,  const char *s2) 
{ 
    char *p = s1; 
    
    while (*p != '\0') 
        p++; 
    while (*s2 != '\0') { 
        *p = *s2; 
        p++; 
        s2++; 
    } 
    *p = '\0'; 
    return s1; 
}

strcat函数的这种写法采用了两步算法:

  1. 确定字符串s1末尾空字符的位置,并且使指针p指向它;
  2. 把字符串s2中的字符逐个复制到p所指向的位置。

函数中的第一个while语句实现了第1步。程序中先把p设定为指向s1的第一个字符。接着p开始自增直到指向空字符为止。循环终止时,p指向空字符。

第二个while语句实现了第2步。循环体把s2指向的一个字符复制到p指向的地方,接着ps2都进行自增;当s2指向空字符时循环终止。

接下来,程序在p指向的位置放置空字符,然后strcat函数返回。

类似于对strlen函数的处理,也可以简化strcat函数的定义,得到下面的版本:

char *strcat(char *s1, const char *s2) 
{ 
    char *p = s1; 
    
    while (*p) 
        p++; 
    while (*p++ = *s2++) 
        ;      
    return s1; 
}

改进的strcat函数的核心是“字符串复制”的惯用法

//惯用法
while (*p++ = *s2++) 
   ;

如果忽略了两个++运算符,那么圆括号中的表达式会简化为普通的赋值:

*p = *s2

这个表达式把s2指向的字符复制到p所指向的地方。正是由于有了这两个++运算符,赋值之后ps2才进行了自增。重复执行此表达式所产生的效果就是把s2指向的一系列字符复制到p所指向的地方。

但是什么会使循环终止呢?因为圆括号中的主要运算符是赋值运算符,所以while语句会测试赋值表达式的值,也就是测试复制的字符。除空字符以外的所有字符的测试结果都为真,因此,循环只有在复制空字符后才会终止。而且因为循环是在赋值之后终止,所以不需要单独用一条语句来在新字符串的末尾添加空字符。

13.7 字符串数组

现在来看一个在使用字符串时经常遇到的问题:存储字符串数组的最佳方式是什么?最明显的解决方案是创建二维的字符数组,然后按照每行一个字符串的方式把字符串存储到数组中。考虑下面的例子:

char planets[][8] = {"Mercury", "Venus", "Earth", 
                     "Mars", "Jupiter", "Saturn", 
                     "Uranus", "Neptune", "Pluto"}; 
/*
注意!!虽然允许省略planets数组的行数(因为这个数很容易从初始化器中元素的数量求出),但是C语言要求指明列数。
*/

并非所有的字符串都足以填满数组的一整行,所以C语言用空字符来填补。

因为大部分字符串集是长字符串和短字符串的混合,所以这些例子所暴露的低效性是在处理字符串时经常遇到的问题。我们需要的是参差不齐的数组(ragged array),即每一行有不同长度的二维数组。C语言本身不提供这种“参差不齐的数组类型”,但它提供了模拟这种数组类型的工具。秘诀就是建立一个特殊的数组,这个数组的元素都是指向字符串的指针

下面是planets数组的另外一种写法,这次把它看作元素是指向字符串的指针的数组:

char *planets[] = {"Mercury", "Venus", "Earth", 
                   "Mars", "Jupiter", "Saturn", 
                   "Uranus", "Neptune", "Pluto"}; 

planets的每一个元素都是指向以空字符结尾的字符串的指针。虽然必须为planets数组中的指针分配空间,但是字符串中不再有任何浪费的字符。

为了访问其中一个行星的名字,只需要对planets数组取下标。由于指针和数组之间的紧密关系,访问行星名字中的字符的方式和访问二维数组元素的方式相同。例如,为了在planets数组中搜寻以字母M开头的字符串,可以使用下面的循环:

for (i = 0; i < 9; i++) 
    if (planets[i][0] == 'M') 
        printf("%s begins with M\n", planets[i]); 

13.7.1 命令行参数

命令行信息不仅对操作系统命令可用,而且它对所有程序都是可用的。为了能够访问这些命令行参数(C标准中称为程序参数),必须把main函数定义为含有两个参数的函数,这两个参数通常命名为argcargv

int main(int argc, char *argv[]) 
{ 
    ... 
}

argc(“参数计数”)是命令行参数的数量(包括程序名本身),argv(“参数向量”)是指向命令行参数的指针数组,这些命令行参数以字符串的形式存储。argv[0]指向程序名,而从argv[1]argv[argc-1]则指向余下的命令行参数。

argv有一个附加元素,即argv[argc],这个元素始终是一个空指针。空指针是一种不指向任何地方的特殊指针。后面会讨论空指针17.1节),目前只需要知道宏NULL代表空指针就够了。

如果用户输入命令行:

ls -l remind.c

那么argc将为3argv[0]将指向含有程序名的字符串,argv[1]将指向字符串"-l"argv[2]将指向字符串"remind.c",而argv[3]将为空指针。

因为根据操作系统的不同,程序名可能会包括路径或其他信息。如果程序名不可用,那么argv[0]会指向空字符串。


因为argv是指针数组,所以访问命令行参数非常容易。常见的做法是,期望有命令行参数的程序会设置循环来按顺序检查每一个参数。设定这种循环的方法之一就是使用整型变量作为argv数组的下标。例如,下面的循环每行一条地显示命令行参数:

int i; 

for (i = 1; i < argc; i++)  
    printf("%s\n", argv[i]); 

另一种方法是构造一个指向argv[1]的指针,然后对指针重复进行自增操作来逐个访问数组余下的元素。因为argv数组的最后一个元素始终是空指针,所以循环可以在找到数组中一个空指针时停止:

char **p; 

for (p = &argv[1]; *p != NULL; p++) 
    printf("%s\n", *p); 

因为p是指向字符的指针的指针,所以必须小心使用。设置p等于&argv[1]是有意义的,因为argv[1]是一个指向字符的指针。所以&argv[1]就是指向指针的指针。因为*pNULL都是指针,所以测试*p!= NULL是没有问题的。对p进行自增操作看起来也是对的——因为p指向数组元素,所以对它进行自增操作将使p指向下一个元素。显示*p的语句也是合理的,因为*p指向字符串中的第一个字符。

13.7.2 程序——核对行星的名字

下一个程序planet.c举例说明了访问命令行参数的方法。设计此程序的目的是为了检查一系列字符串,从而找出哪些字符串是行星的名字。程序执行时,用户将把待测试的字符串放置在命令行中:

planet Jupiter venus Earth fred

程序会指出每个字符串是否是行星的名字。如果是,程序还将显示行星的编号(把最靠近太阳的行星编号为1):

Jupiter is planet 5 
venus is not a planet 
Earth is planet 3 
fred is not a planet

注意!!除非字符串的首字母大写并且其余字母小写,否则程序不会认为字符串是行星的名字。

/*
plants.c
--Checks planet names
*/

#include  
#include  

#define NUM_PLANETS 9 

int main(int argc, char *argv[]) 
{ 
    char *planets[] = {"Mercury", "Venus", "Earth", 
                        "Mars", "Jupiter", "Saturn", 
                        "Uranus", "Neptune", "Pluto"}; 
    int i, j; 
    
    for (i = 1; i < argc; i++) { 
        for (j = 0; j < NUM_PLANETS; j++) 
            if (strcmp(argv[i], planets[j]) == 0) { 
                printf("%s is planet %d\n", argv[i], j + 1); 
            break; 
        } 
        if (j == NUM_PLANETS) 
            printf("%s is not a planet\n", argv[i]); 
    } 
    
    return 0; 
}

程序会依次访问每个命令行参数,把它与planets数组中的字符串进行比较,直到找到匹配的名字或者到了数组的末尾才停止。程序中最有趣的部分是对 strcmp函数的调用,此函数的参数是argv[i](指向命令行参数的指针)和planets[j](指向行星名的指针)。

问与答

问1:字面串可以有多长?

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

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

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

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

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

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

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

```c
char *p = "abc", *q = "abc"; 
```

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

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

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

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

char translation_table[128];

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

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

答:可以,如下例所示:

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

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

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

printf(str);

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

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

答:如果因为错误或到达文件尾而不能读入字符,getchar函数会返回int类型的值EOF22.4节)。下面是改进后的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; 
}

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

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

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函数就是按照这种方法编写的,所以最好不要假设返回值有什么特殊的意义。

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

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

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

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

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

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

问11strlen函数和strcat函数是否真的像13.6节所示的那样编写?

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

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

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

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

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

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

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

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

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


写在最后

本文是博主阅读《C语言程序设计:现代方法(第2版·修订版)》时所作笔记,日后会持续更新后续章节笔记。欢迎各位大佬阅读学习,如有疑问请及时联系指正,希望对各位有所帮助,Thank you very much!

你可能感兴趣的:(C语言,1024程序员节,c语言,笔记,开发语言)