Skip to content

linux下的c语言开发

hsy edited this page Jan 20, 2015 · 22 revisions

ref

部分 I. C语言入门

  • 编写程序可以说就是这样一个过程:把复杂的任务分解成子任务,把子任务再分解成更简单的任务,层层分解,直到最后简单得可以用以上指令来完成。

  • 程序由语句或指令组成,计算机只能执行低级语言中的指令(汇编语言的指令要先转成机器码才能执行),高级语言要执行就必须先翻译成低级语言,翻译的方法有两种--编译和解释,虽然有这样的不便,但高级语言有一个好处是平台无关性。什么是平台?一种平台,就是一种体系结构,就是一种指令集,就是一种机器语言,这些都可看作是一一对应的,上文并没有用“一一对应”这个词,但读者应该能推理出这个结论,而高级语言和它们不是一一对应的,因此高级语言是平台无关的,概念之间像这样的数量对应关系尤其重要。

  • gcc -Wall, 一个好的习惯是打开gcc的-Wall选项,也就是让gcc提示所有的警告信息,不管是严重的还是不严重的,然后把这些问题从代码中全部消灭。

  • C语言标准

C语言标准

C语言的发展历史大致上分为三个阶段:Old Style C、C89和C99。Ken Thompson和Dennis Ritchie最初发明C语言时有很多语法和现在最常用的写法并不一样,但为了向后兼容性(Backward Compatibility),这些语法仍然在C89和C99中保留下来了,本书不详细讲Old Style C,但在必要的地方会加以说明。C89是最早的C语言规范,于1989年提出,1990年首先由ANSI(美国国家标准委员会,American National Standards Institute)推出,后来被接纳为ISO国际标准(ISO/IEC 9899:1990),因而有时也称为C90,最经典的C语言教材[K&R]就是基于这个版本的,C89是目前最广泛采用的C语言标准,大多数编译器都完全支持C89。C99标准(ISO/IEC 9899:1999)是在1999年推出的,加入了许多新特性,但目前仍没有得到广泛支持,在C99推出之后相当长的一段时间里,连gcc也没有完全实现C99的所有特性。C99标准详见[C99]。本书讲C的语法以C99为准,但示例代码通常只使用C89语法,很少使用C99的新特性。

C标准的目的是为了精确定义C语言,而不是为了教别人怎么编程,C标准在表达上追求准确和无歧义,却十分不容易看懂,[Standard C]和[Standard C Library]是对C89及其修订版本的阐释(可惜作者没有随C99更新这两本书),比C标准更容易看懂,另外,参考[C99 Rationale]也有助于加深对C标准的理解。


* C标准库和glibc

C标准库和glibc

C标准主要由两部分组成,一部分描述C的语法,另一部分描述C标准库。C标准库定义了一组标准头文件,每个头文件中包含一些相关的函数、变量、类型声明和宏定义。要在一个平台上支持C语言,不仅要实现C编译器,还要实现C标准库,这样的实现才算符合C标准。不符合C标准的实现也是存在的,例如很多单片机的C语言开发工具中只有C编译器而没有完整的C标准库。

在Linux平台上最广泛使用的C函数库是glibc,其中包括C标准库的实现,也包括本书第三部分介绍的所有系统函数。几乎所有C程序都要调用glibc的库函数,所以glibc是Linux平台C程序运行的基础。glibc提供一组头文件和一组库文件,最基本、最常用的C标准库函数和系统函数在libc.so库文件中,几乎所有C程序的运行都依赖于libc.so,有些做数学计算的C程序依赖于libm.so,以后我们还会看到多线程的C程序依赖于libpthread.so。以后我说libc时专指libc.so这个库文件,而说glibc时指的是glibc提供的所有库文件。

glibc并不是Linux平台唯一的基础C函数库,也有人在开发别的C函数库,比如适用于嵌入式系统的uClibc。


* 虽然结构体的成员名和变量名不在同一命名空间中,但枚举的成员名却和变量名在同一命名空间中,所以会出现命名冲突。

#include <stdio.h>

