Skip to content
This repository has been archived by the owner on Oct 17, 2020. It is now read-only.

Commit

Permalink
增加C字符串和字符指针的介绍
Browse files Browse the repository at this point in the history
  • Loading branch information
ecnelises committed Oct 24, 2015
1 parent 3cdc2d7 commit 69a2460
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 0 deletions.
67 changes: 67 additions & 0 deletions pitfalls/cstring.md
@@ -0,0 +1,67 @@
###字符串###
计算机编程的发展很难抛开字符串这个概念来讲。所谓整数、浮点数,都是计算机内部的概念。仅仅靠这些数学类型,虽然方便了计算机自己的运算,但是难以实现跟用户的交互。这样的话,我们的计算也就失去了意义。

不论是怎样的计算机操作,都离不开输入和输出两个过程。显然,最容易想到也最直观的方式,就是通过屏幕。在图形界面并不发达的年代,命令行的交互还是主流,字符串的处理更显其重要性。任何的类型,最后都要转变成某种意义上的字符串才能输出到屏幕。

在类 UNIX 系统里,这一点表现得淋漓尽致。UNIX 的设计哲学要求每个程序设计成只做一件事情。要完成大的任务,就只需要把不同的程序组装起来。如何实现进程间的交互?用文本交互,而非二进制数据流。说到底,还是字符串的解析。

> Keep It Simple, Stupid.
然而字符串并不是一个简单的类型。它与所谓基本数据类型的根本区别就在于,**字符串是长度不定的**。虽然,理论上整数也可以无限大,但是在多数情况下,设定一个上界并无大碍,甚至还方便求补码。但,要是字符串也像这样限定一个范围,那还不如没有。因此,即使是要实现最基本的字符串功能,那也需要封装,这无疑就增加了语言层面的复杂度,同 C 的设计原则相悖。这也导致了 C 乃至 C++ 字符串处理能力的孱弱,你也少有见到用 C/C++ 写的 _CGI_ 接口。

那……怎么办呢?

####诡异的字符指针####
在我最开始看谭浩强的书的时候,一直没能将字符指针的意义搞清楚,看一次头疼一次。后来稍稍理解一点文件结构的知识以后才明白。在 C 语言中,字符指针的表现特性跟其他指针有所不同。来看看这段代码:

int *foo = {1, 2, 3, 4, 5};
for (int i = 0; i < 5; i++) {
printf("%d\n", foo[i]);
}

顺便强调一下,这段代码在你的编译器上完全有可能报错,因为在``for``循环体里定义循环变量是 _C99_ 标准的新规定,你的编译器有可能不支持或者没有默认打开对应的编译选项。如果是 gcc(诸如 Dev-C++ 或者 Code::Blocks 一类 IDE 以及绝大多数的 Linux 默认编译器),在 IDE 设置里的编译选项加上``-std=c99``即可。实在不会的话,直接把文件后缀名从 .c 改成 .cpp 即可……

回到正题。其实我开始也以为编译器会报错,结果竟然没有,只是给了两个 Warning 哦。具体这两个警告的内容是什么,我先卖个关子。我们执行一下,看看结果。

➜ ~ ./a.out
[1] 11278 segmentation fault ./a.out

什么意思?噢等等我又要解释一下了。笔者写作和编程使用的平台是苹果的 _Mac OS X_ ,这个系统通过了 _POSIX_ 标准,意味着它的系统**可以兼容 _UNIX_** ,在系统调用和命令行等方面跟 _Linux_ 差不多。(关于 UNIX 的故事,以后如果有机会谈系统调用的时候还会细说)我们编译的结果默认叫做 ``a.out``,输入``./a.out``回车表示执行它。然而系统返回给了我们这样一个结果——**段错误** (Segmentation Fault) 。

什么是段错误?所谓的“段”,指的是一个进程自己的内存空间,而每个可执行文件其实都有固定的格式,并且在不同的操作系统或者不同的硬件架构下,这格式也会有所不同。而这个可执行文件运行时所占用的内存会被分为几个部分。在 _ELF_ 格式(Linux 等操作系统的可执行文件格式)下,只有 .data 段,也就是数据段,其中的一部分内容是进程可以更改的。而只有属于这个进程自己的内存地址范围是可读的。**简言之,如果进程试图更改只读内存,或者试图读取除自身外其他的内存空间,在 x86 保护模式下,操作系统会做出处理(一般是直接终止进程),这样的进程错误就叫做段错误。**

听不懂?没关系,日后要是说深了,这事还会讲很多次。而且笔者上文的描述也可能不太准确,一些相关的概念也没有引入。总之,发生段错误,就是看了不该看的东西。

