前言
本节重点:掌握gcc/g++编译器的使用,并了解其过程,原理
一、Linux编译器-gcc/g++使用
1. gcc/g++的基本使用
- 在前面我们学习了vim,知道如何在Linux中编写代码。
- 但又是如何编译代码的?——在Linux中我们编译代码使用的是gcc/g++。
- gcc只能用来编译C语言,g++即可编译C也可编译C++(C++兼容C)。
- 虽然g++也可以编译C,但是一般C语言编译,我们比较推荐gcc。
- gcc/g++他们对应的选项几乎一模一样,所以这里我们学习gcc即可,g++的使用直接平替gcc的使用。
- gcc的基本使用:gcc xx.c ——》gcc编译C源文件,如果不加任何选项,默认直接生成可执行程序a.out(tip:代码如果有语法错误等,gcc也会有编译报错。)
- g++的基本使用:g++ xx.cpp ——》g++编译C/C++源文件,如果不加任何选项,默认也直接生成可执行程序a.out
- 可以通过 -o 选项来自定义输出文件的名称,-o后面紧跟的永远是你自定义命名的目标文件。
示例:
#gcc编译C源文件,如果不加任何选项,默认直接生成可执行程序a.out
gcc xx.c
#g++编译C/C++源文件,如果不加任何选项,默认也直接生成可执行程序a.out
g++ xx.cpp
#如果想自定义命名目标生成的可执行文件,使用-o选项
#-o选项用于指定编译后的输出文件名
gcc xx.c -o xx.exe(推荐写法)/ gcc -o xx.exe xx.c
2. gcc编译程序的四个阶段
文本的C源代码编译成计算机可以认识的二进制程序它需要经过如下四个阶段:
- 预处理(进行宏替换,去注释,头文件展开,条件编译等)
- 编译(生成汇编)
- 汇编(生成机器可识别的二进制代码)
- 链接(生成可执行文件或库文件)
(1)预处理阶段
- 预处理指令是以#号开头的代码行
- 预处理功能主要包括宏替换、头文件展开、条件编译、去注释等
- 宏替换:只做简单的宏替换,不做类型检查,因为在编译阶段才会做语法检查
- 头文件展开:把头文件的内容拷贝到源文件的行为叫做头文件展开
- 条件编译:条件编译可以在编译前对代码进行条件判断和处理,从而实现代码的选择性编译
- 去注释:注释是帮助我们理解代码的,计算机并不需要,所以在预处理阶段就将注释去掉
- 预处理阶段指令格式:gcc -E xx.c -o xx.i
- -E选项:告诉gcc,从现在开始进行程序的翻译,将预处理工作做完就停下来,不要往后走了!
- -o xx.i:预处理之后产生的结果都放在xx.i文件中(tip:不带-o选项,默认把输出结果打印到显示器上)
- “.i”文件为已经预处理的C原始程序,还是C语言。
示例:
加油站:
- 我们为什么可以在Windows或者Linux上进行C/C++或者其他形式的开发?
答:我们的系统一定提前或者后续安装了,C/C++开发相关的头文件、库文件。编译型语言,安装开发包,必定是下载安装对应的头文件+库文件。- 在Linux中开发相关的头文件,一般在/usr/include/这个目录下。
- /usr/include/这个目录是我们Linux系统中gcc/g++编译器,默认搜索头文件时的搜索路径。
- 条件编译:条件编译可以在编译前对代码进行条件判断和处理,从而实现代码的选择性编译
- #ifdef:如果定义了某个宏,则编译随后的代码块
- #else:如果前面的#ifdef条件不满足,则编译#else后面的代码块
- #endif:结束条件编译块,每一个完整的条件编译语句都需要#endif来收尾
- 使用场景:特性开关——根据是否定义了某个宏来启用或禁用某些功能
- 条件编译的宏通常在编译命令中通过 -D 选项定义,格式:gcc xx.c -D宏名字
gcc -E xx.c -o xx.i -DDEBUG
# -E:告诉gcc,从现在开始进行程序的翻译,将预处理工作做完就停下来,不要往后走了
# -o xx.i:把预处理结果放到xx.i文件中
# -D:在gcc命令行中可通过选项-D定义宏
(2)编译阶段
- 在编译阶段,gcc首先要检查代码的规范性、是否有语法错误等,在检查无误后,gcc把代码翻译成汇编语言
- 实例:gcc -S mycode.i -o mycode.s
- -S选项:从现在开始进行程序的翻译,将编译工作做完,就停下来
(3)汇编阶段
- 汇编阶段是把编译阶段生成的“.s”文件转成目标文件
- 实例:gcc -c mycode.s -o mycode.o
- -c选项:从现在开始进行程序的翻译,将汇编工作做完,就停下来
- mycode.o:可重定位目标二进制文件,简称目标文件,.obj文件不可以独立执行,虽然已经是二进制了,但需要经过链接(库)才能执行
- 注意vim是文本编辑器,所以打开的二进制文件显示是乱码!
- 二进制查看工具:od 二进制文件
(4)连接
- 在成功编译之后,就进入连接阶段
- 连接:将可重定位目标二进制文件和库进行连接形成可执行程序
- 实例:gcc mycode.o -o mycode.exe
问题:程序翻译的四个过程中,连接没有选项,但是预处理、编译、汇编都有选项,我们有什么技巧可以帮助我们记忆吗?
答案是,有的,如下:
- 预处理、编译、汇编的选项按照顺序,是ESc,我们可以看键盘的左上角【ESC】键帮助我们记忆
- 预处理、编译、汇编文件的后缀顺序,是iso,镜像文件的后缀就是iso,所以我们可以通过镜像文件后缀帮助我们记忆
- 虽然Linux系统中文件后缀没有意义,但是不代表Linux系统上运行的软件不需要后缀,就比如这里的gcc,所以建议我们一般带上正确的后缀
(5)重要概念:函数库
- 重新理解函数,是个函数他就第一有定义,第二有声明,第三有调用
- 在C语言中我们常常调用库函数来编程,但是在我们的程序中,并没有定义库函数的实现,且在预编译中包含的头文件中也只有该函数的声明,而没有定义库函数的实现,那么,是在哪里实现库函数的呢?
- 答案是:库,库中给我们提供方法的实现
- 因为我们用的是C语言,所以我们使用的就是C语言标准库,其本质是一个文件,有路径(C标准库在Linux系统中默认在/usr/lib64/libc.so*)
- 在不同的平台下库有不同的格式:
- 在Linux下,以.so为后缀的是动态库,以.a为后缀的是静态库
- 在Windows下,以.dll为后缀的是动态库,以.lib为后缀的是静态库
- 库有自己的命名规则,以lib为前缀,以.so.xxx为后缀(xxx是版本号):libname.so.xxx
- 在Linux系统中只会默认帮我们安装动态库,静态库默认是没有安装的
归纳标准库:
- 方法的实现就是在库中
- 库其实就是把源文件(.c),经过一定的翻译,然后打包——只给你提供一个文件即可,不用给你提供太多的源文件,也可以达到隐藏文件的目的
- 头文件提供方法的声明,库文件提供方法的实现(不让我们做重复工作,站在巨人的肩膀)+你的代码 = 你的软件
(6)函数库一般分为静态库和动态库两种
- 静态库:静态库是指编译连接时,把库文件的代码全部拷贝到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。其后缀名一般为“.a”
- 动态库:动态库与之相反,在编译链接时并没有把库文件的代码拷贝到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。动态库一般后缀名为“.so”。
- 帮助理解:
- 动态库我们可以将其比作网吧,当我们想玩游戏的时候,就可以去网吧玩。(动态库即共享库,所以我们的网吧只有一个)但是当有一天网吧被查封了,我们就不能玩了(动态库不能缺失,一但对应的动态库缺失,影响的不止一个程序,可能导致很多程序都无法进行正常运行!)
- 静态库我们将其比作电脑销售商,我们买了电脑之后,没有网吧和电脑销售商倒闭了,也可以有自己的电脑可以玩游戏(静态库在链接的时候,会把自己拷贝到目标程序中,所以该程序可以独立运行)
- 库有动态库和静态库两种,自然我们的.o和库的链接方式也有两种:
- 动态链接:在Linux中,编译形成可执行程序,默认采用的就是动态链接——提供动态库
- 静态链接:在Linux中,如果要按照静态链接的方式,形成可执行程序,需要添加-static选项——
提供静态库- 注意:动态链接只能使用动态库,静态链接只能使用静态库
- 验证:ldd指令可查看一个可执行程序所依赖的动态库
- 补充:file指令也可查看一个可执行程序使用了什么链接方式
- 我们的Linux系统一般只默认安装了动态库,静态库需要我们自己安装
- 如果我们没有静态库,就不能-static静态链接
- 安装静态库:
sudo yum install -y glibc-static # 安装C静态库
sudo yum install -y libstdc++-static # 安装C++静态库
- 如果我们没有动态库,只有静态库,gcc不带-static选项能找到静态库吗——能,gcc只是默认优先动态链接
- -static的本质:改变链接的优先级,所有的链接要求全部变成静态链接(注:只适配一次)
- 连接不一定是纯的全部动态链接或静态链接,混合的。
- 动态库VS静态库:
- 动态库因为是共享库,有效的节省资源(磁盘空间、内存空间、网络空间等)【优点】,动态库一但缺失,导致各个程序都无法运行【缺点】
- 静态库,不依赖库,程序可以独立运行【优点】,但体积大、比较消耗资源【缺点】
(7)debug和release
- 源文件编译时有有两种模式,debug模式和release模式
- debug通常称为调试版本,它包含调试信息,并且不做任何优化,便于程序员调试程序(因为debug模式在形成可执行程序的时候,添加了debug信息,所以可以被追踪调试)
- release称为发布版本,它往往是进行了各种优化,是程序在代码大小和运行速度上都是最优的,以便用户很好的使用
- 在Linux系统上gcc形成的可执行程序,默认是release版本
- 如果想以debug形式编译,我们需要添加-g选项(-g生成调试信息)
- 验证:readelf -S指令可以读取可执行程序对应的二进制构成,再通过grep debug可以将debug信息过滤出来
二、执行可执行程序
- 为什么我们运行自己的可执行程序的时候,要在前面带./呢?
因为执行一个程序,你就必须要先找到这个程序
而要找到这个程序,其本质就是要定位这个程序的二进制文件在系统中的位置
./就是告诉我们的系统,当前我要执行的可执行程序在我的当前路径下
- 重新理解路径
为什么要有路径——答案是:因为要定位文件
- 系统命令也是一个可执行的二进制文件,为什么执行的时候就不用带./呢?
不带./并不代表不用找到,因为系统会默认帮我们找到(通过环境变量)
- 总结:
- 在Linux系统中执行任何一个程序,都必须要找到这个程序对应的二进制文件
- 系统命令因为系统帮我们配了环境变量,系统自己找得到,所以系统命令不用带路径,直接就可以执行
- 而我们自己的程序,系统找不到,所以需要我们自己指定路径./,才能执行