第 17 讲:指针和字符
前文我们提到了近乎所有的指针的用法,不过指针和数组用起来确实挺难的,今天要说到的知识点是字符串、字符数组和指针三者的关系。
什么是字符?
在之前,我们对字符提到的内容很少,所以我们这里统一为大家介绍它们。
字符是以一个单引号包含起来,以区分数值字面量的存在。比如下面的这个写法:
这样就表示 c
变量是一个字符类型的变量,存储的元素是一个字符 1
。这个字符 1
和普通的 1 不同,字符 1
一般用于输出才用到,而数字 1 则可以操作和计算。
在 C 语言里,用单引号把这些符号引起来可以和数字作出区分。但为了灵活使用,字符有时候也可以和数字进行转换,转换关系是通过一个叫做 ASCII 码表来搞定的。比如,在这个表里规定字符 A
的对应整数是 65,a
是 97,而字符 1
对应数值则是 49。
这样定义,来区分字符是有意义的。在计算机处理这些字符的时候,为了可以明确表达数字的行为是计算,而字符的行为是输出,所以 C 语言发明了类型这个体系,来区分字符和整数,所以字符 1
和数字 1 具有截然不同的用法,所以写法和声明(定义)语句上的赋值也是有不同的地方的。
如果你书写的方式是这样:
i
这样只能表达 c
变量表示的字符是 ASCII 码表里编号为 1 的那个字符。
使用字符和数字进行输出
在前文里,我们提到过数字的输出模式。如果是整数,输出则使用 %d
的格式化字符串,它会自动帮我们改为字符串的形式书写出来:
比如上面的这个写法,显然第一个 0 是没有意义的,但用整数输出的时候,你也就看不到这个 0 了,而只有 12
这个字符串。这就是数字的输出模式。
字符的输出使用的是 %c
这个格式化字符串。
1
它就好比是
1
,因为 1
这样才可以得到一个字符 1
。
字符串
字符串(也经常被简称为串,string),是字符的一组序列,即由多个字符构成的这个序列,称为字符串。它的表现形式有两种,一种是字面量字符串,一种是字符数组。我们都来说一下,以及它们的使用。
字面量字符串
字符可以用字面量的形式呈现,也可以用数字呈现,这表现出字符的两种形态。字符串也是这样。第一种呈现形式是通过双引号的形式出现的。比如 "Hello, world!\n"
就是一个合格的字面量字符串写法,用双引号把所有原封不动的字符序列包括起来。
不过,字面量字符串如何赋值和输出呢?赋值的话,它的类型应该是什么呢?
之前说过,字符串是一系列字符,所以它肯定不是单纯的 char
类型了。实际上,它可以使用字符指针来表示,也可以用字符数组表示:
C 语言允许第一种写法,让 char
类型的指针(干脆就叫 char *
类型)来“接收”这个字符串,这样的话,这个指针将会指向这个字符串的首地址(和数组一致,指向数组的第 0 号元素,即 charPointer
本身就等价于 &charPointer[0]
)。这种表示方式的优势是,不用去数和关心字符串到底有多长。
第二种写法也是允许的,C 语言会自动把这个字符串拆解为一个字符数组,然后挨个存放进去,最后得到这个数组的首地址。所以第二种写法下,charArray
变量名依旧等价于数组的首地址。
不过,需要你额外注意一点。它和普通元素类型的数组不同,字符串会被处理成字符数组的形式存储,然后返回的首地址可以用字符指针变量名或字符数组变量名接收。但是,字符串会自动为末尾添加一个结束标记字符 \0
。这个字符是计算机内表达这个字符的写法,输出并非是一个反斜杠和一个数字 0,而是啥都没有。
\0
这个字符仅用于标记字符串的结尾用,没有其它的功能。而如果你非要把它当整数形式表示的话,它在 ASCII 码表里对应的整数也就是 0。所以 0 对应 \0
这个字符。
所以,这个知识点告诉我们,Hello, world!
这个字符串整体长度是 14,除了字面量给定的 10 个英文字母、2 个符号、一个空格以外,还默认在末尾带有一个字符 \0
标记,所以这个字符串长度是 14,并不是 13;而在第二种赋值格式里,这个中括号里就应该是 14,而不是 13。
输出字面量字符串的方式可以用 %s
的方式输出:
s
这个字符串的所有内容。另外,这样书写每次都必须写上 %s
,所以你可以改写为这样:
即直接使用 puts
函数来输出一个字符串。
字符数组
显然,字面量字符串会被处理为字符数组,所以其实还可以书写为字符数组。
但请注意,如果写成字符数组,系统就不会自己给你补充 \0
字符了。所以这种写法必须自己补充一个 \0
符号在字符数组的末尾。
当然,你把 \0
写在字符数组的中间某处也可以,不过这个字符串就会从这个地方截断:
这样的话,这个字符数组长度就是 7 而不是原本的 13 或者 14 了。也就是说,这个 s
接收到的完整的字符数组序列到第一个 \0
就结束了,后面的内容都不在 s
里。不过在内存里,这些字符确实是挨着存储的,不过你必须通过语法的 bug 越界访问数组,才看得到它们了。
同样地,字符数组也具有退化赋值的特性,所以我们依旧可以使用这一点,把字符数组赋值给一个指针。
字符串的使用
讲完了两种字符串的书写格式后,我们来说明一下字符串的基本使用。
考虑如下实例。假设我们在写一个代码,输出一个数字是否是质数,代码大部分已经写好:
这就是前面的例子,只是我为了逻辑清晰,把 isPrime
从 int
改为了 bool
类型(只是这么做就只能让代码运行在允许 C99 标准的地方了)。
现在,考虑输出语句。可以从输出语句里看到,它们大部分输出信息都是相同的,只是多了一个 not
。现在我们可以考虑把代码写成一个三目运算符的方式:
printf("%d is %sa prime.\n", digit, isPrime ? "" : "not ");
注意输出语句里有一个 %sa
,其实它是 %s
和一个字符 a
而已。它想要表示的是,某某数字,是(或不是)一个质数,这样的输出格式。当 isPrime
为 true
的时候,我们就不需要 not
这个单词的修饰,所以这个输出一个空的字符串(只有一个 \0
,反正里面其它东西都没有,到时候计算机会帮我们作出处理);否则加一个 not
修饰符来表示它”不是“质数。
常见字符串函数
我们经常要使用到一些字符串的处理模式,比如提取字符串里其中一部分字符、求长度等等。下面就来看看它们。
求字符串长度
我们需要实现一个函数 strlen
,获取一个字符串的长度。这个算法比较容易实现:
可以从代码里看到,这就是一个基本的计算方式。首先我们定义一个结果 count
变量来表示一共多少字符,然后一个指针变量指向它的第一个元素,这样一会儿我们就可以使用 ++
将变量移动指向,让其指向下一个字符信息。然后最终当 cur
指向 \0
的时候,表示字符统计完毕,跳出 while
循环,返回结果 count
即可。
另外,参数
str
在过程里都不用修改变量的本身指向和指向的内容,所以我们可以对str
使用const
修饰。
这个算法可以改写为这样:
使用 for
循环就会简单不少。另外,cur
指针如果不是指向 \0
字符就一直遍历,这个条件是 *cur != '\0'
,而前文说过 \0
的 ASCII 码表编码数值是 0,所以就相当于 *cur != 0
,而这种写法在条件判断一节的时候说过,它等价于不写 != 0
部分,所以最终可以直接写成 *cur
来表示 *cur != '\0'
。另外,str
就等价于 &str[0]
,所以初始赋值也可以不要地址符号和索引器部分 [0]
。
取出字符串的一部分字符
尝试写出一个 strstr
函数,来获取某个字符串里指定起始点和长度的其中一部分字符。
首先判断输入的所有参数是否都符合条件。如果取出的长度 length
比 0 还小,或者比字符串长度还大,是不满足要求的;同时,如果获取元素的起始索引 startIndex
比 0 小,或超过字符串长度,也依然是无效的。所以这个时候我们给用户输出一个空字符串,来表示错误(当然你也可以给它另外的东西,或者执行指定的某个行为来表示输入信息错误)。
否则,我们尝试让 ptr
指向 str[startIndex]
处的元素,然后不断更新 last
和 ptr
的数值。当 last
减到 0 的时候就是获取结束的时候。
最后得到结果后,我们把 ptr
指向的字符改写为 \0
表示从这里截断,表示这里是字符串的结束。最后返回 str[startIndex]
的地址就可以了。
当然你也可以改写一下:
const