什么是字符串

初见字符串

我们最先遇到的字符串,一般是hello_world程序中用到的"hello world",也就是两个双引号括起来的一串字符,输出时的占位符是%s,可以直接拿去传值,代码如下

1
printf("%s","hello world");

声明字符串变量

有时我们想要先把字符串存起来,再进行操作,那么就使用字符数组,并在初始化的时候把字符串传给它,这样在创建数组时会编译器会自动分配内存给它,代码如下

1
char str[] = "abcdef";

此时我们也可以开启VS的调试,并打开内存监视窗口观察字符串是如何在内存中储存的,如下图

通过观察可以发现,C语⾔字符串的字符串有个规定(特点),就是以字符\0结尾,无论是初始化数组时,还是在分配内存时,都有\0的位置。

strlen()函数

依据以\0为字符串结尾的规则,strlen函数就可以计算字符串的长度,它会从字符串的第一个字符向后扫描,直到遇到\0结束,且\0不进入计数,最后返回字符串的长度,代码如下

1
2
3
4
#include <string.h> //需要引对应的头文件

int len = strlen("abcdef");//len的值为6
int sz = sizeof("abcdef");//sz的大小为7(\0被计入总数)

验证字符串的结尾

正向验证

我们做在字符数组里插入一个\0,来看看函数printfstrlen找到的结尾在哪里,如下图

1
char str[] = {'a','b','c','\0','d','e','f','\0'};

可以看到字符数组似乎被“截断”了,printf只输出了\0前面的内容, strlen算出来的长度也只有3,可见插入的\0被作为了字符串的结尾,字符串提前中指,而没到达字符数组的结尾

反向验证

我们来反向验证一下,\0是字符串结尾的标志,如下图,我们声明一个没有\0结尾的字符数组,看看函数printfstrlen还找不找得到我们“认为”的结尾

1
char str[] = {'a','b','c','d','e','f'};

可以看到函数对字符串的判断出现了严重失误,所以字符数组里没有\0标记结尾是非常严重的问题,不光是找不到字符串的结尾,而且会越界访问!危险操作,写代码的时候一定要注意


从字符串到字符数组

虽然上面已经用到了字符数组,但主要还是为了方便讨论字符串,接下来着重研究字符数组。

先整清楚几个概念

什么是数组:数组是⼀组相同类型元素的集合,会在内存中开辟一段连续的空间,将元素储存在那段内存中

什么是数组元素:存放在数组的值被称为数组的元素,数组在创建的时候可以指定数组的⼤⼩和数组的元素类型。

所以字符数组是一组字符的集合,字符数组里的元素都是字符!,访问到的字符数组里的元素都是字符,像'a','b','c'这种的单个的字符,别和字符串混为一谈!

1
2
3
4
char str = "abc";
int sz = sizeof(str);//这里str代表了整个数组,所以包括\0
,sz的值为4

字符数组的声明

字符数组的声明和其他类型的数组差不多,有初始化不完全初始化声明长度不声明长度

正确的声明代码如下

1
2
3
4
5
6
7
8
9
10
char str1[] = "abc";//初始化,不声明长度

char str2[] = {'a','b','c','\0'};//这也是初始化,且不声明长度

char str3[10] = { 0 };//初始化,用值0(等价于`\0`)填充

char str4[10] = "abc";//不完全初始化,存入字符串"abc",后面都用'\0'填充

char str5[10] = {'a','b','c','d'};//不完全初始化,从下标为0开始,依次往后填充字符 a,b,c,\0

错误的声明代码

1
2
3
4
5
6
char str1[3] = "abc";//数组声明短了,放不下结尾的\0,编译过不了

char str2[3] = {'a','b','c','d'};//同上,放不下

char str3[] = { 0 };//能声明,但是字符数组长度为1,这个数组大概率是用不了的/会被拿去错误使用的

来看看这些声明方式在内存中的表现

不初始化的声明(极度不推荐)

1
char str[];//这个不加长度,直接编译失败(如下图)

1
char str[10];//语法没有问题,来看看此时数组里存了什么


可以看到全都存了-52,对应的中文字符是,这样不好,请在声明字符数组的时候初始化数组

不声明长度的数组声明

1
2
3
char str1[] = "abc";

char str2[] = {'a','b','c','\0'};

如上图,不声明长度时,编译器自动给字符数组分配内存,既不给多,也不给少,初始化给的字符串或者{...}多长,创建的数组就多长。

注意红框,再强调一遍,字符串以\0结尾,看到双引号括起来的字符串,要记得最后隐藏了一个\0,用字符数组储存的时候一定要留足空间

