博客主页:小王又困了
系列专栏:Linux
人之为学,不日近则日退
❤️感谢大家点赞收藏⭐评论✍️
目录
一、快速认识gcc/g++
二、预处理
1.1头文件展开
1.2条件编译
二、编译
三、汇编
四、链接
4.1库的概念
4.2库的特点
4.3库的分类
4.4动态链接
4.5静态链接
️前言:
在前面的文章中我们学会了vim的用法,可以写一些代码,要想让我们的代码运行起来,还需要我们学会编译工具gcc、g++的使用。C语言既可以使用gcc,也可以使用g++;C++只能使用g++,它们的使用形式是相同的,今天以gcc为主,介绍它们的使用方法,带大家快速上手。
当我们写完一段代码,想要编译时可以在命令行输入:gcc code.c,会默认形成a.out的可执行程序。
在输入./a.out,程序就可以执行起来了。
当我们不想形成默认的可执行,想让它形成指定名称的可执行,可以输入:gcc code.c -o mycode 或 gcc -o mycode code.c。
编译主要分为预处理、编译、汇编、链接四个过程,我们将详细讲解这四个过程,带大家学会gcc的使用。
此阶段处理以 .c 或 .cpp 为扩展名的源文件,并执行预处理指令。预处理器的主要任务是头文件展开、条件编译、展开宏定义、移除注释等。预处理指令都是以#开头的代码行。
头文件展开,就是在预处理的时候,将头文件拷贝到引用它的源文件中。通过上图我们可以看到,原本14行的代码经过预编译变成了850行。多出的这么多代码,就是把stdio.h文件中的内容插入到当前源文件中。/usr/include/目录是Linux下gcc/g++头文件的默认搜索路径,该路径下有许多和开发相关的头文件。
头文件的展开过程涉及到#include预处理指令。当预处理器遇到#include指令时,它会打开指定的头文件并将其内容插入到当前源文件的位置。这个过程可以分为两种类型的包含:
- 系统头文件的包含:
使用尖括号
<>
包含系统头文件,例如:#include
预处理器会在系统头文件目录中查找并打开stdio.h文件,并将其内容插入到当前源文件中。
- 用户头文件的包含:
使用双引号 “ ” 包含用户定义的头文件,例如:
#include “test.h”
预处理器会在当前源文件所在目录以及指定的包含路径中查找并打开test.h文件,并将其内容插入到当前源文件中。
条件编译就是处理条件编译指令,如#if
、#ifdef
、#ifndef
、#else
、#elif
、#endif
等。根据条件判断是否编译特定代码块。例如:
#ifdef identifier
// 代码块 A
#else
// 代码块 B
#endif
如果已经定义了标识符identifier
,则编译 // 代码块 A
部分,否则编译 // 代码块 B
部分。
条件编译最重要的意义就是对头文件的保护,防止头文件被重复包含。
#ifndef HEADER_FILE #define HEADER_FILE // 头文件内容 #endif
在展开头文件时,预处理器会继续递归地处理被包含的头文件。这意味着如果一个头文件包含了另一个头文件,那么这个被包含的头文件也会被展开,以此类推,直到所有的头文件都被插入到源文件中。我们使用上面这段条件编译的代码就可以避免这种情况,如果没有定义HEADER_FILE,就执行下面的代码;如果定义了,就不会执行了。
条件编译的主要用途包括:
平台特定代码: 通过条件编译,可以根据不同的操作系统或硬件平台编写特定的代码。
调试和发布版本: 可以使用条件编译来在调试版本和发布版本之间切换代码。
特定功能的开关: 通过条件编译,可以选择性地包含或排除某些功能,以满足特定的编译需求。
配置选项: 根据不同的配置选项选择性地包含或排除代码,以适应不同的编译环境。
在这个阶段,预处理后的源代码(通常是.i
文件)被翻译成汇编语言。gcc 首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,gcc 把代码翻译成汇编语言。生成的汇编代码描述了程序的控制流、数据等信息。
在这个阶段,汇编器将汇编代码(通常是.s
文件)翻译成机器码或可重定位的机器码。它将汇编代码转换成二进制目标文件,其中包含特定于处理器体系结构的指令。这个二进制文件也被叫做可重定位目标二进制文件,简称目标文件。但是它不能被执行。
目标文件为什么不能执行?
目标文件通常没有定义程序的入口点,即程序的起始地址。可执行文件需要一个明确定义的入口点,从这个点开始执行程序。单独的目标文件可能包含了未解决的符号引用,无法独立执行。可执行文件需要在操作系统上运行,而操作系统提供了运行时支持,包括内存管理、文件操作、进程调度等。没有这些支持,程序很难在操作系统上正确运行。所以,我们还要通过链接来解决这些问题,让目标文件与其他目标文件或库文件进行链接,形成最终的可执行文件。
在这个阶段,链接器将一个或多个目标文件(通常是.o
文件)与所需的库文件结合,创建可执行文件。链接的主要任务包括符号解析、地址解析、重定位等,以确保所有部分正确地组合在一起。
库通常指的是包含可重用代码和资源的集合,目的是为了帮助开发者完成特定任务。库可以包含函数、类、变量、子例程等,提供了一组API(应用程序接口)或者工具,使得开发者能够更轻松地完成常见的编程任务,而无需从头开始编写所有的代码。
我们的C程序中,并没有定义printf的函数实现,且在预编译中包含的“stdio.h”中也只有该函数的声明,而没有定义函数的实现,那么,是在哪里实现printf函数的呢? 答案是:系统把这些函数实现都被做到名为 libc.so.6 的库文件中去了,在没有特别指定时,gcc会到系统默认的搜索路径/usr/lib下进行查找,会默认找到C的标准库,它会把我们写的源代码经过编译得到的目标文件与库文件进行链接,也就是链接到 libc.so.6 库函数中去,这样就能实现函数printf了,而这也就是链接的作用。
可重用性: 库中的代码可以在不同的程序中被重复使用,从而减少了代码的冗余,提高了开发效率。
封装性: 库提供了一个封装的接口,隐藏了内部实现的细节,使得开发者可以专注于使用功能而不必关心底层的实现。
模块化: 库通常被组织成模块,每个模块负责一个特定的功能。这种模块化的设计有助于提高代码的可维护性和可扩展性。
标准化: 一些库成为了编程的标准,被广泛接受和使用。这样的库通常提供了一些通用的解决方案,被广泛认可并得到社区的支持。
库分为两类:动态库和静态库。其中Linux环境下,动态库的后缀是.so
,静态库的后缀是.a
。在Windows环境下,动态库的后缀是.dll
,静态库的后缀是.lib
。所有的库文件,都遵守相同的命名规则,即:libname.后缀.xxx
。
静态库(Static Library): 在编译时被链接到程序中,程序在运行前就包含了库的代码。这意味着库的代码被复制到了程序的可执行文件中。
动态库(Dynamic Library): 在运行时被加载到内存中,程序在运行时可以调用库的函数。这样可以减小程序的大小,因为多个程序可以共享同一个库的实例。
常见的库包括标准库(如C标准库、C++标准库)、图形界面库(如Qt、GTK+)、数学库(如NumPy)、网络库(如libcurl)、以及许多其他领域的专用库。
动态链接是一种在程序运行时将代码和数据库链接到程序中的技术。把库中要用到的库函数的地址写到在我们代码中调用这个函数的地方,就是动态链接。
指令ldd 可执行程序
,可以查看一个可执行程序所依赖的动态库。gcc的默认行为是动态链接
动态链接的优点:
- 节省内存: 多个程序可以共享同一份库的实例,从而节省内存。
- 易于更新: 更新库时,不需要重新编译所有依赖于该库的程序。只需替换库的新版本即可。
- 灵活性: 可以在程序运行时动态加载和卸载库,使得程序更加灵活。
动态链接的缺点:
- 依赖性:对库的依赖性强 一旦库丢失,所有使用这个库的程序都无法运行
静态链接是一种在程序编译时将所有代码和库链接到一个独立的可执行文件的过程。这意味着在程序运行之前,所有的代码和库已经被合并成一个单独的可执行文件。把库中的代码拷贝到我们的可执行程序中,就是静态链接。
在Linux中默认是没有静态库的,需要我们自己安装。
静态链接的优点:
- 独立性: 生成的可执行文件是完全独立的,不需要外部的依赖。这使得程序更容易分发和部署,因为用户只需要一个文件就可以运行程序。
- 性能: 静态链接的程序在运行时无需进行额外的库加载,因此在一些情况下可能会稍微快于动态链接的程序。
静态链接的缺点:
- 占用空间: 由于每个可执行文件包含了所需的所有代码和库,因此静态链接的程序通常比动态链接的程序更大。
- 更新困难: 如果库被更新,需要重新编译并重新分发整个可执行文件,而不仅仅是替换库文件。
本次的内容到这里就结束啦。希望大家阅读完可以有所收获,同时也感谢各位读者三连支持。文章有问题可以在评论区留言,博主一定认真认真修改,以后写出更好的文章。你们的支持就是博主最大的动力。