一、字符串和字符串I/O
在字符串和格式化输入/输出一节中我们讲过,字符串是以空字符 “ \0 ”结尾的char类型的数组。由于字符串十分常用,所以C提供了许多专门处理字符串的函数。下面详细讲述字符串的性值、如何声明并初始化字符串、在程序中输入和输出字符串以及如何操控字符串。
#include <stdio.h>
#define MSG "I LOVE C"
#define LARGE 40
int main(void){
// 定义一个字符数组
char words[LARGE] = "I am studying C";
// 定义一个不能改变的字符串指针
const char *Str1 = "that`s funny";
// 打印以下信息
puts("Some string display Here: ");
puts(MSG);
puts(words);
puts(Str1);
// 将words中的第8个元素改为P
words[8] = 'P';
puts(words);
return 0;
}
和printf()函数一样,puts函数也属于stdio.h系列的输入/输出函数。但是,和printf()不同的是,puts()函数只显示字符串(不能打印数字),而且自动在显示的字符串末尾加上换行符。
1.1 定义字符串
在上面的程序中,有三种定义字符串的方式。分别是:
- 字符串字面量(字符串常量)
#define MSG "I LOVE C"
puts(MSG);
- 字符串数组
#define LARGE 40
// 定义一个字符数组
char words[LARGE] = "I am studying C";
// 输出字符串
puts(words);
- 字符串指针
// 定义一个不能改变的字符串指针
const char *Str1 = "that`s funny";
puts(Str1);
1.1.1 字符串字面量(字符串常量)
用双引号括起来的内容称为字符串字面量(string literal),也叫做字符串常量。双引号中的字符和编译器自动加入末尾的 “ \0 ”字符,都作为字符串储存在内存中。
如果字符串字面量之间没有间隔或者用空白字符间隔,C会将其视为串联起来的字符串字面量
#include <stdio.h>
int main(void){
char msg[50] = "Hello""World" "LOVE C";
puts(msg); // 输出结果:HelloWorldLOVE C
return 0;
}
等价于:
#include <stdio.h>
int main(void){
char msg[50] = "HelloWorldLOVE C";
puts(msg); // 输出结果:HelloWorldLOVE C
return 0;
}
如果要在字符串中打印出双引号则需要加上转义符 “ \ ”。
#include <stdio.h>
int main(void){
char msg[50] = "Hello \"World\" LOVE C";
puts(msg); // 输出结果:Hello "World" LOVE C
return 0;
}
字符串常量属于静态存储类别,说明如果在函数中使用字符串常量,该字符串只会被存储一次,在整个程序的生命周期内存在,即时函数被调用多次。
1.1.2 字符串数组和初始化
定义字符串数组时,必须让编译器知道需要多少空间。一种方法是用足够空间的数组存储字符串。如下:
const char str[20] = "I LOVE C";
const表示不会更改这个字符串。
上述初始化比标准数组初始化(下述程序)简单的多:
const char str[20] = {'I', ' ', 'L', 'O', 'V', 'E', ' ', 'C', '\0'};
注意最后的空字符。没有这个空字符,这就不是一个字符串,而是一个字符数组。
在指定数组大小时,要确保数组的元素个数至少比字符串长度多1(为了容纳空字符)。所有未被使用的元素都自动初始化为0(这里的0指的是char形式的空字符,不是数字0)。
通常,让编译器自己确定数组的大小更方便。因为处理字符串的函数通常都不知道数组的大小,这些函数通过查找字符串末尾的空字符确定字符串在何处结束。
const char str[] = "I LOVE C";
声明数组时,数组的大小必须是可求值的整数。在C99新增边长数组之前,数组的大小必须是整型常量。
int n = 8;
char str1[1]; // 有效
char str2[2+5]; // 有效
char str3[2*sizeof(int)+1]; // 有效
char str4[n]; // 在C99标准之前无效,C99之后是变长数组
1.1.3 数组和指针
const char *pt1 = "LOVE C";
const char ar1[] = "LOVE C";
在上面的声明中,数组形式(ar1[ ])在计算机的内存中分配一个含有7个 元素的数组(每个元素对应一个字符,还加上末尾的空字符 ‘\0’),每个元素被初始化为字符串字面量对应的字符。通常,字符串都作为可执行文件的一部分存储在数据段中。当把程序载入内存时,也载入了程序中的字符串。字符串存储在静态存储区中。但是,当程序开始运行时才会为该数组分配内存。此时,才将字符串拷贝到数组中。此时字符串有两个副本,一个是在静态内存中的字符串字面量,另一个是存储在ar1数组中的字符串。此后,编译器便把数组名ar1识别为该数组首元素地址(ar1[0])的别名。在数组形式中,ar1是地址变量。不能更改ar1,如果改变了ar1。则意味着改变了数组的存储位置(即地址)。可以进行类似ar1+1这样的操作,标识数组的下一个元素。但是不允许进行++ar1这样的操作。递增运算符只能用于变量名前,不能用于常量。
指针形式也使得编译器为字符串在静态存储区预留7个元素的空间。另外,一旦开始执行程序,他会为指针变量pt1留出一个存储位置,并把字符串的地址存储在指针变量中。该变量最初指向该字符串的首字符,但是它的值可以改变。因此可以使用递增操作。
字符串字面量被视为const数据。由于pt1指向这个const数据,所以应该把pt1声明为指向const数据的指针。这意味着不能用pt1改变它所指向的数据,但是仍然可以改变pt1的值。
总之,初始化数组把静态存储区的字符串拷贝到数组中,而初始化指针只把字符串的地址拷贝给指针。
#include <stdio.h>
#define MSG "LOVE C"
int main(void){
const char ar[] = MSG;
const char *pt = MSG;
printf("the address of ar :%p\n", ar);//the address of ar :00000094583ff831
printf("the address of pt :%p\n", pt);//the address of pt :00007ff63fa6a000
printf("the address of MSG :%p\n", &MSG);//the address of MSG :00007ff63fa6a000
return 0;
}
1.1.4 数组和指针的区别
两者的主要区别在于:数组名ar是常量,而指针名pt是变量。
两者中只有指针表示法可以进行递增操作。
数组的元素是变量(除非数组被声明为const),但是数组名不是变量。
建议指针初始化字符串时使用const限定符:
const char *pt = "LOVE C";
总之,如果打算修改字符串,就不要用指针指向字符串字面量
如果要改变字符串或为字符串输入预留空间,不要使用指向字符串字面量的指针。
二、字符串输入
如果想把一个字符串读入程序,首先必须预留存储该字符串的空间,然后用输入函数获取该字符串。
2.1 分配空间
要做的第一件事就是要为稍后读入的字符串分配足够的空间。
在上述的代码中,编译器给出了警告。在读入name时,name可能会擦写掉程序中的数据或代码,从而导致程序异常终止。因为scanf()要把信息拷贝至参数指定的地址上,而此时该参数是个未初始化的指针,name可能指向任何地方。
解决上述问题最简单的方法是在声明时指明数组的大小:
char name[40];
2.2 gets()函数 (不建议使用)
在读取字符串时,scanf()和转换说明%s只能读取一个单词。可在程序中经常要读取一整行输入,而不仅仅是一个单词。许多年前,gets()函数就用于处理这种情况。
gets()函数简单易用,它读取整行输入,直到遇到换行符,然后丢弃换行符,存储其余字符,并在这些字符的末尾添加一个空字符使其成为一个C字符串。
#include <stdio.h>
#define LEN 40
int main(void){
char words[LEN];
puts("Enter a string :");
gets(words); // 典型用法
printf("%s\n", words);
puts(words);
puts("Done");
return 0;
}
但gets()函数在一些编译器中会弹出警告信息。问题在于gets()唯一的参数,它无法检查数组是否装的下输入行,前面介绍过,数组名会被转换成该数组元素的首地址,因此,gets()函数只知道数据的开始处,并不知道数组中有多少个元素。
如果输入的字符串过长,会导致缓冲区溢出,即多余的字符超出了指定的目标空间。如果这些多余的字符只是占用了未使用的内存,就不会立即出现问题;如果它们擦写掉程序中的其他数据,会导致程序异常终止。
因为gets()函数的这个特性,有些人通过系统编程,运行一些破环系统安全的代码。
C99标准委员承认了gets()的问题并建议不要使用它。C11标准委员会则从标准中直接废除了gets()函数。
然而在实际应用中,编译器为了兼容以前的代码,大部分都继续支持gets()函数。
2.3 gets()的替代品
2.3.1 fgets()函数(和fputs())
fgets()函数通过第二个参数限制读入的字符数来解决溢出的问题。该函数专门设计用于处理文件输入。所以一般情况下不太好用。fgets()和gets()的区别如下:
1.fgets()函数的第二个参数指明了读入字符的最大数量。如果这个值是n,那么fgets()将读入n-1个字符,或者读到遇到的第一个换行符。
2. 如果fgets()读到一个换行符,会把它储存在字符串中。这点与gets()不同,gets()会丢弃换行符。
3. fgets()函数的第三个参数指明要读入的文件。如果读入的从键盘输入的数据,则以stdin作为参数,该标识符定义在stdio.h中。
因为fgets()函数把换行符放在字符串的末尾(假设输入行不溢出),通常要与fputs()函数(和puts()类似)配对使用,除非该函数不在字符串末尾添加换行符。fputs()函数的第二个参数指明它要写入的文件。如果要显示在计算机显示器上,应使用stdout作为参数。
#include <stdio.h>
#define SIZE 4
int main() {
char words[SIZE];
puts("please enter a string:");
fgets(words, 4, stdin);
fputs(words,stdout);
return 0;
}
2.3.2 gets_s()函数
C11新增的get_s()函数(可选)和fgets()类似,用一个参数限制读入的字符数。两者的主要区别在于:
- get_s()只从标准输入中读取数据,所以不需要第3个参数。
- 如果get_s()读到换行符,会丢弃而不是储存它。
- 如果get_s()读到最大字符数都没有读到换行符,会执行以下几步。首先把目标数组中的首字符设置为空字符,读取并丢弃随后的输入直至读到换行符或文件结尾,然后返回空指针。接着,调用依赖实现的“处理函数”,可能会中止或退出程序。
#include <stdio.h>
#define SIZE 14
// 需要在C11中执行
int main() {
char words[SIZE];
puts("please enter a string:");
gets_s(words, 4);
fputs(words,stdout);
return 0;
}
2.3.3 scanf()函数
scanf()函数有两种方法确定输入结束。无论哪种方法,都是从第1个非空白字符作为字符串的开始,如果使用 “%s”,转换说明,以下一个空白字符(空格、空行、制表符和换行符)作为字符串的结束(字符串不包括空白字符)。如果指定了字段宽度,如%10s,那么scanf()将读取10个字符或读到第1个空白字符停止(先满足的条件即是结束输入的条件)
输入语句 | 原输入序列 | name中的内容 | 剩余输入序列 |
---|---|---|---|
scanf(“%s”, name) | fleebert hup | fleebertt | [空格]hup |
scanf(“%5s”, name) | fleebert hup | fleeb | ert hup |
scanf(“%5s”, name) | ann ular | ann | [空格]ular |
四、字符串输出
C有3个标准库函数用于打印字符串:puts()、fputs()和printf()
4.1 puts()函数
puts()函数很容易使用,只需把字符串的地址作为参数传递给它即可。
#include <stdio.h>
#define MSG "Hello World"
#define SIZE 40
int main(void){
char flower[SIZE] = "I LOVE C";
puts(flower);
puts(MSG);
return 0;
}
//输出结果:
I LOVE C
Hello World
如上所示,每个字符串单独占一行,因为puts()在显示字符串时会自动在其末尾添加一个换行符。
该程序再次说明,用双引号括起来的内容是字符串常量,且被视为该字符串的地址。
#include <stdio.h>
#define MSG "Hello World"
#define SIZE 40
int main(void){
// flower不是字符串数组,而是一个字符数组
char flower[SIZE] = {'I', 'L', 'O', 'V', 'E', 'C'};
puts(flower);
puts(MSG);
return 0;
}
上述中的flower不是一个字符串,因为缺少一个表示结束的空白符。
4.2 fputs()函数
fputs()函数是puts()针对文件定制的版本。它们的区别如下:
- fputs()函数的第二个参数指明要写入数据的文件。如果要打印在显示器上,可以用定义在stdio.h中的stdout(标准输出)作为参数。
- 与puts()不同,fputs()函数不会在输出的末尾添加换行符。
4.3 printf()函数
和puts()函数一样,printf()也把字符串的地址作为参数,printf()函数用起来没有puts()函数那么方便,但是它更加多才多艺,因为它可以格式化不同的数据类型。
与puts()不同的是,printf()不会自动在每个字符串的末尾加上换行符。因此,必须在参数中指明应该在哪里使用换行符。
五、字符串函数
C库提供了多个处理字符串的函数,ANSI C把这些函数的原型放在<string.h>头文件中。其中最常用的函数有strlen()、strcat()、strcmp()、strncmp()、 strcpy()和strncpy()。
5.1 strlen()函数
strlen()函数用于统计字符串长度。
#include <stdio.h>
#include "string.h"
#define MSG "Hello world"
int main(void){
//"%llu"为unsigned long long int
printf("%llu", strlen(MSG));
return 0;
}
5.2 strcat()函数
strcat()(用于拼接字符串)函数接收两个字符串作为参数。该函数把第2个字符串的备份附加在第一个字符串的末尾,并把拼接后形成的新字符串作为第一个字符串,第二个字符串不变。strcat()函数类型是char *(即指向char的指针)。strcat()函数返回第一个参数,即拼接第二个字符串后的第一个字符串的地址。
#include <stdio.h>
#include "string.h"
#define MSG " Hello world"
#define SIZE 80
int main(void){
char flower[SIZE];
puts("please enter a flower:");
gets(flower);
strcat(flower, MSG);
printf("%s\n", flower); //iris Hello world
return 0;
}
5.3 strncat()函数
strcat()函数无法检查第一个数组是否容纳第二个数组。如果分配给第一个数组的空间不够大,多出来的字符溢出到相邻存储单元时就会出问题。
strncat()函数该函数的第三个参数指定了最大添加字符数。例如:strncat(flower,MSG, 4)
将把MSG字符串的内容附加给flower,在加到第4个字符或遇到空字符停止。因此算上空字符flower数组应该足够大。
#include <stdio.h>
#include "string.h"
#define MSG " Hello world"
#define SIZE 40
int main(void){
char flower[SIZE];
puts("please enter a flower:");
gets(flower);
strncat(flower, MSG, 8);
printf("%s\n", flower); // iris Hello w
return 0;
}
5.4 strcmp()和strncmp()函数
两个函数都是比较两个字符串参数是否相同。如果两个字符串相同则返回0,否则返回非零值。
5.4.1 strcmp()函数
该函数要比较的是字符串的内容,不是字符串的地址。
#include <stdio.h>
#include "string.h"
#define MSG "iris"
#define SIZE 40
int main(void){
char flower[SIZE];
puts("please enter a flower:");
gets(flower);
// 返回值为0或非零值
int bl = strcmp(flower, MSG);
printf("%d\n", bl);
return 0;
}
strcmp()函数比较的是字符串,不是整个数组。虽然数组占用了40个字节,而存储在其中的iris只占用了5个字节(还有一个空字符),strcmp()函数只会比较空字符前面的部分。
注意:strcmp()函数比较的是字符串,不是字符。
5.4.2 strncmp()函数
strncmp()函数比较字符串中的字符,直到发现不同的字符为止,这一过程可能会持续到字符串的末尾。而strncmp()函数在比较两个字符串时,可以比较到字符不同的地方,也可以只比较第三个参数指定的字符数。
#include <stdio.h>
#include "string.h"
#define MSG "iris"
#define SIZE 40
int main(void){
char flower[SIZE];
puts("please enter a flower:");
gets(flower);
int bl = strncmp(flower, MSG, 4);
printf("%d\n", bl);
return 0;
}
5.5 strcpy()和strncpy()函数
5.5.1 strcpy()函数
如果要拷贝整个字符串,要使用strcpy()函数。
strcpy()函数第二个参数指向的字符串被拷贝至第一个参数指向的数组中。拷贝出来的字符串被称为目标字符串,最初的字符串被称为源字符串。即第一个是目标字符串,第二个是源字符串。
#include <stdio.h>
#include "string.h"
#define MSG "iris"
#define SIZE 40
int main(void){
char flower[SIZE];
puts("please enter a flower:");
gets(flower);
puts(flower);
strcpy(flower, MSG);
printf("%s\n", flower);
return 0;
}
strcpy()函数的其他属性:
- strcpy()的返回类型是char *,该函数返回的是第一个参数的值,即一个字符的地址。
- 第一个参数不必指向数组的开始。这个属性可用于拷贝数组的一部分。
5.5.2 strncpy()函数
strcpy()函数和strcat()函数无法检查第一个数组是否容纳第二个数组。如果分配给第一个数组的空间不够大,多出来的字符溢出到相邻存储单元时就会出问题。
拷贝字符串用strncpy()函数更安全,该函数的第3个参数指明可拷贝的最大字符数。
#include <stdio.h>
#include "string.h"
#define MSG "iris"
#define SIZE 40
int main(void){
char flower[SIZE];
puts("please enter a flower:");
gets(flower);
puts(flower);
strncpy(flower, MSG, 2);
printf("%s\n", flower);
return 0;
}
5.6 sprintf()函数
sprintf()函数声明在<stdio.h>中,而不是"string.h"中。该函数和printf()类似,但是它是把数据写入字符串,而不是打印在显示器上。因此,该函数可以把多个元素组合成一个字符串。sprintf()函数的第一个参数是目标字符的地址。其余参数和printf()相同,即格式字符串和代写入项的列表。
#include <stdio.h>
#define MSG "iris"
#define SIZE 40
int main(void){
char flower[SIZE];
char pt[2 * SIZE + 10];
puts("please enter a flower:");
gets(flower);
puts(flower);
sprintf(pt, "%s, %s", MSG, flower);
printf("%s\n", pt);
return 0;
}