——字符串总是“板着一副面孔”,但它是我们唯一能指望的交流纽带。
前几章虽然使用过char
类型变量和char
类型数组,但我们始终没有谈到处理字符序列(C
语言的术语是字符串
)的便捷方法。本章就来补上这一课,并将介绍字符串常量
(在C标准中称为字面串
)和字符串变量
。其中,字符串变量可以在程序运行过程中发生改变。
13.1节
介绍有关字面串的规则,包括如何在字面串中嵌入转义序列,以及如何分割较长的字面串。13.2节
讲解声明字符串变量的方法,字符串变量其实就是字符数组,不过末尾要加上一个特殊的空字符来标示字符串的末尾。13.3节
描述了读/写字符串的方法。13.4节
讨论用来处理字符串的函数的编写方法。13.5节
涵盖了一些C
语言函数库中处理字符串的函数。13.6节
介绍处理字符串时经常会采用的惯用法。13.7节
描述如何创建这样的数组:其元素是指向不同长度字符串的指针。这一节还会说明C
语言如何使用这种数组为程序提供命令行支持。
字面串(string literal)
是用一对双引号括起来的字符序列:
"When you come to a fork in the road, take it."
我们是在第2章
中首次遇到字面串的。字面串常常作为格式串出现在printf
函数和scanf
函数的调用中。在标准中,字面串和字符串是彼此紧密相关但又不同的。
字面串可以像字符常量一样包含转义序列
(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"
包含两个字符(\123
和4
),而字符串"\189"
包含3
个字符(\1
、8
和9
)。而十六进制数的转义序列则不限于
3
个数字,而是直到第一个非十六进制数字符截止。思考一下,如果字符串包含转义序列\xfc
,那么会出现什么情况。\xfc
代表Latin1
字符集中的字符ü
,Latin1
是ASCII
的常见扩展。字符串"Z\xfcrich"(“Zürich”)
有6
个字符(Z
、\xfc
、r
、i
、c
和h
),但是字符串"\xfcber"
(不是“über”
)只有2
个字符(\xfcbe
和r
)。大部分编译器会拒绝接收后面那种字符串,因为十六进制数的转义序列范围通常限制在\x0
~\xff
。
如果发现字面串太长而无法放置在单独一行以内,只要把第一行用字符\
结尾,那么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");
我们经常在printf
函数调用和scanf
函数调用中用到字面串。但是,当调用printf
函数并且用字面串作为参数时,究竟传递了什么呢?为了回答这个问题,需要明白字面串是如何存储的。
就本质而言,C
语言把字面串作为字符数组来处理。当C
语言编译器在程序中遇到长度为n
的字面串时,它会为字面串分配长度为n+1
的内存空间。这块内存空间将用来存储字面串中的字符,以及一个用来标志字符串末尾的额外字符(空字符
)。空字符是一个所有位都为0
的字节,因此用转义序列\0
来表示。
请注意!!不要混淆
空字符('\0')
和零字符('0')
。空字符的码值为0
,而零字符则有不同的码值(ASCII中为48
)。
例如,字面串"abc"
是作为有4
个字符的数组来存储的(a
、b
、c
和\0
)。字面串可以为空。字符串""
作为单独一个空字符来存储。
既然字面串是作为数组来存储的,那么编译器会把它看作char *
类型的指针。例如,printf
函数和scanf
函数都接收char *
类型的值作为它们的第一个参数。思考下面的例子:
printf("abc");
当调用printf
函数时,会传递"abc"
的地址(即指向存储字母a
的内存单元的指针)。
通常情况下可以在任何
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 ***/ //改变字面串可能会导致程序崩溃或运行不稳定。
只包含一个字符的字面串不同于字符常量。字面串"a"
是用指针来表示的,这个指针指向存放字符"a"
(后面紧跟空字符)的内存单元。字符常量'a'
是用整数(字符集的数值码)来表示的。
请注意!!不要在需要字符串的时候使用字符(反之亦然)。函数调用
printf("\n"); /*** right ***/ //因为printf函数期望指针作为它的第一个参数。然而,下面的调用却是非法的: printf('\n'); /*** WRONG ***/
一些编程语言为声明字符串变量提供了专门的string
类型。
C
语言采取了不同的方式:只要保证字符串是以空字符结尾的,任何一维的字符数组都可以用来存储字符串。这种方法很简单,但使用起来有很大难度。有时很难辨别是否把字符数组作为字符串来使用。如果编写自己的字符串处理函数,请千万注意要正确地处理空字符。而且,要确定字符串长度没有比逐个字符地搜索空字符更快捷的方法了。
假设需要用一个变量来存储最多有80
个字符的字符串。由于字符串在末尾处需要有空字符,我们把变量声明为含有81
个字符的数组:
#define STR_LEN 80
...
char str[STR_LEN+1];
这里把STR_LEN
定义为80
而不是81
,强调的是str
可以存储最多有80
个字符的字符串,然后才在str
的声明中对STR_LEN
加1
。这是C
程序员常用的方式。
请注意!!当声明用于存放字符串的字符数组时,要始终保证数组的长度比字符串的长度多一个字符。这是因为
C
语言规定每个字符串都要以空字符结尾
。如果没有给空字符预留位置,可能会导致程序运行时出现不可预知的结果,因为C
函数库中的函数假设字符串都是以空字符结束的。
声明长度为STR_LEN+1
的字符数组并不意味着它总是用于存放长度为STR_LEN
的字符串。字符串的长度取决于空字符的位置,而不是取决于用于存放字符串的字符数组的长度。有STR_LEN+1
个字符的数组可以存放多种长度的字符串,范围是从空字符串到长度为STR_LEN
的字符串。
字符串变量可以在声明时进行初始化:
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
了。)如果初始化器很长,那么省略字符串变量的长度是特别有效的,因为人工计算长度很容易出错。
一起来比较下面这两个看起来很相似的声明:
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
写入内存会导致未定义的行为。
使用printf
函数或puts
函数来写字符串是很容易的。读字符串却有点麻烦,主要是因为输入的字符串可能比用来存储它的字符串变量长。为了一次性读入字符串,可以使用scanf
函数,也可以每次读入一个字符。
转换说明%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
函数总会添加一个额外的换行符,从而前进到下一个输出行的开始处。
转换说明
%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节
)则是一种好得多的选择。
因为对许多程序而言,
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
函数在字符串的末尾放置一个空字符。scanf
和gets
等标准函数会自动在输入字符串的末尾放置一个空字符;然而,如果要自己写输入函数,必须人工加上空字符。
字符串是以数组的方式存储的,因此可以使用下标来访问字符串中的字符。例如,为了对字符串
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
函数无法说明差异。一些编程语言提供的
运算符
可以对字符串进行复制、比较、拼接、选择子串等操作,但C
语言的运算符根本无法操作字符串。在C语言中把字符串当作数组来处理,因此对字符串的限制方式和对数组的一样,特别是它们都不能用C
语言的运算符进行复制和比较操作。
请注意!!直接复制或比较字符串会失败。例如,假定
str1
和str2
有如下声明: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 ***/
这条语句把
str1
和str2
作为指针来进行比较,而不是比较两个数组的内容。因为str1
和str2
有不同的地址,所以表达式str1 == str2
的值一定为0
。
幸运的是,字符串的所有操作功能都没有丢失:C
语言的函数库为完成对字符串的操作提供了丰富的函数集。这些函数的原型驻留在
头(23.6
节)中,所以需要字符串操作的程序应该包含下列内容:
#include
在
中声明的每个函数至少需要一个字符串作为实际参数。字符串形式参数声明为char *
类型,这使得实际参数可以是字符数组、char *
类型的变量或者字面串——上述这些都适合作为字符串。然而,要注意那些没有声明为const
的字符串形式参数。这些形式参数可能会在调用函数时发生改变,所以对应的实际参数不应该是字面串。
中有许多函数,这里将介绍几种最基本的。在后续的例子中,假设str1
和str2
都是用作字符串的字符数组
。
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
中复制到空字符。
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
不会测量数组本身的长度,而是返回存储在数组中的字符串的长度。
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
函数会试图把字符d
、e
、f
和\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
以确保为空字符留下空间。
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? */
...
通过选择适当的关系运算符(<、<=、>、>=)
或判等运算符(==、!=)
,可以测试str1
与str2
之间任何可能的关系。
类似于字典中单词的编排方式,
strcmp
函数利用字典顺序进行字符串比较。更精确地说,只要满足下列两个条件之一,那么strcmp
函数就认为s1
是小于s2
的:
s1
与s2
的前i
个字符一致,但是s1
的第i+1
个字符小于s2
的第i+1
个字符。例如,"abc"
小于"bcd"
,"abd"
小于"abe"
。s1
的所有字符与s2
的字符一致,但是s1
比s2
短。例如,"abc"
小于"abcd"
。当比较两个字符串中的字符时,
strcmp
函数会查看字符对应的数值码
。一些底层字符集的知识可以帮助预测strcmp
函数的结果。例如,下面是ASCII
字符集的一些重要性质。
A~Z
、a~z
、0~9
这几组字符的数值码是连续的。所有的大写字母都小于小写字母
。(在ASCII
码中,65~90
的编码表示大写字母,97~122
的编码表示小写字母。)数字小于字母
。(48~57
的编码表示数字。)空格符小于所有打印字符
。(ASCII
码中空格符的值是32
。)为了说明
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
函数,但是作为实际的提醒程序,它还缺少一些东西。该程序显然有许多需要完善的地方,从小调整到大改进都有(例如,当程序终止时把提醒保存到文件中)。本章和后续各章末尾的编程题将讨论一些改进。
处理字符串的函数是特别丰富的惯用法资源。本节将探索几种最著名的惯用法,并利用它们编写
strlen
函数和strcat
函数。当然,我们可能永远都不需要编写这两个函数,因为它们是标准函数库的一部分,但类似的函数还是有可能需要编写的。本节使用的简洁风格是在许多
C
程序员中流行的风格。即使不准备在自己的程序中使用,也应该掌握这种风格,因为很可能会在其他程序员编写的程序中遇到。在开始之前最后再说一点。如果你想在本节尝试自己编写
strlen
和strcat
,请修改函数的名字(比如,把strlen
改成my_strlen
)。如2.1.1
节中解释的那样,我们不可以编写与库函数同名的函数,即使不包含该函数所属的头也不行。事实上,所有以str
和一个小写字母开头的名字都是保留的(以便在未来的C
标准版本中往头里加入函数)。
许多字符串操作需要搜索字符串的结尾。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
正好指向空字符后面的位置。
复制字符串是另一种常见操作。为了介绍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
函数的这种写法采用了两步算法:
s1
末尾空字符的位置,并且使指针p
指向它;s2
中的字符逐个复制到p
所指向的位置。函数中的第一个
while
语句实现了第1
步。程序中先把p
设定为指向s1
的第一个字符。接着p
开始自增直到指向空字符为止。循环终止时,p
指向空字符。第二个
while
语句实现了第2
步。循环体把s2
指向的一个字符复制到p
指向的地方,接着p
和s2
都进行自增;当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
所指向的地方。正是由于有了这两个++
运算符,赋值之后p
和s2
才进行了自增。重复执行此表达式所产生的效果就是把s2
指向的一系列字符复制到p
所指向的地方。但是什么会使循环终止呢?因为圆括号中的主要运算符是赋值运算符,所以
while
语句会测试赋值表达式的值,也就是测试复制的字符。除空字符以外的所有字符的测试结果都为真,因此,循环只有在复制空字符后才会终止。而且因为循环是在赋值之后终止,所以不需要单独用一条语句来在新字符串的末尾添加空字符。
现在来看一个在使用字符串时经常遇到的问题:存储字符串数组的最佳方式是什么
?最明显的解决方案是创建二维的字符数组,然后按照每行一个字符串的方式把字符串存储到数组中。考虑下面的例子:
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]);
命令行信息不仅对操作系统命令
可用,而且它对所有程序都是可用的。为了能够访问这些命令行参数(C
标准中称为程序参数),必须把main
函数定义为含有两个参数的函数,这两个参数通常命名为argc
和argv
:
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
将为3
,argv[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]
就是指向指针的指针。因为*p
和NULL
都是指针,所以测试*p!= NULL
是没有问题的。对p
进行自增操作看起来也是对的——因为p
指向数组元素,所以对它进行自增操作将使p
指向下一个元素。显示*p
的语句也是合理的,因为*p
指向字符串中的第一个字符。
下一个程序
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"
一次,并且把p
和q
都指向此字面串。如果试图通过指针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
函数会把%
认定为转换说明的开始。
问8:
read_line
函数如何检测getchar
函数读入字符是否失败?
答:如果因为错误或到达文件尾而不能读入字符,getchar
函数会返回int
类型的值EOF
(22.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
函数的返回值可能是源于函数的传统编写方式。看一下Kernightan和Ritchie的《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++))
;
问11:
strlen
函数和strcat
函数是否真的像13.6
节所示的那样编写?
答:有可能
。但是对编译器供应商来说,用汇编语言代替C
语言来编写这些函数和许多其他字符串函数是很普遍的做法。字符串函数的处理速度越快越好,因为它们很常用并且必须能处理任意长度的字符串。利用CPU
可能提供的专门的字符串处理指令,用汇编语言编写的这些函数能够获得很高的效率。
问12:为什么
C
标准采用术语“程序参数”而不是“命令行参数”?
答:程序不总是在命令行中运行的。例如,在常见的图形用户界面下,程序是通过点击鼠标来启动的。在这类环境中,虽然可能有给程序传递信息的其他方式,但是没有传统意义上的命令行了。术语“程序参数”适用于这样的环境。
问13:是否必须使用
argc
和argv
作为main
函数的参数名?
答:不是的
。使用argc
和argv
作为参数名仅仅是一种习惯,而不是语言本身的要求。
问14:我曾见过把
argv
声明为**argv
而不是*argv[]
的做法。这是否合法?
答:当然合法。在声明形式参数时,不管a
的元素类型是什么,*a
的写法和a[]
的写法总是一样的。
问15:我们已经见过如何创建其元素是指向字面串的指针的数组。指针数组是否还有其他应用?
答:有的
。虽然到目前为止主要讨论其元素是指向字符串的指针的数组,但这不是指针数组的唯一应用。我们可以同样简单地创建其元素是指向任何数据类型的指针的数组,无论该数据是否以数组的形式组织。指针数组与动态存储分配(17.1
节)一起使用是特别有用的。
本文是博主阅读《C语言程序设计:现代方法(第2版·修订版)》时所作笔记,日后会持续更新后续章节笔记。欢迎各位大佬阅读学习,如有疑问请及时联系指正,希望对各位有所帮助,Thank you very much!