=C入门=深入研究 字符串与字符数组
什么是字符串
初见字符串
我们最先遇到的字符串,一般是hello_world
程序中用到的"hello world"
,也就是两个双引号括起来的一串字符,输出时的占位符是%s
,可以直接拿去传值,代码如下
1 | printf("%s","hello world"); |
声明字符串变量
有时我们想要先把字符串存起来,再进行操作,那么就使用字符数组,并在初始化的时候把字符串传给它,这样在创建数组时会编译器会自动分配内存给它,代码如下
1 | char str[] = "abcdef"; |
此时我们也可以开启VS的调试,并打开内存和监视窗口观察字符串是如何在内存中储存的,如下图
通过观察可以发现,C语⾔字符串的字符串有个规定
(特点),就是以字符\0
结尾,无论是初始化数组时,还是在分配内存时,都有\0
的位置。
strlen()函数
依据以\0
为字符串结尾的规则,strlen
函数就可以计算字符串的长度,它会从字符串的第一个字符向后扫描,直到遇到\0
结束,且\0
不进入计数,最后返回字符串的长度,代码如下
1 |
|
验证字符串的结尾
正向验证
我们做在字符数组里插入一个\0
,来看看函数printf
和strlen
找到的结尾在哪里,如下图
1 | char str[] = {'a','b','c','\0','d','e','f','\0'}; |
可以看到字符数组似乎被“截断”了,printf
只输出了\0
前面的内容, strlen
算出来的长度也只有3
,可见插入的\0
被作为了字符串的结尾,字符串提前中指,而没到达字符数组的结尾
反向验证
我们来反向验证一下,\0
是字符串结尾的标志,如下图,我们声明一个没有\0
结尾的字符数组,看看函数printf
和strlen
还找不找得到我们“认为”的结尾
1 | char str[] = {'a','b','c','d','e','f'}; |
可以看到函数对字符串的判断出现了严重失误,所以字符数组里没有\0
标记结尾是非常严重的问题,不光是找不到字符串的结尾,而且会越界访问!危险操作,写代码的时候一定要注意
从字符串到字符数组
虽然上面已经用到了字符数组,但主要还是为了方便讨论字符串,接下来着重研究字符数组。
先整清楚几个概念
什么是数组
:数组是⼀组相同类型元素的集合,会在内存中开辟一段连续的空间,将元素储存在那段内存中
什么是数组元素
:存放在数组的值被称为数组的元素,数组在创建的时候可以指定数组的⼤⼩和数组的元素类型。
所以字符数组
是一组字符
的集合,字符数组里的元素
都是字符
!,访问到的字符数组里的元素都是字符,像'a'
,'b'
,'c'
这种的单个的字符,别和字符串
混为一谈!
1 | char str = "abc"; |
字符数组的声明
字符数组的声明和其他类型的数组差不多,有初始化,不完全初始化,声明长度,不声明长度
正确的声明代码如下
1 | char str1[] = "abc";//初始化,不声明长度 |
错误的声明代码
1 | char str1[3] = "abc";//数组声明短了,放不下结尾的\0,编译过不了 |
来看看这些声明方式在内存中的表现
不初始化的声明(极度不推荐)
1 | char str[];//这个不加长度,直接编译失败(如下图) |
1 | char str[10];//语法没有问题,来看看此时数组里存了什么 |
可以看到全都存了-52
,对应的中文字符是烫
,这样不好,请在声明字符数组的时候初始化数组
不声明长度的数组声明
1 | char str1[] = "abc"; |
如上图,不声明长度时,编译器自动给字符数组分配内存,既不给多,也不给少,初始化给的字符串
或者{...}
多长,创建的数组就多长。
注意红框,再强调一遍,字符串以\0
结尾,看到双引号括起来的字符串,要记得最后隐藏了一个\0
,用字符数组储存的时候一定要留足空间
声明长度的数组声明
1 | char str3[10] = { 0 }; |
可以看到,在声明的长度足够长时,你初始化的时候给它多少字符,它就从下标0
处开始按顺序存进去多少,剩下的部分自动用'\0'
填充,
所以实际上上面代码中的str5
因为长度10
>初始化给的4
个字符,后面六个元素用\0
填充了,所以str5
里存了有结尾的完整字符串
错误声明
1 | char str1[3] = "abc"; |
可以看到上面两种错误的声明方式,甚至直接编译失败,所以声明字符数组的时候一定要留足空间
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 | char place_holder = 'A'; |
使用scanf
函数
由上面的探究已知:对于已声明的字符数组,无论有没有const
修饰,都可已用scanf
修改内容,那么scanf
怎么用,又具体怎么工作的,我们接着往下探究
使用示例
1 | char str[10] = { 0 }; |
注意!这边的数组名str
储存的是数组首元素的地址,而&str
储存的是整个数组的地址
,值是一样的,两者皆可用于传参,但指针类型不一样,要做好区分
scanf
都做了什么
先来看看它分别对用{ 0 }
初始化和不初始化的数组做了什么
1 | char str1[10] = { 0 }; |
两个数组的输入均为abc
可以看到,对str1
,字符非常正常地填充进去了,因为整个数组原本是用\0
填充的,看不出什么端倪
而对于str2
,观察发现,除了输入进去的字符a
,b
,c
,它还自动在结尾补了一个\0
,使str2
里储存了一个完整的字符串。但是,剩下的部分还是用值-52
填充,即未初始化的状态,所以依然不提倡声明的时候没有初始化
然后是在字符数组内已有内容的情况下,再次使用scanf
的情况
1 | char str1[5] = "abc"; |
如图,scanf
做的是把输入的字符串覆盖
式存入字符数组,比原来长,就完全覆盖,比原来短,就部分覆盖,未覆盖的部分无改动
关于scanf
的危险操作
由于scanf
无法预测字符数组能否存下输入的字符串,如果字符数组声明的长度不够,就可能出现越界访问,随之而来的便是奇奇怪怪的bug
1 | char str[4] = { 0 };//先声明一个长度为4的数组 |
可以看到,确实越界访问了,所以声明字符数组的时候,建议比预计最大输入,在多些长度,防止越界访问。