1
0
wiki/dev/C/字符串.md

601 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
id: 字符串
title: 字符串
sidebar_position: 10
data: 2022年3月30日
---
## 简介
C 语言没有单独的字符串类型,字符串被当作字符数组,即`char`类型的数组。比如字符串“Hello”是当作数组`{'H', 'e', 'l', 'l', 'o'}`处理的。
编译器会给数组分配一段连续内存所有字符储存在相邻的内存单元之中。在字符串结尾C 语言会自动添加一个全是二进制`0`的字节,写作`\0`字符,表示字符串结束。字符`\0`不同于字符`0`,前者的 ASCII 码是0二进制形式`00000000`),后者的 ASCII 码是48二进制形式`00110000`。所以字符串“Hello”实际储存的数组是`{'H', 'e', 'l', 'l', 'o', '\0'}`。
所有字符串的最后一个字符,都是`\0`。这样做的好处是C 语言不需要知道字符串的长度,就可以读取内存里面的字符串,只要发现有一个字符是`\0`,那么就知道字符串结束了。
```c
char localString[10];
```
上面示例声明了一个10个成员的字符数组可以当作字符串。由于必须留一个位置给`\0`所以最多只能容纳9个字符的字符串。
字符串写成数组的形式是非常麻烦的。C 语言提供了一种简写法,双引号之中的字符,会被自动视为字符数组。
```c
{'H', 'e', 'l', 'l', 'o', '\0'}
// 等价于
"Hello"
```
上面两种字符串的写法是等价的,内部存储方式都是一样的。双引号里面的字符串,不用自己添加结尾字符`\0`C 语言会自动添加。
注意,双引号里面是字符串,单引号里面是字符,两者不能互换。如果把`Hello`放在单引号里面,编译器会报错。
```c
// 报错
'Hello'
```
另一方面,即使双引号里面只有一个字符(比如`"a"`也依然被处理成字符串存储为2个字节而不是字符`'a'`存储为1个字节
如果字符串内部包含双引号,则该双引号需要使用反斜杠转义。
```c
"She replied, \"It does.\""
```
反斜杠还可以表示其他特殊字符,比如换行符(`\n`)、制表符(`\t`)等。
```c
"Hello, world!\n"
```
如果字符串过长,可以在需要折行的地方,使用反斜杠(`\`)结尾,将一行拆成多行。
```c
"hello \
world"
```
上面示例中,第一行尾部的反斜杠,将字符串拆成两行。
上面这种写法有一个缺点就是第二行必须顶格书写如果想包含缩进那么缩进也会被计入字符串。为了解决这个问题C 语言允许合并多个字符串字面量只要这些字符串之间没有间隔或者只有空格C 语言会将它们自动合并。
```c
char greeting[50] = "Hello, ""how are you ""today!";
// 等同于
char greeting[50] = "Hello, how are you today!";
```
这种新写法支持多行字符串的合并。
```c
char greeting[50] = "Hello, "
"how are you "
"today!";
```
`printf()`使用占位符`%s`输出字符串。
```c
printf("%s\n", "hello world")
```
## 字符串变量的声明
字符串变量可以声明成一个字符数组,也可以声明成一个指针,指向字符数组。
```c
// 写法一
char s[14] = "Hello, world!";
// 写法二
char* s = "Hello, world!";
```
上面两种写法都声明了一个字符串变量`s`。如果采用第一种写法,由于字符数组的长度可以让编译器自动计算,所以声明时可以省略字符数组的长度。
```c
char s[] = "Hello, world!";
```
上面示例中,编译器会将数组`s`的长度指定为14正好容纳后面的字符串。
字符数组的长度,可以大于字符串的实际长度。
```c
char s[50] = "hello";
```
上面示例中,字符数组`s`的长度是`50`但是字符串“hello”的实际长度只有6包含结尾符号`\0`所以后面空出来的44个位置都会被初始化为`\0`。
字符数组的长度,不能小于字符串的实际长度。
```c
char s[5] = "hello";
```
上面示例中,字符串数组`s`的长度是`5`小于字符串“hello”的实际长度6这时编译器会报错。因为如果只将前5个字符写入而省略最后的结尾符号`\0`,这很可能导致后面的字符串相关代码出错。
字符指针和字符数组,这两种声明字符串变量的写法基本是等价的,但是有两个差异。
第一个差异是,指针指向的字符串,在 C 语言内部被当作常量,不能修改字符串本身。
```c
char* s = "Hello, world!";
s[0] = 'z'; // 错误
```
上面代码使用指针,声明了一个字符串变量,然后修改了字符串的第一个字符。这种写法是错的,会导致难以预测的后果,执行时很可能会报错。
如果使用数组声明字符串变量,就没有这个问题,可以修改数组的任意成员。
```c
char s[] = "Hello, world!";
s[0] = 'z';
```
为什么字符串声明为指针时不能修改,声明为数组时就可以修改?原因是系统会将字符串的字面量保存在内存的常量区,这个区是不允许用户修改的。声明为指针时,指针变量存储的值是一个指向常量区的内存地址,因此用户不能通过这个地址去修改常量区。但是,声明为数组时,编译器会给数组单独分配一段内存,字符串字面量会被编译器解释成字符数组,逐个字符写入这段新分配的内存之中,而这段新内存是允许修改的。
为了提醒用户,字符串声明为指针后不得修改,可以在声明时使用`const`说明符,保证该字符串是只读的。
```c
const char* s = "Hello, world!";
```
上面字符串声明为指针时,使用了`const`说明符,就保证了该字符串无法修改。一旦修改,编译器肯定会报错。
第二个差异是,指针变量可以指向其它字符串。
```c
char* s = "hello";
s = "world";
```
上面示例中,字符指针可以指向另一个字符串。
但是,字符数组变量不能指向另一个字符串。
```c
char s[] = "hello";
s = "world"; // 报错
```
上面示例中,字符数组的数组名,总是指向初始化时的字符串地址,不能修改。
同样的原因,声明字符数组后,不能直接用字符串赋值。
```c
char s[10];
s = "abc"; // 错误
```
上面示例中,不能直接把字符串赋值给字符数组变量,会报错。原因是字符数组的变量名,跟所指向的数组是绑定的,不能指向另一个地址。
为什么数组变量不能赋值为另一个数组原因是数组变量所在的地址无法改变或者说编译器一旦为数组变量分配地址后这个地址就绑定这个数组变量了这种绑定关系是不变的。C 语言也因此规定,数组变量是一个不可修改的左值,即不能用赋值运算符为它重新赋值。
想要重新赋值,必须使用 C 语言原生提供的`strcpy()`函数,通过字符串拷贝完成赋值。这样做以后,数组变量的地址还是不变的,即`strcpy()`只是在原地址写入新的字符串,而不是让数组变量指向新的地址。
```c
char s[10];
strcpy(s, "abc");
```
上面示例中,`strcpy()`函数把字符串`abc`拷贝给变量`s`,这个函数的详细用法会在后面介绍。
## strlen()
`strlen()`函数返回字符串的字节长度,不包括末尾的空字符`\0`。该函数的原型如下。
```c
// string.h
size_t strlen(const char* s);
```
它的参数是字符串变量,返回的是`size_t`类型的无符号整数,除非是极长的字符串,一般情况下当作`int`类型处理即可。下面是一个用法实例。
```c
char* str = "hello";
int len = strlen(str); // 5
```
`strlen()`的原型在标准库的`string.h`文件中定义,使用时需要加载头文件`string.h`。
```c
#include <stdio.h>
#include <string.h>
int main(void) {
char* s = "Hello, world!";
printf("The string is %zd characters long.\n", strlen(s));
}
```
注意,字符串长度(`strlen()`)与字符串变量长度(`sizeof()`),是两个不同的概念。
```c
char s[50] = "hello";
printf("%d\n", strlen(s)); // 5
printf("%d\n", sizeof(s)); // 50
```
上面示例中字符串长度是5字符串变量长度是50。
如果不使用这个函数,可以通过判断字符串末尾的`\0`,自己计算字符串长度。
```c
int my_strlen(char *s) {
int count = 0;
while (s[count] != '\0')
count++;
return count;
}
```
## strcpy()
字符串的复制,不能使用赋值运算符,直接将一个字符串赋值给字符数组变量。
```c
char str1[10];
char str2[10];
str1 = "abc"; // 报错
str2 = str1; // 报错
```
上面两种字符串的复制写法,都是错的。因为数组的变量名是一个固定的地址,不能修改,使其指向另一个地址。
如果是字符指针,赋值运算符(`=`)只是将一个指针的地址复制给另一个指针,而不是复制字符串。
```c
char* s1;
char* s2;
s1 = "abc";
s2 = s1;
```
上面代码可以运行,结果是两个指针变量`s1`和`s2`指向同一字符串,而不是将字符串`s1`的内容复制给`s2`。
C 语言提供了`strcpy()`函数,用于将一个字符串的内容复制到另一个字符串,相当于字符串赋值。该函数的原型定义在`string.h`头文件里面。
```c
strcpy(char dest[], const char source[])
```
`strcpy()`接受两个参数,第一个参数是目的字符串数组,第二个参数是源字符串数组。复制字符串之前,必须要保证第一个参数的长度不小于第二个参数,否则虽然不会报错,但会溢出第一个字符串变量的边界,发生难以预料的结果。第二个参数的`const`说明符,表示这个函数不会修改第二个字符串。
```c
#include <stdio.h>
#include <string.h>
int main(void) {
char s[] = "Hello, world!";
char t[100];
strcpy(t, s);
t[0] = 'z';
printf("%s\n", s); // "Hello, world!"
printf("%s\n", t); // "zello, world!"
}
```
上面示例将变量`s`的值,拷贝一份放到变量`t`,变成两个不同的字符串,修改一个不会影响到另一个。另外,变量`t`的长度大于`s`,复制后多余的位置(结束标志`\0`后面的位置)都为随机值。
`strcpy()`也可以用于字符数组的赋值。
```c
char str[10];
strcpy(str, "abcd");
```
上面示例将字符数组变量赋值为字符串“abcd”。
`strcpy()`的返回值是一个字符串指针(即`char*`),指向第一个参数。
```c
char* s1 = "beast";
char s2[40] = "Be the best that you can be.";
char* ps;
ps = strcpy(s2 + 7, s1);
puts(s2); // Be the beast
puts(ps); // beast
```
上面示例中,从`s2`的第7个位置开始拷贝字符串`beast`,前面的位置不变。这导致`s2`后面的内容都被截去了,因为会连`beast`结尾的空字符一起拷贝。`strcpy()`返回的是一个指针,指向拷贝开始的位置。
`strcpy()`返回值的另一个用途,是连续为多个字符数组赋值。
```c
strcpy(str1, strcpy(str2, "abcd"));
```
上面示例调用两次`strcpy()`,完成两个字符串变量的赋值。
另外,`strcpy()`的第一个参数最好是一个已经声明的数组,而不是声明后没有进行初始化的字符指针。
```c
char* str;
strcpy(str, "hello world"); // 错误
```
上面的代码是有问题的。`strcpy()`将字符串分配给指针变量`str`,但是`str`并没有进行初始化,指向的是一个随机的位置,因此字符串可能被复制到任意地方。
如果不用`strcpy()`,自己实现字符串的拷贝,可以用下面的代码。
```c
char* strcpy(char* dest, const char* source) {
char* ptr = dest;
while (*dest++ = *source++);
return ptr;
}
int main(void) {
char str[25];
strcpy(str, "hello world");
printf("%s\n", str);
return 0;
}
```
上面代码中,关键的一行是`while (*dest++ = *source++)`,这是一个循环,依次将`source`的每个字符赋值给`dest`,然后移向下一个位置,直到遇到`\0`,循环判断条件不再为真,从而跳出循环。其中,`*dest++`这个表达式等同于`*(dest++)`,即先返回`dest`这个地址,再进行自增运算移向下一个位置,而`*dest`可以对当前位置赋值。
`strcpy()`函数有安全风险,因为它并不检查目标字符串的长度,是否足够容纳源字符串的副本,可能导致写入溢出。如果不能保证不会发生溢出,建议使用`strncpy()`函数代替。
## strncpy()
`strncpy()`跟`strcpy()`的用法完全一样只是多了第3个参数用来指定复制的最大字符数防止溢出目标字符串变量的边界。
```c
char* strncpy(
char* dest,
char* src,
size_t n
);
```
上面原型中,第三个参数`n`定义了复制的最大字符数。如果达到最大字符数以后,源字符串仍然没有复制完,就会停止复制,这时目的字符串结尾将没有终止符`\0`,这一点务必注意。如果源字符串的字符数小于`n`,则`strncpy()`的行为与`strcpy()`完全一致。
```c
strncpy(str1, str2, sizeof(str1) - 1);
str1[sizeof(str1) - 1] = '\0';
```
上面示例中,字符串`str2`复制给`str1`,但是复制长度最多为`str1`的长度减去1`str1`剩下的最后一位用于写入字符串的结尾标志`\0`。这是因为`strncpy()`不会自己添加`\0`,如果复制的字符串片段不包含结尾标志,就需要手动添加。
`strncpy()`也可以用来拷贝部分字符串。
```c
char s1[40];
char s2[12] = "hello world";
strncpy(s1, s2, 5);
s1[5] = '\0';
printf("%s\n", s1); // hello
```
上面示例中指定只拷贝前5个字符。
## strcat()
`strcat()`函数用于连接字符串。它接受两个字符串作为参数,把第二个字符串的副本添加到第一个字符串的末尾。这个函数会改变第一个字符串,但是第二个字符串不变。
该函数的原型定义在`string.h`头文件里面。
```c
char* strcat(char* s1, const char* s2);
```
`strcat()`的返回值是一个字符串指针,指向第一个参数。
```c
char s1[12] = "hello";
char s2[6] = "world";
strcat(s1, s2);
puts(s1); // "helloworld"
```
上面示例中,调用`strcat()`以后,可以看到字符串`s1`的值变了。
注意,`strcat()`的第一个参数的长度,必须足以容纳添加第二个参数字符串。否则,拼接后的字符串会溢出第一个字符串的边界,写入相邻的内存单元,这是很危险的,建议使用下面的`strncat()`代替。
## strncat()
`strncat()`用于连接两个字符串,用法与`strcat()`完全一致,只是增加了第三个参数,指定最大添加的字符数。在添加过程中,一旦达到指定的字符数,或者在源字符串中遇到空字符`\0`,就不再添加了。它的原型定义在`string.h`头文件里面。
```c
char* strncat(
const char* dest,
const char* src,
size_t n
);
```
`strncat()`返回第一个参数,即目标字符串指针。
为了保证连接后的字符串,不超过目标字符串的长度,`strncat()`通常会写成下面这样。
```c
strncat(
str1,
str2,
sizeof(str1) - strlen(str1) - 1
);
```
`strncat()`总是会在拼接结果的结尾,自动添加空字符`\0`,所以第三个参数的最大值,应该是`str1`的变量长度减去`str1`的字符串长度,再减去`1`。下面是一个用法实例。
```c
char s1[10] = "Monday";
char s2[8] = "Tuesday";
strncat(s1, s2, 3);
puts(s1); // "MondayTue"
```
上面示例中,`s1`的变量长度是10字符长度是6两者相减后再减去1得到`3`,表明`s1`最多可以再添加三个字符,所以得到的结果是`MondayTue`。
## strcmp()
如果要比较两个字符串无法直接比较只能一个个字符进行比较C 语言提供了`strcmp()`函数。
`strcmp()`函数用于比较两个字符串的内容。该函数的原型如下,定义在`string.h`头文件里面。
```c
int strcmp(const char* s1, const char* s2);
```
按照字典顺序,如果两个字符串相同,返回值为`0`;如果`s1`小于`s2``strcmp()`返回值小于0如果`s1`大于`s2`返回值大于0。
下面是一个用法示例。
```c
// s1 = Happy New Year
// s2 = Happy New Year
// s3 = Happy Holidays
strcmp(s1, s2) // 0
strcmp(s1, s3) // 大于 0
strcmp(s3, s1) // 小于 0
```
注意,`strcmp()`只用来比较字符串,不用来比较字符。因为字符就是小整数,直接用相等运算符(`==`)就能比较。所以,不要把字符类型(`char`)的值,放入`strcmp()`当作参数。
## strncmp()
由于`strcmp()`比较的是整个字符串C 语言又提供了`strncmp()`函数,只比较到指定的位置。
该函数增加了第三个参数,指定了比较的字符数。它的原型定义在`string.h`头文件里面。
```c
int strncmp(
const char* s1,
const char* s2,
size_t n
);
```
它的返回值与`strcmp()`一样。如果两个字符串相同,返回值为`0`;如果`s1`小于`s2``strcmp()`返回值小于0如果`s1`大于`s2`返回值大于0。
下面是一个例子。
```c
char s1[12] = "hello world";
char s2[12] = "hello C";
if (strncmp(s1, s2, 5) == 0) {
printf("They all have hello.\n");
}
```
上面示例只比较两个字符串的前5个字符。
## sprintf()snprintf()
`sprintf()`函数跟`printf()`类似,但是用于将数据写入字符串,而不是输出到显示器。该函数的原型定义在`stdio.h`头文件里面。
```c
int sprintf(char* s, const char* format, ...);
```
`sprintf()`的第一个参数是字符串指针变量,其余参数和`printf()`相同,即第二个参数是格式字符串,后面的参数是待写入的变量列表。
```c
char first[6] = "hello";
char last[6] = "world";
char s[40];
sprintf(s, "%s %s", first, last);
printf("%s\n", s); // hello world
```
上面示例中,`sprintf()`将输出内容组合成“hello world”然后放入了变量`s`。
`sprintf()`的返回值是写入变量的字符数量(不计入尾部的空字符`\0`)。如果遇到错误,返回负值。
`sprintf()`有严重的安全风险,如果写入的字符串过长,超过了目标字符串的长度,`sprintf()`依然会将其写入导致发生溢出。为了控制写入的字符串的长度C 语言又提供了另一个函数`snprintf()`。
`snprintf()`只比`sprintf()`多了一个参数`n`,用来控制写入变量的字符串不超过`n - 1`个字符,剩下一个位置写入空字符`\0`。下面是它的原型。
```c
int snprintf(char*s, size_t n, const char* format, ...);
```
`snprintf()`总是会自动写入字符串结尾的空字符。如果你尝试写入的字符数超过指定的最大字符数,`snprintf()`会写入 n - 1 个字符,留出最后一个位置写入空字符。
下面是一个例子。
```c
snprintf(s, 12, "%s %s", "hello", "world");
```
上面的例子中,`snprintf()`的第二个参数是12表示写入字符串的最大长度不超过12包括尾部的空字符
`snprintf()`的返回值是写入格式字符串的字符数量(不计入尾部的空字符`\0`)。如果`n`足够大,返回值应该小于`n`,但是有时候格式字符串的长度可能大于`n`,那么这时返回值会大于`n`,但实际上真正写入变量的还是`n-1`个字符。如果遇到错误,返回一个负值。因此,返回值只有在非负并且小于`n`时,才能确认完整的格式字符串写入了变量。
## 字符串数组
如果一个数组的每个成员都是一个字符串,需要通过二维的字符数组实现。每个字符串本身是一个字符数组,多个字符串再组成一个数组。
```c
char weekdays[7][10] = {
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday"
};
```
上面示例就是一个字符串数组一共包含7个字符串所以第一维的长度是7。其中最长的字符串的长度是10含结尾的终止符`\0`所以第二维的长度统一设为10。
因为第一维的长度,编译器可以自动计算,所以可以省略。
```c
char weekdays[][10] = {
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday"
};
```
上面示例中,二维数组第一维的长度,可以由编译器根据后面的赋值,自动计算,所以可以不写。
数组的第二维长度统一定为10有点浪费空间因为大多数成员的长度都小于10。解决方法就是把数组的第二维从字符数组改成字符指针。
```c
char* weekdays[] = {
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday"
};
```
上面的字符串数组其实是一个一维数组成员就是7个字符指针每个指针指向一个字符串字符数组
遍历字符串数组的写法如下。
```c
for (int i = 0; i < 7; i++) {
printf("%s\n", weekdays[i]);
}
```