声明长度的数组声明

1
2
3
4
5
char str3[10] = { 0 };

char str4[10] = "abc";

char str5[10] = {'a','b','c','d'};

可以看到,在声明的长度足够长时,你初始化的时候给它多少字符,它就从下标0处开始按顺序存进去多少,剩下的部分自动'\0'填充,

所以实际上上面代码中的str5因为长度10>初始化给的4个字符,后面六个元素用\0填充了,所以str5里存了有结尾的完整字符串

错误声明

1
2
char str1[3] = "abc";
char str2[3] = { 'a','b','c','d' };

可以看到上面两种错误的声明方式,甚至直接编译失败,所以声明字符数组的时候一定要留足空间

1
char str3[] = { 0 };

如上图,可以看到这样写还是编译成功了,但是在监视查看数组长度的时候,发现长度为1,里面存了一个\0,这么的数组能用吗?只能用一点点,甚至还不如直接声明一个char类型的字符变量

当字符数组加上const

1
const char str[5] = "abc";

一些性质

可以看到,声明时加了const之后,字符数组str在声明时的初始化之后便不可更改了,只能访问其元素,而不能通过访问元素来改变数组内容

那么scanf还能写入内容吗?答案是可以!(如下图)

那它能拿来初始化别的数组吗?很遗憾,不能

对应的指针类型

可以看到,这里得用const char*来储存字符数组的地址,而使用char*就会报错

那么用双引号括起来的字符串,是否也有地址,能用指针储存它的地址呢?

如图,可以看到,字符串"abc"是属于const char类型的数组,对应的指针是const char*,不能通过访问元素来改变内部的值,也不能用char*来储存地址

在内存中的表现

可以看到哪怕是字符串"abc",也是在内存中开辟了一段空间,并把字符储存在内存中了的

但是,不要试图用scanf去改变字符串的值


如何向字符数组里添加内容

添加的方法多种多样,搞不好可能还会出错,所以把字符数组学明白很重要!

以下使用的数组样例声明如下

1
char str[10] = { 0 };

初始化

在初始化的时候就把值传进去,有哪些初始化方式上面已经介绍过了,这里不多赘述

访问数组元素

通过[]可以访问数组元素,并对没有const修饰的数组,修改其元素,例如:

1
str[0] = 'A';//将数组的第一个元素改成字符A

我们也可以通过循环的方式,将数组的所有元素填充为某个字符

1
2
3
4
5
char place_holder = 'A';
for(int i = 0; i < 10 ; i++)//这里使用左闭右开区间,10为数组的大小
{
str[i] = place_holder;
}

使用scanf函数

由上面的探究已知:对于已声明的字符数组,无论有没有const修饰,都可已用scanf修改内容,那么scanf怎么用,又具体怎么工作的,我们接着往下探究

使用示例

1
2
3
char str[10] = { 0 };
scanf("%s",str);//占位符是 %s ,右边的参数是 str ,也就是数组名
//或者 scanf("%s",&str)

注意!这边的数组名str储存的是数组首元素的地址,而&str储存的是整个数组的地址,值是一样的,两者皆可用于传参,但指针类型不一样,要做好区分

scanf都做了什么

先来看看它分别对用{ 0 }初始化不初始化的数组做了什么

1
2
3
4
char str1[10] = { 0 };
scanf("%s",str1);
char str2[10];
scanf("%s",str2);

两个数组的输入均为abc

可以看到,对str1,字符非常正常地填充进去了,因为整个数组原本是用\0填充的,看不出什么端倪

而对于str2,观察发现,除了输入进去的字符a,b,c,它还自动在结尾补了一个\0,使str2里储存了一个完整的字符串。但是,剩下的部分还是用-52填充,即未初始化的状态,所以依然不提倡声明的时候没有初始化

然后是在字符数组内已有内容的情况下,再次使用scanf的情况

1
2
3
4
char str1[5] = "abc";
char str2[5] = "abc";

scanf("%s %s",str1,str2);

如图,scanf做的是把输入的字符串覆盖式存入字符数组,比原来长,就完全覆盖,比原来短,就部分覆盖,未覆盖的部分无改动

关于scanf的危险操作

由于scanf无法预测字符数组能否存下输入的字符串,如果字符数组声明的长度不够,就可能出现越界访问,随之而来的便是奇奇怪怪的bug

1
2
char str[4] = { 0 };//先声明一个长度为4的数组
scanf("%s",str);//这次输入abcdef试试

可以看到,确实越界访问了,所以声明字符数组的时候,建议比预计最大输入,在多些长度,防止越界访问。