从编译链接到cmake

.c(.cpp)文件到可执行文件

对于一份简单的.c/.cpp为后缀的源文件,他所使用的语言是人类可以阅读并看懂的,但是对于计算机来说,其可理解并执行的是二进制的机器码。

也就是说,计算机所能运行的是二进制的机器码,而早期为了方便人类阅读,使用一些简单的助记符来代替机器码,比如MOV,LOOP...等,而为了进一步增加编程的可读性,便诞生了c语言以及后续的一众高级语言,如c++,python等。

c 复制代码
#include <stdio.h>
int main()
{
    printf("Hello, world!\n");
    return 0;
} // 人类语言,计算机仅可执行01010101....重复的机器码

因此要想让我们写出的c或者cpp代码变为计算机可以执行的二进制机器码,就需要使用一些程序来通过一些手段将源代码编译成可执行的二进制机器码。

一般来说,编译器的工作流程如下:

  • 预处理: 源代码中的宏进行一切的宏展开,宏替换,以及条件编译等操作,此操作后文件中无任何宏定义。(.i文件)
  • 编译:将预处理后的文件通过编译器的词法分析,语法分析,语义分析,优化,代码生成等操作,生成对应的汇编语言文件(.s文件)
  • 汇编:将汇编语言文件通过汇编器的汇编操作,生成对应的机器码文件(.o文件)
  • 链接:将多个.o文件进行链接,生成最终的可执行文件(.exe文件/linux下无后缀)

因此,编译器所做的工作就是通过自己的一套规则将源代码转换为机器码。规则的制定一般是有对应的语言标准来规定,而编译器可以通过不同的实现来实现该标准即可。编译器更像是一个翻译软件,而它的工作就是根据人类的翻译需求来将一种语言翻译成另一种语言。常见的编译器有gcc(开源),clang(jetbrain系列),msvc(微软)等,不同的编译器支持的最新标准也不同。

至于编译器是怎么来的,可以上网搜一搜。

本次培训使用的编译器是gcc,可以使用gcc --version命令查看当前gcc版本。

若没有安装gcc,可以使用sudo apt-get install build-essential命令安装gcc。

下面我们以gcc为例,来看一下如何将.c/.cpp文件一步一步编译成可执行文件。

c 复制代码
#include <stdio.h>
#define first 1
int main()
{
    printf("Hello, world!\n");
    int num = first;
    printf("%d", num);
    return 0;
} // 假设hello.c

预处理

在终端中输入gcc -E hello.c -o hello.i指令,-E选项表示只进行预处理,-o选项指定输出文件名为hello.i

然后打开hello.i文件,可以看到经过预处理后的源代码如下:

c 复制代码
# 0 "hello.c"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
.....//此处省略一堆东西
int main()
{
    printf("Hello, world!\n");
    int num = 1;
    printf("%d", num);
    return 0;
}

如果回到.c文件中,我们查看stdio这个头文件,可以发现其中的内容和hello.i文件前面的内容是一样的,无非是头文件的宏直接变成了头文件所在的路径。而且我们定义的宏first也被替换成了1

编译

在终端中输入gcc -S hello.i指令,-S选项表示将文件进行编译,输出文件为翻译为汇编的.s文件。

然后打开hello.s文件,可以看到经过编译后的源代码如下:

asm 复制代码
	.file	"hello.c"
	.text
	.section	.rodata
.LC0:
	.string	"hello world!"
.LC1:
	.string	"%d"
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc
	endbr64
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	subq	$16, %rsp
	leaq	.LC0(%rip), %rax
	movq	%rax, %rdi
	movl	$0, %eax
	call	printf@PLT
	movl	$1, -4(%rbp)
    ........;省略一堆

可以看出,c语言通过编译器变为了可读性更差的汇编语言,但是还有一些东西是人可以看懂的,比如movcall等指令。

汇编

在终端中输入gcc -c hello.s 指令,-c选项表示将文件进行汇编,输出文件为二进制文件hello.o

可以看到确实变成二进制文件了,而且由于没有对应的解码器,无法直接看到其中内容。

链接

二进制文件链接

所谓的链接,就是将多个.o文件进行链接,生成最终的可执行文件。对于链接,针对的是多文件项目的编译,因为一个可执行文件中可能包含多个源文件,而这些源文件可能包含不同的函数,因此需要将这些源文件中的函数进行链接,生成最终的可执行文件。

对于上述的hello.o文件,由于是单文件,因此可以简单的使用
gcc hello.o -o hello生成可执行文件hello然后运行

shell 复制代码
./hello
Hello, world!

对于多文件来说并非如此,比如我们有两个个文件:

c 复制代码
int sub(int a, int b)
{
    return a - b;
} // sub.c
c 复制代码
#include <stdio.h>
int sub(int a, int b); // 声明sub函数
int main()
{
    int a = 10, b = 5;
    int result = sub(a, b);
    printf("%d", result);
} // main.c

如果直接使用gcc -c main.c生成可执行文件,回报出如下错误:

shell 复制代码
main.c:(.text+0x25): undefined reference to `sub'

即找不到sub函数的定义,这是显然的,sub函数的实现在sub.c中,main.c中我们只是告诉了编译器有一个函数sub,这个行为在预处理->编译->汇编的过程中不会报出任何错误,但是在链接过程中,编译器无法在参与链接的文件中找到sub的实现,因此报出了链接错误。

因此要想不报错,可以这样:
gcc main.c sub.c -o main,这样编译器会将两个文件编译为二进制文件,并将sub.o参与到链接过程中,生成最终的可执行文件main。要注意,链接的是二进制文件,而不是源文件。

对于使用多个二进制文件参与链接生成可执行文件的方法,其好处就是,将项目拆分为多个模块,当要对项目的某一部分进行修改时,只需要重新编译该模块为新的.o文件即可,而不需要重新编译整个项目。

库文件链接

库文件是指一些预先编译好的二进制文件,可以被多个程序共用。分为动态库(linux下的.so文件/windows下的.dll文件)和静态库(linux下的.a文件/windows下的.lib文件)之分。它们的区别就是静态库是连接时全部链接进可执行文件,而动态库是运行时才链接进可执行文件。这里就不演示使用gcc生成和链接库文件的过程了,可以参考相关资料。

至此,我们就走遍了如何从源代码到可执行文件的整个流程。

cmake

可以看到,对于比较大的项目,使用gcc的命令来进行编译会很繁琐和麻烦,就像由机器码到汇编再到c语言一样,为了简化编译流程,便有了从gcc -> makefile -> CMakeLists这样的变化。

简单介绍makefile

Makefile 是用于管理项目构建过程的工具,广泛用于 C/C++ 等语言的编译。它通过定义规则和指令,自动化编译、链接等步骤,大大简化了开发者的工作。也就是说,makefile通过一些规则来告诉编译器如何编译源代码,如何链接库文件,如何生成可执行文件。通过编写规则即可完成编译,而不需要手动指定每个编译选项。(我也不会makefile,这里简单介绍一下)当编写完makefile后,只需要运行make命令,makefile就会自动按照规则编译生成对应的可执行文件。注意make命令后接的是makefile的路径。

cmake与CMakeLists.txt

对于一些复杂的项目,makefile的规则编写起来会比较麻烦,因此出现了cmake。cmake是一种跨平台的编译工具,可以用来管理项目的构建,通过编写cmake,可以自动生成对应的makefile,然后运行make命令编译生成可执行文件。

cmake的配置文件是CMakeLists.txt,它是cmake的核心配置文件,主要用于定义项目的源文件、头文件、库文件等信息,以及编译选项等。

最简单的CMakeLists.txt如下:

cmake 复制代码
cmake_minimum_required(VERSION 3.6)
# 指定项目所需要的cmake最小版本 
project(hello)
# 指定项目名称
add_executable(hello hello.cpp)
# 添加可执行文件hello,并指定源文件为hello.cpp

结束后,在终端中运行cmake.命令,cmake会自动生成对应的makefile,然后运行make命令编译生成可执行文件。但是在生成的时候会出现很多配置文件,因此习惯上建立一个名字为build的文件夹,然后在该文件夹下运行cmake..命令,生成的makefile就在build文件夹中,然后运行make命令编译生成可执行文件。

后面关于cmake的使用,转如下链接

cmake的简单使用

安装opencv库并使用

所谓的库,即为我们提供了头文件以及一些预编译好的二进制文件,我们可以通过安装库文件来使用一些功能。

编译安装opencv

这里我们采用下载源码编译安装的方式来安装opencv。所谓的安装,其实就是将别人写好的代码编译为库,然后将头文件和编译好的库文件拷贝到系统的指定目录,同时还附带着一些说明文件,方便编译时找到头文件和库文件。

源码下载链接[https://github.com/opencv/opencv/releases]

安装教程参考:

[https://blog.csdn.net/weixin_44796670/article/details/115900538]

使用opencv

参考cmake的简单使用后半部分

这里给出一个简单的demo测试是否安装成功:

cpp 复制代码
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace cv;

int main()
{
    Mat src = Mat::zeros(500, 500, CV_8UC3); // 创建一个500x500的空白图像
    circle(src, Point(250, 250), 50, Scalar(255, 255, 255), -1); // 在图像中画一个白色的圆
    imshow("src", src); // 显示图像
    waitKey(0); // 等待按键
}

运行结果应该为黑色图像上出现了一个白色的圆。