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

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

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

1.常量字符串

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

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

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

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

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

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

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

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

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*,而需要用另外一种方式。

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

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

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

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

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

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

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

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'作为字符串的结束标志,该结束标志也需要占用一个字节的空间。

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

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

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

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

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

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函数的使用比较简单,只有两个参数,分别是源字符串和目标字符串。

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

该函数有几点说明:

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

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

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刷题了,就知道慕雪所言何物了。