距离上次更新本文已经过去了 181 天,文章部分内容可能已经过时,请注意甄别

字符串,就是一长串的字符类型,每一个位上都是一个单独的 char 字符,连起来组合成了字符串。

你在各个客户端 / 网页里面看到的文字,在编程语言中都可以认为是一个字符串

在 C 语言中,字符串对应的是 const char*char* 类型;其本质上是一个 char 字符数组

1. 常量字符串

常量字符串的类型是 const char*,我们直接使用的 "字符串",就是常量字符串。

cpp
1
const char* str = "hello world"; // 常量字符串

常量字符串不能被修改,但可以像数组一样通过下标和指针的方式访问字符串的单个字符

c
1
2
3
const char* str = "hello world"; // 常量字符串
char c1 = str[0]; // 访问字符h
char c2 = (*str+4); // 访问字符o

而没有 const 修饰的 char* 类型,是不能直接接受常量字符串赋值的

cpp
1
char* str = "hello world"; // 无法接受常量字符串,这行代码有误

虽然这行代码在 VS2019 中没有报错,在 Linux 下的 g++ 也仅仅是报了 warning 警告,但其本质是有问题的!

下面的文字就是 Linux 下 g++ 编译时的警告,翻译过来是禁止将常量字符串转换为 char * 类型。

plaintext
1
2
3
4
5
$ g++ test.cpp -o test
test.cpp: In function ‘int main()’:
test.cpp:137:17: warning: ISO C++ forbids converting a string constant to ‘char*’ [-Wwrite-strings]
char *str = "hello world\n"; // 无法接受常量字符串
^~~~~~~~~~~~~~~

2. 普通字符串

前面提到,字符串就是一个字符的数组。但我们想定义一个字符串数组,也不能用 char*,而需要用另外一种方式。

下面这种在变量名后带 [] 的写法,就是定义数组的语法,在前文已经介绍过了。

cpp
1
char str[] = "hello world"; // 普通字符串的基本定义方式

上方是声明时赋值的写法,编译器在识别到这一行代码后,会自动帮我们创建字符串对应大小的空间,并让 str 指向这片空间的起始地址。

需要注意的是,只有初始化的时候能这么写。后续如果想修改这个字符串,需要使用 strcpy 函数,而不能用 = 直接赋值。strcpy 函数会在后文介绍。

cpp
1
2
3
char str[] = "hello world";
str = "hello"; //错误,不能通过=直接修改
strcpy(str,"hello"); // 正确修改方式

这里的 str 变量本质上是 char* 类型,是一个字符指针。所有数组变量都是对应类型的指针。

如果想在声明时不赋初值,可以用下面的办法来定义一个字符串。

cpp
1
2
3
4
5
6
7
8
char str1[15];  // 定义一个长度为15的字符串数组
str1[0] = 'h'; // 通过下标访问的方式赋值
str1[1] = 'e';
str1[2] = 'l';
str1[3] = 'l';
str1[4] = 'o';
str1[5] = '\0'; // 最后需要加上字符串的结束标志
printf(str1); // 打印字符串

运行结果如下,成功打印出 hello 字符串。

2.1 字符串结束标志 \0

请注意,所有字符串数组最后都会带上一个隐藏的'\0' 作为字符串的结束标志,该结束标志也需要占用一个字节的空间。

如果缺少了这个结束标志,那么程序就无法确定字符串什么时候结束,字符串就不完整了。

cpp
1
2
3
char str4[] = "hello world"; // 该字符串末尾会自动带上一个\0
str4[11] = 'a'; // 尝试删掉最末尾的\0,看看会发生什么?
printf(str4); // 打印字符串

可以看到,原本应该打印完毕 hello worlda 就停止的程序,却继续往后打印了非常 “经典” 的乱码字符烫烫🤣。这也是初学者经常遇到的错误之一。这就是缺少 \0 结束标志的后果。

可能又有初学者在这里会犯第二个错误:越界访问。

在数组章节中,我们已经介绍过越界访问的概念。访问了不属于用户的数组空间就是越界访问。

