【C-Primer-Plus读书笔记】第11章:字符串和字符串函数
表示字符串和字符串I/O
在程序中定义字符串
字符串字面量(字符串常量)
- 用双引号括起来的内容称为字符串字面量,也叫作字符串常量。双引号中的字符和编译器自动加入末尾的
\0
字符,都作为字符串存储在内存中。 - 从ANSI C标准起,如果字符串字面量之间没有间隔,或者用空白字符分割,C会将其视为串联起来的字符串字面量。
- 字符串常量属于静态存储类别,这说明如果在函数中使用字符串常量,该字符串只会被存储一次,在整个程序的生命周期内存在,即使函数被调用多次。用双引号括起来的内容被视为指向该字符串存储位置的指针。这类似于把数组名作为指向数组位置的指针。
- 用双引号括起来的内容称为字符串字面量,也叫作字符串常量。双引号中的字符和编译器自动加入末尾的
字符串数组和初始化
定义字符串数组时,必须让编译器知道需要多少空间:- 一种方法是用足够空间的数组存储字符串。在指定数组大小时,要确保数组元素个数至少比字符串长度多1(为了容纳空字符)。所有未被使用的元素都被自动初始化为0(这里的0指的是char形式的空字符,不是数字字符0)。
- 另一种方法就是让编译器确定数组大小,即前面讲过的省略数组初始化声明中的大小,编译器会自动计算数组大小。让编译器计算数组的大小只能用在初始化数组时。如果创建一个稍后再填充的数组,就必须在声明时指定大小。
- 同数组,字符串的声明可以使用数组表示法和指针表示法。
数组和指针
- 初始化数组把静态存储区的字符串拷贝到数组中(此时字符串有两个副本),而初始化指针只把字符串的地址拷贝给指针。
- 这里关键要理解,在数组形式中,数组名是地址常量。不能更改数组名的值,如果更改了则意味着改变了数组的存储位置(即地址)。例如递增之类的操作,而指针表示法则可以。
- 字符串字面量被视为const数据,所以不能通过指针修改其值,且该指针应该声明为指向const数据的指针,但是可以修改指针本身的值(即指向的地址,如上描述可以使用递增之类的操作)。
数组和指针的区别
初始化字符数组来存储字符串和初始化指针来指向字符串(“指向字符串”指的是指向字符串的首地址)的区别:- 数组名是常量,而指针名是变量;
- 两者都可以使用数组表示法;
- 两者都能进行指针加法操作,但是只要指针表示法可以进行递增操作;
- 可以用指针表示法去指向数组表示法的地址以达到两者统一,但不能反向赋值(赋值运算符左边必须是变量或概括地说是可修改的左值);
- 可以改变数组表示法中的元素值(因为是字符串字面量的副本),数组元素是变量但数组名不是。总之,如果不修改字符串不要用指针表示法声明(即指向字符串字面量)。
字符串数组
- 创建一个字符数组(即数组里面每个元素是一个字符数组)会很方便,可以通过数组下标访问多个不同的字符串(此处讲的是二维数组的情况,一个数组内有多个字符串)。当然也可以采用指针数组(即数组里面每个元素是一个字符串字面量,就是指向字符串字面量的地址)。两者在使用上基本一致,但区别在于存储上:前者是字符数组,后者是指针数组,占用空间上前者大于后者,但前者可修改值,后者不可修改。
- 综上所述,如果要使用数组表示一系列待显示的字符串,请使用指针数组,因为它比二位数组的效率高。但是如果要改变字符串或为字符串输入预留空间,不要使用指向字符串字面量的指针。
指针和字符串
实际上,字符串的绝大多数操作都是通过指针完成的。即大多数操作只需要知道地址就可以了,这样效率更高,如果确实需要拷贝整个数组,可以使用strcpy()
和strncpy()
(下面字符串函数讲解)。
字符串输入
把一个字符串读入程序的基本步骤:首先必须预留存储该字符串的空间,然后用输入函数获取该字符串。
分配空间
- 分配空间时必须要保证足够的空间,不要指望计算机在读取字符串时顺便计算它的长度然后再分配空间(计算机不会这样做,除非你写了一个处理该任务的函数)。
- 最简单的方法就是在声明时显式指明数组的大小;还有一种方法是使用C库函数来分配内存(下章内存管理讲解)。
不幸的gets()
函数
在读取字符串时,scanf()和转换说明%s只能读取一个单词。可经常需要读取一整行输入,以前,gets()
函数就是用于处理这种情况。
gets()
函数读取整行输入,直至遇到换行符,然后丢弃换行符,存储其余字符,并在末尾添加一个空字符使其成为C字符串。gets()
函数经常跟puts()
函数配对使用,该函数用于显示字符串,并在末尾添加换行符。gets()
函数的致命缺陷是它只有一个参数用于指定字符数组的地址,但它无法检查数组是否装得下输入行。所以现代编译器一般都会给出警告信息。如果输入的字符串过长会导致缓冲区溢出,即多余的字符超出了指定的目标空间。gets()
函数在C11标准已经废除,不建议使用
gets()
的替代品
现在有两种替代方案:fgets()
和gets_s()
。
fgets()函数和fputs()
fgets()函数通过第2个参数限制读入的字符数来解决溢出的问题。该函数专门设计用于处理文件输入,所以一般情况下可能不太好用。
fgets()
和gets()
的区别如下:fgets()
函数的第2个参数指明了读入字符的最大数量(如果该参数是n,则读入n-1个字符,因为需要为空字符预留一个位置,或者遇到换行符就停止读取)- 如果
fgets()
读到一个换行符,会把它存储在字符串中 fgets()
函数的第3个参数指明要读入的文件(读取键盘输入,则以stdin
作为该参数)
- 因为换行符的原因,
fgets()
通常与fputs()
配对使用(因为fputs()
不会在末尾添加换行符),fputs()
函数的第二个参数指明它要写入的文件(如果要显示在计算机显示器上,则应使用stdout
作为该参数) fgets()
函数返回指向char的指针。如果一切顺利返回的地址与传入的第一个参数相同。但是如果读到文件结尾,它将返回一个特殊的指针:空指针。该指针保证不会指向有效的数据,可以用数字0来代替,不过C语言中常用宏NULL
来代替。- 系统使用缓冲的I/O。这意味着用户在按下
Return
键之前,输入都被存储在临时存储区,即缓冲区中。按下Return
键就在输入中增加了一个换行符,并把整行输入发送给fgets()
。对于输出,fputs()
把字符发送给另一个缓冲区,当发送换行符时,缓冲区中的内容被发送至屏幕上。
- gets_s函数
C11新增的gets_s函数(可选)和fgets()类似,用一个参数限制读入的字符数。
gets_s()
与fgets()
的区别如下:
gets_s()
只从标准输入(stdin
)中读取数据,所以不需要第三个参数gets_s()
读到换行符会丢弃换行符不予存储gets_s()
读到最大字符都没有碰到换行符,会执行下面几步:首先把目标数组中的首字符设置为空字符,并且读取并丢弃随后的输入直至读到换行符或文件结尾,然后返回空指针。接着,调用依赖实现的“处理函数”(或你选择的其他函数),可能会中止或退出程序。
最后比较一下gets()
、fgets()
和gets_s()
的适用性,首先gets()
不安全且被废弃不推荐使用。而当输入与预期不符时gets_s()
完全没有fgets()
函数方便、灵活(原因主要在gets_s()
与fgets()
的区别的第三条),鉴于此fgets()
通常是处理类似情况的最佳选择。
- s_gets()函数
由于种种原因,上述介绍的三个字符串输入函数gets()
、fgets()
和gets_s()
各有不足(在fgets()
是最佳选择的情况下,去除换行符,并清除输入缓冲区的剩余内容)也许并不能满足我们的要求,所以我们需要在这里创建一个s_gets()
函数
char * s_gets(char * st, int n)
{
char * ret_val;
int i = 0;
ret_val = fgets(st, n, stdin);
if (ret_val) //即,ret_val != NULL
{
while (st[i] != '\n' && st[i] != '\0')
i++;
if (st[i] == '\n')
st[i] = '\0';
else
while (getchar() != '\n')
continue;
}
return ret_val;
}
scanf()
函数
我们已经知道scanf()
和%s
转换说明读取字符串。scanf()
和gets()
或fgets()
的区别在于它们如何确定字符串的末尾:
scanf()
更像是获取单词的函数,而不是获取字符串;- 如果预留的存储区装得下输入行,
gets()
和fgets()
会读取第1个换行符之前所有的字符。 scanf()
函数有两种方法确定输入结束。无论哪种方法,都从第1个非空白字符作为字符串的开始。- 如果使用
%s
转换说明,以下一个空白字符作为字符串的结束。如果指定了字符宽度,那么scanf()
将读取指定字符宽度的字符或第1个空白字符停止。
scanf()
和gets()
类似,也存在一些潜在的缺点。如果输入行的内容过长,scanf()
也会导致数据溢出,不过在%s
转换说明中使用字段宽度可防止溢出。
字符串输出
接下来,我们讨论字符串输出。C有3个标准函数用于打印字符串:puts()
、fputs()
和printf()
。
puts()
函数puts()
函数很容易使用,只需把字符串的地址作为参数传递给它即可。puts()
在显示字符串时会自动在其末尾添加一个换行符。puts()
在遇到空字符时就停止输出,所以必须确保有空字符。
fputs()
函数fputs()
函数是puts()
函数针对文件定制的版本。它们的区别如下:fputs()
函数的第2个参数指明要写入数据的文件。如果要打印在显示器上,使用stdout
。- 与
puts()
不同,fputs()
不会在输出的末尾添加换行符。
printf()
函数printf()
函数用起来没有puts()
函数那么方便,但它可以格式化不同的数据类型。printf()
函数不会自动在每个字符串末尾加上一个换行符。printf()
函数打印多个字符串更加简单
综上所述,我们在此总结一下字符串输入输出函数,基本上每个输入函数都有其对应的输出函数以配对使用,各输入输出函数配对的依据又主要以对换行符的处理方式为依据。主要有以下3对输入输出函数:gets()
(推荐使用gets_s()
)-puts()
、fgets()
-fputs()
、scanf()
-printf()
自定义输入/输出函数
如果需要自定义所需的输入/输出函数,请使用getchar()
和putchar()
函数。理论上所有字符串处理函数底层原理都是这两个函数(或概括的说这两个函数类似的处理方式)在依次处理单个字符以完成整个字符串的处理。
字符串函数
以下函数(1~4)都需要包含strings.h
头文件
strlen()
函数——用于统计字符串的长度strcat()
和strncat()
函数——用于拼接字符串- 该函数把第2个字符串的备份附加在第1个字符串末尾,并把拼接后形成的新字符串作为第1个字符串,第2个字符串不变
strcat()
不进行溢出检查(第1个字符串)strncat()
可指定最大字符串长度以防止溢出(第3个参数)
strcmp()
和strncmp()
函数——用于字符串比较- 该函数要比较的是字符串的内容,不是字符串的地址
- 返回值为0就代表相同,否则返回非0值;其次如果在字母表中的第1个字符串位于第2个字符串前面,该函数就返回负数,反之则返回正数
- 该函数比较的是字符串(比较至空字符即停止比较),不是整个数组,这是非常好的功能
strcmp()
依次比较每个字符直到发现差异strncmp()
可以比较字符的不同地方,也可以限制比较的字符数量
strcpy()
和strncpy()
函数——用于拷贝字符串- 该函数相当于字符串赋值运算符
- 返回值为第1个参数值,即第1个字符的地址
- 第1个参数不必指向数组的开始,这个属性可用于拷贝数组的一部分
strcpy()
不进行溢出检查(第1个字符串)strncpy()
可指定最大字符串长度以防止溢出(第3个参数)
sprintf()
函数——用于把多个元素组合成一个字符串其他处理函数
除了上面列出的,还有以下常用函数(找到即返回该位置地址,否则返回空指针):strchr()
——找出一个字符串里指定字符首次出现的位置strpbrk()
——监测一个字符串里是否包含另一个字符串中的任一字符strrchr()
——找出一个字符串里指定字符最后出现的位置strstr()
——监测一个字符串里是否包含指定字符串并返回首字符位置
字符串示例:字符串排序
- 排序指针而非字符串
- 选择排序算法(冒泡排序)
利用for
循环依次把每个元素与首元素比较。如果待比较的元素在当前元素前面,则交换两者。循环结束时首元素就是机器排序序列最靠前的字符串。然后外层for循环重复这一过程,这次从第2个元素开始。外层循环指明正在处理数组的哪一个元素,内层循环找出应存储在该元素的值。
ctype.h
字符函数和字符串
- 虽然
ctype.h
中的函数不能处理整个字符串,但是可以处理字符串中的字符。合理使用亦能有大用处 - 另外
ctype.h
中的函数通常作为宏实现
命令行参数
- 命令行是在命令行环境中,用户为运行程序输入命令的行
- 命令行参数是同一行的附加项
- C编译器允许
main()
没有参数或者有两个参数。第一个参数(argc
)是个整型,存储命令行中的字符串的数量;第二个参数(argv
)是一个指针数组,存储命令行每个字符串的地址。 - 命令行中,可以把多个单词用双引号括起来,即算作一个独立的参数字符串
把字符串转换为数字
- 数字既能以字符串形式存储,也能以数值形式存储。把数字存储为字符串就是存储数字字符
- C要求用数值形式进行数值运算,但是在屏幕上显示数字则要求字符串形式,因为屏幕显示的是字符
printf()
和sprintf()
函数通过%d
和其他转换说明,把数值形式转换为字符串形式,scanf()
可以把输入字符串转换为数值形式。C还提供一些专门的函数用于把字符串形式转换成数值形式(需包含
stdlib.h
头文件):atoi()
函数用于把字母数字转换成整数atof()
函数用于把字符串转换成double
类型的值atol()
函数用于把字符串转换成long
类型的值
比上面更智能的函数,能识别和报告字符串中首字符是否是数字:
strtol()
函数用于把字符串转换成long
类型的值,能指定进制strtoul()
函数用于把字符串转换成unsigned long
类型的值,能指定进制strtod()
函数用于把字符串转换成double
类型的值