enum coordinate_type { RECTANGULAR = 1, POLAR };//OK, global int main(void) { //enum coordinate_type { RECTANGULAR = 1, POLAR };//f1.c:7: error: ‘RECTANGULAR’ redeclared as different kind of symbol int RECTANGULAR;//local printf("%d %d\n", RECTANGULAR, POLAR);

return 0;

}


* 数据抽象: 这里的复数存储表示层和复数运算层称为抽象层(Abstraction Layer),从底层往上层来看,复数越来越抽象了,把所有这些层组合在一起就是一个完整的系统。组合使得系统可以任意复杂,而抽象使得系统的复杂性是可以控制的,任何改动都只局限在某一层,而不会波及整个系统。

* gcc -E/ cpp, 这里介绍一种新的语法:用#define定义一个常量。实际上编译器的工作分为两个阶段,先是预处理(Preprocess)阶段,然后才是编译阶段,用gcc的-E选项可以看到预处理之后、编译之前的程序gcc -E main.c。可见在这里预处理器做了两件事情,一是把头文件stdio.h和stdlib.h在代码中展开,二是把#define定义的标识符N替换成它的定义。此外,用cpp main.c命令也可以达到同样的效果,只做预处理而不编译,cpp表示C preprocessor。

* 写代码时应尽可能避免硬编码,这其实也是一个“提取公因式”的过程,和第 2 节 “数据抽象”讲的抽象具有相同的作用,就是避免一个地方的改动波及到大的范围。

* gcc -g, gdb 单步调试 http://akaedu.github.io/book/ch10s01.html

* 算法 算法的时间复杂度分析 http://akaedu.github.io/book/ch11s03.html
* 几种常见的时间复杂度函数按数量级从小到大的顺序依次是:Θ(lgn),Θ(sqrt(n)),Θ(n),Θ(nlgn),Θ(n2),Θ(n3),Θ(2n),Θ(n!)。

* 数据结构, 队列 栈 环形队列 深度优先搜索算法 广度有线搜索算法, 算法和数据结构见wiki对应章节。

### 部分 II. C语言本质
* 十进制转二进制:除以2取余

我们将13反复除以2取余数就可以提取出上式中的1101四个数字,为了让读者更容易看清楚是哪个1和哪个0,上式和下式中对应的数字都加了下标:

13÷2=6...10 6÷2=3...01 3÷2=1...12 1÷2=0...13

* Side Effect与Sequence Point
* see https://github.com/cheyiliu/All-in-One/wiki/Side-Effect-&-Sequence-Point

* 段错误我们已经遇到过很多次了,它是这样产生的:

 用户程序要访问的一个VA,经MMU检查无权访问。

 MMU产生一个异常,CPU从用户模式切换到特权模式,跳转到内核代码中执行异常服务程序。

 内核把这个异常解释为段错误,把引发异常的进程终止掉。


* 第 18 章 x86汇编程序基础

`as hello.s -o hello.o` `ld hello.o -o hello`

* ELF, readelf

 ELF文件格式是一个开放标准,各种UNIX系统的可执行文件都采用ELF格式,它有三种不同的类型:

 可重定位的目标文件(Relocatable,或者Object File)

 可执行文件(Executable)

 共享库(Shared Object,或者Shared Library)

* 反汇编 $ gcc main.c -g $ objdump -dS a.out。  gcc -S main.c,这样只生成汇编代码main.s

* gcc main.c -o main命令编译一个程序,其实也可以分三步做,第一步生成汇编代码,第二步生成目标文件,第三步生成可执行文件
$ gcc -S main.c   
$ gcc -c main.s   
$ gcc main.o   

* 我们只关心符号表,如果只看符号表,可以用readelf命令的-s选项,也可以用nm命令$ nm /usr/lib/crt1.o 

* 用gcc的-v选项可以了解详细的编译过程 $ gcc -v main.c -o main

* 按照惯例,退出状态为0表示程序执行成功,退出状态非0表示出错。

* 标识符的链接属性

标识符的链接属性(Linkage)有三种:

外部链接(External Linkage),如果最终的可执行文件由多个程序文件链接而成,一个标识符在任意程序文件中即使声明多次也都代表同一个变量或函数,则这个标识符具有External Linkage。具有External Linkage的标识符编译后在符号表中是GLOBAL的符号。例如上例中main函数外面的a和c,main和printf也算。

