13 字符串

13.1 字符串字面量

说明:用一对双括号括起来的字符序列。

13.1.1 字符串字面量中的转义序列

说明:char型字面量中能够使用的转义字符字符串字面量中都可以使用。
注意:数字转义字符并不常用,但使用时会需要注意一些char中不会遇到的问题

数字转义字符 格式 字符串字面量额外规则
八进制转义字符 \最多含有3位数字的八进制数字 在3个数字之后结束,或者在第一个非八进制数字符处结束。
十六进制转义字符 \x十六进制数 没有字数限制,直到第一个非十六机制数字符截止(通常还限制十六进制数大小为\x0~\x7f\x0~\xff
1
2
3
4
5
6
7
//字符串字面量中使用8进制转义字符
"\1234"//2个字符(\123和4)
"\189"//3个字符(\1,8,9)

//字符串字面量中使用16进制转义字符
"Z\x81rich"//6个字符(Z, \81, r, i, c, h)
"\x81ber"//2个字符(\x81be和r)

13.1.2 延续字符串字面量

方式一:用字符\结尾

说明:只要在一行用字符\结尾,那么c语言就允许在下一行延续字符串字面量。
注意:除了(看不见)的末尾的换行符,在同一行不可以有其他字符跟在\后面。
扩展:不只字符串,字符\还可以用来分隔任何长的符号。
缺点:字符字面量必须从下一行的起始位置继续,破坏了程序的缩紧结构。

1
2
printf("Put a disk in drive A,then \
press any key to continue\n");

方式二:字面量自动合并(c标准)

说明:当两条或更多条字符串字面量相连时(仅用空白字符分割),编译器必须把他们合并成单独一条字符串。
优点:不必破坏缩紧。

1
2
printf("Put a disk in drive A, then"
"press any key to continue\n");

13.1.3 如何存储字符串字面量

存储形式:c语言把字符串字面量作为字符数组来处理,相当于char *
空字符(\0):ASCII字符集中的第一个字符。
内存分配:当c语言编译器在程序中遇到长度为n的字符串字面量时,它会为字符串字面量分配长度为n+1的内存空间,用来存储字符串子民啊量中的字符,以及额外的一个字符——空字符

1
printf("abc");//当调用printf时,会传递"abc"的地址(即指向字母`a`存储单元的指针)

13.1.4 字符串字面量的操作

说明:可以将字符串字面量赋值给char *指针。
特点:

  • c语言允许指针添加下标,因此可以给字符串字面量添加下标
1
2
char ch;
ch = "abc"[1];//b
  • 允许改变字符串字面量的字符(不推荐)
1
2
3
//一些编译器可能会出现异常
char *p = "abc";
*p = 'b';//字符串字面量被修改为"bbc"
1
2
char *p;
p = "abc";//这个赋值操作不是复制"abc"中的字符,而仅仅是使用p指向字符串的第一个字符。

13.1.5 字符串字面量与字符常量

说明:只包含一个字符的字符串字面量不同于字符常量。

例子 类型 特点
"a" 字符串字面量 用指针来表示
'a' 字符常量 用整数(字符的ASCII码)来表示
1
2
3
printf("\n");//合法,相当于传递`char *`指针作为参数

printf('\n');//会报错,传递的不是指针而是整数

13.2 字符串变量

存储方式:char str[STR_LEN+1]

  • 载体为一维的字符数组
  • 以空字符串结尾(数组的长度比字符串的长度多一个字符)

注意:如果没有给空字符预留位置,可能导致程序运行时出现不可预知的结果,因为c函数库中的函数假设字符串都以空字符串结束。
技巧:字符串的长度取决于空字符的位置,而不是取决于存放字符串的字符数组的长度。

1
2
#define STR_LEN 80
char str[STR_LEN+1]

13.2.1 初始化字符串变量

两种字面量:字符串字面量数组字初始化式

字符串

底层:c编译器会把它看成是数组初始化式的缩写形式。
规则:

  • 如果初始化式太短以至于不能填满字符串变量时,c编译器会讲多出的部分都赋值为\0
  • 如果没有空间给空字符串,将使数组无法作为字符串使用

注意:一定要确保数组的长度要长于初始化式的长度。

1
2
//编译器将把字符床"June 14"中的字符复制到数组date1中,然后追加一个空字符串从而使date1可以作为字符串使用
char date[8] = "June 14";
J u n e 1 4 \0 \0

数组

规则:如果初始化式比本身短,会把余下的字符数组元素初始化为\0

1
2
//数组长度可以省略由编译器自己计算,手工计算很容易出错
char date1[8] = {'J', 'u', 'n', 'e', ' ', '1', '4', '\0'};

13.2.2 字符数组和字符指针

说明:数组变量有两种

字符数组 字符指针
声明 char varStr[] char *varStr
元素是否可修改 否(因为指针指向的是不可修改的字符串)
变量本身是否可改变指向 否(数组名和数组绑定在一起且无法改变指向)

注意:声明字符指针必需指向字符数组后才能使用,无论是字面量(两种形式的字面量)还是已经声明好的字符数组。

1
2
char *p, str[STR_LEN+1];
p = srt;//p指向了str的第一个字符

13.3 字符串的读/写

13.3.1 用printf函数和puts函数写字符串

13.3.1.1 printf函数

说明:转换说明为%[m][.p]s,下面分3中情景讨论。


情景1:%s(不限制宽度,不截断)

情景 表现
末尾提供了空字符 逐个写字符直到遇到空字符
末尾没有空字符 会越过字符串的末尾继续写,直到最终在内存的某个地方找到空字符为止

情景2:%ms(限制宽度,不截断)

m大小 表现 备注
>字符串长度 多出部分显示空格,字符串右对齐 m前使用-强制左对齐
<字符串长度 忽略设置的m,显示整个字符串 不会截断

情景3:%m.ps(限制宽度,截断)
会使字符串的前p个字符在大小为m的区域内显示。

1
2
3
4
5
char str[] = "Are we having fun yet?";

printf("Value of str:%s\n", str);//Are we having fun yet?

puts(str);////Are we having fun yet?(\n)

13.3.1.2 put函数

参数:需要显示的字符串
说明:

  • 不使用转换说明和格式串
  • 在写完字符串后,puts函数总会添加一个额外的换行符

13.3.2 用scanf函数和gets函数读字符串

13.3.2.1 scanf函数

说明:

  • 不需要在str前添加运算符&,因为str是数组名,编译器会自动把它当作指针来处理
  • 空白符(换行符、空格符、制表符)会使scanf函数停止读入,因此用scanf输入的字符串永远不会包含空白符
  • scanf函数始终会在字符串末尾存储一个空字符(否则无法当作正常字符串使用)

注意:scanf不会检测何时填满数组,可能会越过数组边界,导致异常。
技巧:使用%ns代替%s可以使scanf函数更安全(n 指可以存储的最大字符的数量)

限制:通常不用于读入一整行输入。

13.3.2.2 gets函数

说明:类似scanf函数,把读入的字符放在数组中,然后存储一个空字符

  • 不会在开始读字符串之前跳过空白字符(scanf函数会跳过)
  • 会持续读入直到找到换行符(不是任意空白符)

扩展:gets函数天生就是不安全的,fgets函数是更加安全的选择。

1
2
3
4
char sentence[STR_LEN+1];

printf("Enter a sentence:\n");//To , or not to c
gets(sentence);//sentence的值为"To , or not to c"

13.3.3 逐个字符读字符串

说明:利用getchar自定义更加灵活的输入函数
编程思路:

  1. 在开始存储字符串之前,函数应该跳过空白字符吗?
  2. 什么字符会导致函数停止读取:换行符、任意空白字符、还是其他一些字符?需要存储这类字符还是忽略掉?
  3. 如果输入的字符串太长以致无法存储,那么程序应该做些什么:胡咧额外的字符,还是把它们留给下一次的操作输入?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 不会跳过空白符,在第一个换行符处(不把换行符存储到字符串中)停止读取,
* 并且忽略额外的字符。
*/

int read_line(char str[], int n){
char ch;
int i = 0;
while((ch = getchar()) != '\n'){
if(i < n){
str[i++] = ch;
}
}
str[i] = '\0';
return i;
}

13.4 访问字符串中的字符

说明:访问字符串中的字符存在两种方式

  1. 数组下标
  2. 指针

13.4.1 用数组下标

说明:既然字符串是以数组的方式存储的,那么可以使用下标来访问字符串中的字符。
const形式参数使用const修饰可以防止数组被修改。

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 使用数组下标遍历数组计算数组中空格的数量
*/

int count_spaces(const char s[]){
int count = 0, i;
for(i = 0; s[i] != '\0'; i++){
if(s[i] == ' '){
coun++;
}
return count;
}
}

13.4.2 用指针

说明:使用指针代替数组下标访问字符串中的字符会更加便捷。
const:用const修饰字符串指针可以避免传进来的实参的指向被改变,即便如此,因为传进来的指针的副本,所以可以可以自增。

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 使用指针遍历数组计算数组中空格的数量
*/

int count_spaces(const char *s{
int count = 0;
for(; s != '\0'; s++)
{

if(*s == ' '){
coun++;
}
return count;
}
}

13.5 使用c语言的字符串库

说明:c语言的函数库为字符串的操作提供了丰富的函数集,包含在string.h中。
参数:

  • 每个函数至少需要一个字符串作为实际参数
  • 字符串形式参数类型可以是char *
  • 合法的实际参数类型:字符数组、char *类型变量、字符串字面量

技巧:

  • 形式参数没有声明为const的函数会在调用函数时修改形式参数,因此对应的实际参数不能为字符串字面量(字符串字面量的特点在于不可修改)。
1
#include <string.h>

13.5.1 strcpy函数

背景:不能使用赋值运算符将字符串字面量赋值给字符数组(但字符串指针可以)。因为数组名在c语言中是不能作为左值使用的。

1
2
3
4
5
6
7
8
//字符数组形式声明的字符串
char strArr[] = {'a', 'b', 'c', '\0'};
//指针方式声明的字符串
char *strPointer = "def";

// strArr = "aaa";//这种方式会报错
strPointer = "bbb";
printf("%s\n", strPointer);

原型:char *strcpy(char *s1, const char *s2)
用途:s2指向的字符串复制到s1指向的数组中。
返回值:被赋值的字符串的首地址,可以被用来实现多重赋值。

1
strcpy(s1, strcpy(s2, "abc"));//将abc复制给s1和s2

注意:

  • 函数会将s2指向的字符串逐个复制过去直到遇到一个空字符为止,因此假设s2长度为n,如果s1长度小于n包含(\0),s1后面的内存也会被覆盖
  • 因为不会修改s2指向的字符串,因此s2被声明为const
1
2
3
4
5
6
7
8
//字符数组形式声明的字符串
char strArr[] = {'a', 'b', 'c', '\0'};
//指针方式声明的字符串
char *strPointer = "def";

// strArr = "aaa";//这种方式会报错
strPointer = "bbb";
printf("%s\n", strPointer);

13.5.2 strcat函数

原型:char *strcats(char *s1, const char *s2)
说明:s2指向的字符串追加到s1指向的字符串的后面
返回值:s1指向的字符串(指针)
注意:如果s1指向的数组的大小不足以容纳所有的字符,将会多余的字符复制到数组后面的内存,到处错误。

1
2
3
strcpy(s1, "abc");
strcpy(s2, "def");//"abcdefhgi"
strcpy(s1, strcat(s2, "ghi"));//"defhgi"

13.5.3 strcmp函数

原型:int strcmp(const char *s1, const chat *s2)
说明:比较两个字符串的大小

返回值 说明
小于0 s1 < s2
0 s1 == s2
大于0 s1 > s2

底层:依据对应ASCII字符集的大小

  • 字典顺序
  • i个字符相同,第i+1个字符大的大(若其中一个没有第i+1个字符,则字符数多的大)
  • 所有大写字母(65~90)都小于所有小写字符(97~122 )
  • 数字(48~57)小于字母
  • 空格符(32)小于所有打印字符

13.5.4 strlen函数

原型:size_t strlen(const char *s)
说明:求字符串的长度,即字符串中第一个空字符串前的字符的个数(不包括空字符)。
size_ t:无符号整数类型(unsigned int 或 unsigned long int ),在c函数库定义的。

注意:当用数组作为函数的实际参数时,strlen函数不会测量数组本身的长度,而是返回存储在数组中的字符串的长度。

1
2
3
4
5
int len;
fen = strlen("abc");//3
len = strlen(" ");//0
strcpy(str1, "abc");
len = strlen(str1);//3

13.5.5 程序:显示一个月的提示列表

1
2
3
4
5
6
7
8
9
10
$./remind
Enter day and reminder:1 dd
Enter day and reminder:2 ff
Enter day and reminder:3 ff
Enter day and reminder:0

Day Reminder
1 dd
2 ff
3 ff
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
/**
* Prints a one-month reminder list
* 程序需要读入一系列天和提示的组合(记录备忘列表),并且按照顺训进行存储(按日期排序)
* 额外需求:
* 1. 天在两个字符的域中右对齐
* 2. 确定用户没有输入两位以上的数字
*/

#include <stdio.h>
#include <string.h>

#define MAX_REMIND 50//备忘录数量
#define MSG_LEN 60//每条备忘的最大长度

int read_line(char str[], int n);

int main(){
//备忘录列表
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:");

//把日期读入到整形变量day中,即使输入更多的数字,在%与d之间的数2也会通知scanf函数在读入两个数字后停止
scanf("%2d", &day);

//如果日期输入0,则停止循环,不在输入任何备忘
if(day == 0){
break;
}

//把day的值转换为字符串并写到day_str中
sprintf(day_str, "%2d", day);//day_str会包含一个空字符结尾的合法字符串
read_line(msg_str, MSG_LEN);//读入备忘信息

//找到备忘录的位置(根据日期从小到大排序的位置)
for(i = 0; i < num_remind; i++){
//确定这一天的位置
if(strcmp(day_str, reminders[i]) < 0){
break;
}
}

//将包括该位置之后的备忘信息向后移动
for(j = num_remind; j > i; 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 < num_remind; i++){
printf("%s\n", reminders[i]);
}
return 0;
}

/**
* 读入一行
* @param str 存储字符串的位置
* @param n 字符串长度上限
* @return 该条字符串的长度
*/

int read_line(char str[], int n){
char ch;
int i = 0;

while((ch = getchar()) != '\n'){
if(i < n){
str[i++] = ch;
}
}
str[i] = '\0';
return i;
}

13.6 字符串惯用法

13.6.1 搜索字符串的结尾

相关惯用法:

  1. while(*s){s++;}
  2. while(*s++){...} ;

13.6.1.1 原始版本

思路:从左到右扫描字符串,n自增。当s最终指向一个空字符串时,n的长度就是字符串的长度。

1
2
3
4
5
6
7
size_t strlen(const char *s){
size_t n;
for(n = 0; *s != '\0'; s++){
n++;
}
n++;
}

13.6.1.2 优化版本

思路:

  1. 空字符的ASCII码值为0,而0在c语言中可以代表“假”
  2. 字符串是被当作字符数组来处理的,而数组不同元素的存储地址之差和下标之差相同,因此可以通过地址之差计算数组长度,避免了频繁的自增操作,从而提高速度
1
2
3
4
5
6
7
size_t strlen(const chat *s){
const char *p = s;
while(*s){
*s++;
}
return s - p;
}

13.6.2 复制字符串

惯用法:字符串复制惯用法

1
2
while(*p++ = *s2++)
;

13.6.2.1 原版

说明:自定义实现strcat函数
思路:

  1. 查找字符串s1末尾空字符串的位置,并使指针p指向它
  2. 把字符串s2中的字符逐个复制到p所指向的位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
char *strcat(char *s1, const chat *s2){
char *p;
p = s1;
//1. 查找字符串s1末尾空字符串的位置,并使指针p指向它
while(*p != '\0'){
p++;
}
//2. 把字符串s2中的字符逐个复制到p所指向的位置
while(*s2 != '\0'){
*p = *s2;
p++;
s2++;
}
*p = '\0';
return s1;
}

13.6.2.2 优化版

1
2
3
4
5
6
7
8
9
10
11
char *strcat(char *s1, const char *s2){
char *p = s1;
//1. 查找字符串s1末尾空字符串的位置,并使指针p指向它
while(*p){
p++;
}
//2. 把字符串s2中的字符逐个复制到p所指向的位置
while(*p++ = s2++)
;
return s1;
}

13.7 字符串数组

两种方式:二维字符数组一维指针数组
技巧:得益于指针和数组之间的紧密联系,访问一维指针数组中的元素的方式和访问而为字符数组中元素的方式相同。

二维字符数组

缺点

  1. 需要知道所有字符串的长度,以最长的字符串的长度来确定二位数组每一行的长度。
  2. 不能填满数组的一整行的字符会用空字符填补,浪费空间。
1
char planets[][8] = {"Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto"};

Alt text

一维字符指针数组

说明:大部分字符串集都是长短字符串的混合,c语言本身不提供“参次不齐的数组”,但可以通过字符数组指针的数组模拟。
参次不齐的数组(ragged array):数组的每一行有不同的长度。

1
2
3
4
5
6
7
m

for(i = 0; i < 9; i++){

if(planets[i][0] == 'M'){
printf("%s begins with M\n", planets[i]);
}
}

Alt text

13.7.1 命令行参数

程序参数(command-line atgument):不仅是操作系统命令,所有程序都有命令行信息。Q&A为了能够访问这些命令行参数,必须为main函数定义为含有两个参数的函数,这两个参数通常命名为argcargv
空指针:是一种不指向任何内容的特殊指针。宏NULL代表空指针。
参数向量(argv)和参数计数(argc):

  • argv[0]:指向程序名的字符串
  • argv[1]~argv[argc-1]:余下的命令行参数
  • argv[argc]:指向空指针。
1
2
3
4
5
6
7
/**
*argc:参数计数,命令行参数的数量(包括程序名本身)
*argv: 参数向量,指向命令行参数的指针数组(以字符串的行书存储)
*/

int main(int argc, char *argv[]){
...
}

Alt text

命令行输入参数案例

1
$ ls -l remind.c

获取参数代码(利用数组)

1
2
3
4
int i;
for(i = 1; i<argc; i++){
printf("%s\n", argv[i]);
}

获取参数代码(利用指针)

1
2
3
4
char **p;//p用来指向argv中的字符串,因为argv本身作为数组就是指针,而其中的字符串(字符数组)也是指针,所以声明p为指针的指针
for(p = &argv[1]; *p != NULL; p++){
printf("%s\n", *p);
}

13.7.2 程序:核对行星的名字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include <string.h>
#define NUM_PLANETS 9//行星的数量

int main(int argc, char const *argv[])
{

//9大行星
char *planets[] = {"Mercury", "Venus", "Eath", "Mars", "Jupiter", "Saturn", "Uranus", "Nepture", "Pluto"};

//以此访问每个参数,并遍历9大行星寻找匹配的目标
int i, j;
for(i = 1; i < argc; i++){
for(j = 0; j < NUM_PLANETS; j++){
if(strcmp(argv[i], planets[j]) == 0){
printf("%s is planets %d\n", argv[i], j+1);
break;
}
if(j == NUM_PLANETS){
printf("%s is not a planet\n", argv[i]);
}
}
}

return 0;
}
1
2
$ ./planet Pluto  
Pluto is planets 9