Hello#include <stdio.h> int main (void) { printf("Hello, world!\n"); return 0; }
gcc –Wall hello.c –o hello ( 其中 –Wall 打开所有常用到的编译警告,-o 输出文件名 )
编译后,会生成名为hello的可执行文件,使用 ./hello,就可以运行文件,显示 “Hello, world!”
编译多个文件
顺便说一下,#include”file.h”和 include<file.h>前者是先在当前目录搜索”file.h”,然后再查看包含系统头文件的目录,后者是搜索系统的头文件,默认情况下,不会在当前目录下查找头文件。
main.c#include "hello.h" int main(void) { hello("world"); return 0; }
hello.hvoid hello(const char * name);
hello_fn.c#include <stdio.h> #include "hello.h" void hello(const char * name) { printf("Hello,%s!\n", name); }
gcc –Wall main.c hello_fn.c –o newhello,.h 头文件不需要参与编译。
独立编译文件
由于很多时候,是多个源文件组成在一起,这时就不需要全部重新编译,而是独立的编译每个源文件,然后再链接在一起,分为两个步骤进行:
1)文件被编译但不生成可执行文件,生成的结果被称为对象文件(obj文件),用GCC时有“.o”后缀名
2)各个对象文件由一个被称为链接器的单独程序合成在一起,链接器吧所有的对象文件组合在一起生成单个的可执行文件。
对象文件包含有机器码,其中任何对其他文件中的函数(或变量)的内存地址的引用都留着没有被解析,这样就准许在互相之间不直接引用的情况下编译各个源代码文件,链接器在生成可执行文件时会填写这些还缺少的地址。
生成对象文件命令 –c 例如 gcc –Wall –c main.c 这样就生成了 main.o (同名.o文件 )
由于 main.c 和 hello_fn.c 中有 #include声明,hello.h会自动被包括进来,所以在命令行上不需要指定该头文件。
链接在一起 : gcc main.o hello_fn.o –o haha ( 这个阶段要么成功,要么失败,不会出现什么错误警告,只有在引用不能解析的情况下才会链接失败)
-
类Unix系统中,命令执行搜索时,从左到右查找,这意味着包含函数定义的对象文件应当出现在调用这些函数的任何文件的后边,由于main调用了hello,所以hello_fn.o要出现在main.o后边,因为当解析到main.o时发现了外部函数hello,这时,它要搜索这个外部函数,并从命令行中,main.o后边开始搜索,如果hello_fn.o放到main.o前边,则无法搜索的到,就会出现错误。(基本上绝大多数编译器和链接器都会全部搜索命令行,不过还是由少部分不是这样)
如果函数的原型也改变了,那需要修改和重新编译所有用到它的其他源文件。
与外部库文件链接
库是已经编译好并能被链接入程序的对象文件(.o文件)的集合。库通常被存储在扩展名为”.a”的特殊归档文件中,被称为静态库,标准的系统库通常在”/usr/lib”和”/lib”目录下找到,例如”/usr/include/math.a”而该 库中的相应的函数的原型声明在头文件”/usr/include/math.h”中。C标准库自身存放在“/usr/liblibc.a”中
gcc –Wall main.c /usr/lib/libm.a –o abc 其中/usr/lib/libm.a是库文件所在位置,此命令是指定程序中调用库文件位置。由于指定路径台长,编译器提供 –l 选项用于链接库文件。例如 :
gcc –Wall main.c –lm –o abc,与上面的全路径是一样的,-lNAME 试图链接标准库目录下的文件名为“libNAME.a”中的对象,另外可以通过命令行和环境变量指定的目录,在大型程序中通常会用到很多“-l”选项,来链接类似数据库,图形库,网络库。
使用库的头文件
就是指要在程序中指定#include <math.h> 之类的,即便是像上边在命令行指定了路径或者什么,也要在程序中指定该路径对应的头文件,因为头文件中有函数声明。
编译选项
设置搜索路径
默认情况下,gcc会在下面目录中搜索头文件
/usr/local/include/
/usr/include/
在下面目录中搜索库
/usr/local/lib/
/usr/lib/
搜索头文件的目录列表被称为 include 路径,而搜索库的目录列表被称为搜索路径或链接路径
gcc –Wall –I/路径/include –L/路径/lib main.c –o 其中的 –I 和 –L 分别是将不再默认路径中的库添加进来。
环境变量
通过 shell 中的环境可以控制头文件和库的搜索路径。类似使用环境变量 C_INCLUDE_PATH(针对C)和CPP_INCLUDE_PATH(针对C++),把其他的目录添加到 include 路径中,类似的LIBRARY_PATH设置库文件。
C_INCLUDE_PATH = /opt/gdbm-1.8.3/include
export C_INCLUDE_PATH
LIBRARY_PATH = /opt/gdbm-1.8.3/lib
export LIBRARY_PATH
通常是使用 –I 和 –L 指定路径。这样快
共享库与静态库
外部库通常用两种形式提供:共享库和静态库。
静态库:就是前面说过的“.a”文件,当程序与一个静态库链接时,该程序用到的外部函数(在用到的静态库包含的对象文件中)的机器码被从库中复制到最终生成的可执行文件中。
共享库:它会使得可执行文件比较小,使用“.so”后缀名,代表共享对象。一个共享库链接的可执行文件仅仅包含它用到函数相关的一个表格,而不是外部函数所在对象文件的整个机器码,在执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上该共享库中复制到内存中,这个过程叫动态链接
共享库使得升级库而无需重新编译用到它的程序。
由于以上优点,所以共享库的优先级大于静态库。所以当链接库时,编译器会首选动态库,如果非要使用静态库,则 gcc –Wall –static –I/opt/gdbm-1.8.3/include/ –L/opt/gdbm-1.8.3/lib/ abc.c –lgdbm
-ansi 与 –pedantic 参数
-ansi 表示参考 C 的标准,比如在程序中定义了一个变量 asm,因为asm是汇编语言的关键字,所以在GNU编译中,不能执行,但是如果使用-ansi参数就是参考C的标准,就可以执行。
gcc –Wall –ansi ansi.c
-W
-W 和 –Wall 一般会同时使用
预处理和定义宏
预处理器在源文件被编译以前展开其中的宏。
宏#include "hello.h" int main(void) { #ifdef TEST printf("Test mode\n"); #endif printf("Running...\n"); return 0; }
gcc –Wall –W –DTEST dtest.c –o hello
这其中的 –D,“-DNAME”中的NAME是命令行中指定
宏通常都是未定义的,除非用“-D”选项在命令行上指定,或在源文件中(或库的头文件)用#define定义。有些宏是由编译器自动定义的,这些宏会用到双下划线 __开始保留名字空间。
给宏赋值
宏赋值#include "hello.h" int main(void) { printf("Value of NUM is %d\n", NUM); return 0; }
gcc –Wall –W –DNUM=100 dtestval.c
注意,宏在字符串中不会被展开,命令行“-D”选项可以为宏赋值,形式为“-DNAME=VALUE”
以上程序输出的结果是,value of NUM is 100,这个例子定义的是数字的宏,其实宏可以定义成任何形式,只是直接替换,例如定义
gcc –Wall –W –DNUM=“2+2” detestval.c
那么,用()将宏括起来是好主意。宏中如果想包含“”号,则需要转义字符 \
预处理源文件
gcc –E hello.c 只运行预处理器,并且此时宏已经替换。
gcc –c –save-temps hello.c 选项除了保存预处理过的 ”.i”预处理过的文件”.s”汇编文件”.o”对象文件。
待调试信息的编译
检查 core文件 “-g”选项除了准许程序在调试器控制下运行意外,另一个有用的应用是找到程序崩溃的环境,当一个程序异常退出时,操作系统会写一个常规被称为“core”的文件,设置可以显示”core”,ulimit –c unlimited,gdb调试器可以使用下面命令载入 core文件
gdb EXECUTABLE-FILE COR-FILE ,注意原来的可执行文件和core文件都需要提供
gdb abc.out core
gdb调试器
print p 显示信息
backtrace 显示回溯堆栈
编译优化
1)源码级优化 ( 减少计算次数 )
-
公共子表达式消除 例如 x = cos(v) * (1 + sin(u/2) ) + sin(w) * (1-sin(u/2)) 重写为 t = sin( u/2),x=cos(v)*(1+t) + sin(w)*(1-t)
-
函数内嵌 ( 减少函数保存内容 ) 例如 计算平方函数,下边的循环由于没有用到函数,所以节省了保存的函数地址啊等等内容,由于循环很多次,所以这个内容也比较可观,但是一般这方面的优化不是很明显。
函数内嵌double sq(double x) { return x * x; } //比较01 for(i=0; i<1000000; i++){ sum += sq(i+0.5); } //比较02 for(i=0; i<1000000 i++){ double t = (i + 0.5); sum += t * t; }
优化级别
调试时 “-OLEVEL”,“-O0”
实际部署 “-OLEVEL”, “-O2”
平台相关编译
例如:gcc –Wall -march=pentium4 hello.c
其他一些工具
ar : 将对象文件打包成静态库,例如:ar cr libhello.a hello1.o hello2.o
其中 cr 代表 create and replace
显示静态库的内容列表:ar t libhello.a 显示结果是 hello1.o hello2.o
gprof : 记录每个函数调用次数和每个函数调用所花时间。
“-pg”例如:gcc –Wall –c –pg coll.c
如果是多个文件,则每个源文件编译时都要加 –pg,并且最后链接组合时,也要加 –pg
运行后,该目录下会有一个 gmon.out的文件,里边存储这剖析信息,用 gprof coll.out 即用 gprof 和可执行文件名,就会显示出那些剖析的信息。
编译过程概述
- 预处理(对宏进行扩展)
- 编译(从源代码到汇编语言)
- 汇编(从汇编语言到机器语言)
- 链接(生成最小可执行文件)