内部链接(Internal Linkage),如果一个标识符在某个程序文件中即使声明多次也都代表同一个变量或函数,则这个标识符具有Internal Linkage。例如上例中main函数外面的b。如果有另一个foo.c程序和main.c链接在一起,在foo.c中也声明一个static int b;,则那个b和这个b不代表同一个变量。具有Internal Linkage的标识符编译后在符号表中是LOCAL的符号,但main函数里面那个a不能算Internal Linkage的,因为即使在同一个程序文件中,在不同的函数中声明多次,也不代表同一个变量。

无链接(No Linkage)。除以上情况之外的标识符都属于No Linkage的,例如函数的局部变量,以及不表示变量和函数的其它标识符。

* 存储类修饰符

存储类修饰符(Storage Class Specifier)有以下几种关键字,可以修饰变量或函数声明:

static,用它修饰的变量的存储空间是静态分配的,用它修饰的文件作用域的变量或函数具有Internal Linkage。

auto,用它修饰的变量在函数调用时自动在栈上分配存储空间,函数返回时自动释放,例如上例中main函数里的b其实就是用auto修饰的,只不过auto可以省略不写,auto不能修饰文件作用域的变量。

register,编译器对于用register修饰的变量会尽可能分配一个专门的寄存器来存储,但如果实在分配不开寄存器,编译器就把它当auto变量处理了,register不能修饰文件作用域的变量。现在一般编译器的优化都做得很好了,它自己会想办法有效地利用CPU的寄存器,所以现在register关键字也用得比较少了。

extern,上面讲过,链接属性是根据一个标识符多次声明时是不是代表同一个变量或函数来分类的,extern关键字就用于多次声明同一个标识符,下一章再详细介绍它的用法。

typedef,在第 2.4 节 “sizeof运算符与typedef类型声明”讲过这个关键字,它并不是用来修饰变量的,而是定义一个类型名。在那一节也讲过,看typedef声明怎么看呢,首先去掉typedef把它看成变量声明,看这个变量是什么类型的,那么typedef就定义了一个什么类型,也就是说,typedef在语法结构中出现的位置和前面几个关键字一样,也是修饰变量声明的,所以从语法(而不是语义)的角度把它和前面几个关键字归类到一起。


* 多目标文件的链接 

$ gcc main.c stack.c -o main OR $ gcc -c main.c $ gcc -c stack.c $ gcc main.o stack.o -o main


* static关键字声明具有Internal Linkage的函数也是出于这个目的, 封装。

* gcc -I, 对于用角括号包含的头文件,gcc首先查找-I选项指定的目录,然后查找系统的头文件目录(通常是/usr/include,在我的系统上还包括/usr/lib/gcc/i486-linux-gnu/4.3.2/include);而对于用引号包含的头文件,gcc首先查找包含头文件的.c文件所在的目录,然后查找-I选项指定的目录,然后查找系统的头文件目录。

* 重复包含头文件有以下问题:
一是使预处理的速度变慢了,要处理很多本来不需要处理的头文件。

二是如果有foo.h包含bar.h,bar.h又包含foo.h的情况,预处理器就陷入死循环了(其实编译器都会规定一个包含层数的上限)。

三是头文件里有些代码不允许重复出现,虽然变量和函数允许多次声明(只要不是多次定义就行),但头文件里有些代码是不允许多次出现的,比如typedef类型定义和结构体Tag定义等,在一个程序文件中只允许出现一次。

还有一个问题,既然要#include头文件,那我不如直接在main.c中#include "stack.c"得了。这样把stack.c和main.c合并为同一个程序文件,相当于又回到最初的例 12.1 “用堆栈实现倒序打印”了。当然这样也能编译通过,但是在一个规模较大的项目中不能这么做,假如又有一个foo.c也要使用stack.c这个模块怎么办呢?如果在foo.c里面也#include "stack.c",就相当于push、pop、is_empty这三个函数在main.c和foo.c中都有定义,那么main.c和foo.c就不能链接在一起了。


