C 语言在 Linux 系统中的重要性自然是无与伦比、不可替代,所以我写 Linux 江湖系列不可能不提 C 语言。C 语言是我的启蒙语言,感谢 C 语言带领我进入了程序世界。虽然现在不靠它吃饭,但是仍免不了经常和它打交道,特别是在 Linux 系统下。
Linux 系统中普遍使用的是 GNU-C,这里有一份Gnu-C语言手册.pdf。The GNU C Reference Manual 的主页在这里:http://www.gnu.org/software/gnu-c-manual/。C 语言的内核极其紧凑,该手册总共只有 91 页,去掉目录、附录和索引后就只有 70 页。我一般一个多小时就可以将其从头至尾复习一遍。我曾有过将其翻译成中文的想法,后来还是放弃了。翻译这种字斟句酌的事情还是让别人来干吧。我只写写我自己的感悟。
感悟一:C 语言标准干不过 GNU 扩展
最近为了研究 X Window 的底层协议,开始尝试使用 XCB 编程。当我打开 XCB 的头文件的时候,我被大量的 __restrict__ 关键字惊呆了,好在有 GNU C 语言手册为我答疑解惑。__restrict__ 又是一个 GNU 扩展的关键字,后面我会详细讲解该关键字的用途。其实 C 语言的 C99 标准中已经引入了 restrict 关键字,没有前后的下划线,但是在大量的开源代码中,使用最普遍的还是 GNU 的扩展,而不是 C 语言标准。
和restrict关键字有相同命运的还有 inline、_Complex 等,它们都是在 C99 标准中引入的关键字,但是其实在 C99 标准出来之前,GNU C 中早就有了 __inline__、__complex__ 等扩展关键字。还记得多年前我学习 Linux 0.11 版的源代码时,看到大量的 __inline__ 曾经疑惑不已,不知道为什么 Linus 在 91 年就能用上了如此先进的语言功能,后来才知道,这是 GNU 的扩展关键字。
C 语言的标准有 C89 和 C99,使用 GCC 的时候甚至要显示指定 -std=c99 才能全面支持 C99 标准,所以在开源界,大家还是喜欢首选 GNU 的扩展关键字。比如 __inline__、__complex__ 和 __restrict__。总而言之, C 语言标准干不过 GNU 扩展。
下面来看看 __restrict__ 的真正含义。还记得 CSDN 上曾经载过一篇文章《为什么有些语言会比别的快》,其中提到“很长一段时间,相同的两个程序在 Fortran 和 C(或者C++)中运行,Fortran 会快一些,因为 Fortran 的优化做的更好。这是真的,就算 C 语言和 Fortran 的编译器用了相同的代码生成器也是一样。这个不同不是因为 Fortran 的某种特性,事实上恰恰相反,是因为 Fortran 不具备的特性。”这是因为 C 语言中的指针给编译器的优化带来了困难,文章中继续说道:“问题就来了。这些指针可以代替任何内存地址。更重要的是,他们可以重叠。输出数组的内存地址也可以同时是输入数组的。甚至可以部分重叠,输出数组可以覆盖一个输入数组的一半。这对编译器优化来说是个大问题,因为之前基于数组的优化不再适用。特别的,添加元素的顺序也成问题,如果输出重叠的数组,计算的结果会变得不确定,取决于输出元素的动作是发生在元素被覆盖之前还是之后。”
有了 __restrict__,C 语言的该问题将不复存在。用 __restrict__ 修饰一个指针后,①该指针只能在定义的时候被初始化;②不会再有别的指针指向该指针指向的内存,因此编译器可以对算法进行优化。如下代码:
int * __restrict__ p = (int*)malloc(100*sizeof(int));
指针 p 有 __restrict__ 关键字修饰,所以它只能在定义的时候被初始化,以后不能赋值,而没有 __restrict__ 修饰的指针,可以随时赋值,如下:
int arr[100]; int* pArr; pArr = arr;
指针 pArr 没有被 __restrict__ 关键字修饰,所以可以将数组的首地址赋值给它。
比如我们定义一个函数对两块数据进行操作,结果放入第 3 块内存,如下:
void func1(void* p1, void* p2, void* p3, int size){ for(int i=0; i<size; i++){ p3[i] = p1[i] + p2[i]; } }
很显然,由于编译器没办法判断指针 p1、p2、p3 指向的内存是否重叠,所以无法进行优化,加上 __restrict__ 关键字后,如下:
void func1(void* __restrict__ p1, void* __restrict__ p2, void* __restrict__ p3, int size){ for(int i=0; i<size; i++){ p3[i] = p1[i] + p2[i]; } }
相当于明确告诉编译器这几块内存不会重叠,所以编译器就可以放心大胆对程序进行优化。
另一个关键字是 _Complex,C99 才引入,而且需要包含 <complex.h> 头文件。其实在 GNU C 中,早就有__complex__、__real__、__imag__ 等扩展关键字。如下代码:
1 #include <stdlib.h> 2 #include <stdio.h> 3 4 int main(){ 5 __complex__ a = 3 + 4i; 6 __complex__ b = 5 + 6i; 7 __complex__ c = a + b; 8 __complex__ d = a * b; 9 __complex__ e = a / b; 10 printf("a + b = %f + %fi\n", __real__ c, __imag__ c); 11 printf("a * b = %f + %fi\n", __real__ d, __imag__ d); 12 printf("a / b = %f + %fi\n", __real__ e, __imag__ e); 13 return 0; 14 }
可以看到,在 C 语言中也可以直接对复数进行计算。数值计算再也不是 Fortran 的专利。
感悟二:指针和数组还真是不一样
从学 C 语言开始,老师就教导我们说指针和数组是一样的,它们可以用同样的方式进行操作。而事实上,指针和数组还是有差别的。直到多年后读《C专家编程》,才直到所谓指针和数组一样是一个美丽的错误,只是因为在《The C Programming Language》这本书里,把“作为函数参数时,指针和数组一样”这样一句话前后分开分别印到了两页而已。
比如,指针不保存数据的长度信息,而数组有,如下代码:
1 #include <stdlib.h> 2 #include <stdio.h> 3 4 int main(){ 5 int* p = (int*)malloc(100*sizeof(int)); 6 int arr[100] = {0}; 7 printf("The size of p: %d\n", sizeof(p)); 8 printf("The size of arr: %d\n", sizeof(arr)); 9 return 0; 10 }
这段代码的运行结果为:
The size of p: 8
The size of arr: 400
我们经常可以使用如下的代码片段来获得一个数组中有多少个元素,如下:
int arr[100]; size_t length = sizeof(arr)/sizeof(int);
但是,当使用数组作为函数的参数的时候,数组会退化成指针。如下代码:
1 #include <stdlib.h> 2 #include <stdio.h> 3 4 void test_array(int arr[]){ 5 printf("The size of arr in function: %d\n", sizeof(arr)); 6 return; 7 } 8 9 int main(){ 10 int arr[100] = {0}; 11 printf("The size of arr in main: %d\n", sizeof(arr)); 12 test_array(arr); 13 return 0; 14 }
这段代码的运行结果为:
The size of arr in main: 400
The size of arr in function: 8
感悟三:C 语言中的不完全类型(Incomplete Types)
在 GNU C 中可以定义不完全类型,不完全类型主要有两种,一种是空的结构,一种是空的数组,比如:
struct point; char name[0];
空的结构不能定义变量,只能使用空结构的指针。空结构可以在后面再将它补充完整,如下:
struct point{ int x,y; };
空结构在定义链表的时候经常用到,如下:
struct linked_list{ struct linked_list* next; int x; /*other elements here perhaps */ } struct linked_list* head;
还有一种不完全类型就是将一个结构的最后一项定义为一个空的数组,这样可以用来表示一个可变长度的结构或数组,演示该技术的代码如下:
1 #include <stdlib.h> 2 #include <stdio.h> 3 4 typedef struct { 5 int length; 6 int arr[0]; 7 } incomplete_type; 8 9 int main(){ 10 char hello[] = "Hello, world!"; 11 int length = sizeof(hello) / sizeof(char); 12 incomplete_type* p = (incomplete_type*)malloc(sizeof(int) + length*sizeof(char)); 13 p->length = length; 14 for(int i=0; i<p->length; i++){ 15 p->arr[i] = hello[i]; 16 } 17 printf("p->length=%d\n", p->length); 18 printf("p->arr=%s\n", p->arr); 19 }
打造 C/C++ 的 IDE
后面的内容展示如何将 Vim 打造成一个半自动的 C/C++ IDE。读过我的 Java 博客的朋友应该知道,其实我更喜欢用 Eclipse。只有在需要写非常简单的程序(比如做习题)的情况下,我才会用 Vim。这在我的《打造属于自己的Vim》中有论述。在这篇文章中我展示了怎么使用 Vundle 管理插件以及怎么怎么阅读帮助文档,同时展示了 taglist.vim 的简单用法。如果要用 Vim 来写 C/C++程序,还需要做少许扩展。
第一,安装以下几个插件,由于使用 Vundle 管理插件,所以只需要把插件名写入 .vimrc 配置文件,然后运行 :BundleInstall 即可,如下图:
分别介绍一下这几个插件。The-NERD-tree 是一个浏览目录和文件的插件,可以使用 :help NERD_tree.txt 查看它的帮助文档。taglist.vim 是浏览符号以及在符号之间跳转的插件,使用 :help taglist.txt 查看它的帮助文档。 a.vim 是在源代码文件和头文件之间跳转的插件,不需要帮助文档,它的命令就是 :A。c.vim 是提供IDE功能的主要插件,它提供的功能有自动注释、反注释、自动插入代码块及自动运行,如果安装了 splint,还可以对代码进行静态检查,使用 :help csupport.txt 查看它的文档。OmniCppComplete 是一个提供自动补全功能的插件,使用 :help omnicppcomplete.txt 查看它的文档。
这些插件中,taglist.vim 和 OmniCppComplete 需要 ctags 软件的支持,所以需要安装 exuberant-ctags 软件包,在 Fedora 20 中,只需要使用 yum install ctags 即可自动安装。
第二,生成 tags 数据库,并将其加入到 Vim 中。
我们写C程序的时候,使用到的文件主要存在于两个地方,一个是我们工作的当前目录,另外一个是 /usr/include。所以要到 /usr/include 目录下使用 ctags 命令生成 tags 数据库文件。为了使 tags 数据库中包含尽可能多的信息(结构、枚举、类、函数、宏定义等等),需要指定 ctags 的参数,如下:
然后将该 tags 文件的路径加入到 .vimrc 配置文件中,同时设置一个键盘映射,使得按 Ctrl+F12 时,在工作目录中调用 ctags 命令。如下配置文件的最后两行:
然后,在使用 Vim 写 C 程序的时候,如果输入了 .、-> 这样的元素,则其成员会自动补全。如果输入的是一个字符串(比如函数名),可以按 Ctrl-X Ctrl-O 调用自动补全,如下图:
不仅会弹出候选窗口,而且在最上面的窗口中会显示函数的完整的签名,及其所在的文件。这对于我们经常记不全函数名、记不清函数签名的人来说,已经是莫大的福音了。
taglist.vim 和 OmniCppComplete 插件提供的功能用起来都只需要一个命令,而 c.vim 提供的命令就比较多了。而且在 c.vim 的帮助文档中并没有列出所有功能的命令,有一个办法可以学习这些命令,那就是打开 GVim,通过 GVim 菜单中的 C/C++ 菜单来学习 c.vim 提供的功能和命令。
相比网上其它的将 Vim 打造成 IDE 的文章,我的配置比较简单,基本上只安装了几个插件,而没有做过多的设置。当我需要某个功能的时候,我会使用命令显式地调用它,所以,称它为半自动化 IDE 吧。
(京山游侠于2014-06-29发布于博客园,转载请注明出处。)
最新进展
最近几年 Linux 越来越被受到重视,因此 Linux 系统下的编辑软件发展很快。特别是像微软这样实力雄厚的公司,一旦发威,对 Linux 世界的影响可以用翻天覆地来形容。就说在编辑器领域吧,微软推出的 Visual Studio Code 已经成为了我编辑程序代码的首选。当然,使用 vim 编辑一下配置文件还是很顺手的,但是确实没有必要想尽办法把它配置成 IDE 了。
另外,Eclipse 也不错,用 Eclipse 做 C/C++ 的 IDE 是绝对够格的,其体验应该是绝对能超过 vim 的。而且在 Linux 下使用 C/C++, Gnu Autotools 应该是一个绕不过的坎。具体内容,请看我这一篇 使用 Eclipse 和 Gnu Autotools 管理 C/C++ 项目
(京山游侠于2016-08-20更新于博客园,转载请注明出处。)