├── .gitignore ├── 01_前言 └── README.md ├── 02_编译我们的Hello_World ├── README.md ├── hello_world.cpp └── hello_world.sh ├── 03_指定输出文件名 └── README.md ├── 04_多文件工程 ├── Makefile ├── README.md ├── main.cpp └── sum.cpp ├── 05_稍微多说点目标文件 └── README.md ├── 06_编译期符号检查 ├── README.md └── struct1.cpp ├── 07_声明与定义的区别 └── README.md ├── 08_前言 └── README.md ├── 09_编译过程 ├── README.md ├── main.cpp ├── main.s ├── sum.cpp └── sum.h ├── 10_编译期优化选项_pipe ├── README.md ├── main.cpp └── showTmp.sh ├── 11_编译期优化选项_O ├── README.md ├── main.O0.s ├── main.O1.s ├── main.O2.s └── main.cpp ├── 12_编译期优化选项_W ├── README.md ├── return-type.cpp └── uninitialized.cpp ├── 13_预编译期选项_I ├── README.md ├── header │ ├── header │ └── header.h └── src │ ├── main.cpp │ └── src.h ├── 14_预编译期选项_D ├── README.md └── main.cpp ├── 15_库的使用_使用 ├── Makefile ├── README.md └── zipdemo.c └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.e 3 | *.out 4 | main -------------------------------------------------------------------------------- /01_前言/README.md: -------------------------------------------------------------------------------- 1 | # 第一节:前言 2 | 3 | ## 为啥要介绍GCC呢? 4 | 5 | 其实这个事源于光哥问我的一个问题: 6 | “C语言中宏定义影响的范围有多大?” 7 | 8 | 现在,IDE的易用与普及,使coder们越来越远离命令行的编译方式。IDE确实简单方便,这个毋庸置疑。 9 | 10 | 不过IDE造成的问题是,很多原理性的东西大家可能并不了解。我写本系列文章的目的,就是为了让大家能够更深入地了解一些原理性的东西。 11 | 12 | 本人水平有限,错误在所难免,希望大家看见以后能帮忙指出。 13 | 14 | ## 预备知识 15 | 16 | 本系列文章不介绍基础知识,所以在阅读本系列文章前需要有以下知识: 17 | 18 | 1. C/C++。本系列文章只介绍gcc与C/C++相关的用法。 19 | 2. shell(bash)。因为是在命令行下编译,所以你至少得会命令行啊。不用会的太多,基本的几个命令知道就行。 20 | 21 | ## 顺便一提 22 | 23 | 1. GNU。这个不需要我介绍吧? 24 | 2. 理查德·马修·斯托曼。GCC的最初作者,自由软件运动的精神领袖,GNU计划及自由软件基金会(FSF)的创立者。 25 | 26 | ## 好了,下面简单介绍一下我们的GCC吧。 27 | 28 | GCC刚开始的名字叫GNU C语言编译器`GNU C Compiler`, 29 | 是大神 _理查德·马修·斯托曼_ 在1985年写的。 30 | 那时候斯托曼只是为了写个好用的C语言编译器。 31 | 32 | 后来呢,GCC不止能编译C语言,开始能够编译各种语言, 33 | 包括:`C++`, `Fortran`, `Pascal`, `Objective-C`, `Java`, `Ada`和`Go`等。 34 | 这个时候呢,它的名字也改了。改成叫GNU编译器套装`GNU Compiler Collection`,但是缩写没变,还是GCC。 35 | 36 | 不过,我们这个系列文章使用是它本来的概念:`GNU C Compiler`。因为我们止以C/C++为例介绍GCC的使用。 37 | 38 | # 五分钟到了,下课。 39 | -------------------------------------------------------------------------------- /02_编译我们的Hello_World/README.md: -------------------------------------------------------------------------------- 1 | # 第二节:编译我们的Hello World 2 | 3 | ## 干点啥事都得从Hello World开始,这是谁规定的? 4 | 5 | 代码: 6 | 7 | #include 8 | using namespace std; 9 | int main(){ 10 | cout<<"Hello World"< 3 | using namespace std; 4 | int main(){ 5 | cout<<"Hello World"< 30 | 31 | using namespace std; 32 | 33 | int sum(int a, int b); 34 | 35 | int main(){ 36 | cout << sum(4, 7) << endl; 37 | return 0; 38 | } 39 | 40 | ## 怎么编译呢? 41 | 42 | 错误方法: 43 | 44 | g++ -o main main.cpp 45 | 46 | 如果您这样编译的话,会得到错误: 47 | 48 | /tmp/xxxx.o: In function `main': 49 | main.cpp:(.text+0xf): undefined reference to `sum(int, int)' 50 | collect2: ld returned 1 exit status 51 | 52 | 很明显,从错误信息的第三行可以看出来(ld),是链接时错误。 53 | 54 | 造成这个错误的原因也很简单,main.cpp里面没有sum函数的定义,所以链接器(ld)会报怨,找不到sum函数的定义。 55 | 56 | ## 目标文件 57 | 58 | C/C++是靠目标文件(扩展名为.o的文件)来实现多文件工程的。 59 | 60 | 首先、编译器将源文件编译成目标文件。 61 | 然后、链接器将多个目标文件链接成一个可执行文件。 62 | 63 | 那么,我们用gcc该如何做呢? 64 | 65 | ### 第一步,将源文件编译成目标文件 66 | 67 | gcc使用-c参数来指定, 68 | 我要编译的是目标文件, 69 | 不是直接生成可执行文件。 70 | 71 | 这里用到了我们上一节课讲的-o参数, 72 | 它的含义,相信您一定记得。 73 | 74 | g++ -c -o main.o main.cpp 75 | g++ -c -o sum.o sum.cpp 76 | 77 | ### 第二步,将所有的目标文件链接起来,生成可执行文件 78 | 79 | 这没有新的参数, 80 | 和最初的编译方法一样。 81 | 82 | g++ -o main main.o sum.o 83 | 84 | ### 执行一下试试? 85 | 86 | ./main 87 | 88 | 输出: 89 | 90 | 11 91 | 92 | ## 本节完 93 | -------------------------------------------------------------------------------- /04_多文件工程/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | using namespace std; 3 | int sum(int a,int b); 4 | int main(){ 5 | cout<_data; 20 | } 21 | void set_data(data){ 22 | this->_data = data; 23 | } 24 | private: 25 | int _data; 26 | } 27 | 28 | int bar(Foo* foo, int data){ 29 | int origin_data = foo->data(); 30 | foo->set_data(data); 31 | return origin_data; 32 | } 33 | 34 | 这个代码编译是没有问题的。 35 | (当然链接的时候会报错,说没有main函数)。 36 | 37 | 但是如果只声明Foo而没有写它的定义会怎样呢? 38 | 39 | class Foo; 40 | int bar(Foo* foo, int data){ 41 | int origin_data = foo->data(); 42 | foo->set_data(data); 43 | return origin_data; 44 | } 45 | 46 | 编译器会报错,说找不到`类型Foo`的定义。 47 | 48 | 为什么一定要有`Foo`的定义呢? 49 | 因为编译器需要知道: 50 | 51 | 1. Foo有没有data的set_data这两个方法。 52 | 2. data和set_data这两个方法的参数类型和返回值类型是什么。 53 | 54 | 有了这两点,编译器才能对`bar`这个函数的函数体做类型检查。 55 | 56 | 但是如果`bar`这个函数没有函数体呢? 57 | 或者说`bar`也只是一个声明,而不是定义,会怎么样呢? 58 | 59 | class Foo; 60 | int bar(Foo* foo, int data); 61 | 62 | 这个代码就不会有编译错误。 63 | 64 | 实际工程中这种做法很常用。 65 | 比如把类型的声明和函数的声明写在`a.h`里面, 66 | 把实现写在`a.cpp`里面。 67 | 68 | 如果`b.cpp`需要使用`a.h`中的类型和函数, 69 | 它只需要`#include "a.h"`一下就好了。 70 | 71 | 由于`a.h`只包含声明, 72 | 这个文件会比较短。 73 | 这样会加快`b.cpp`的编译速度。 74 | 75 | 这样的技术叫做`前向声明`。 76 | 77 | ## 链接 78 | 79 | 前面的例子,`b.cpp`会编译成目标文件`b.o`, 80 | 但是由于`b.cpp`没有`bar`函数的定义, 81 | 所以`b.o`里面会记录着: 82 | 83 | > 我需要`bar`的定义。 84 | 85 | 那么同时,`a.cpp`也会编译成目标文件`a.o`, 86 | 它里面包含`bar`函数的定义, 87 | 所以`a.o`里面会记录着: 88 | 89 | > 我提供`bar`的定义。 90 | 91 | 最终在链接的时候, 92 | 将`a.o`和`b.o`链接起来, 93 | 它们就幸福地生活在了一起。 94 | -------------------------------------------------------------------------------- /06_编译期符号检查/README.md: -------------------------------------------------------------------------------- 1 | # 第六节:编译期符号检查 2 | 3 | 上一节,我们说了从源文件到目标文件的编译过程。 4 | 这一节,我想讨论一下编译期符号检查的问题。 5 | 6 | 比如,第四节的例子中, 7 | main.cpp文件中只有sum()函数的声明、而没有定义。 8 | 9 | > 相信您能够分辨C/C++中声明和定义的区别。 10 | 11 | 可是将main.cpp编译成main.o的过程中,没有报任何错误。 12 | 13 | 这里,我们看下面这个例子: 14 | 15 | 我们只声明了一个结构体而没有定义这个结构体, 16 | 然后定义一个这个结构体的变量。 17 | 代码: 18 | 19 | struct Poo; 20 | int main() 21 | { 22 | Poo a; 23 | return 0; 24 | } 25 | 26 | 只编译而不链接: 27 | 28 | g++ -c struct1.cpp 29 | 30 | 这个时候它会报错: 31 | 32 | struct1.cpp: In function ‘int main()’: 33 | struct1.cpp:4:6: error: aggregate ‘Poo a’ has incomplete type and cannot be defined 34 | 35 | 这说明,只有声明没有定义的结构体不能够定义变量。 36 | 37 | 把代码稍微改一下,定义这个结构体的一个指针: 38 | 39 | struct Poo; 40 | int main() 41 | { 42 | Poo *a; 43 | return 0; 44 | } 45 | 46 | 只编译不链接,没有任何问题。 47 | 48 | 这说明,虽然不能定义变量,但是可以定义指针。 49 | 50 | 再把代码稍微改一下,实例化一下: 51 | 52 | struct Poo; 53 | int main() 54 | { 55 | Poo *a=new Poo; 56 | return 0; 57 | } 58 | 59 | 这个时候也会报错: 60 | 61 | struct1.cpp: In function ‘int main()’: 62 | struct1.cpp:4:13: error: invalid use of incomplete type ‘struct Poo’ 63 | 64 | 总结: 65 | 66 | 编译期会不会报错。 67 | (链接期会不会报错是另外一回事了,这里不讨论) 68 | 69 | 1. 编译期不会报错的几种情况: 70 | 1. 调用一个只有声明没有定义的函数。 71 | 2. 定义一个只有声明没有定义的类型的指针。 72 | 3. 使用一个只有声明没有定义的变量。 73 | 2. 编译期会报错的几种情况: 74 | 1. 定义一个只有声明没有定义的类型的变量。 75 | 2. 实例化一个只有声明没有定义的类型。 76 | 77 | 变量如何只声明不定义? 78 | `int a;`就已经是定义了。 79 | 好吧,留个悬念,大家可以自己研究一下。 80 | -------------------------------------------------------------------------------- /06_编译期符号检查/struct1.cpp: -------------------------------------------------------------------------------- 1 | struct Poo; 2 | int main() 3 | { 4 | Poo *a=new Poo; 5 | return 0; 6 | } 7 | -------------------------------------------------------------------------------- /07_声明与定义的区别/README.md: -------------------------------------------------------------------------------- 1 | # 第七节:声明与定义的区别 2 | 3 | 解释了好多遍,还是有同学不了解在C/C++中声明和定义的区别。 4 | 这里简单介绍一下。 5 | 6 | ## 如何声明和定义 7 | 8 | ### 变量 9 | 10 | 声明: 11 | 12 | extern int a; 13 | 14 | 定义: 15 | int a; 16 | 17 | ### 函数 18 | 19 | 声明: 20 | 21 | void fun(); 22 | 23 | 定义: 24 | 25 | void fun(){ 26 | } 27 | 28 | ### 结构体/类 29 | 30 | 声明: 31 | 32 | class Bar; 33 | 34 | 定义: 35 | 36 | class Bar{ 37 | public: 38 | Bar(); 39 | int foo(); 40 | }; 41 | 42 | 这里虽然`Bar::Bar`和`Bar::foo`都只有声明,没有定义。 43 | 但是`class Bar`已经定义完了。 44 | 45 | ## 声明和定义的本质区别是什么? 46 | 47 | 声明没有为符号分配存储空间、定义会为符号分配存储空间。 48 | 49 | ### 变量 50 | 51 | extern int a; 52 | 53 | 这句话是告诉编译器,变量a在别的地方有了, 54 | 所以不需要在这为它分配存储空间了。 55 | 56 | int a; 57 | 58 | 这句话是告诉编译器,我需要创建一个变量,请为它分配存储空间。 59 | 60 | ### 函数 61 | 62 | void fun(); 63 | 64 | 函数体包含若干语句,编译后会产生若干指令。 65 | 声明是告诉编译器,这个函数的指令保存在别的地方了。 66 | 67 | void fun(){ 68 | } 69 | 70 | 这里会为这个函数分配存储空间、保存函数的指令。 71 | 72 | ### 结构体/类 73 | 74 | class Bar; 75 | 76 | 同样不分配存储空间。 77 | 78 | class Bar{}; 79 | 80 | 一个结构体/类在定义的时候会产生它的各种指针, 81 | 其中最重要的是它的函数地址表。 82 | 83 | 结构体内部的成员变量会在它初始化的时候才会分配内存。 84 | 85 | ## 为什么会有编译期错误 86 | 87 | 为什么一个没有定义的结构体/类, 88 | 在定义它的变量的时候会报告编译期错误? 89 | 90 | 因为有的结构体/类不能定义它的变量。 91 | (比如它的构造函数是private的。 92 | 在`设计模式`中,C++常用这种技术来实现单例模式。) 93 | 94 | 编译器只有在看到它的定义, 95 | 才能知道它是否可以定义变量。 96 | 97 | ## 结尾 98 | 99 | 感觉解释的不是很清楚,不过我能力也就如此了。 100 | 101 | 本节完。 102 | -------------------------------------------------------------------------------- /08_前言/README.md: -------------------------------------------------------------------------------- 1 | # 第八节:前言 2 | 3 | 谁规定前言必须在最前面了? 4 | 前言说点啥呢?想到啥就说啥吧。 5 | 6 | ## 看书比例不超过20% 7 | 8 | 感觉对于一个工程师来说,重要的是实践。 9 | 我个人在学习C++的过程中,看书的时间绝对不超过20%,剩下的时间全部用来实践了。 10 | 我现在(2011年8月)积累下来的C++代码应该快要到2W行了(POJ上150道AC,有1W行。DGP项目有1W行)。 11 | 12 | 写本系列,很多东西都是我曾经犯下的错误。有句话说,实践越多,犯错误的机会越多。犯错误越多,学习的越扎实。 13 | 14 | 书上讲了很多东西,放下书就都忘光光了。可是犯一次错误会让自己记住好久。 15 | 16 | ## 文档是万能的 17 | 18 | gcc的man手册那么长(感觉比长城都长。) 19 | 不过less命令有增量搜索功能(我严重怀疑man命令是调用less来查看man手册内容的,证据是界面和很多命令都是相同的)。 20 | 需要某个选项的文档,直接在man里面搜索。 21 | 22 | 另外,man手册是英文的,所以我觉得,想做大牛,英语必须要好。 23 | 不过多好才算好呢?我有一个同学,英语六级,给她find命令的man文档,她完全看不懂。 24 | 我英语四级还不到,可是我能看懂。 25 | 所以,英语多好才算好呢? 26 | 我真的不知道怎么回答。 27 | 28 | ## 博客是认识大牛最直接的方法 29 | 30 | 我真正提高,学到了大量在书本和实践中学不到的东西, 31 | 是在今年(2011年)年初,从一位学长的博客中。 32 | 33 | 后来,又陆续通过rss订阅了许多大牛的博客, 34 | 学到了非常非常多的从未接触过的东西,开阔眼界。 35 | 36 | ## 朋友的鼓励和支持是我坚持下去的唯一动力 37 | 38 | 其实我感觉,我C++和gcc的水平也大概只有2W行相当。 39 | 而且对于软件工程、设计模式完全不懂。 40 | 41 | 不过我希望能够将我知道的东西发布出来,以期得到大牛们的指导。 42 | 由于工作原因,我可能无法每天坚持更新, 43 | 不过,朋友的鼓励和支持是我坚持下去的唯一动力。 44 | 只要有人看,我就愿意继续写下去。 45 | 46 | 先说这么多,以后如果想到别的东西我会再发一篇《前言续》。 47 | -------------------------------------------------------------------------------- /09_编译过程/README.md: -------------------------------------------------------------------------------- 1 | # 第九节:编译过程 2 | 3 | 接下来几节,我会讲一些编译期相关的优化选项及预编译期相关的优化选项。 4 | 至于汇编期和链接期……我就完全不懂了 5 | 6 | 我不懂汇编语言。 7 | 链接期也涉及了很多我完全不懂的问题。 8 | 9 | 那么,编译期是什么意思? 10 | 预编译期又是什么意思? 11 | 12 | 这个涉及到编译的整个过程。 13 | 我记得前面某节好像提到过这个话题,这里详细的讲一下。 14 | 15 | ## 从源代码(xxx.cpp)生成可执行文件(a.out)一共分为四个阶段: 16 | 17 | ### 1. 预编译阶段 18 | 19 | 此时编译器会处理源代码中所有的预编译指令。 20 | 预编译指定非常有特点,全部以“#”开头 21 | 22 | 想想,以“#”开头的命令有哪些 23 | 24 | 不同的命令有不同的处理方法,#include命令的处理方法就是赤裸裸的复制粘贴。 25 | 将#include后面的文件的内容赤裸裸地复制粘贴到#include命令所在的位置。 26 | 27 | \#define命令分为带参宏和不带参宏。 28 | \#define命令的处理方法,学名叫宏展开。 29 | 其实不带参宏的处理方法就是赤裸裸的字符串替换。 30 | 31 | 此阶段完成后,会产生一篇完全不包含预编译指令的代码。 32 | 33 | 使用gcc的-E选项可以查看预编译结果: 34 | 35 | g++ -E xxx.cpp 36 | 37 | 但是这个命令不会把处理结果保存在文件中,而是放在标准输出中。 38 | 39 | 例子:见本文最后例一。 40 | 41 | ### 2. 汇编阶段 42 | 43 | 此时编译器会将预处理过的代码进行汇编。 44 | 45 | 使用gcc的-S选项可以查看汇编结果: 46 | 47 | g++ -S xxx.cpp 48 | 49 | 之后会在当前目录下产生一个xxx.s的文件,里面保存的是汇编代码。 50 | 51 | 汇编我懂的不多,就不帖了。 52 | 53 | ### 3. 编译阶段 54 | 55 | 此时编译器会将汇编代码编译成目标文件。 56 | 57 | 使用gcc的-c选项可以生成目标文件: 58 | 59 | g++ -c xxx.s 60 | 61 | 也可以从源代码直接生成目标文件: 62 | 63 | g++ -c xxx.cpp 64 | 65 | gcc会通过扩展名自动判断处理的是汇编代码还是C++代码。 66 | 67 | 到此,(狭义上的)编译器已经完成它的全部工作。 68 | 69 | ### 4. 链接阶段 70 | 71 | 此时已经没有编译器的事情了。链接工作交由链接器来处理。 72 | 链接器会将多个目标文件链接成可执行文件。 73 | 74 | 我们可以通过gcc来进行链接,但是实际上,gcc还是调用ld命令来完成链接工作的。 75 | 76 | g++ xx1.o xx2.o 77 | 78 | ## 本节完 79 | 80 | 接下来几节, 81 | 计划先讲编译阶段的pipe选项、O选项和W选项。 82 | 因为编译阶段事情比较少。 83 | 84 | 然后讲预编译阶段的I选项和D选项。 85 | 86 | ## 例子 87 | 88 | ### 例一 89 | 90 | main.cpp 91 | 92 | #include "sum.h" 93 | #define SUCCESS 0 94 | int main(){ 95 | int c=sum(1,2); 96 | return SUCCESS; 97 | } 98 | 99 | sum.h 100 | 101 | #ifndef SUM_H 102 | #define SUM_H 103 | 104 | int sum(int a,int b); 105 | 106 | #endif // SUM_H 107 | 108 | 预编译 109 | 110 | g++ -E main.cpp 111 | 112 | 输出 113 | 114 | # 1 "main.cpp" 115 | # 1 "" 116 | # 1 "" 117 | # 1 "main.cpp" 118 | # 1 "sum.h" 1 119 | 120 | int sum(int a,int b); 121 | # 2 "main.cpp" 2 122 | 123 | int main(){ 124 | int c=sum(1,2); 125 | return 0; 126 | } 127 | -------------------------------------------------------------------------------- /09_编译过程/main.cpp: -------------------------------------------------------------------------------- 1 | #include "sum.h" 2 | #define SUCCESS 0 3 | int main(){ 4 | int c=sum(1,2); 5 | return SUCCESS; 6 | } 7 | -------------------------------------------------------------------------------- /09_编译过程/main.s: -------------------------------------------------------------------------------- 1 | .file "main.cpp" 2 | .text 3 | .globl main 4 | .type main, @function 5 | main: 6 | .LFB0: 7 | .cfi_startproc 8 | pushq %rbp 9 | .cfi_def_cfa_offset 16 10 | movq %rsp, %rbp 11 | .cfi_offset 6, -16 12 | .cfi_def_cfa_register 6 13 | subq $16, %rsp 14 | movl $2, %esi 15 | movl $1, %edi 16 | call _Z3sumii 17 | movl %eax, -4(%rbp) 18 | movl $0, %eax 19 | leave 20 | .cfi_def_cfa 7, 8 21 | ret 22 | .cfi_endproc 23 | .LFE0: 24 | .size main, .-main 25 | .ident "GCC: (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2" 26 | .section .note.GNU-stack,"",@progbits 27 | -------------------------------------------------------------------------------- /09_编译过程/sum.cpp: -------------------------------------------------------------------------------- 1 | #include "sum.h" 2 | int sum(int a,int b){ 3 | return a+b; 4 | } 5 | -------------------------------------------------------------------------------- /09_编译过程/sum.h: -------------------------------------------------------------------------------- 1 | #ifndef SUM_H 2 | #define SUM_H 3 | 4 | int sum(int a,int b); 5 | 6 | #endif // SUM_H 7 | -------------------------------------------------------------------------------- /10_编译期优化选项_pipe/README.md: -------------------------------------------------------------------------------- 1 | # 第十节:编译期优化选项(一)——pipe 2 | 3 | ## 中间文件 4 | 5 | 上一节讲到,从源代码生成最终的可执行文件需要四个步骤,并且还会产生中间文件。 6 | 7 | 可是我们在对一个源文件编译的时候,直接执行g++ xxx.cpp能够得到可执行文件a.out。 8 | 并没有中间文件啊!中间文件在哪里? 9 | 10 | 答案是,在/tmp目录下。想看吗?跟着我做。 11 | 12 | 1. 在终端中执行g++ xxx.cpp 13 | 2. 在另外一个终端中执行`ls /tmp/cc* 2>/dev/null` 14 | 15 | 看见什么了? 16 | 什么也没有啊! 17 | 说明你太慢了。 18 | 19 | 你需要在第一个命令完成前,执行第二个命令,否则什么也看不见。 20 | 你大概只有不到0.1秒的时间。 21 | 22 | 你需要在0.1秒的时间内完成 23 | 24 | * 切换终端 25 | * 输入命令 26 | * 回车 27 | 28 | 这些事情,你做得到吗? 29 | 30 | 所以, 31 | 还是写一个脚本来看吧。 32 | 33 | #!/bin/bash 34 | g++ main.cpp & 35 | sleep 0.05 36 | ls --color=auto /tmp/cc* 37 | 38 | 在我的电脑上,时间是0.05的时候可以看到如下结果: 39 | 40 | /tmp/cc9CD8ah.o /tmp/ccj9uXNd.s 41 | 42 | 可以看到,有.s汇编文件,.o目录文件。 43 | 44 | 所以,实际上gcc将中间文件放在了/tmp目录下,并且在编译完成后会将其删除。 45 | 46 | ## 编译优化 47 | 48 | 不得不说, 49 | C/C++的编译速度真是慢的出奇。 50 | 51 | 如果你曾经有编译一百万行以上C++代码的经历的话, 52 | 你会知道, 53 | 电脑开机一周只是为了编译,其实不算是过分的事情。 54 | 55 | 但是如果你认为,gcc已经放弃治疗了, 56 | 那你就太误解gcc了。 57 | 58 | C/C++中有很多技术用来提高编译速度, 59 | 比如`前向声明`。 60 | 61 | 而且gcc也提供了一些选项来加快编译速度。 62 | 63 | 回到本文一开始的话题来。 64 | 如果编译过程中产生很多临时文件的话, 65 | 那么磁盘IO将会成为编译速度的重要瓶颈。 66 | 67 | 我们需要将上一步的结果交给下一步处理,有没有什么比较快的方法? 68 | 69 | 如果您了解linux的话,会立即想到一个牛X闪闪的东西:管道。 70 | 71 | 将上一步编译的结果通过管道传递给下一步,这不需要IO操作,全部在内存中完成, 72 | 效率上会有非常大的提高。 73 | 74 | gcc提供了这个功能,方法是使用-pipe选项。 75 | 76 | g++ -pipe main.cpp 77 | 78 | 下面是gcc的man手册中关于-pipe选项的解释: 79 | 80 | -pipe 81 | Use pipes rather than temporary files for communication between the 82 | various stages of compilation. This fails to work on some systems 83 | where the assembler is unable to read from a pipe; but the GNU 84 | assembler has no trouble. 85 | 86 | ## 本节完 87 | -------------------------------------------------------------------------------- /10_编译期优化选项_pipe/main.cpp: -------------------------------------------------------------------------------- 1 | int main(){ 2 | return 0; 3 | } 4 | -------------------------------------------------------------------------------- /10_编译期优化选项_pipe/showTmp.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | g++ main.cpp & 3 | sleep 0.05 4 | ls -l --color=auto /tmp/cc* 5 | cat /tmp/cc*.s 6 | -------------------------------------------------------------------------------- /11_编译期优化选项_O/README.md: -------------------------------------------------------------------------------- 1 | # 第十一节:编译期优化选项(二)——O(上) 2 | 3 | ## 一篇很简单的代码 4 | 5 | int createNum(); 6 | void putNum(int a); 7 | 8 | int sum(int a,int b) 9 | { 10 | return a+b; 11 | } 12 | 13 | int main() 14 | { 15 | int x=createNum(); 16 | int y=createNum(); 17 | int z=sum(x,y); 18 | putNum(z); 19 | return 0; 20 | } 21 | 22 | 我们来查看一下它的汇编代码: 23 | 24 | g++ -s main.cpp 25 | 26 | 得到一个main.s。打开这个文件,截取其中main函数的一小段,加上一些注释,如下: 27 | 28 | call _Z9createNumv ;调用createNum()函数 29 | movl %eax, -4(%rbp) ;将返回值压栈 30 | call _Z9createNumv ;再调用createNum()函数 31 | movl %eax, -8(%rbp) ;将返回值压栈 32 | movl -8(%rbp), %edx ;将栈顶数据放在寄存器edx中 33 | movl -4(%rbp), %eax ;同上,放在寄存器eax中 34 | movl %edx, %esi ;将寄存器edx中的数据作为sum()函数的第一个参数 35 | movl %eax, %edi ;将寄存器eax中的数据作为sum()函数的第二个参数 36 | call _Z3sumii ;调用sum()函数 37 | movl %eax, -12(%rbp) ;将返回值压栈 38 | movl -12(%rbp), %eax ;将栈顶数据放在寄存器eax中 39 | movl %eax, %edi ;将寄存器eax中的数据作为putNum()函数的第一个参数 40 | call _Z6putNumi ;调用putNum()函数 41 | 42 | ## 初步优化 43 | 44 | 大家觉得,是不是很麻烦? 45 | 46 | 每次调用一个函数之后,先压栈,然后又转到寄存器中,这很浪费时间。 47 | 48 | gcc会这么笨吗?当然不会。 49 | gcc的-O选项(注意,是大写。回想一下,小写-o选项是干什么的?前面讲过)就是用来处理编译期优化的。 50 | 51 | 我们重新产生一下汇编代码,但是使用-O选项。 52 | 53 | g++ -O -s main.cpp -o main.O1.s 54 | 55 | 现在打开main.O1.s文件, 56 | 看,里面函数的返回值没有经过入栈和出栈的过程, 57 | 直接传入下一个函数的参数。这样减少了六条汇编代码。 58 | 59 | ## 进一步优化 60 | 61 | 可是细想想,sum()函数有点多余。 62 | 它实际上只是做了一个加法, 63 | 但是我们仍然需要调用这个函数来完成它的功能。 64 | 65 | gcc会这么笨吗? 66 | 当然不会。 67 | 68 | -O选项还可以加数字,表示优化的级别。 69 | 70 | 没有数字默认是1,最大可以加到3。 71 | 优化级别越高,产生的代码的执行效率就越高。 72 | 73 | 我们用级别2试一下: 74 | 75 | g++ -O2 -s main.cpp -o main.O2.s 76 | 77 | 现在打开main.O2.s, 78 | 大家可以看到,调用sum()函数的代码都不见了, 79 | 取而代之的是一条加法指令来完成两个整数的相加。 80 | 81 | -O3的效果我就不试了。而且,如果不加-O选项,优化级别就是0。 82 | 83 | ## 编译效率 84 | 85 | 既然-O后面的的数字越大,产生的代码越优化,那么为什么不直接用-O3? 86 | 87 | 原因是,优化的级别越高, 88 | 虽然最后生成的代码的执行效率就会越高, 89 | 但是编译的过程花费的时间就会越长。 90 | 91 | 在执行效率和编译时间之间, 92 | 需要做出一个权衡。 93 | 94 | gcc没有擅自做这个决定, 95 | 而是把决定的权力留给了用户。 96 | 97 | 在linux的世界里,有这样一个观点: 98 | 不限制用户的发挥, 99 | 但是让用户为自己的行为负责。 100 | 101 | 所以linux的很多软件都有很多选项。 102 | 103 | 这样有好处也有坏处, 104 | 好处是,用户拥有更多的权力, 105 | 坏处就是,彩笔们一看到这么多选项,瞬间就傻了。 106 | 107 | ## 本节完 108 | -------------------------------------------------------------------------------- /11_编译期优化选项_O/main.O0.s: -------------------------------------------------------------------------------- 1 | .file "main.cpp" 2 | .text 3 | .globl _Z3sumii 4 | .type _Z3sumii, @function 5 | _Z3sumii: 6 | .LFB0: 7 | .cfi_startproc 8 | pushq %rbp 9 | .cfi_def_cfa_offset 16 10 | movq %rsp, %rbp 11 | .cfi_offset 6, -16 12 | .cfi_def_cfa_register 6 13 | movl %edi, -4(%rbp) 14 | movl %esi, -8(%rbp) 15 | movl -8(%rbp), %eax 16 | movl -4(%rbp), %edx 17 | leal (%rdx,%rax), %eax 18 | leave 19 | .cfi_def_cfa 7, 8 20 | ret 21 | .cfi_endproc 22 | .LFE0: 23 | .size _Z3sumii, .-_Z3sumii 24 | .globl main 25 | .type main, @function 26 | main: 27 | .LFB1: 28 | .cfi_startproc 29 | pushq %rbp 30 | .cfi_def_cfa_offset 16 31 | movq %rsp, %rbp 32 | .cfi_offset 6, -16 33 | .cfi_def_cfa_register 6 34 | subq $16, %rsp 35 | call _Z9createNumv 36 | movl %eax, -4(%rbp) 37 | call _Z9createNumv 38 | movl %eax, -8(%rbp) 39 | movl -8(%rbp), %edx 40 | movl -4(%rbp), %eax 41 | movl %edx, %esi 42 | movl %eax, %edi 43 | call _Z3sumii 44 | movl %eax, -12(%rbp) 45 | movl -12(%rbp), %eax 46 | movl %eax, %edi 47 | call _Z6putNumi 48 | movl $0, %eax 49 | leave 50 | .cfi_def_cfa 7, 8 51 | ret 52 | .cfi_endproc 53 | .LFE1: 54 | .size main, .-main 55 | .ident "GCC: (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2" 56 | .section .note.GNU-stack,"",@progbits 57 | -------------------------------------------------------------------------------- /11_编译期优化选项_O/main.O1.s: -------------------------------------------------------------------------------- 1 | .file "main.cpp" 2 | .text 3 | .globl _Z3sumii 4 | .type _Z3sumii, @function 5 | _Z3sumii: 6 | .LFB0: 7 | .cfi_startproc 8 | leal (%rsi,%rdi), %eax 9 | ret 10 | .cfi_endproc 11 | .LFE0: 12 | .size _Z3sumii, .-_Z3sumii 13 | .globl main 14 | .type main, @function 15 | main: 16 | .LFB1: 17 | .cfi_startproc 18 | pushq %rbx 19 | .cfi_def_cfa_offset 16 20 | .cfi_offset 3, -16 21 | call _Z9createNumv 22 | movl %eax, %ebx 23 | call _Z9createNumv 24 | movl %eax, %esi 25 | movl %ebx, %edi 26 | call _Z3sumii 27 | movl %eax, %edi 28 | call _Z6putNumi 29 | movl $0, %eax 30 | popq %rbx 31 | .cfi_def_cfa_offset 8 32 | ret 33 | .cfi_endproc 34 | .LFE1: 35 | .size main, .-main 36 | .ident "GCC: (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2" 37 | .section .note.GNU-stack,"",@progbits 38 | -------------------------------------------------------------------------------- /11_编译期优化选项_O/main.O2.s: -------------------------------------------------------------------------------- 1 | .file "main.cpp" 2 | .text 3 | .p2align 4,,15 4 | .globl _Z3sumii 5 | .type _Z3sumii, @function 6 | _Z3sumii: 7 | .LFB0: 8 | .cfi_startproc 9 | leal (%rsi,%rdi), %eax 10 | ret 11 | .cfi_endproc 12 | .LFE0: 13 | .size _Z3sumii, .-_Z3sumii 14 | .p2align 4,,15 15 | .globl main 16 | .type main, @function 17 | main: 18 | .LFB1: 19 | .cfi_startproc 20 | pushq %rbx 21 | .cfi_def_cfa_offset 16 22 | .cfi_offset 3, -16 23 | call _Z9createNumv 24 | movl %eax, %ebx 25 | call _Z9createNumv 26 | leal (%rax,%rbx), %edi 27 | call _Z6putNumi 28 | xorl %eax, %eax 29 | popq %rbx 30 | .cfi_def_cfa_offset 8 31 | ret 32 | .cfi_endproc 33 | .LFE1: 34 | .size main, .-main 35 | .ident "GCC: (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2" 36 | .section .note.GNU-stack,"",@progbits 37 | -------------------------------------------------------------------------------- /11_编译期优化选项_O/main.cpp: -------------------------------------------------------------------------------- 1 | int createNum(); 2 | void putNum(int a); 3 | int sum(int a,int b) 4 | { 5 | return a+b; 6 | } 7 | int main() 8 | { 9 | int x=createNum(); 10 | int y=createNum(); 11 | int z=sum(x,y); 12 | putNum( z ); 13 | return 0; 14 | } 15 | -------------------------------------------------------------------------------- /12_编译期优化选项_W/README.md: -------------------------------------------------------------------------------- 1 | # 第十三节:编译期优化选项(四)——W 2 | 3 | ## 优秀的程序员不应该忽略任何的warning 4 | 5 | 先看第一段代码: 6 | 7 | int fun(){ 8 | } 9 | int main(){ 10 | fun(); 11 | } 12 | 13 | 很简单,对吧? 14 | 15 | 有错误吗?事实上是没有的。 16 | 17 | 编译一下:`g++ return-type.cpp`。 18 | 也没有任何问题。 19 | 20 | 可是事实上,fun函数没有return语句。 21 | 22 | 这很有可能是程序员的疏忽造成的。 23 | 而且,很多严重的问题可能就是由一些很幼小的疏忽造成的。 24 | 25 | 我们希望,gcc在遇见这类问题的时候,能够给我们一个提示。 26 | 27 | ## -W选项 28 | 29 | 还好,gcc提供了一个-W选项。 30 | 我们使用这样的命令来编译: 31 | 32 | g++ -Wreturn-type return-type.cpp 33 | 34 | 它仍然能够正常编译,生成可执行文件, 35 | 但是,它会输出一句warning: 36 | 37 | return-type.cpp: In function ‘int fun()’: 38 | return-type.cpp:3:1: warning: no return statement in function returning non-void 39 | 40 | 不错吧? 41 | 42 | 43 | 解释一下,-W是打开警告输出,后面接的是警告的种类。 44 | gcc将警告分为好多种(将近一百种)。 45 | return-type只是检查返回值类型。 46 | 47 | ## 再来一个 48 | 49 | //uninitialized.cpp 50 | int fun(){ 51 | int a; 52 | return a; 53 | } 54 | int main(){ 55 | fun(); 56 | } 57 | 58 | 按照正常方式编译:g++ uninitialized.cpp。没有任何问题。 59 | 60 | 我们打开uninitialized种类的警告,这样编译: 61 | 62 | g++ -Wuninitialized uninitialized.cpp 63 | 64 | 它输出的warning是这样的: 65 | 66 | uninitialized.cpp: In function ‘int fun()’: 67 | uninitialized.cpp:4:12: warning: ‘a’ is used uninitialized in this function 68 | 69 | ## 开启全部警告 70 | 71 | 但是,种类那么多,一个一个加会不会很麻烦? 72 | 73 | 哈哈!gcc的-W选项有个种类叫all。 74 | 猜是什么意思? 75 | 76 | 打开所有种类的警告。 77 | 78 | 很方便吧? 79 | 80 | ## 强制不许忽略警告 81 | 82 | 上面只是在可能出现问题的地方输出了警告, 83 | 但是警告并不影响正常编译。 84 | 85 | 有没有办法要求必须处理掉所有的警告呢? 86 | 87 | 有。 88 | 89 | gcc有个选项,叫-Werror。 90 | 它会把所有的警告当成错误进行处理。 91 | 92 | ## 本节完。 93 | -------------------------------------------------------------------------------- /12_编译期优化选项_W/return-type.cpp: -------------------------------------------------------------------------------- 1 | //g++ -Wreturn-type return-type.cpp 2 | int fun(){ 3 | } 4 | int main(){ 5 | fun(); 6 | } 7 | -------------------------------------------------------------------------------- /12_编译期优化选项_W/uninitialized.cpp: -------------------------------------------------------------------------------- 1 | //g++ -Wuninitialized uninitialized.cpp 2 | int fun(){ 3 | int a; 4 | return a; 5 | } 6 | int main(){ 7 | fun(); 8 | } 9 | -------------------------------------------------------------------------------- /13_预编译期选项_I/README.md: -------------------------------------------------------------------------------- 1 | # 第十四节:预编译期选项(一)——I 2 | 3 | ## include命令 4 | 5 | 相信大家对C/C++中的`#include`指令有所了解, 6 | 相信您能够区别引号和尖括号的作用。 7 | 8 | 简单来说: 9 | 10 | * 引号的作用:先在当前目录下搜索文件,然后在系统目录下搜索文件 11 | * 尖括号的作用:仅在系统目录下搜索文件。 12 | 13 | 但是,如果大家做过Qt的开发,就会发现: 14 | 15 | * 包含一个Qt库的头文件时,需要用尖括号即可。 16 | * Qt库的头文件并不在系统目录下。 17 | 18 | 例如,我用的是ubuntu linux maverick,使用apt-get安装qt4。 19 | 20 | 以``这个头文件为例,它的存放路径是`/usr/include/qt4/QtCore/QObject`。 21 | 22 | 那么Qt是如何做到的呢? 23 | 24 | ## -I选项 25 | 26 | 我们可以打开qmake生成的Makefile,看到Qt在编译参数的变量中加上了这样一句: 27 | 28 | -I/usr/include/qt4/QtCore 29 | 30 | 这个-I是gcc的一个选项, 31 | 它的作用是把一个路径加入到系统路径, 32 | 这样当使用include指令时, 33 | 就可以用尖括号来指定文件。 34 | 35 | 那么怎么指定多个路径呢?多写几遍就可以了! 36 | 37 | -I/usr/share/qt4/mkspecs/linux-g++ -I../../../dgp/othello/client -I/usr/include/qt4/QtCore -I/usr/include/qt4/QtNetwork -I/usr/include/qt4/QtGui -I/usr/include/qt4 -I../../../dgp/Dlut-Game-Platform/api/include -I../../../dgp/roommodel/include/client -I../../../dgp/roommodel/include/common -I. -I. -I../../../dgp/othello/client -I. 38 | 39 | 可以用绝对路径、相对路径。 40 | 甚至可以把当前路径加入到系统路径中, 41 | 这样就可以用尖括号来指定当前目录下的一个文件了。 42 | 43 | ## 扩展阅读:Qt的pro文件中的 qt += network 的作用 44 | 45 | 假如我要使用Qt中的`QTcpServer`这个类, 46 | 我需要写上 47 | 48 | #include 49 | 50 | 但是如果我在.pro文件中加上 51 | 52 | qt += network 53 | 54 | 之后,我的代码中只需要写 55 | 56 | #include 57 | 58 | 就可以了。 59 | 60 | 原理是因为在加上 61 | 62 | qt += network 63 | 64 | 之后,Makefile中就会多出 65 | 66 | -I/usr/include/qt4/QtNetwork 67 | 68 | 这样一句。 69 | ## 本节完。 70 | -------------------------------------------------------------------------------- /13_预编译期选项_I/header/header: -------------------------------------------------------------------------------- 1 | // header/header 2 | #include "header.h" 3 | -------------------------------------------------------------------------------- /13_预编译期选项_I/header/header.h: -------------------------------------------------------------------------------- 1 | // header/header.h 2 | #ifndef HEADER_H 3 | #define HEADER_H 4 | 5 | void fun(); 6 | 7 | #endif // HEADER_H 8 | 9 | -------------------------------------------------------------------------------- /13_预编译期选项_I/src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "header" 2 | #include "src.h" 3 | 4 | int main(){ 5 | return 0; 6 | } 7 | -------------------------------------------------------------------------------- /13_预编译期选项_I/src/src.h: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/24OI/gcc_five_minute/d21a0fd9fc85abeceb5c450e0125e4ee7b8887ae/13_预编译期选项_I/src/src.h -------------------------------------------------------------------------------- /14_预编译期选项_D/README.md: -------------------------------------------------------------------------------- 1 | # 第十五节:预编译期选项(二):D 2 | 3 | ## debug版和release版 4 | 5 | 一般我们在开发项目时,都会做两个版本,一个debug版,一个release版。 6 | 7 | 通常我们在debug版本中会加入调试输出,方便查找BUG。 8 | 9 | 而在release版本中,删除这些输出,以提高执行速度并减少可执行文件的大小。 10 | 11 | 实现的方式是通过宏。 12 | 13 | 以一个简单的函数为例,这个函数仅仅输出参数的值。但在debug版本中,输出一些多余的信息,比如函数名。 14 | 15 | ## 例1 16 | 17 | #define __DEBUG__ 18 | 19 | void output(int a){ 20 | #ifdef __DEBUG__ 21 | cerr<<"this is debug infor : "<<__FUNCTION__<<' '< 2 | 3 | using namespace std; 4 | 5 | // #define __DEBUG__ 6 | 7 | void output(int a){ 8 | #ifdef __DEBUG__ 9 | cerr<<"this is debug infor : "<<__FUNCTION__<<' '< 30 | 31 | #### 2.2.使用libzip中的函数。 32 | 函数的列表及每个函数的详细介绍可以在libzip的man手册中找到。 33 | ### 3.编译链接。 34 | #### 3.1.由于zip.h头文件是放在/usr/include下的,因此不需要显式地指定include目录。编译的步骤没有任何区别。 35 | 36 | gcc -c -o zipdemo.o zipdemo.c 37 | 38 | #### 3.2.链接时,需要指定链接的动态库的名字。这里我们使用-l参数。 39 | 40 | gcc -lzip -o zipdemo zipdemo.o 41 | 42 | zipdemo源代码及Makefile可以参见本项目git仓库。 43 | 44 | ## 本节完 45 | 46 | > 附. 其实 _debian_ 的软件仓库里面基本上已经涵盖了我们需要的各类编程库,尤其以C库居多。 47 | 每次当我需要某功能的时候,我都会先在 _debian_ 的软件仓库中搜索一下,有没有已经提供的库。 48 | 搜索地址:[http://www.debian.org/distrib/packages] 49 | -------------------------------------------------------------------------------- /15_库的使用_使用/zipdemo.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | void usage(){ 4 | printf("zipdemo filename\n"); 5 | } 6 | 7 | int main(int argc, char *argv[]){ 8 | int errorp = 0; 9 | struct zip* zipObj = NULL; 10 | int num_entries = 0; 11 | int i; 12 | 13 | if( argc < 2){ 14 | usage(); 15 | return 1; 16 | } 17 | 18 | zipObj = zip_open(argv[1],0,&errorp); 19 | if( NULL == zipObj ){ 20 | printf("open zip file failed , error code : %d\n",errorp); 21 | return 2; 22 | } 23 | 24 | num_entries = zip_get_num_files(zipObj) ; 25 | for( i = 0 ; i < num_entries ; ++i ){ 26 | printf("%s\n",zip_get_name(zipObj,i,0) ); 27 | } 28 | 29 | zip_close(zipObj); 30 | zipObj = NULL ; 31 | 32 | return 0; 33 | }; 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gcc五分钟系列 2 | 3 | ## 简介 4 | 5 | * 通过大量简单的例子介绍 `gcc` 的基本用法 6 | * 每节内容简短,控制在5分钟以内 7 | * 讲的是一些最基础的用法 8 | 9 | ## 关于版本 10 | 写本系列文章时,`gcc`的版本是4.6.2。 11 | 本系列文章参照此版本进行介绍。 12 | 13 | 如果`gcc`的最新版本的参数或用法发生更改, 14 | 本系列文章可能不会及时更新。 15 | 所以请您在正式使用前 *务必* 确认您的`gcc`版本并参考`gcc`的说明文档。 16 | 17 | ## 电子书下载 18 | 19 | 电子书下载地址: http://lexdene.github.io/gcc-five-minutes-ebook/downloads.html 20 | --------------------------------------------------------------------------------