在字符串和格式化输入/输出一节中我们讲过,字符串是以空字符 “ \0 ”结尾的char类型的数组。由于字符串十分常用,所以C提供了许多专门处理字符串的函数。下面详细讲述字符串的性值、如何声明并初始化字符串、在程序中输入和输出字符串以及如何操控字符串。
#include
#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()函数只显示字符串(不能打印数字),而且自动在显示的字符串末尾加上换行符。
在上面的程序中,有三种定义字符串的方式。分别是:
#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);
用双引号括起来的内容称为字符串字面量(string literal),也叫做字符串常量。双引号中的字符和编译器自动加入末尾的 “ \0 ”字符,都作为字符串储存在内存中。
如果字符串字面量之间没有间隔或者用空白字符间隔,C会将其视为串联起来的字符串字面量
#include
int main(void){
char msg[50] = "Hello""World" "LOVE C";
puts(msg); // 输出结果:HelloWorldLOVE C
return 0;
}
等价于:
#include
int main(void){
char msg[50] = "HelloWorldLOVE C";
puts(msg); // 输出结果:HelloWorldLOVE C
return 0;
}
如果要在字符串中打印出双引号则需要加上转义符 “ \ ”。
#include
int main(void){
char msg[50] = "Hello \"World\" LOVE C";
puts(msg); // 输出结果:Hello "World" LOVE C
return 0;
}
字符串常量属于静态存储类别,说明如果在函数中使用字符串常量,该字符串只会被存储一次,在整个程序的生命周期内存在,即时函数被调用多次。
定义字符串数组时,必须让编译器知道需要多少空间。一种方法是用足够空间的数组存储字符串。如下:
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之后是变长数组
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
#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;
}
两者的主要区别在于:数组名ar是常量,而指针名pt是变量。
两者中只有指针表示法可以进行递增操作。
数组的元素是变量(除非数组被声明为const),但是数组名不是变量。
建议指针初始化字符串时使用const限定符:
const char *pt = "LOVE C";
总之,如果打算修改字符串,就不要用指针指向字符串字面量
如果要改变字符串或为字符串输入预留空间,不要使用指向字符串字面量的指针。
如果想把一个字符串读入程序,首先必须预留存储该字符串的空间,然后用输入函数获取该字符串。
要做的第一件事就是要为稍后读入的字符串分配足够的空间。
在上述的代码中,编译器给出了警告。在读入name时,name可能会擦写掉程序中的数据或代码,从而导致程序异常终止。因为scanf()要把信息拷贝至参数指定的地址上,而此时该参数是个未初始化的指针,name可能指向任何地方。
解决上述问题最简单的方法是在声明时指明数组的大小:
char name[40];
在读取字符串时,scanf()和转换说明%s只能读取一个单词。可在程序中经常要读取一整行输入,而不仅仅是一个单词。许多年前,gets()函数就用于处理这种情况。
gets()函数简单易用,它读取整行输入,直到遇到换行符,然后丢弃换行符,存储其余字符,并在这些字符的末尾添加一个空字符使其成为一个C字符串。
#include
#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()函数。
fgets()函数通过第二个参数限制读入的字符数来解决溢出的问题。该函数专门设计用于处理文件输入。所以一般情况下不太好用。fgets()和gets()的区别如下:
1.fgets()函数的第二个参数指明了读入字符的最大数量。如果这个值是n,那么fgets()将读入n-1个字符,或者读到遇到的第一个换行符。
2. 如果fgets()读到一个换行符,会把它储存在字符串中。这点与gets()不同,gets()会丢弃换行符。
3. fgets()函数的第三个参数指明要读入的文件。如果读入的从键盘输入的数据,则以stdin作为参数,该标识符定义在stdio.h中。
因为fgets()函数把换行符放在字符串的末尾(假设输入行不溢出),通常要与fputs()函数(和puts()类似)配对使用,除非该函数不在字符串末尾添加换行符。fputs()函数的第二个参数指明它要写入的文件。如果要显示在计算机显示器上,应使用stdout作为参数。
#include
#define SIZE 4
int main() {
char words[SIZE];
puts("please enter a string:");
fgets(words, 4, stdin);
fputs(words,stdout);
return 0;
}
C11新增的get_s()函数(可选)和fgets()类似,用一个参数限制读入的字符数。两者的主要区别在于:
#include
#define SIZE 14
// 需要在C11中执行
int main() {
char words[SIZE];
puts("please enter a string:");
gets_s(words, 4);
fputs(words,stdout);
return 0;
}
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()
puts()函数很容易使用,只需把字符串的地址作为参数传递给它即可。
#include
#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
#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不是一个字符串,因为缺少一个表示结束的空白符。
fputs()函数是puts()针对文件定制的版本。它们的区别如下:
和puts()函数一样,printf()也把字符串的地址作为参数,printf()函数用起来没有puts()函数那么方便,但是它更加多才多艺,因为它可以格式化不同的数据类型。
与puts()不同的是,printf()不会自动在每个字符串的末尾加上换行符。因此,必须在参数中指明应该在哪里使用换行符。
C库提供了多个处理字符串的函数,ANSI C把这些函数的原型放在
strlen()函数用于统计字符串长度。
#include
#include "string.h"
#define MSG "Hello world"
int main(void){
//"%llu"为unsigned long long int
printf("%llu", strlen(MSG));
return 0;
}
strcat()(用于拼接字符串)函数接收两个字符串作为参数。该函数把第2个字符串的备份附加在第一个字符串的末尾,并把拼接后形成的新字符串作为第一个字符串,第二个字符串不变。strcat()函数类型是char *(即指向char的指针)。strcat()函数返回第一个参数,即拼接第二个字符串后的第一个字符串的地址。
#include
#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;
}
strcat()函数无法检查第一个数组是否容纳第二个数组。如果分配给第一个数组的空间不够大,多出来的字符溢出到相邻存储单元时就会出问题。
strncat()函数该函数的第三个参数指定了最大添加字符数。例如:strncat(flower,MSG, 4)
将把MSG字符串的内容附加给flower,在加到第4个字符或遇到空字符停止。因此算上空字符flower数组应该足够大。
#include
#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;
}
两个函数都是比较两个字符串参数是否相同。如果两个字符串相同则返回0,否则返回非零值。
该函数要比较的是字符串的内容,不是字符串的地址。
#include
#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()函数比较的是字符串,不是字符。
strncmp()函数比较字符串中的字符,直到发现不同的字符为止,这一过程可能会持续到字符串的末尾。而strncmp()函数在比较两个字符串时,可以比较到字符不同的地方,也可以只比较第三个参数指定的字符数。
#include
#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;
}
如果要拷贝整个字符串,要使用strcpy()函数。
strcpy()函数第二个参数指向的字符串被拷贝至第一个参数指向的数组中。拷贝出来的字符串被称为目标字符串,最初的字符串被称为源字符串。即第一个是目标字符串,第二个是源字符串。
#include
#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()函数和strcat()函数无法检查第一个数组是否容纳第二个数组。如果分配给第一个数组的空间不够大,多出来的字符溢出到相邻存储单元时就会出问题。
拷贝字符串用strncpy()函数更安全,该函数的第3个参数指明可拷贝的最大字符数。
#include
#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;
}
sprintf()函数声明在
#include
#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;
}