英文原版:P277
本章将介绍处理字符串的便捷方法,包括字符串字面量和字符串变量,其中字符串变量会随着程序的执行而改变。
- 13.1节介绍有关字符串字面量的规则,包括如何在字符串字面量中嵌入转义序列,如何分隔较长的字符串字面量。
- 13.2节介绍如何声明字符串变量:字符串变量就是字符数组,最后一个字符是空字符
null
,用来标记字符串的结束。 - 13.3节介绍了若干种读写字符串的方法。
- 13.4节描述了如何编写函数来处理字符串。
- 13.5节介绍了在C语言库里的一些字符串处理函数。
- 13.6节列举了处理字符串时常用的编程约定。
- 13.7节描述了如何创建数组元素是指向不同长度字符串的指针的数组,及介绍了在C语言中如何使用这类数组来为程序提供命令行的信息。
13.1节 字符串字面量
字符串字面量就是用双引号
""
括起来的字符序列。
理解字符串字面量
- C语言将字符串字面量当做字符数组。
- C编译器认为字符串字面量的类型为
char *
。 -
"abc"
是一个指针,该指针指向一个内存位置,该内存位置包含字符abc,及一个额外的空字符null
,用于标记该字符串字面量的结束。 -
"abc"
等价于初始化式{'a', 'b','c', '\0'}
; - 经常作为格式化字符串出现在
scanf
和printf
的调用中;
例1 字符串字面量
"When you come to a fork in the road, take it."
13.1.1节 在字符串字面量中的转义序列
C语言允许字符串字面量中像字符常量一样包含转义序列。
例2 在字符串字面量中出现转义序列
"Candy\nIs dandy\nBut liquor\n Is Quicker.\n --Ogden Nash\n"
注意:
- 虽然八进制数和十六进制数的转移序列在字符串字面量中是合法的,但是它们没有字符转义序列常用。
- 八进制数的转义序列会在3个数字后或者首个非8进制字符处结束;比如"\1234"包含两个字符(\123和4),"\189"包含3个字符(\1、8、9)。
- 对16进制的转义序列的长度没有限制,它会持续直到出现首个非16进制字符处结束;比如"\xfc"包含一个字符,"Z\fcrich"包含6个字符(Z、\fc、r、i、c、h),"\xfcber"包含2个字符(\xfcbe、r)。
13.1.2节 延续字符串常量
方法一:首行以反斜线\
结尾,且字符串必须从下一行的起始处开始。
例3 使用反斜线来分隔较长的字符串字面量
printf("When you come to a fork in the road, take it. \n
--Yogi Berra");
方法二:仅使用空格来分隔两个或者多个毗邻的字符串字面量
例4 使用空格来分隔多个字符串常量
printf("When you come to a fork in the road, take it."
"Yogi Berra");
13.1.3节 如何存储字符串字面量
C语言将字符串字面量当做字符数组。
C编译器认为字符串字面量的类型为char *
。
当C编译器遇到一个长度为的字符串字面量时,它会为该字符串分配大小为个字节的存储空间。
这块存储空间包含在字符串字面量里的所有字符,及一个额外的空字符null
,用于标记该字符串字面量的结束。
空字符null
是一个所有位都是0的字节,可用\0
来表示。
注意
区分空字符null
(\0
)和0字符('0'
)
- 空字符
null
的ASCII码值为0; - 0字符的ASCII码值为48;
例5 理解printf
调用时是如何传递字符串字面量的
// printf函数的首个参数值类型就是char *
// 调用printf时,是将字符串字面量"abc"的地址(即指向首字符'a'的内存地址的指针)传递给printf函数。
printf("abc");
13.1.4节 字符串字面量有哪些操作?
C语言允许:在任何可以使用char *
的地方,都可以使用字符串字面量,比如
- 赋值运算符的右边
- 对字符串字面量取下标操作
注意:
尽量避免去修改字符串字面量,因为尝试去修改字符串字面量会导致未定义的行为。
例1 赋值
char *p;
// 赋值并没有拷贝字符串"abc"中的任意字符;
//赋值只是使得p指向该字符串的首个字符
p = "abc";
例2 下标运算
char ch;
//ch的值是字符'b'
ch = "abc"[1];
例3 将0到15的数字转换成等价的十六进制字符
char digit_to_hex_char(int digit)
{
return "0123456789abcdef"[digit];
}
例4 错误示例:修改字符串字面量
char *p = "abc";
//修改字符串字面量可能会导致程序崩溃或者出错
*p = 'd';
13.1.5节 字符串字面量 VS 字符常量
-
'a'
:表示的是一个数字,即该字符的ASCII码; -
"a"
:表示的是一个指针,该指针指向一个内存地址,该内存地址里包含字符a
和空字符null
;
注意:
- 永远不要在需要使用字符串字面量的地方使用字符常量。
例5 错误示例
//这个是非法的
printf('\n');
13.2节 字符串变量
如何存储字符串变量?
- 只要保证最后一个字符是空字符
null
,则任何一个一维字符数组都可用来存储字符串。 - 当声明使用一个字符数组来存储字符串时,通常会使得数组的长度比字符串多一个字符,因为C语言约定每个字符串以空字符
null
结尾。 - 如果没有预留出空字符的位置,则程序的行为是不可预测的,因为在C语言库里的函数都假设字符串是以
null
结尾的。
缺点:
- 很难区分字符串数组是否被当做字符串来使用;
- 如果要编写自己的字符串处理函数,则需要特别小心地处理空字符
null
; - 除了一个接一个字符地遍历搜索外,没有更快地方法来判断一个字符串的长度。
例1 存储一个最多有80个字符的字符串
#define STR_LEN 80
char str[STR_LEN+1];
解释:
- 由于字符串在结尾处需要一个空字符
null
,则需声明一个长度为81的字符数组。 - 声明一个字符数组的长度为
STR_LEN+1
,并不意味着该字符数组包含一个长度为STR_LEN
的字符; - 一个字符串的长度取决于空字符
null
的位置,而不是存储字符串的数组的大小; - 一个长度为
STR_LEN+1
的字符数组可持有的字符串的长度的范围是0到STR_LEN
,对应的是空字符串到长度为STR_LEN
的字符串;
13.2.1节 字符串变量初始化
- 初始化式的长度+1等于字符数组长度:正常存储,以空字符
null
结束 - 初始化式的长度小于字符数组长度:以空字符
null
结束,并在末尾处补\0
- 初始化式的长度大于字符数组长度:由于没给空字符
null
预留位置,则忽略空字符null
- 如果初始化式的长度太长的话,就省略一个字符串变量的长度,因为数长度很容易出错
例1 初始化式的长度+1等于字符数组长度
char date1[8] = "June 14";
例2 初始化式的长度小于字符数组长度
char date2[9] = "June 14";
例3 初始化式的长度大于字符数组长度
char date3[7] = "June 14";
例4 省略字符串数组的长度
char date4[] = "June 14"
解释:C编译器会自动计算长度,为date4
分配8个字符的空间,够存储在字符串"June 14"
里的字符,加1个空字符null
。
13.3.2节 字符数组 VS 字符指针
例1 比较字符和数组和字符指针
char date[] = "June 14";
char *date = "June 14";
字符数组和字符指针的不同之处:
- 在数组版本里,存储在
date
里的字符是可被修改的。在指针版本里,date
指向的是一个字符串字面量,不应该被修改。 - 在数组版本里,
date
是一个数组名。在指针版本里,date
是一个变量,该变量在程序执行过程中可指向其他字符串。
如果我们需要一个字符串可被修改,则有3种方法:
方法一:创建一个字符数组来存储该字符串。
方法二:声明一个字符指针p
,并使p
指向一个字符数组。比如:
char str[STR_LEN+1], *p;
p = str;
方法三:声明一个字符数组,使其之下宁一个动态分配字符串。
注意:
永远不要将一个未初始化的指针变量作为字符串使用。
例1 错误示例
char *p;
//由于指针p没有被初始化,所以如下对p的操作会到导致未定义的行为
p[0] = 'a';
p[1] = 'b';
p[2] = 'c';
p[3] = '\0';
13.3节 字符串的读写
写字符串
-
printf
函数和puts
函数
读字符串
- 在一步内读取一个字符串:
scanf
函数和gets函数
- 一次读取字符串里的一个字符
13.3.1节 使用printf
和puts
来写字符串
printf
函数是如何输出字符串的?
-
printf
函数会逐个输出字符串里的字符,直到遇见空字符null
为止。 - 如果该字符串缺失空字符
null
,则printf
将会越过该字符串的末尾,直到在内存的某处找到空字符null
为止。
puts
函数是如何写字符串的?
- 格式:
puts(str)
; - 在写完字符串后,
puts
函数会额外添加一个换行符;
转换说明%s
:输出整个字符串;
%.ps
:输出字符串的前p个字符;
%ms
:在大小为m的域内显式字符串
- 如果字符串的长度大于m,则会输出整个字符串;
- 如果字符串的长度小于m,则会右对齐输出字符串;
%-ms
:在大小为m的域内显式字符串
- 如果字符串的长度大于m,则会输出整个字符串;
- 如果字符串的长度小于m,则会左对齐输出字符串;
%m.ps
:在大小为m的域中输出字符串的前p个字符
例1 使用%s
来写字符串
#include
int main(void)
{
char str[] = "Are we having fun yet?";
printf("printf函数效果展示\n");
printf("%s\n", str);
printf("%.6s\n", str);
printf("%10s\n", str);
printf("%24s\n", str);
printf("%-24s\n", str);
printf("%24.6s\n", str);
printf("%-24.6s\n", str);
puts("put函数效果展示");
puts(str);
return 0;
}
输出
printf函数效果展示
Are we having fun yet?
Are we
Are we having fun yet?
Are we having fun yet?
Are we having fun yet?
Are we
Are we
put函数效果展示
Are we having fun yet?
13.3.2节 使用scanf
和gets
函数来读字符串
转换说明%s
允许scanf
将字符串读入字符数组。
scanf
是如何读字符串的?
- 当调用
scanf
时,scanf
会跳过空白字符,然后逐个将字符读入字符数组str
,直到遇见空白字符为止。 -
scanf
通常会在字符串的末尾存储一个空字符null
。 - 用
scanf
读取的字符串永远不会包含空白符。 - 通常不使用
scanf
来读取一整行的输入,因为换行符、空格符、tab符会使scanf
停止读入。
gets
函数是如何读字符串的?
-
gets
函数将输入字符读入到一个字符数组中,然后在末尾存储一个空字符null
。
gets
函数和scanf
函数有两点不同:
-
gets
函数在开始读字符串之前不会跳过空白符,而scanf
会跳过; -
gets
函数会逐个读入字符直到发现一个换行符停止,然后忽略换行符,不把换行符存储到数组中,取而代之的是存储一个空字符null
。而scanf
会在任何一个空白符处停止。
例2 比较scanf
函数和gets
函数
#include
#define SENT_LEN 80
//To C, or not to C: that is the question
int main(void)
{
char sentence[SENT_LEN+1];
printf("Enter a sentence:\n");
//scanf将sentence当做是一个指针,因为sentence是一个数组名
scanf("%s", sentence);
printf("%s\n", sentence);
return 0;
}
输出
Enter a sentence:
To C, or not to C: that is the question.
To
输入
#include
#define SENT_LEN 80
//To C, or not to C: that is the question
int main(void)
{
char sentence[SENT_LEN+1];
printf("Enter a sentence:\n");
gets(sentence);
printf("%s\n", sentence);
return 0;
}
输出
Enter a sentence:
warning: this program uses gets(), which is unsafe.
To C, or not to C: that is the question.
To C, or not to C: that is the question.
注意:
- 使用
scanf
和gets
来将字符串读入数组时,scanf
和gets
是没法检测该数组是否已经满了的。因此,scanf
和gets
可能越过字符数组的末尾来存储字符,从而导致未定义的行为。 - 通过在
scanf
中使用%ns
可使得scanf
更安全。 - 一般使用
fgets
替代使用gets
。
13.3.3节 逐个字符地读取字符串
标准库函数没有提供这个功能,需要C程序员手动编写输入函数来实现。
在设计逐个字符读入字符串时,需要考虑3个基本问题:
- 在开始读字符串前是否要跳过空白符?
- 哪些字符会导致读停止:换行符还是任何一个空白符?该字符是被存储在字符串中还是丢掉?
- 如果输入字符串过于长该怎么办:丢掉额外的字符还是留给下一次输入?
例1 实现read_line
函数
目标:
- 不跳过空白字符;
- 在第一个换行符处停止,但不存储该换行符;
- 丢掉额外的字符;
函数原型
int read_line(char str[], int n);
函数定义
int read_line(char str[], int n)
{
int ch, i=0;
while((ch = getchar()) != '\n'){
if (i
13.4节 如何访问字符串中的字符?
由于字符串是存储在字符数组中的,所以可以使用下标来访问字符串中的字符。
例1 统计在字符串中的空白符的个数
下标版本:
int count_spaces(const char s[])
{
int count=0, i;
for(i=0; s[i] != '\0';i++){
if (s[i] == ' ') {
count++;
}
}
return count;
}
指针版本:
int count_spaces(char *s)
{
int count=0;
for(;*s != '\0';s++){
if (*s == ' ') {
count++;
}
}
return count;
}
编写字符串函数时需要思考3个基本问题:
- 访问在字符串中的字符,是用数组操作更好,还是用指针操作更好?
都可以。
C程序员更倾向于使用指针来处理字符串。 - 字符串参数是该声明为数组还是指针?
没有区别,因为编译器会将数组声明当做指针处理。 - 形式参数的格式(
s[]
或者*s
)会对实际参数产生影响吗?
不会,因为调用count_space
函数时,可以传递数组名、指针变量、字符串字面量作为实际参数。
13.5节 如何使用C语言的字符串库?
- 在C语言中,不能使用运算符来处理字符串,因为字符串被当做数组,对数组操作的限制同样适用于字符串,比如不能使用运算符来拷贝和比较字符串等。
- 在C语言中可使用字符串库
中提供的函数来对字符串进行操作。 - 进行字符串操作的程序需要使用预处理指令
#include
。
例1 不能使用运算符来拷贝和比较字符串
//错误示例1
char str1[10], str2[10];
//数组名不能用作赋值运算符的左操作数
str1 = "abc";
str2 = str1;
// 来自于12.3节的错误示例2
//可以使用数组名作为指针,但不能给数组名赋新的值
while(*a != 0){
a++;
}
// 来自于12.3节的正确示例1
//使用数组名作为指针,将a拷贝给一个指针变量p,然后对指针p进行修改
p = a;
while (*p != 0) {
p++;
}
//正确示例2
//可在声明语句中使用等号来给数组初始化
char str1[10] = "abc";
13.5.1 字符串拷贝函数strcpy
和strncpy
函数strcpy
原型:
char *strcpy(char *s1, const char *s2);
使用函数strcpy
的注意事项:
- 在调用
strcpy(str1, str2)
时,函数strcpy
是没法检测str2
指向的字符串跟str1
指向的字符串的长度是否匹配的。 - 如果
str1
指向的是长度为n的字符串,str2
指向的是长度不超过n-1的字符串,则拷贝就会成功。 - 如果
str1
指向的是长度为n的字符串,str2
指向的是长度大于n的字符串,则拷贝就会导致未定义的行为。因为函数strcpy
会一直拷贝直到遇见一个空字符null
停止,所以此时函数strcpy
会越过str1
指向的数组的末尾。
例1 拷贝字符串
#include
#include
int main(void)
{
char str1[10], str2[10];
strcpy(str2, "abc");
printf("%s\n", str2);
strcpy(str1, str2);
printf("%s\n", str1);
return 0;
}
函数strncpy
原型:
char * strncpy(char *s1, const char *s2, int n);
函数strncpy
调用:
strncpy(str1, str2, sizeof(str1));
函数strncpy
使用注意事项:
- 只要
str1
足够大可以持有str2
指向的字符串(包括空字符null
),则拷贝就会成功。 - 如果
str2
指向的字符串的长度大于等于str1
数组,则函数strncpy
会使str1
指向的字符串保存时没有以空字符null
结尾。
例2 拷贝字符串
#include
#include
int main(void)
{
char str1[8], str2[12];
strncpy(str2, "hello world", sizeof(str2)-1);
str2[sizeof(str2) - 1] = '\0';
printf("%s\n", str2);
strncpy(str1, str2, sizeof(str1));
str1[sizeof(str1)-1] = '\0';
printf("%s\n", str1);
return 0;
}
输出
hello world
hello w
13.5.2节 函数strlen
函数原型:
size_t strlen(const char *s);
解释:
- 类型
size_t
是在C语言库中定义的一种typedef
别名,表示的是一类无符号整数。 - 函数
strlen
的返回的是字符串s
的长度:在s
中的第一个空字符null
之前的所有字符的数量。
例3 输出字符串的长度
#include
#include
int main(void)
{
int len;
char str1[10];
len = strlen("abc");
printf("%d\n", len);
len = strlen("");
printf("%d\n", len);
strcpy(str1, "abc");
len = strlen(str1);
printf("%d\n", len);
return 0;
}
输出
3
0
3
函数strcat
函数原型:
char *strcat(char *s1, char *s2);
功能:
函数strcat
将字符串s2
的内容添加到字符串s1
的末尾。
返回值
函数strcat
返回s1
,是一个指向结果字符串的指针。
例1 字符串拼接
#include
#include
int main(void)
{
char str1[10];
char str2[10];
//字符串字面量的拼接
//strcpy(str1, "abc");
//strcat(str1, "def");
//printf("%s\n",str1);
//字符串变量的拼接
strcpy(str1, "abc");
strcpy(str2, "def");
strcat(str1, str2);
printf("%s\n", str1);
return 0;
}
使用strcat
有几个注意事项:
- 如果
str1
指向的数组的长度不够容纳来自str2
的额外的字符时,则调用strcat(str1, str2)
的结果是未定义的。
例2 比较安全地使用字符串拼接:使用函数strncat
strncat(str1, str2, sizeof(str1) - strlen(str1) - 1);
解释:
- 函数
strncat
会让str1
以空字符nul
结尾,该空字符不包含在第三个参数中。 - 第三个参数首先计算在
str1
中的剩余空间,然后再减一预留出位置来存储空字符null
。
函数strcmp
函数原型
int strcmp(const *s1, const *s2);
功能
函数strcmp比较的是字符串s1和s2的内容。
如果字符串s1小于字符串s2,则返回小于0的值;
如果字符串s1等于字符串s2,则返回0;
如果字符串s1大于字符串s2,则返回大于0的值;
函数strcmp比较字符串大小的规则:
- 函数strcmp比较字符串的字符时,实际上比较的是表示该字符的数值码;
- 如果s1和s2的前i个字符都是相同的,但是s1的第(i+1)个字符小于s2的第(i+1)个字符,则s1小于s2。比如"abc"小于"bcd","abd"小于"abe"。
- 如果s1的所有字符都跟s2匹配,但s1的长度小于s2,则s1小于s2。比如"abc"小于"abcd"。
有关ASCII字符集的相关规律:
- 字符序列A-Z、a-z、0-9都有连续的数值码。
- 所有的大写字母都小于小写字母。大写字母的范围是65-90,小写字母的范围是97-122。
- 数字小于字母。数字的范围是48-57。
- 空格符小于所有打印字符。空格符的数值码是32。
示例程序:输出一个月的提醒列表
输出示例:
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"
源文件remind.c
#include
#include
#define MAX_REMIND 50
#define MSG_LEN 60
int read_line(char str[], int n);
// 输出一个月的提醒列表
// 输出示例:
// 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"
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);
//i表示要插入的提醒的位置
//为新输入的提醒查找到合适的插入位置,将原位置的所有提醒统一向后移动一位
for(i=0;ii;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