c
1
2
3
4
char str4[] = "hello world"; // 该字符串末尾会自动带上一个\0
str4[11] = 'a'; // 尝试删掉最末尾的\0
str4[12] = '\0';// 既然删掉了,我给他加上一个不就行了?
printf(str4); // 打印字符串

但实际上,我们的 str4 字符串,在定义的时候赋值了 "hello world",这个字符串的长度是 12。分别是可见的 hello world 一共 11 个字符(空格也算入字符中),和末尾隐藏的 \0 字符。

编译器会自动为我们开长度 12 的 char 数组,即等价于 char str4[12] = "hello world";

数组 [] 里面的数字是通过下标访问的,下标是从 0 开始的,数组中的第一位的下标是 0。所以,str4[11] 访问的实际上是第十二个位置的 \0,而 str4[12] 访问的数组的第十三个位置,该位置的空间实际上并不属于我们,此时的访问就是越界访问了!

在 windows 下,这份代码会因为错误提前退出;在 linux 下,运行会报段错误;

下图中就是 windows 下异常退出的情况,可以看到进程退出代码是负数,而且最后一行的 printf 也并没有打印出结果,这是因为代码在下图第 24 行中的 str4[12] = '\0'; 就已经因为越界访问提前异常退出程序了。

正常程序中,如果运行正常,程序终止后的退出代码应该是 0;非零值都代表运行出错!

2.2 strcpy 函数

除了一个一个通过下标访问的方式来赋值外,还可以用库函数 strcpy,点我查看文档

strcpy 函数的使用比较简单,只有两个参数,分别是源字符串和目标字符串。

cpp
1
char * strcpy ( char * destination, const char * source );

该函数有几点说明:

  • 拷贝的时候,会把源字符串末尾的 \0 也拷贝过去;
  • 目标字符串所能容纳的长度必须大于源字符串(长度要包括 \0),否则会有越界访问;
  • 返回值是目标字符串的指针,即返回 destination

在当前场景中,我们可以用下面的方式使用 strcpy,只要保证我们自己设定的字符数组空间大于源字符串即可。

cpp
1
2
char str5[15]; // 定义一个长度为15的字符串数组
strcpy(str5,"hello world"); // 使用strcpy函数,拷贝数据过去

2.3 为什么会打印乱码字符?

前文中的这份代码,我们将原有的'\0' 截止字符替换掉后,因为没有结束标志,printf 函数就会一直往后打印,于是就出现了一些常见越界访问和未定义空间才会出现的生僻汉字。

你可能会疑惑,理论上后续的空间是没有人使用的空间,那为什么会打印出这些不知道哪里冒出来的字符呢?那最后又为什么停止了?

其实这个问题的答案很简单:在一个程序中,所有未使用的内存空间,内部都存放的是随机值。这些随机值组合起来,和 GBK/UTF-8 这类文字编码的部分编码对应,就会打印出这些生僻字,也就是网络上俗称的 “乱码字符”。

所谓 “文字编码”,就是将语言里面的文字转为机器可识别的二进制的一个转换表。ASCII 码就是一个文字编码表,但其内部只包含了英语字母和部分符号的转换,于是就会有各类的文字编码,将各国语言以规定的格式转换成机器可以识别的二进制。比较常用的文字编码是 UTF-8;

至于为什么会停止?回到 ASCII 码表上,查表可知,'\0' 字符对应的十进制是 0

也就是说,只要我们原本定义的字符串,后续的内存空间中出现了一个随机值为 0 的位置,那么 printf 打印到这里就会把他当作截止标志,停止打印了。

结语

对字符串的介绍到这里就 over 了。总结一下,字符串中最需要关注的两个问题:

  • 字符串(字符数组)的空间长度,注意不要越界访问;
  • 末尾的这个'\0' 一定不能漏,否则没有结束标志了。

在 OJ 刷题中,经常会有拼接字符串相关的题目,如果字符串结束标志控制不到位,就容易把自己弄进一个很难找到的混乱 BUG 中。说多了都是泪,后续等你开始 OJ 刷题了,就知道慕雪所言何物了。