好了,程序的执行结果知道了,段错误我们也介绍完了。那一开始编译代码的时候编译器报的警告是什么呢?

char.c:9:15: warning: incompatible integer to pointer conversion
initializing 'int *' with an expression of type 'int'
[-Wint-conversion]
int *e = {1, 2, 3, 4, 5};
^
char.c:9:18: warning: excess elements in scalar initializer
int *e = {1, 2, 3, 4, 5};
^
2 warnings generated.

啊。是不是感觉又看不懂了?英语要好好学啊。我来简单解释一下。第一个警告的意思是,把一个``int``类型的值给了一个``int*``类型的变量。第二个的意思是,有多余的初始化元素。

咦,画风有变啊。我们本来想的是,用数组的方式初始化指针合不合法。但是编译器对此的理解似乎与我们的想法大相径庭。按照编译器警告透露出的信息,我们这里赋值所做的操作,实际上是直接把花括号里列表的第一个元素,也就是这个整数 ``1``,赋给了指针``e``,然后第二、三……个元素当然就多余了。为什么编译器会这样理解呢?要是你看到代码可以这样写,就不奇怪了:

int foo = {15};
printf("%d\n", foo);

输出结果毫无违和感!懂了吧。你直接把一个指针指向的地址定义成一个整数,而且还是像 1 这样极小的整数,出现内存访问错误是当然的了。话说回来,这种错误,恐怕绝大多数也出现在 C/C++ 这种带指针的“奇葩”语言里了。

那么,我们如果把这里的``int``更换成``char``,试图以一个字符列表来初始化``char*``,会有什么后果呢?

嘣!一样的警告,一样的段错误。先别灰心丧气,换种写法试试:

char *foo = "abc";
printf("%s\n", foo);

程序表现得很正常,没有错误,结果也如同我们料想。那这是怎么一回事呢?``"abc"````{'a', 'b', 'c'}``有什么区别呢?字符指针和其他类型的指针到底有什么不一样呢?

还记不记得我们最开始讨论那个 Hello, world 程序的时候,提到过一个叫“字符串字面值”的概念?所谓字符串字面值(翻译有点拗口,英文是 String Literal ,谁能给翻译个更好的)在 C 语言里指的就是用双引号包着的一串字符。(我尽量避免在行文中直接下定义,在写下这段文字之前还特意去查了一下维基百科,写 C 语言的东西用如履薄冰形容毫不为过,说实的一个东西很容易被找到例外或者例子反驳)这是 C 中一个非常特殊的概念。有多特殊?以后提预处理的时候也会说到这个。

事实上,我们可以把这个当做是 C 语言对于字符串做的一个特殊设计,只有字符可以写在一起成为一个常量。你可能会纳闷,一个可用的指针必定是指向内存当中的某块地址的。那这个指向字符串常量的指针,值是什么呢?当然也是地址了。不过这个地址比较特殊。前面说过,在操作系统的可执行文件里有好几个段。以 Linux 系统的 ELF 文件格式为例,像 .data 段里保存了所谓的**** (Heap) 和**** (Stack) 的内容。前者指动态分配的内存,后者指静态分配的内存,比如你在程序里用普通的方式定义的变量。而我们提到的这个字符串常量,则是保存在 .text 段里的固定内容,所谓字符指针指向的地址,就是这个常量在内存里的地址。如果要对它进行修改,就是对 .text 段进行修改,程序会段错误的。
6 changes: 6 additions & 0 deletions pitfalls/title.md
@@ -0,0 +1,6 @@
##C 语言的坑##
C 语言在 UNIX 系统诞生的那段时间的流行要得益于它跨平台的特性。当然这里说的跨平台跟现在 Java 这些语言的跨平台还不太一样。在那个年代,多数程序的编写是用汇编语言完成的。由于不同机器的汇编指令和硬件参数有所不同,把写完的程序从这类机器移植到那类机器,需要修改大量代码。而 C 语言对此作了抽象,只要每台机器上有对应的 C 编译器就行了。

回头看计算机科学发展的历史我们可以发现软件行业的发展史也可以看作是一部工具的抽象程度越来越高的历史。有了 C 以后,人们渐渐觉得 C 还是太偏向底层了,缺少很多好用的“轮子”。也许实现一个并不复杂的功能,用 C 语言的话要写不少的代码。于是我们有了面向对象,有了垃圾收集和引用计数,有了更抽象的动态语言。那么回头看,既然 C 是一门比较贴近计算机系统底层的语言,那么很多跟计算机系统相关的又不常为人知的知识使 C 语言显得充满了“坑”,写起来危机四伏。

那么这一章,我们就要来说说 C 语言里的各种坑。

0 comments on commit 69a2460

Please sign in to comment.