* static lib, $ gcc -c stack/stack.c stack/push.c stack/pop.c stack/is_empty.c, ar rs libstack.a stack.o push.o pop.o is_empty.o 选项r表示将后面的文件列表添加到文件包,如果文件包不存在就创建它,如果文件包中已有同名文件就替换成新的。s是专用于生成静态库的,表示为静态库创建索引,这个索引被链接器使用。然后我们把libstack.a和main.c编译链接在一起:$ gcc main.c -L. -lstack -Istack -o main   -L选项告诉编译器去哪里找需要的库文件,-L.表示在当前目录找。-lstack告诉编译器要链接libstack库,-I选项告诉编译器去哪里找头文件。注意,即使库文件就在当前目录,编译器默认也不会去找的,所以-L.选项不能少。编译器默认会找的目录可以用-print-search-dirs选项查看。 编译器会在这些搜索路径以及-L选项指定的路径中查找用-l选项指定的库,比如-lstack,编译器会首先找有没有共享库libstack.so,如果有就链接它,如果没有就找有没有静态库libstack.a,如果有就链接它。所以编译器是优先考虑共享库的,如果希望编译器只链接静态库,可以指定-static选项。


* 共享库 $ gcc -shared -fPIC -o libtest.so test.c,  gcc main.c -L. -lstack -o main (gcc hello.c -o  hello ./libtest.so)
* ldd ldd命令查看可执行文件依赖于哪些共享库
* 共享库路径的搜索顺序:
首先在环境变量LD_LIBRARY_PATH所记录的路径中查找。

然后从缓存文件/etc/ld.so.cache中查找。这个缓存文件由ldconfig命令读取配置文件/etc/ld.so.conf之后生成,稍后详细解释。

如果上述步骤都找不到,则到默认的系统路径中查找,先是/usr/lib然后是/lib。

* 按照共享库的命名惯例,每个共享库有三个文件名:real name、soname和linker name。

* 虚拟内存管理

第一,虚拟内存管理可以控制物理内存的访问权限。 第二,虚拟内存管理最主要的作用是让每个进程有独立的地址空间。 第三,VA到PA的映射会给分配和释放内存带来方便,物理地址不连续的几块内存可以映射成虚拟地址连续的一块内存。 第四,一个系统如果同时运行着很多进程,为各进程分配的内存之和可能会大于实际可用的物理内存,虚拟内存管理使得这种情况下各进程仍然能够正常运行。因为各进程分配的只不过是虚拟内存的页面,这些页面的数据可以映射到物理页面,也可以临时保存到磁盘上而不占用物理页面,在磁盘上临时保存虚拟内存页面的可能是一个磁盘分区,也可能是一个磁盘文件,称为交换设备(Swap Device)。


* 预处理的步骤

1、把第 2 节 “常量”提到过的三连符替换成相应的单字符。 2、把用\字符续行的多行代码接成一行。 3、把注释(不管是单行注释还是多行注释)都替换成一个空格。 4、经过以上两步之后去掉了一些换行,有的换行在续行过程中去掉了, 有的换行在多行注释之中,也随着注释一起去掉了,剩下的代码行称为逻辑代码行。 然后预处理器把逻辑代码行划分成Token和空白字符,这时的Token称为预处理Token, 包括标识符、整数常量、浮点数常量、字符常量、字符串、运算符和其它符号。 继续上面的例子,两个源代码行被接成一个逻辑代码行,然后这个逻辑代码行被划分成Token 和空白字符:#,define,空格,STR,空格,"hello, ",Tab,Tab,"world"。 5、在Token中识别出预处理指示,做相应的预处理动作,如果遇到#include预处理指示, 则把相应的源文件包含进来,并对源文件做以上1-4步预处理。如果遇到宏定义则做宏展开。 6、找出字符常量或字符串中的转义序列,用相应的字节来替换它,比如把\n替换成字节0x0a。 7、把相邻的字符串连接起来。 8、经过以上处理之后,把空白字符丢掉,把Token交给C编译器做语法解析,这时就不再是预处理Token,而称为C Token了。这里丢掉的空白字符包括空格、换行、水平Tab、垂直Tab、分页符。


