从零学习GCC,Makefile,CMakeList编译

近期想系统地学习一下C++软件开发,因此记录一下自己的学习笔记,方便复习。大多数内容都是整理搬运别的博主文章内容,加上自己的理解归纳。如果大家想了解更深的内容或者有不明白的地方,可以阅读参考中记录的博客。
平台:Windows10+mingw
参考

【1】Linux编译工具:gcc入门
【2】Linux下gcc生成和使用静态库和动态库详解
【3】一个简单的make&makefile编写指南
【4】Make 编译脚本上手
【5】CMake入门实践
准备

假设我们工程里有三个文件

    main.c

#include <stdio.h>
#include <fun.h>

int main(){
    printf("this is main
");
    fun();
    return 0;
}


    fun.c

#include <stdio.h>
#include <fun.h>

void fun(){
    printf("This is fun!
");
    return;
}


    fun.h

#ifndef MAIN_FUN_H
#define MAIN_FUN_H
void fun(void);
#endif



我们要如何编译这个工程呢?
GCC编译
– gcc简介

gcc的全称是GNU Compiler Collection,它是一个能够编译多种语言的编译器。最开始gcc是作为C语言的编译器(GNU C Compiler),现在除了c语言,还支持C++、java、Pascal等语言。gcc支持多种硬件平台。
– gcc编译过程

gcc编译程序主要经过四个过程:

    预处理(Pre-Processing)
    编译 (Compiling)
    汇编 (Assembling)
    链接 (Linking)

   预处理实际上是将头文件、宏进行展开。编译阶段,gcc调用不同语言的编译器,例如c语言调用编译器ccl。gcc实际上是个工具链,在编译程序的过程中调用不同的工具。汇编阶段,gcc调用汇编器进行汇编。链接过程会将程序所需要的目标文件进行链接成可执行文件。汇编器生成的是可重定位的目标文件,学过操作系统,我们知道,在源程序中地址是从0开始的,这是一个相对地址,而程序真正在内存中运行时的地址肯定不是从0开始的,而且在编写源代码的时候也不能知道程序的绝对地址,所以重定位能够将源代码的代码、变量等定位为内存具体地址。下面以一张图来表示这个过程,注意过程中文件的后缀变化,编译选项和这些后缀有关。

– gcc编译实例

编译单文件
gcc -c main.c / gcc -o main main.c
区别是前者生成main.o,还需要链接到exe文件,后者会直接生成main.exe。
注意1:有一些博主写的是gcc -c main.c -o main,但我发现这样很容易报错,错误原因未知,也许跟系统有关。所以Windows下还是建议最好-o在前。我这里就都写成-o在前了。
注意2:我使用gcc -o main -c main.c编译时无法生成o文件或exe文件,会生成一个没有后缀的文件。使用gcc -o main.o -c main.c是可以的,但使用gcc -o main.exe -c main.c不行。看来-c, -o同时使用只支持生成o文件,而且只支持编译一个文件。好奇怪…

或者一步步进行

1 gcc -o main.i  -E main.c     #对main.c文件进行预处理,生成预处理文件
2 gcc -o main.s -S main.i     #对预处理文件进行编译,生成了汇编文件
3 gcc -o main.o -c main.s   #对汇编文件进行编译,生成了目标文件
4 gcc -o main main.o          #对目标文件进行链接,生成可执行文件


使用gcc时可以加上-Wall选项,否则编译器不会报出警告
gcc -Wall -o bad bad.c

编译多文件
gcc -o main fun1.c fun2.c #生成可执行文件main
注意:这里也是,使用gcc -o main -c fun1.c fun2.c会报错gcc.exe: fatal error: cannot specify -o with -c, -S or -E with multiple files 不确定这是不是linux和windows的区别。

gcc -Wall -o fun1.o -c fun1.c
gcc -Wall -o fun2.o -c fun2.c
gcc -Wall -o main fun1.o fun2.o


– 使用外部库

库分为静态库和动态库

  •     静态库(Windows->.lib Linux->.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。静态库比较占用磁盘空间,而且程序不可以共享静态库。运行时也是比较占内存的,因为每个程序都包含了一份静态库。
  •     动态库(Windows->.dll Linux->.so):程序在运行的时候才去链接共享库的代码,多个程序共享使用库的代码,这样就减少了程序的体积。值得一提的是,在Windows下的动态链接也可以用到.lib为后缀的文件,但这里的.lib文件叫做导入库,是由.dll文件生成的。


动/静态库的优缺点:C语言的静态链接和动态链接

Linux下:
编译如果直接使用标准库目录的库函数

一般头文件或库文件的位置在标准库目录:

  •     /usr/include及其子目录底下的include文件夹
  •     /usr/local/include及其子目录底下的include文件夹
  •     /usr/lib
  •     /usr/local/lib


静态库链接时搜索顺序:

  1.     ld会去找GCC命令中的参数-L
  2.     再找gcc的环境变量LIBRARY_PATH
  3.     再找内定目录 /lib /usr/lib /usr/local/lib 这是当初compile gcc时写在程序内的


动态库链接时搜索顺序:

  1.     编译目标代码时指定的动态库搜索路径-L;
  2.     环境变量LD_LIBRARY_PATH指定的动态库搜索路径;
  3.     配置文件/etc/ld.so.conf中指定的动态库搜索路径;
  4.     默认的动态库搜索路径/lib;
  5.     默认的动态库搜索路径/usr/lib。


例如main.c需要libm.a(标准数学库)
gcc -o main main.c -lm
数学库的文件名是 libm.a。前缀lib和后缀.a是标准的,m是基本名称,GCC 会在-l选项后紧跟着的基本名称的基础上自动添加这些前缀、后缀。
当目录中同时存在同名静态库和动态库时,优先链接动态库。可以添加-static来强制链接静态库。如gcc -o main main.c -lhello -static
如果报错不知道-lc,使用yum install glibc-static

如果需要使用标准库目录之外的库函数,如:

    <开发目录>/include
    <开发目录>/lib

方法:

  1.     把链接库作为一般的目标文件,为 GCC 指定该链接库的完整路径与文件名。例如,如果链接库名为 libm.a,并且位于 /lib 目录,那么下面的命令会让 GCC 编译 main.c,然后将 libm.a 链接到 main.o:gcc -o main main.c /lib/libm.a
  2.     使用-L选项,为 GCC 增加另一个搜索链接库的目录:gcc -o main main.c -L/lib -lm
  3.     把包括所需链接库的目录加到环境变量 LIB_LIBRARY_PATH 中。export LD_LIBRARY_PATH=lib:$LD_LIBRARY_PATH


Windows下
编译如果直接使用标准库目录的库函数

静/动态库链接时搜索顺序:

  1.     当前目录(./)
  2.     系统目录(C:/Windows/System32、C:/Windows/System)
  3.     Windows目录(C:/Windows)或者环境变量PATH中申明的路径中寻找对应的dll文件。


在程序编译的链接过程中,只有动态链接库dll文件是不够的,还需要使用对应的.lib文件。lib文件中包含了所生成可执行文件所依赖的库函数地址,链接器(linker)需要在lib文件中找到对应的函数符号(symbol)。这样编译生成的可执行文件就可以在运行时找到对应库函数实现代码在内存中的地址,从而调用对应的模块。

如果需要使用标准库目录之外的库函数,如:

    <开发目录>/include
    <开发目录>/lib
    这部分内容我只找到了使用Visual Studio的:

  1.     头文件:项目属性>配置属性>C/C++>常规>附加包含目录
  2.     lib路径:项目属性>配置属性>链接器>常规>附加库目录
  3.     指定lib:项目属性>配置属性>链接器>输入>附加依赖项


–编译函数生成为外部库

生成为静态库
为了生成.a文件,我们需要先生成.o文件。下面这行命令将我们的hello.o打包成静态库libhello.a:

ar rcs libfun.a fun.o #Linux
ar rcs libfun.lib fun.o #Windows 


ar是gun归档工具,rcs表示replace and create,如果libfun之前存在,将创建新的libfun.a并将其替换。
然后就可以使用我们生成的静态库。(生成的静态库就在工作路径下)

gcc -Wall -o main main.c -L. -lfun #Linux
gcc -Wall -o main main.c libfun.lib #Windows

用MinGW可以生成.a后缀和.lib后缀的静态链接库,但是调用方法不一样。-L的方式貌似只用于Linux, 好晕…

生成为动态库
为了生成.so文件,我们还是需要先生成.o文件。
gcc -shared -fPIC -c fun.c

gcc -shared -fPIC -o fun.so fun.o #Linux
gcc -shared -fPIC -o libfun.dll fun.o #Windows


“PIC”命令行标记告诉GCC产生的代码不要包含对函数和变量具体内存位置的引用,这是因为现在还无法知道使用该消息代码的应用程序会将它连接到哪一段内存地址空间。这样编译出的hello.o可以被用于建立共享链接库。建立共享链接库只需要用GCC的”-shared”标记即可。
然后就可以使用生成的动态库。(生成的动态库就在工作路径下)

gcc -Wall -o main main.c  -L. -lfun #Linux
gcc -Wall -o main main.c  libfun.dll #Linux


–常用参数

  1.     –version 查看版本
  2.     -v verbose(冗长的),输出编译的详细信息
  3.     -std 指定标准
  4.     -o 指定输出文件的名称
  5.     -Wall 输出所有的警告信息
  6.     -c 只将源文件编译为 object 文件(*.o),而不进行链接,之后可以使用 gcc -o myprog.exe file1.o file2.o 将多个 object 文件连接成可执行文件
  7.     -shared 编译为共享库(.dll,.so
  8.     -S 编译为汇编代码


Make编译
–Make编译简介

假设我们有三个文件,fun.c, main.c, fun.h。
如果使用gcc编译 gcc -o main fun.c main.c -I.
gcc 编译器会编译两个 C 源文件并把可执行程序命名为 main。参数“-I.”用以指示 gcc 在当前目录“.”下寻找头文件 hellomake.h。但是如果我们修改了源文件,那么每次都需要重新输入编译命令。当要编译的源文件很多时,这样做就很没有效率。其次,如果你只对一部分源文件做了修改,每次都重新编译所有的文件耗时且低效。

通过 make 命令,可以将上面的编译进行有效自动化管理。通过将从输入文件到输出文件的编译无则编写成 Makefile 脚本,Make 工具将自动处理文件间依赖及是否需要编译的检测。因为makefile实现了增量编译的效果,执行子任务时它会先检查输入文件是否比输出文件新,如果说输入文件是新的,需要重新生成输出文件,此时才会执行子任务。因此优化了gcc命令编译效率低的问题。

make 命令所使用的编译配置文件可以是 Makefile,makefile 或 GUNMake。其中定义任务的基本语法为:

target1 [target2 ...]: [pre-req-1 pre-req-2 ...]
        [command1
         command2
     ......]


上面形式也可称作是一条编译规则(rule)。其中,
target 为任务名或文件产出。如果该任务不产出文件,则称该任务为 Phony Targets。make 内置的 phony target 有 all, install 及 clean 等,这些任务都不实际产出文件,一般用来执行一些命令。
pre-req123... 这些是依赖项,即该任务所需要的外部输入,这些输入可以是其他文件,也可以是其他任务产出的文件。
command 为该任务具体需要执行的 shell 命令。需要注意的是makefile 中的命令必须以 tab 开始,不能使用空格。
–Makefile编译实例

以上文介绍的三个文件,fun.c, main.c, fun.h。为例。编译它们的makefile为:

all:main

main: main.c fun.c fun.h
    gcc -o main fun.c main.c -I

clean:
    rm main


这里的makefile定义了三个任务:all, main以及clean。调用时可使用make <target name>来单独调用任务。如果只调用make那么只执行all任务。
三个任务中:

  1.     all 为内置的任务名,一般一个 Makefile 中都会包含,当直接调用 make 后面没有跟任务名时,默认执行的就是 all。此例子中,调用all任务就等于调用main任务,因为 all 的输入依赖为 main文件。Make 在执行任务前会先检查其输入的依赖项,执行 all 时发现它依赖 main 文件,于是本地查找,发现本地没有,再从 Makefile 中查找看是否有相应任务会产生该文件,结果确实有相应任务能产生该文件,所以先执行能够产生依赖项的任务。
  2.     main为编译工程,产出main.out文件。在这里main为target任务名,main.c, fun.c为pre-req依赖项,gcc命令为command所需的shell命令。
  3.     clean任务为phony target任务,此clean任务清楚刚才生成的main.out文件。phony任务每次都会执行。


如果一条编译规则中所要执行的shell命令很长,可以通过[space]来换行,后的空格是必须的否则识别出错。
–Makefile编译实例(宏/变量)

Makefile 中可使用变量(宏)来让脚本更加灵活和减少冗余。

其中变量使用 $ 加圆括号或花括号的形式来使用,$(VAR),定义时类似于 C 中定义宏,所以变量也可叫 Makefile 中的宏,例如刚才的例子,我们可以使用CC=gcc来定义编译工具,然后makefile将变为:

CC=gcc
CFLAGS = -I.
DEPS = fun.h
all:main

main: mian.c fun.c $(DEPS)
    $(CC) -o main -c main.c fun.c $(DEPS) $(CFLAGS)

clean:
    rm main


这样随着平台或环境的改变,我们可以很方便地修改编译工具。特别是当编译大型工程时。

自动变量
自动变量/Automatic Variables 是在编译规则匹配后工具进行设置的,具体包括:

$@:代表产出文件名
$*:代表产出文件名不包括扩展名
$<:依赖项中第一个文件名
$^:空格分隔的去重后的所有依赖项
$+:同上,但没去重
$?:同上,但只包含比产出更新的那些依赖

这些变量都只有一个符号,区别于正常用字母命名的变量需要使用 $(VAL) 的形式来使用,自动变量无需加括号。利用自动变量,前面实例可以改造成:

CC=gcc
CFLAGS = -I.
TARGET=main

all:$(TARGET)

$(TARGET): main.c fun.c fun.h
    $(CC) -o $@ $^ $(CFLAGS)

clean:
    rm $(TARGET)


搜索路径
当我们想对工程进行分文件夹管理时,可通过 VPATH 指定依赖文件及产出文件的搜索目录。

VPATH = src include
通过小写的 vpath 可指定具体的文件名及扩展名类型,

vpath %.c src
vpath %.h include
此处% 表示文件名。
–Makefile编译实例(规则)

如果在一个工程中我们有很多的文件要产出,那么我们需要写很多的command命令,例如:

Main.o : Main.h Test1.h Test2.h
    g++ -g -o $@ -c $<
Test1.o : Test1.h Test2.h
    g++ -g -o $@ -c $<
Test2.o : Test2.h
    g++ -g -o $@ -c $<


这样代码将非常冗余,包含许多重复命令,此时我们可以利用匹配规则来精简代码。定义一条匹配规则,可以认为像python中的lambda函数一样,制造一个出通用函数。比如:

%.o: %.c
    gcc -o $@ $^

    1
    2

这条编译规则以为这将所有匹配这条的 “将c文件编译为out文件” 任务全部执行。而记录这些任务的语句叫做依赖规则,比如:

Main.o : Main.h Test1.h Test2.h
Test1.o : Test1.h Test2.h
Test2.o : Test2.h


像这种,只定义了产出与依赖没包含任务命令的无则,叫作依赖无则。因为它只定义了某个产出依赖哪些输入,故名。这种规则可达到这种效果,即,右边任何文件有变更,左边的产出便成为过时的了,需要重新编译。依赖规则不能直接调用,它相当于定义了一些输入和输出信息,而实际其中做什么工作需要匹配规则去告知。

将匹配规则和依赖规则组合起来,我们就可以达到最开始的一次执行多文件产出的效果:

%.o : %.cpp
    g++ -g -o $@ -c $<
Main.o : Main.h Test1.h Test2.h
Test1.o : Test1.h Test2.h
Test2.o : Test2.h


CMake编译
-CMake简介

你或许听过好几种 Make 工具,例如 GNU Make ,QT 的 qmake ,微软的 MS nmake,BSD Make(pmake),Makepp,等等。这些 Make 工具遵循着不同的规范和标准,所执行的 Makefile 格式也千差万别。这样就带来了一个严峻的问题:如果软件想跨平台,必须要保证能够在不同平台编译。而如果使用上面的 Make 工具,就得为每一种标准写一次 Makefile ,这将是一件让人抓狂的工作。

cmake 是 makefile 的上层工具,它们的目的正是为了产生可移植的makefile,并简化自己动手写makefile时的巨大工作量。在 linux 平台下使用 CMake 生成 Makefile 并编译的流程如下:

  1.     编写 CMake 配置文件 CMakeLists.txt 。
  2.     执行命令 cmake PATH 或者 ccmake PATH 生成 Makefile。其中, PATH 是 CMakeLists.txt 所在的目录。因为CMake往往会生成很多文件,让工程层级看起来不清晰了,所以有一个比较好的方法是再建立一个build文件夹。
  3.     使用 make 命令进行编译。


代码为:

mkdir build
cd build
cmake ..
make


–CMake编译实例

编译单文件
单文件的CMakeLists.txt非常简单。

# CMake 最低版本号要求
cmake_minimum_required (VERSION 3.12)

# 项目名
project (main)

# 添加C++标准
set(CMAKE_CXX_STANDARD 11)

# 指定生成目标
add_executable(main main.c)


然后按照简介中的1,2,3运行即可。需要注意的是:这是Linux下的运行方式,在Windows中,如果直接cmake .的话不会生成Makefile,需要使用cmake . -G "Unix Makefiles"替代。

编译多文件
假设我们有三个文件:main.c, fun.c, fun.h。我们要只要将新的fun.c和fun.h写入CMakeLists里即可。

# CMake 最低版本号要求
cmake_minimum_required (VERSION 3.12)

# 项目名
project (main)

# 添加C++标准
set(CMAKE_CXX_STANDARD 11)

# 包含文件目录
include_directories(./)

# 指定生成目标,将fun.c也加入进去
add_executable(main main.c fun.c)


当文件比较多的时候,我们可以使用aux_source_directory命令,该命令会查找指定目录下的所有源文件,然后将结果存进指定变量名。使用语法为:aux_source_directory(<dir> <variable>)
例如上述例子可以改为:

# CMake 最低版本号要求
cmake_minimum_required (VERSION 3.12)

# 项目名
project (main)

# 添加C++标准
set(CMAKE_CXX_STANDARD 11)

# 包含文件目录
include_directories(./)

# 查找源文件,存入src变量
aux_source_directory(. src)

# 指定生成目标
add_executable(main ${src})




PS:我尝试的时候遇到了previous definition here错误。这里也记录一下。

    第一种是在include <xxx.h> 时写成了 include <xxx.c>。
    一个函数多次重定义,应添加以下宏定义。

#ifndef TEST_H
#define TEST_H
// code here
#endif



    源码直接写在头文件.h里没有对应的.cpp文件
    需要每个函数前添加 inline 声明

 inline void assign(sqlite3_stmt* stmt, int& item, size_t I){
        item = sqlite3_column_int(stmt, I);
 }

编译多目录多文件
假设我们把fun.c放入文件夹src中,fun.h放入文件夹include中,需要怎么修改CMakeLists呢。其实也很简单,只要活用命令include_directories和aux_source_directory就可以了。例如我们可以将CMakeLists改为:

# CMake 最低版本号要求
cmake_minimum_required (VERSION 3.12)

# 项目名
project (main)

# 添加C++标准
set(CMAKE_CXX_STANDARD 11)

# 包含文件目录
include_directories(./include)

# 查找源文件
aux_source_directory(. src1)
aux_source_directory(./src src2)

# 指定生成目标
add_executable(main ${src1} ${src2})



创建使用链接库

假设我们需要将fun.c构建成一个静态库或者动态库。我们需要将fun.c和fun.h放在一个目录下,并构建另一个CMakeLists。即:

./
  |
  +---CMakeLists.txt
  |
  +---main.c
  |
  +---src
      +---CMakeLists.txt  #生成链接库
      |
      +---fun.c
      |
      +---fun,h


CMake中生成链接库的方法是很相似的。如果我们想要生成一个静态库libfun.a或者动态库libfun.so。则CMakeLists可以写成:

# 添加头文件搜索路径
include_directories(.)

# 定义源文件列表
aux_source_directory(. src)

# 指定生成目标 目标名字随便写
add_library(fun ${src}) #静态库
add_library(fun SHARED ${src}) #动态库

add_executable()用来构建可执行程序,这里用add_library()来构建静态库或者动态库。
生成完的libfun.a/libfun.so在src下。如果我们要调用这个库,那么需要更改CMakeLists为:

# CMake 最低版本号要求
cmake_minimum_required (VERSION 3.12)

# 项目名
project (main)

# 添加C++标准
set(CMAKE_CXX_STANDARD 11)

# 包含文件目录, fun.h已经被放入src下了
include_directories(./src)

# 添加引用的外部库的搜索路径
link_directories(./src)

# 指定生成目标
add_executable(main main.c)
# 将静态库链接到应用程序
target_link_libraries(main libfun.a) #这个命令只链接静态库
target_link_libraries(main fun) #这个命令自动搜索动态


————————————————
版权声明:本文为CSDN博主「amateur_」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/amateur_/article/details/113780562

原文地址:https://www.cnblogs.com/yipianchuyun/p/15441417.html