Liunx系统下的C/C++程序编译
文章目录
前言
本文将主要介绍Liunx系统下的C/C++软件的编译,从gcc、g++编译小程序,到使用make工具,再到利用cmake工具生成makefile文件,为什么我们需要使用这些工具,这些工具能够给我们带来哪些效益,本文将思考这些问题,并记录下一些好的学习资料。本文将持续更新,若有错误,欢迎各位读者批评指正;本文多数内容来源网络,若有侵权或未注明出处的地方,提出必删。一、GCC(GNU Compiler Collection)
GCC(GNU Compiler Collection,GNU编译器套件)是由GNU开发的编程语言译器。GNU编译器套件包括C、C++、Objective-C、 Fortran、Java、Ada和Go语言前端,也包括了这些语言的库(如libstdc++,libgcj等)
1.1 基本约定
文件后缀 | 默认规则 |
---|---|
.h | 程序所包含的头文件 |
.c | C语言源代码文件 |
.C、.cc、cxx、cpp、c++ | C++源代码文件 |
.i | C源代码文件且不应该对其执行预处理 |
.ii | C++源代码文件且不应该对其执行预处理 |
.s | 汇编语言源代码文件 |
.S | 支持预处理的汇编语言源代码1 |
.o | 编译后的目标文件 |
1.2 常用参数
参数 | 作用 | 示例 |
---|---|---|
无参数 | 将hello.c预处理、汇编、编译并链接形成可执行文件,默认生成a.out可执行文件 | gcc hello.c |
-E | 预处理:展开宏、头文件,替换编译条件,删除注释,空行,空白 | gcc -E hello.c -o hello.i |
-S | 编译:生成汇编文件,耗时最长,生成 .s 汇编代码文件 | gcc -S hello.i / gcc -S hello.c |
-c | 汇编:只编译,不链接成为可执行文件,生成 .o 为后缀的目标文件 | gcc -c hello.s / gcc -c hello.c |
-I | 大写的i,指定头文件路径 | gcc -I include hello.c |
-l | 小写的L,手动添加链接库 | gcc hello.c -o main.out -lm |
-L | 指定链接库路径 | gcc hello.c -o main.out -L ./ -lm |
-g | 产生符号调试工具(GNU的gdb)所必要的符号资讯,要想对源代码进行调试,我们就必须加入这个选项 | gcc hello.c -o hello -g |
-On | 优化,进行优化需要跟上优化的数值,比如O1,O2,O3,默认是2级,范围是0-3,n的范围越大优化越好 | gcc hello.c -o hello -O3 |
-D | 在编译的时候可以指定宏执行,动态注册宏 | gcc -DHELLO hello.c -o hello |
-Wall | Wall,可以提示更多的警告 | gcc hello.c -o hello -Wall |
注意:
- GCC在链接多个库时,如果库本身存在引用,例如 A.a引用了B.a的函数,则在GCC参数上 -lA要放在-lB的前面,否则可能报“对xx未定义的引用”。
- 库的地址为多个时,可以使用多个-L选项,或者在一个-L选项内使用冒号分割的路径列表。
1.3 C/C++程序的编译实例
C/C++程序编译流程2
预处理–>编译–>汇编–>链接
- 预处理:读取源程序,对其中的伪指令(以# 开头的指令)和特殊符号进行处理。
- 编译 :编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。
- 汇编:汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程。
- 链接:链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。
1.3.1 单文件工程
// hello.c
#include <stdio.h>
int main(){
printf("Hello World!\n");
}
#按步骤编译
gcc -E hello.c -o hello.i #将hello.c预处理输出hello.i文件
gcc -S hello.i #编译
gcc -c hello.s #汇编
gcc hello.o -o hello #链接
#快捷编译
gcc hello.c -o hello
1.3.2 多文件工程
工程目录结构
- hello(文件夹)
- include(文件夹)
- hello.h
- hello.c
- main.c
- include(文件夹)
// hello.h
#ifndef __HELLO_H
#define __HELLO_H
void printHelloWorld();
#endif
// hello.c
#include <stdio.h>
#include "hello.h"
void printHelloWorld(){
printf("Hello World!\n");
}
// main.c
#include "hello.h"
int main(){
printHelloWorld();
return 0;
}
#快捷编译
gcc -I include hello.c main.c -o hello #-I directory 指定 include 包含文件的搜索目录
1.4 比较:gcc与g++
gcc和g++并不是编译器,也不是编译器的集合,它们只是一种驱动器,根据参数中要编译的文件的类型,调用对应的GUN编译器而已
- 对于 .c 文件,gcc 按照 C 规范进行编译,g++ 按照 C++ 规范进行编译;对于 .cpp 文件,gcc和 g++ 统一按照 C++ 规范进行编译(c和cpp的语法强度是不一样的)
- 使用 g++ 编译文件时,g++ 会自动链接标准库STL,而 gcc 不会自动链接STL( gcc -lstdc++)
- gcc在编译C文件时,可使用的预定义宏是比较少的
- gcc / g++ 在编译 .c 文件和 .cpp 文件时(这时候 gcc 和 g++ 调用的都是 cpp 文件的编译器),会加入一些额外的宏,这些宏如下:
#define __GXX_WEAK__ 1
#define __cplusplus 1
#define __DEPRECATED 1
#define __GNUG__ 4
#define __EXCEPTIONS 1
#define __private_extern__ extern
1.4.1 gcc/g++ 与 extern “C”
使用extern "C"
// hello.h
#include "./include/hello.h"
#include <stdio.h>
extern "C"{
void printHelloWorld(){
printf("Hello World!\n");
}
}
// hello.c
#include "./include/hello.h"
#include <stdio.h>
extern "C"{
void printHelloWorld(){
printf("Hello World!\n");
}
}
.c文件,使用gcc生成汇编代码失败,使用g++命令生成成功
C语言中不支持extern "C"声明,所以会出错,在C语言中也没必要使用
[xswl hello]$ gcc -S hello.c
In file included from hello.c:1:0:
./include/hello.h:3:8: 错误:expected identifier or ‘(’ before string constant
extern "C"
^~~
hello.c:3:8: 错误:expected identifier or ‘(’ before string constant
extern "C"
^~~
[xswl hello]$
[xswl hello]$ g++ -S hello.c
[xswl hello]$
生成了hello.s文件,汇编程序中的函数名采用的为C风格的命名方式
.file "hello.c"
.section .rodata
.LC0:
.string "Hello World!"
.text
.globl printHelloWorld
.type printHelloWorld, @function
printHelloWorld:
......
去除extern "C"
// hello.h
#include "./include/hello.h"
#include <stdio.h>
void printHelloWorld(){
printf("Hello World!\n");
}
// hello.c
#include "./include/hello.h"
#include <stdio.h>
void printHelloWorld(){
printf("Hello World!\n");
}
使用 gcc 命令生成汇编程序代码
[xswl hello]$
[xswl hello]$ gcc -S hello.c
.file "hello.c"
.section .rodata
.LC0:
.string "Hello World!"
.text
.globl printHelloWorld
.type printHelloWorld, @function
printHelloWorld:
......
使用 g++ 命令生成汇编程序代码
[xswl hello]$
[xswl hello]$ g++ -S hello.c
.file "hello.c"
.section .rodata
.LC0:
.string "Hello World!"
.text
.globl _Z15printHelloWorldv
.type _Z15printHelloWorldv, @function
_Z15printHelloWorldv:
......
此时生成的汇编文件中printHelloWorld函数名发生了改变,采用了C++规范,因为C++支持函数重载,可以根据函数的入参决定调用的具体函数。
总结
.c 文件:gcc 按照 c 规范,g++ 按照 c++ 规范
.cpp、.cc 等文件:gcc / g++ 均按照 c++ 规范
二、GNU Make介绍
2.1 GNU Make是什么?
make是一条计算机指令,是在安装有GNU Make的计算机上的可执行指令。该指令是读入一个名为makefile的文件,然后执行这个文件中指定的指令。百度百科
make带来直接好处就是——“自动化编译”。一旦写好,只需要一个make命令,整个工程完全自动编译,所以十分方便。而Makefile文件就是告诉make命令怎么样地去编译和链接程序。
功能
- 用户傻瓜式使用:用户执行make指令,便可以根据makefile内建立的规则进行构建和安装你的程序,无需知道具体的编译规则和依赖关系。
- 简化开发人员编译操作:自动完成编译工作。这些工作包括:如果仅修改了某几个源文件,则只重新编译这几个源文件;如果某个头文件被修改了,则重新编译所有包含该头文件的源文件。它也会自动决定文件更新的适当顺序,以避免要更新的文件依赖于另一个同样需要更新的文件。利用这种自动编译可大大简化开发工作,避免不必要的重新编译。
- GNU Make能实现的功能不仅于此,你可以用make来控制安装和卸载软件包,或者用来生成标签表,以及其他的任何你想要做的,当然前提是你写好怎么做。
2.2 为什么需要make工具?
对于简单的工程文件通过gcc/g++命令也可以很方便的构建我们的程序,但随着工程的不断壮大,使用gcc/g++命令进行编译就比较费事,并且若我们修改了某一个文件,采用上述的方法需要将整个工程全部编译链接一遍,这样太耗时,我们希望只重新编译修改过的代码。当然,我们可以自己写 shell 脚本实现这种功能,但目前我们可以使用现成的工具make!
1、包含多个源文件的项目在编译时有长而复杂的命令行,可以通过makefile保存这些命令行来简化该工作
2、make可以减少重新编译所需要的时间,因为make可以识别出哪些文件是新修改的
3、Make维护了当前项目中各文件的相关关系,从而可以在编译前检查是否可以找到所有的文件3
2.3 make的使用
2.3.1 使用示例
工程目录结构
- hello(文件夹)
- include(文件夹)
- hello.h
- hello.c
- main.c
- makefile
- include(文件夹)
// hello.h
#ifndef __HELLO_H
#define __HELLO_H
void printHelloWorld();
#endif
// hello.c
#include <stdio.h>
#include "hello.h"
void printHelloWorld(){
printf("Hello World!\n");
}
// main.c
#include "hello.h"
int main(){
printHelloWorld();
return 0;
}
命令行执行
#使用 gcc
[xswl hello]$ gcc -I include hello.c main.c -o hello #-I directory 指定 include 包含文件的搜索目录
makefile文件
hello:
gcc -I ./include main.c hello.c -o hello
clean:
rm -f hello *.o
命令行执行
#使用 make
[xswl hello]$ make
2.3.1 makefile文件编写规范
编写makefile时有一定的规则:
hello :
gcc -I ./include main.c hello.c -o hello
目标(target): 需要的条件(dependencies) (注意冒号两边有空格)
命令(system command) (注意前面用tab键开头)
解释一下:
- 目标可以是一个或多个,可以是Object File,也可以是执行文件,甚至可以是一个标签。
- 需要的条件就是生成目标所需要的文件或目标
- 命令就是生成目标所需要执行的脚本
hello : hello.o main.o
@echo "linking hello dependences hello.o main.o"
gcc hello.o main.o -o hello
hello.o : hello.c
@echo "compiling hello.c"
gcc -I ./include -c hello.c
main.o : main.c
@echo "compiling main.c"
gcc -I ./include -c main.c
小知识:Makefile里的echo和rm前面带了@表示不要打印执行该命令时候命令本身的输出,比如 echo “compiling hello.c” 在执行的时候会输出这句命令 echo “compiling hello.c”,如果把echo改为@echo, 再make的时候就不会输出 echo “compiling hello.c” 命令本身了。
编写一个可以实现自动新增新的源程序文件,自动实现变化增量编译的makefile文件还是比较费事的,不过,不过这种费事又重复的工作程序员们自然会开发出另外的工具来实现makefile文件的自动生成,并且可以,这个强大的工具便是CMake!
CC = gcc
TOPDIR := $(shell pwd)
INCDIR := $(TOPDIR)/include
SRCDIR := $(TOPDIR)/
CPPFLAGS := -I $(INCDIR)
CFLAGS := -fno-builtin -Wall -O2
export CC TOPDIR SRCDIR CPPFLAGS CFLAGS
TARGET = main
SRCS := $(wildcard *.c)
SRCS += $(foreach dir,$(SRCDIR),$(wildcard $(dir)/*.c))
OBJS := $(patsubst %.c,%.o, $(SRCS))
OBJS_D := $(patsubst %.c,%.d,$(SRCS))
#all:$(TARGET) debug
$(TARGET):$(OBJS)
$(CC) $(OBJS) -o $(TARGET)
$(OBJS):%.o:%.c
$(CC) -c $(CPPFLAGS) -o $@ $(CFLAGS) $^
.PHONY :debug
debug:
@echo "srcs = $(SRCS),objs = $(OBJS)."
.PHONY : clean
clean:
rm -rf add.o sub.o main.o main.d
make clean -C src
三、CMake
接下来,让我们一起来学习下一个更牛逼的工具 CMake,利用这个工具可以让我们更简单的去构建、测试和打包我们的C/C++工程,CMake官网更是打出了标语:Build with CMake. Build with Confidence.
3.1 CMake简介
3.1.1 为什么选择使用CMake
CMake是快捷高效的
- CMake使开发人员可以将更多时间花在编写代码上,而将更少的时间花在构建系统上
- CMake是开源的,可免费用于任何项目
CMake功能强大。
- CMake支持同一项目上的多个开发环境和编译器(例如,Visual Studio IDE,QtCreator,JetBrains,vim,emacs,gcc,MSVC,clang,Intel)
- CMake支持多种语言,包括C / C ++ / CUDA / Fortran / Python,并且还支持运行任意自定义命令作为构建的一部分
- CMake通过CTest与Jenkins,Travis,CircleCI,GitlabCI以及几乎所有CI系统一起支持连续集成(CI)测试。使用CDash(www.cdash.org)显示测试结果。
- CMake支持将第三方库集成到您的项目中。
3.2 CMake的使用
非常好的学习教程:
CMake菜谱(CMake Cookbook中文版)
编写CMakeLists.txt文档
# set minimum cmake version
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
# project name and language
project(recipe-01 LANGUAGES C)
add_executable(hello hello.c main.c)
工程目录结构
- hello(文件夹)
- hello.c
- CMakeLists.txt
构建项目
$ mkdir -p build
$ cd build
$ cmake ..
$ cmake --build . #编译项目
标准构建方式-跨平台
$ cmake -H. -Bbuild
cmake --build ./build #编译项目
该命令是跨平台的,-H 表示当前目录中搜索根 CMakeLists.txt 文件。-Bbuild 告诉 CMake在一个名为 build 的目录中生成所有的文件。
NOTE:
- cmake -H. -Bbuild 也属于CMake标准使用方式
- 默认情况下,在GNU/Linux和macOS系统上,CMake使用Unix Makefile生成器。Windows上,Visual Studio是默认的生成器。
一个常用的 CMakeLists.txt 大全
# set minimum cmake version
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
# project name and language
project(recipe-01 LANGUAGES C)
# generate a library from sources STATIC/SHARED/OBJECT/MODULE
add_library(message STATIC Message.hpp Message.cpp)
add_executable(hello-world hello-world.cpp)
target_link_libraries(hello-world message)
# 以下同时生成静态库和动态库
add_library(message-objs
OBJECT
Message.hpp
Message.cpp
)
# this is only needed for older compilers
# but doesn't hurt either to have it
set_target_properties(message-objs
PROPERTIES
POSITION_INDEPENDENT_CODE 1
)
add_library(message-shared
SHARED
$<TARGET_OBJECTS:message-objs>
)
add_library(message-static
STATIC
$<TARGET_OBJECTS:message-objs>
)
总结
本文主要是自我学习的记录总结。