* 宏
* gcc的-E选项或cpp命令查看宏展开结果
   ```
#define MAX(a, b) ((a)>(b)?(a):(b))
k = MAX(i&0x0f, j&0x0f)

housy@housy-desktop:~/桌面/libc$ cpp macro.c 
# 1 "macro.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "macro.c"

k = ((i&0x0f)>(j&0x0f)?(i&0x0f):(j&0x0f))
housy@housy-desktop:~/桌面/libc$
   ```

* 函数式宏定义和真正的函数调用有什么不同

1、函数式宏定义的参数没有类型,预处理器只负责做形式上的替换,而不做参数类型检查,所以传参时要格外小心。

2、调用真正函数的代码和调用函数式宏定义的代码编译生成的指令不同。如果MAX是个真正的函数, 那么它的函数体return a > b ? a : b;要编译生成指令, 代码中出现的每次调用也要编译生成传参指令和call指令。 而如果MAX是个函数式宏定义,这个宏定义本身倒不必编译生成指令, 但是代码中出现的每次调用编译生成的指令都相当于一个函数体, 而不是简单的几条传参指令和call指令。 所以,使用函数式宏定义编译生成的目标文件会比较大。

3、定义这种宏要格外小心,如果上面的定义写成#define MAX(a, b) (a>b?a:b), 省去内层括号,则宏展开就成了k = (i&0x0f>j&0x0f?i&0x0f:j&0x0f),运算的优先级就错了。 同样道理,这个宏定义的外层括号也是不能省的,想一想为什么。

4、调用函数时先求实参表达式的值再传给形参,如果实参表达式有Side Effect, 那么这些Side Effect只发生一次。例如MAX(++a, ++b),如果MAX是个真正的函数, a和b只增加一次。但如果MAX是上面那样的宏定义,则要展开成 k = ((++a)>(++b)?(++a):(++b)),a和b就不一定是增加一次还是两次了。

5、即使实参没有Side Effect,使用函数式宏定义也往往会导致较低的代码执行效率。

尽管函数式宏定义和真正的函数相比有很多缺点,但只要小心使用还是会显著提高代码的执行效率, 毕竟省去了分配和释放栈帧、传参、传返回值等一系列工作,因此那些简短并且被频繁调用的函数经常用 函数式宏定义来代替实现。例如C标准库的很多函数都提供两种实现,一种是真正的函数实现, 一种是宏定义实现,这一点以后还要详细解释。


* #、##运算符和可变参数
* 在函数式宏定义中,#运算符用于创建字符串,#运算符后面应该跟一个形参(中间可以有空格或Tab),例如:#define STR(s) # s     
STR(hello 	world)  用cpp命令预处理之后是"hello␣world",自动用"号把实参括起来成为一个字符串,并且实参中的连续多个空白字符被替换成一个空格。
* 用##运算符把前后两个预处理Token连接成一个预处理Token,和#运算符不同,##运算符不仅限于函数式宏定义,变量式宏定义也可以用。#define CONCAT(a, b) a##b    CONCAT(con, cat)    预处理之后是concat

* 在宏定义中,可变参数的部分用__VA_ARGS__表示,实参中对应...的几个参数可以看成一个参数替换到宏定义中__VA_ARGS__所在的地方。

* 宏展开的步骤

sh(sub_z)要用sh(x)这个宏定义来展开,形参x对应的实参是sub_z,替换过程如下:

#x要替换成"sub_z"。

n##x要替换成nsub_z。

除了带#和##运算符的参数之外,其它参数在替换之前要对实参本身做充分的展开,所以应该先把sub_z展开成26再替换到alt[x]中x的位置。

现在展开成了printf("n" "sub_z" "=%d, or %d\n",nsub_z,alt[26]),所有参数都替换完了,这时编译器会再扫描一遍,再找出可以展开的宏定义来展开,假设nsub_z或alt是变量式宏定义,这时会进一步展开。

* 预处理特性

#ifndef HEADER_FILENAME #define HEADER_FILENAME /* body of header */ #endif

#if defined x || y || VERSION < 3

FILE LINE func


* 第 22 章 Makefile基础
略, cmake automake is preferable

* 第 23 章 指针

在函数原型中,如果参数是数组,则等价于参数是指针的形式,数组形参弱化为指针。 //TODO




### 部分 III. Linux系统编程
Clone this wiki locally