现在进度已经过半,接下来这一章虽然很长,但是干货不算太多。
全书共分17章,这是关于本书第11章内容的博客。本章介绍了关于字符串的内容,并介绍了很多关于字符串的函数。博客的目录和书上目录是相似的。此系列博客的代码都在Visual Studio 2022环境下编译运行。
我目前大一刚刚结束,水平有限,博客中若有错误或者总结不到位的地方也请见谅。
目录
11.1 表示字符串和字符串I/O
字符串十分常用,C库提供了很多专门用于处理字符串的函数。
下面程序有表示字符串的几种方式。
#include<stdio.h>
#define MSG "I am a symbolic string constant."
#define MAXLENGTH 81
int main(void)
{
char words[MAXLENGTH] = "I am a string in an array.";
const char* pt1 = "Something is poingting at me";
puts("Here are some strings:");
puts(MSG);
puts(words);
puts(pt1);
words[8] = 'p';
puts(words);
return 0;
}
11.1.1 在程序中定义字符串
11.1.1.1 字符串字面量(字符串常量)
用双引号括起来的内容称为字符串字面量,也叫字符串常量。编译器会自动在结尾加入\0字符。双引号中的字符和编译器加入的\0字符都作为字符串存储在内存中。
如果字符串常量之间没有间隔,或者用空白字符分隔,C语言会将其视为串联起来的字符串常量。
如果在字符串内部使用双引号,必须在双引号前面加上一个反斜杠。
字符串常量输入静态存储类别,该字符串只会被存储一次,在整个程序的生命周期内存在。用双引号括起来的内容被视为指向该字符串存储位置的指针。
11.1.1.2 字符串数组和初始化
定义字符串数组时,必须让编译器知道需要多少空间。
可以用足够空间的数组存储字符串,初始化时等号后面是字符串,这样比标准形式的初始化方便很多。
指定数组大小时数组的元素个数至少比字符串长度多1(需要容纳空字符)。也可以让编译器确定大小(仅限于初始化时)。
字符串数组名也是首元素的地址。
也可以用指针表示法创建字符串。创建一个char类型指针,初始化时等号后接一个字符串。这种情况由字符串本身决定存储空间。
用数组和指针这两种表示字符串的方式并不完全相同。
11.1.1.3 数组和指针
数组形式:字符串存储在静态存储区中,但是程序开始运行时才会为数组分配内存,才将字符串的内容拷贝到数组中。此时字符串有两个副本,一个存放在静态内存中,一个存储在数组中。此后对于数组名可以进行关于指针的操作。
指针形式:字符串也存储在静态存储区中,开始执行程序后为指针留出一个存储位置,将字符串的地址存储在指针中。
字符串常量被视为const数据,如果指针指向字符串则不能修改内容,但是可以指向其他字符串。如果把字符串拷贝给数组就可以修改数组中的内容。
初始化数组把静态存储区的字符串拷贝到数组中,初始化指针只把字符串的地址拷贝给指针。
11.1.1.4 数组和指针的区别
建议把指针初始化为字符串常量时使用const限定符。
如果打算修改字符串,就不要用指针指向字符串常量。
11.1.1.5 字符串数组
访问多个字符串可以使用指向字符串的指针数组和char类型的二维数组。
二维数组内存的效率相对低,但是指针数组指向的内容不能修改。
11.1.2 指针和字符串
字符串绝大多数操作都是通过指针完成的。给指针初始化为字符串时实际上拷贝地址,没有拷贝具体内容。
11.2 字符串输入
要把字符串读入程序,首先要预留足够的空间,然后用输入函数获取内容。
11.2.1 分配空间
在输入字符串前为字符串预留空间时,要使用数组(不要用指针),并声明数组的大小。
11.2.2 不幸的gets()函数
gets()函数读取整行输入,直至遇到换行符,然后丢弃换行符,存储其余字符,并在末尾添加空字符以组成一个字符串。
gets()函数只有一个参数,就是一个存储输入的地址。
gets()函数虽然简单易用,但是无法检查数组是否容得下输入行。如果输入字符串过长,会导致缓冲区溢出,这可能导致一些意外。因此gets()函数不安全,有一定的隐患。
C11标准废除了gets()函数。
11.2.3 gets()的替代品
11.2.3.1 fgets()函数
fgets()函数有三个参数,第一个还是存储输入的地址,第二个参数是读入字符的最大数量,第三个参数是要读入的文件(从键盘输入就是stdin)。
如果第二个参数的值是n,函数最多读取n-1个字符,并且遇到换行符会停止。fgets()会将遇到的换行符存储在字符串中。最后会加上空字符。
fgets()返回指向char的指针,如果一切顺利,返回的地址与传入的第一个参数相同。如果读到末尾会返回空指针。空指针不会指向有效数据,C语言中通常用NULL表示。如果函数读入数据时出现错误,也返回空指针。
fgets()函数对换行符的处理可能带来一些麻烦,并且可能有多余的输入留在缓冲区,这也可能导致一些问题。
11.2.3.2 gets_s()函数
C11标准新增了可选的gets_s()函数,gets_s函数有两个参数,第一个还是存储输入的地址,第二个是存入字符的最大数量。gets_s()函数和fgets()函数第二个参数都是限制读入字符数量。
gets_s()函数也是丢弃换行符,如果读到最大字符数还没有读到换行符,会进行一些其他操作。
如果目标区域装得下输入行,fgets(),gets(),gets_s()都可以,但其中fgets()会保留换行符。如果输入行太长,gets()函数有安全隐患,gets_s()函数会进行一些操作,需要知道其他知识,fgets()函数最容易使用。fgets()是处理这类情况的最佳选择。
11.2.3.3 s_gets()函数
作者编写了s_gets()函数处理字符串输入。读取整行输入并用空字符代替换行符,或读取一部分输入丢弃其余部分。此书后面章节的很多程序都包含这个函数。代码如下:
char* s_gets(char* st, int n)
{
char* ret_val;
int i;
ret_val = fgets(st, n, stdin);
if (ret_val)
{
while (st[i] != '\n' && st[i] != '\0')
i++;
if (st[i] == '\n')
st[i] = '\0';
else
while (getchar() != '\n')
continue;
}
return ret_val;
}
11.2.4 scanf()函数
scanf()函数用%s读取字符串,scanf()函数读取时从第一个非空白字符作为开始,到下一个空白字符结束。如果指定了字段宽度,那么遇到空白字符或读取数量等于字段宽度时停止。
scanf()一般用来读取一个单词,读取并转换混合数据类型为标准形式。
11.3 字符串输出
11.3.1 puts()函数
puts()函数很好用,只要把字符串的地址作为参数即可。
puts()在显示字符串时会在末尾输出换行符。
puts()遇到空字符停止输出,如果没有空字符会输出其他内容(VS经典烫烫烫烫烫)。
11.3.2 fputs()函数
fputs()函数有两个参数,第一个是字符串的地址,第二个是要输出的文件(输出到屏幕上用stdout)。
fputs()不会在结尾添加换行符。
11.3.3 printf()函数
printf()把地址作为参数,使用%s转换说明。
printf()也不会自动输出换行符,需要加\n。
11.4 自定义输入/输出函数
不一定要使用C库中的标准函数,可以使用getchar()和putchar()自定义函数。
11.5 字符串函数
C库提供了多个处理字符串的函数,原型放在string.h头文件中。下面的函数不特殊说明都是在string.h头文件中,
11.5.1 strlen()函数
strlen()函数统计字符串的长度。该函数唯一的参数是一个指向字符串的指针。
11.5.2 strcat()函数
strcat()函数用于拼接字符串。该函数接受两个字符串作为参数,把第二个字符串附加在第一个字符串的结尾(包括空字符)。拼接后的字符串作为第一个字符串,第二个字符串内容不变。
strcat()函数是char*类型,返回第一个字符串的地址。
11.5.3 strncat()函数
strcat()函数不检查第一个数组是否能容纳第二个字符串,可能会导致溢出。
strncat()函数前两个参数和strcat()一样,第三个参数是最大添加字符数。在达到最大添加字符数或遇到空字符后停止。strncat()函数最后一定添加空字符。
11.5.4 strcmp()函数
strcmp()函数可以用来比较两个字符串的内容。如果两个字符串完全相同,返回0。如果比较字符串不同,那么返回非零值。如果第一个字符串在第二个字符串前,返回一个负值(VS为-1),如果第一个字符串在第二个字符串后,返回一个正值(VS为1)。前后的判断是依次比较,直至遇到第一个不相同字符为止,第一个不相同字符大的字符串在后面。strcmp()比较的是ASCII码值,且比较所有字符。
strncmp()函数与strcmp()函数类似,前两个参数相同,第三个参数是比较的最大字符数。在比较字符数量达到最大字符数或遇到空字符后停止比较。比较部分都相同返回0,第一个在第二个前返回负值,第一个在第二个后返回正值。
11.5.5 strcpy()和strncpy()函数
如果拷贝整个字符串,需要strcpy()函数。strcpy()有两个参数,都是字符串。第二个参数指向的字符串被拷贝到第一个参数指向的数组中(包括空字符)。要确保第一个参数的空间足够。拷贝后会覆盖原有内容。
strcpy()返回第一个字符串的地址。第一个参数不必指向数组的开始。
strncpy()函数前两个参数和strcpy()一样,第三个参数是最大数量。如果第二个字符串字符数小于最大数量,strncpy()拷贝整个字符串(包括空字符)。但是,如果到达最大数量后,还没有遇到空字符,就不会增加空字符。
11.5.6 sprintf()函数
sprintf()函数声明在头文件stdio.h中。和printf()类似,但是把数据写入字符串。第一个参数是目标字符串的地址,其余参数和printf()相同。sprintf()可以把多个元素组合成一个字符串。
11.5.7 其他字符串函数
strchr()函数有两个参数,第一个是一个字符串,第二个是一个字符。如果前者包含后者,函数返回该字符首次出现的指针,否则返回NULL。
strrchr()函数有两个参数,第一个是一个字符串,第二个是一个字符。如果前者包含后者,函数返回该字符最后一次出现的指针,否则返回NULL。
strstr()函数有两个参数,两个参数都是字符串。如果前者包含后者,函数返回第二个字符串首元素第一次出现的指针,否则返回NULL。
11.6 命令行参数
命令行是在命令行环境中,用户为运行程序输入命令的行。命令行参数是同一行的附加项。
C语言允许main()没有参数或者有两个参数。当有两个参数时,第一个参数是命令行的字符串数量,习惯用argc表示。系统用空格表示一个字符串的结束和下一个字符串的开始。字符串中除第一个外都供程序使用。程序把命令行字符串存储在内存中,把每个字符串的地址存储在指针数组中。数组的地址被存放在第二个参数中。这个指向指针的指针一般称为argv。一些系统把程序本身名称赋给argv[0],随后第一个字符串是argv[1],以此类推。
Microsoft Visual Studio可以在项目->属性->调试中输入命令行。
11.7 把字符串转换为数字
atoi()函数可以把字母数字转化为整数,接受一个字符串作为参数,返回相应的整数值。如果命令行参数不是数字,结果是未定义的。
atoi()函数定义在stdlib.h头文件中。该头文件还有atof()函数和atol()函数,atof()函数把字符串转换成double类型的值,atol()函数把字符串转换成long类型的值。
strtol()函数把字符串转换成long类型的值,strtoul()函数把字符串转换成unsigned long类型的值。strtod()把字符串转换成double类型的值。这些函数能识别和报告字符串首字符是否是数字。
strtol()和strtoul()函数还可以指定数字的进制。
strtol()函数有三个参数,第一个是指向待转换字符串的指针,第二个参数是一个指针的地址,该指针被设置为标识输入数字结束字符的地址,第三个参数表明以什么进制写入数字。
最多可以转换三十六进制,'a'-'z'都可用作数字,表示10-35。
strtoul()函数原理相似,strtod()函数只以十进制转换,不需要第三个参数。
许多实现使用itoa()和ftoa()函数分别把整数和浮点数转换成字符串,但是这两个不是C标准库的成员,可以用兼容性更好的sprintf()代替。