程序员C语言快速上手——工程篇(十三) 女爷i 2023-10-11 13:11 30阅读 0赞 ### 文章目录 ### * C语言工程构建 * * shell脚本(bat脚本) * Makefile 脚本 * * 基本语法规则 * 补充说明 * CMake工具 * * 安装 * 简单示例 * 基础规则 * * 外部构建 * 定义变量 * 内置变量 * 命令 * 指定构建环境 * * 生成 Makefile文件 * 生成 Visual Studio工程 * 其他环境 * 补充 * 欢迎关注我的公众号:编程之路从0到1 # C语言工程构建 # **为什么需要编译脚本?** 当C语言工程很大,源码非常多时,如果还去使用GCC命令编译程序,几乎是不现实的。这时候,可以通过编写shell脚本去执行编译命令,当然这并不是一种好的方式。在Linux上我们可以写shell脚本,在Windows上则可以编写bat脚本 **本篇以如下源码作为示例工程,需要编译一个`main.exe`程序出来** `add.c` int add(int a, int b){ return a+b; } `sub.c` int sub(int a, int b){ return a-b; } `mul.c` int mul(int a, int b){ return a*b; } `div.c` int div(int a, int b){ return a*b; } `calc.h` int add(int a, int b); int sub(int a, int b); int mul(int a, int b); int div(int a, int b); `main.c` #include <stdio.h> #include "calc.h" int main(){ printf("1+2=%d\n",add(1,2)); printf("18-9=%d\n",sub(18,9)); return 0; } ## shell脚本(bat脚本) ## 由于在Windows平台,使用MinGW环境,这里编写的是bat脚本,创建一个名为`build`的文件(文件名任意),修改其扩展名为`build.bat`,使用文本编辑器编辑该文件(Linux平台上,则保存扩展名`build.sh`) gcc add.c sub.c mul.c div.c main.c -o main.exe 可以看到,只需要执行`build.bat`就能编译生成`main.exe`,这比每次手敲命令方便太多了。如果有多个源码文件,只需要写入脚本中,通过执行脚本完成编译。 ## Makefile 脚本 ## Makefile 脚本文件是`GNU make` 工具的输入文件,它也包含一套自己的语法规则,它也能帮助C语言实现编译和链接。既然可以通过命令行脚本(shell)完成编译工作,为什么还需要Makefile脚本文件呢? 虽然命令行脚本也能帮助编译链接,但是它的能力还太弱,它每次都会将所有文件重新编译,例如有几百个源文件,我仅仅只修改了其中一个源文件,那么重新编译时,这几百个源文件也都会重新编译,这样每次编译一下都会耗费大量时间。而make 工具会自动根据修改情况完成源文件的对应`.o`文件的更新、库文件的更新以及最终的可执行程序的更新,它实际上是通过比较对应文件的最后修改时间,来决定哪些文件需要更新、那些文件不需要更新。 现在将命令行脚本改写为Makefile脚本,在源码目录下创建一个名为`Makefile`的文件(亦可以写作`makefile`),注意,它没有拓展名,编辑如下内容: # 编译一个main.exe 程序 main.exe: main.o add.o sub.o mul.o div.o gcc main.o add.o sub.o mul.o -o main.exe main.o: main.c calc.h gcc -c main.c add.o: add.c gcc -c add.c sub.o: sub.c gcc -c sub.c mul.o: mul.c gcc -c mul.c div.o: div.c gcc -c div.c # 伪目标,删除所有.o文件 clean: rm *.o `cd`到当前目录,执行输入`make`命令,即可快速编译生成`main.exe`程序,当我们需要清理整个工程时,即全部重新编译时,可以输入`make clean`命令,即可删除当前目录下的所有`.o`文件。 ### 基本语法规则 ### 注意,`#`号开头的行表示注释 语法结构如下 target1 target2 target3...: prerequisite1 prerequisite2 prerequisite3... command1 command2 command3 * target 表示目标。通常有三种情况:可以是一个目标文件(`.o`文件);可以是一个可执行文件;可以是一个标签,标签被称为**伪目标** * prerequisite 表示条件。实际上表达的是一种依赖关系,即要生成前面的target,所需要依赖的文件或是另一个目标 * command 表示需要执行的命令。即要生成这个目标,对应执行的命令 > 需要注意,在冒号的左边,可以是一个或多个目标,而在冒号的右边,则可以是零个或多个依赖条件。目标顶格写,而command前面则必须有一个制表符(即`Tab`键) 要想写`Makefile`文件,必须对C语言的编译链接阶段有基本的了解,总的来说,就是将`.c`源码文件编译为`.o`目标文件,然后将`.o`文件链接为可执行程序,而`Makefile`脚本正是将这个依赖关系反过来描述,即一个可执行程序需要依赖哪些`.o`文件,每一个`.o`文件又依赖于哪些`.c`、`.h`文件。 **简化版本** 除了上面那种标准版本,我们还可以利用make工具的自动推导能力,省略对目标文件的条件依赖描述,包括编译命令。 # 编译一个main.exe 程序 main.exe: main.o add.o sub.o mul.o div.o gcc main.o add.o sub.o mul.o -o main.exe main.o: calc.h add.o: sub.o: mul.o: div.o: # 伪目标,删除所有.o文件和可执行文件 clean: rm *.o main.exe 另一种风格 # 编译一个main.exe 程序 main.exe: main.o add.o sub.o mul.o div.o gcc main.o add.o sub.o mul.o -o main.exe main.o: calc.h # 另一种风格,写在同一行 add.o sub.o mul.o div.o: # 伪目标,删除所有.o文件和可执行文件 clean: rm *.o main.exe 在make工具中,它能够自动完成对`.c`文件的编译并生成对应的`.o`文件。它默认执行命令`cc -c`来编译`.c`源文件,以`main.o`为例,它会默认执行`cc -c main.c -o main.o`。但是要注意,我们如果在Windows上执行以上简化版的`make`,则会报错,这是因为在Linux系统中,`cc`命令会默认的链接到`gcc`命令上,执行`cc`命令就是在执行`gcc`命令,而我们Windows系统中是没有`cc`命令的。解决办法非常简单粗暴,就是进入`gcc.exe`所在目录,将`gcc.exe`再复制一份,并更名为`cc.exe`即可。 **伪目标** 伪目标就是一个标签,它本身既不是目标文件也不是可执行文件,例如上面例子中的`clean`,我们可以通过伪目标定义一些命令,然后在make中去执行。 上面例子中的伪目标在定义上存在一些问题,假如源码目录下真的存在一个名为`clean`的文件,则会与当前的伪目标冲突。将一个目标声明为伪目标需要将它作为特殊目标`.PHONY`的依赖,这样定义的伪目标就不会和源码目录下的文件名冲突。 正确的定义伪目标 .PHONY: clean clean: rm *.o main.exe 再看一个例子 # 定义一个伪目标print,它执行命令行的echo命令输出hello,world .PHONY: print print: echo "hello,world" 然后在命令行执行`make print`,就会输入出被执行的完整命令,以及命令执行的结果 我们可以根据自己的需要在`Makefile`中定义自己的伪目标,通常会定义`clean`、`install`这些伪目标,`install`一般定义拷贝命令,将生成的可执行程序拷贝到应用安装目录下。在Linux平台下,通常是将C语言的源代码和`Makefile`脚本一同发布出去,用户只需要在源码目录下分别执行命令`make`、`make install`即完成了程序的编译和安装,可以看到,有了`make`工具后,让开源的C程序的编译使用过程变得非常简单。 ### 补充说明 ### 实际上完整的Makefile 语法体系是非常复杂灵活的,学习完整Makefile语法不亚于学习一门新的编程语言,而且许多语法功能并不是常用的,另一方面,在大型的复杂工程中,自己手写`Makefile`是极为不明智的选择。`make`工具是一个比较古老的工具,已经有一些工具可以帮助我们自动生成`Makefile`文件,例如Linux上的`Autoconf`,当然,现在更好的工具是`cmake`,它可以自动生成跨平台编译脚本,而且还能用于Android端的NDK开发,是最被推荐的构建工具。 ## CMake工具 ## 它首先允许开发者编写一种平台无关的 `CMakeLists.txt` 文件来定制整个编译流程,然后再根据目标用户的平台进一步生成所需的本地化 `Makefile` 或工程文件,如`Linux` 下的 `Makefile`文件 或 Windows 的 Visual Studio 工程文件。 简单说,以前我们编写的C语言编译脚本是不能跨平台编译的,例如上面示例中编写的 `Makefile` ,它只能在GCC环境下编译,通常是Linux系统上,而在Windows下的Visual Studio里面就没法用,得重新改造,如果是一个大型项目,那就是灾难。现在我们用CMake工具编写构建脚本,就与平台无关了,它会自动生成对应平台的构建方案,再也不用程序员去操心了。更准确的说,CMake工具真正厉害的地方并不只是跨平台,而是跨编译环境。 ### 安装 ### 进入[cmake官网下载页][cmake] 下载zip包或安装器,安装后,将cmake的`bin`目录加入PATH环境变量中,命令行输入`cmake --version`检查环境是否配置成功 ### 简单示例 ### 以上面的代码为例,在源码目录下创建 `CMakeLists.txt` 文件 # CMake最低版本号要求 cmake_minimum_required (VERSION 2.8) # 配置项目名 project (ch1) # 指定生成目标,main2为生成的可执行程序名,后面是源码列表 add_executable (main2 add.c sub.c mul.c div.c main.c) 当前面目录下执行以下命令,注意`.`不能掉 cmake . 在我们的目录下自动生成了一个 Visual Studio 工程,因为我本地安装了Visual Studio开发环境。可以双击打开`ch1.sln`文件或`main2.vcxproj`文件,这里会打开Visual Studio IDE,就能直接在IDE里面编译了。 这里,如果我想生成MinGW开发环境的`Makefile`,则只需要加一个`-G`参数,来指定一个明确的编译环境,从而生成对应的构建脚本。 cmake -G "MinGW Makefiles" 要注意,以上命令直接在CMD命令行执行可能会报错,它需要一个`sh`环境,这里有两种解决办法 * 将`sh.exe`所在目录加入到环境变量中,它位于`MinGW`根目录下的`git\bin`下,修改环境变量后,打开新的命令行窗口然后再执行以上命令 * 第二种就是偷懒的做法,如果你本地安装了`git`工具,则直接鼠标右键,选择`Git Bash Here`打开一个`bash`来执行以上命令 命令执行完毕,本地目录下就会自动生成一个`Makefile`文件,然后执行`make`命令即可编译。我们如果打开这个`Makefile`文件,会发现看不懂,里面内容比较复杂。 到这里我们已经学会了`cmake`构建的简单流程,接下来只需要学习一下 `CMakeList.txt`文件的编写规则 ### 基础规则 ### `CMakeLists.txt`文件由命令、注释和空格组成,其中**命令是不区分大小写的**。`#`开头的行表示注释。命令由命令名称、小括号和参数组成,参数之间使用空格进行间隔。例如`add_executable (main2 add.c sub.c mul.c div.c main.c)` #### 外部构建 #### 在上面的示例中,执行`cmake`命令会在源码工程的目录下生成很多无法自动删除的中间文件或临时文件,这就弄乱了源码工程的目录,如果要发布源码,还得手动一个个去删除这些文件,这显然不是一种好的构建方式,这种方式被称为**内部构建**,相应的,我们需要使用**外部构建**的方式来解决问题。 在源码工程的根目录下创建一个`build`文件夹,然后在命令行里`cd`到`build`下,执行`cmake ..`或 `cmake -G "MinGW Makefiles" ..`命令,此时会将所有的中间文件生成到`build`目录中,包括`Makefile`,然后执行`make`编译。当我们需要删除临时文件时,只需要删除`build`目录即可,不会对源码工程造成任何影响。 #### 定义变量 #### 源文件较多时,可以定义一个变量来保存,后续只需要引用该变量即可,如下,定义`src_list`来保存源文件列表,引用是使用`${}`包裹. 定义变量使用`set`命令,取消命令可使用`unset`命令 # 定义变量 src_list set (src_list add.c sub.c mul.c div.c main.c) # 打印日志 message (STATUS "源文件列表:${src_list}") # 引用变量 add_executable (main2 ${src_list}) `message`命令是用来打印日志的,它的第一个参数是mode,可省略,常用值如下 <table> <thead> <tr> <th align="left">mode</th> <th align="left">简述</th> </tr> </thead> <tbody> <tr> <td align="left">(none)</td> <td align="left">重要信息</td> </tr> <tr> <td align="left">STATUS</td> <td align="left">附带消息</td> </tr> <tr> <td align="left">WARNING</td> <td align="left">CMake警告,继续处理</td> </tr> <tr> <td align="left">AUTHOR_WARNING</td> <td align="left">CMake警告(dev),继续处理</td> </tr> <tr> <td align="left">SEND_ERROR</td> <td align="left">CMake错误,继续处理,但会跳过生成</td> </tr> <tr> <td align="left">FATAL_ERROR</td> <td align="left">CMake错误,停止处理和生成</td> </tr> </tbody> </table> #### 内置变量 #### 在`cmake`中已经内置了一些变量,我们可以直接使用,也可使用`set`命令去修改 * `CMAKE_SOURCE_DIR`或`PROJECT_SOURCE_DIR` 表示工程的根目录 * `CMAKE_BINARY_DIR`或`PROJECT_BINARY_DIR` 表示编译目录。如果是内部构建,则编译目录与工程根目录相同,如果是外部构建,则表示外部构建创建的编译目录,如上例中的`build`目录 * `CMAKE_CURRENT_SOURCE_DIR` 表示当前处理的`CMakeLists.txt`所在文件夹的路径 * `CMAKE_CURRENT_LIST_FILE` 当前`CMakeLists.txt`文件的完整路径 * `CMAKE_C_COMPILER`和`CMAKE_CXX_COMPILER` 分别表示C和C++编译器的路径 * `PROJECT_NAME` 该变量可获取`project`命令配置的项目名 可以使用`message`命令打印这些内置变量的值 cmake_minimum_required (VERSION 2.8) project (ch1) message (${CMAKE_SOURCE_DIR}) message (${PROJECT_SOURCE_DIR}) message (${CMAKE_BINARY_DIR}) message (${PROJECT_BINARY_DIR}) message (${CMAKE_CURRENT_SOURCE_DIR}) message (${CMAKE_CURRENT_LIST_FILE}) message (${CMAKE_C_COMPILER}) message (${CMAKE_CXX_COMPILER}) message (${PROJECT_NAME}) * `EXECUTABLE_OUTPUT_PATH` 设置该变量可修改可执行程序的生成路径 * `LIBRARY_OUTPUT_PATH` 设置该变量可修改库文件生成路径 # build/bin/ SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin) # build/lib/ SET(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib) * `BUILD_SHARED_LIBS` 指定默认生成的库的类型 #### 命令 #### `CMakeLists.txt`文件基本上就是由命令和参数组成的,例如之前的`set`、`message`这些,下面就了解一下常用的命令 * `add_executable` 使用给定的源文件,生成一个可执行程序 * `add_library` 使用给定的源文件,生成一个库(静态库或共享库) * `add_subdirectory` 添加一个子目录,该子目录也必须包含一个CMakeLists.txt文件 * `include_directories` 添加头文件路径 * `add_definitions` 添加编译参数 * `target_link_libraries` 链接指定的库 * `find_library` 查找指定的库,并将库文件路径保存到一个变量 * `set_target_properties` 设置目标的一些属性,从而改变构建方式 * `link_directories` 添加库的搜索路径 * `aux_source_directory` 查找指定路径下的所有源文件 **综合实例** 调整上面示例工程的结构,在工程跟目录下创建四个文件夹,分别是`build`、`calc`、`include`、`src`,具体工程结构如下所示 ch1 | +--- build/ | +--- calc/ | +--- add.c | +--- div.c | +--- mul.c | +--- sub.c | +--- CMakeLists.txt | +--- include/ | +--- calc.h | +--- src/ | +--- main.c | +--- CMakeLists.txt `calc`目录作为一个子项目,用于编译一个`libcalc.a`静态库,主工程源码在`src`下,且需链接静态库。 子项目`calc`下需要一个`CMakeLists.txt`文件,内容如下 cmake_minimum_required (VERSION 2.8) # 创建静态库calc,其生成的文件名为libcalc.a add_library (calc STATIC add.c sub.c mul.c div.c) 工程根目录下也需要`CMakeLists.txt`文件,内容如下 cmake_minimum_required (VERSION 2.8) # 配置项目名 project (ch1) # 添加一个子文件夹 calc,这里写的相对路径 add_subdirectory (calc) # 定义变量 SRCS_DIR, 指向src目录的绝对路径 set (SRCS_DIR "${PROJECT_SOURCE_DIR}/src") # 添加头文件目录,即添加工程根目录下的include目录 include_directories ("${PROJECT_SOURCE_DIR}/include") # 添加库的搜索路径,即libcalc.a所在的目录(build/calc/libcalc.a) link_directories ("${PROJECT_BINARY_DIR}/calc") # 用于生成可执行文件 main.exe add_executable (main "${SRCS_DIR}/main.c") # 为main程序指定链接静态库calc target_link_libraries(main calc) 首先执行`cmake -G "MinGW Makefiles" ..`命令自动生成`Makefile`文件,然后执行`make`命令进行编译,完成后`build`目录下即生成`main.exe` 当链接已经编译好的库时,推荐使用`find_library`来查找库,因为`link_directories`命令传入相对路径时,会直接将相对路径传给编译器,导致出现找不到问题。 `find_library`命令原型如下,第一个参数为变量,第二个参数为库名称,最后面可以填入多个路径 `find_library(<VAR> name1 [path1 path2 ...])` # 在指定的目录下查找名为calc的库, # 并将库文件的绝对路径保存到变量STATIC_LIB中 find_library(STATIC_LIB calc "${PROJECT_BINARY_DIR}/calc") message (${STATIC_LIB}) # 为main程序指定链接静态库calc target_link_libraries(main ${STATIC_LIB}) **静态库与动态库** 使用`add_library`命令默认生成静态库,如`add_library (calc add.c sub.c mul.c div.c)`,亦可加上参数`STATIC`显式指定,如需生成动态库,则添加参数`SHARED`,如`add_library (calc SHARED add.c sub.c mul.c div.c)`,此外,还可以通过设置变量`BUILD_SHARED_LIBS`来修改默认行为,当该变量为真时,默认会生成动态库,如 # 使用option命令定义选项 option(BUILD_SHARED_LIBS "build shared or static libraries" ON) **自动获取源码列表** 当我们工程的源码非常多时,一个个去手写源码列表是非常麻烦的,以上述`calc`目录下的`CMakeLists.txt`文件为例,这时可以使用`aux_source_directory`命令 cmake_minimum_required (VERSION 2.8) # 获取当前目录下的源文件路径列表,并保存到变量SRC_LIST中 aux_source_directory (. SRC_LIST) # 打印 message (STATUS ${SRC_LIST}) add_library (calc STATIC ${SRC_LIST}) 该命令原型如下,第一个参数为搜索的路径,第二个参数为变量 aux_source_directory(<dir> <variable>) 这个命令只能识别源码文件,不能识别其他文件,比如`.h`文件就不能扫描出来,因此存在一定缺陷,想知道能识别哪些拓展名的源文件,可打印两个内置变量获取 message (STATUS ${CMAKE_C_SOURCE_FILE_EXTENSIONS}) message (STATUS ${CMAKE_CXX_SOURCE_FILE_EXTENSIONS}) **递归获取文件列表** `aux_source_directory`命令只能获取源码文件列表,且无法递归获取给定路径下的嵌套子文件夹下的各种源文件,这时可以使用`file`命令,结合`GLOB_RECURSE`参数,对指定的文件拓展名进行递归获取。 # 递归遍历当前目录下的所有.c .cpp后缀名的文件,并将结果列表保存到SRC_LIST变量中 FILE(GLOB_RECURSE SRC_LIST *.c *.cpp) # 打印 message (STATUS ${SRC_LIST}) add_library (calc STATIC ${SRC_LIST}) 原型如下 file(GLOB_RECURSE variable [RELATIVE path] [FOLLOW_SYMLINKS] [globbing expressions]...) 如不需递归,可将`GLOB_RECURSE`改为`GLOB` **指定库的输出名称** add_library (calc STATIC ${SRC_LIST}) # 将生成 libcalculate.a set_target_properties(calc PROPERTIES OUTPUT_NAME "calculate") **定义宏与条件编译** 可使用`add_definitions`命令,传入`-D`加上宏名称来定义宏,以下定义宏`USER_PRO` # 定义宏 USER_PRO add_definitions(-DUSER_PRO) # 等价于 #define VER 1 、#define Foo 2 add_definitions(-DVER=1 -DFoo=2) 配合使用`option`命令,实现条件编译 project(test) option(USER_PRO "option for user" OFF) if (USER_PRO) add_definitions(-DUSER_PRO) endif() `option`命令原型: option(<option_variable> "描述选项的帮助性文字" [initial value]) `add_definitions`命令主要用来添加编译参数,`add_compile_options`命令也具有相同的功能,示例如下 add_compile_options(-std=c99 -Wall) add_definitions(-std=c99 -Wall) ### 指定构建环境 ### 前面已经学会了`-G`参数指定构建环境,那么到底可以指定哪些构建环境呢?这里根据官方文档,整理一下`-G`后面可以跟哪些值。 #### 生成 Makefile文件 #### 以下是不同环境下的Makefile文件 * `Borland Makefiles` * `MSYS Makefiles` * `MinGW Makefiles` * `NMake Makefiles` * `NMake Makefiles JOM` * `Unix Makefiles` * `Watcom WMake` #### 生成 Visual Studio工程 #### * `Visual Studio 6` * `Visual Studio 7` * `Visual Studio 7 .NET 2003` * `Visual Studio 8 2005` * `Visual Studio 9 2008` * `Visual Studio 10 2010` * `Visual Studio 11 2012` * `Visual Studio 12 2013` * `Visual Studio 14 2015` * `Visual Studio 15 2017` * `Visual Studio 16 2019` #### 其他环境 #### * `Green Hills MULTI` * `Xcode` * `CodeBlocks` * `CodeLite` * `Eclipse CDT4` * `Kate` * `Sublime Text 2` #### 补充 #### * `Ninja` 这里重点说一下`Ninja`,当前的官方文档中没有写`Ninja`,实际上CMake从2.8.9版本开始可以支持`Ninja`构建 > `Ninja` 是一个注重速度的小型构建系统。它与其他构建系统在两个主要方面不同:它被设计为使其输入文件由更高级别的构建系统生成,并且被设计为尽可能快地运行构建。 简单说,它被设计出来是为了替代`make`工具以及`Makefile`文件的,它与`make`工具的显著区别是,`Makefile`是设计出来给人手写的,而`Ninja`的`build.ninja`设计出来是给其它程序生成的。`Makefile`是一个DSL,`Ninja`则只是一种配置文件。 `Makefile`支持分支、循环等流程控制,而`Ninja`仅支持一些固定形式的配置。 **两者的对应关系**: `ninja`对应`make`,`build.ninja`文件对应于`Makefile`文件 **安装** 到[下载链接][Link 1] 下载对应版本的`ninja`工具,解压后配置PATH环境变量,输入`ninja --version`检查环境 **生成** `build.ninja`文件 cmake -G "Ninja" .. **编译** ninja # 欢迎关注我的公众号:编程之路从0到1 # ![编程之路从0到1][0_1] [cmake]: https://cmake.org/download/ [Link 1]: https://github.com/ninja-build/ninja/releases [0_1]: https://img-blog.csdnimg.cn/20190301102949549.jpg
相关 程序员C语言快速上手——工程篇(十三) 文章目录 C语言工程构建 shell脚本(bat脚本) Makefile 脚本 基本语法规则 补 女爷i/ 2023年10月11日 13:11/ 0 赞/ 31 阅读
相关 程序员C语言快速上手——基础篇(三) 文章目录 小拓展:C语言中int的正确使用姿势 语法基础 表达式 算术运算符 关系运算符 待我称王封你为后i/ 2022年01月23日 00:19/ 0 赞/ 271 阅读
相关 程序员C语言快速上手——高级篇(九) 文章目录 高级篇 结构体 背景 结构体的声明与使用 结构体变量的初始化 太过爱你忘了你带给我的痛/ 2021年12月09日 20:55/ 0 赞/ 340 阅读
还没有评论,来说两句吧...