Makefile及cmake学习

Makefile及cmake学习

  • 1. g++,gcc以及cpp的区别
  • 2. Makefile
    • 2.1 介绍一个例子
    • 2.2 避免头文件重复包含的方法
      • 2.2.1 宏定义
      • 2.2.2 #pargma once
    • 2.3 使用Makefile编译文件
      • 2.3.1 手动编译
      • 2.3.2 Makefile编译-版本1
      • 2.3.3 Makefile编译-版本2
      • 2.3.4 Makefile编译-版本3
      • 2.3.5 Makefile编译-版本4
    • 2.4 使用CMake编译

1. g++,gcc以及cpp的区别

有文章将这几个之间的关系理清了,这里直接参考:原文链接。

2. Makefile

下面来学习一下常见的Makefile文件的编写。参考来源【b站于仕琪】

2.1 介绍一个例子

下面的例子是用来介绍基本的Makefile文件的编写:

hello/
├──main.cpp
├──factorial.cpp
├──printhello.cpp
└──functions.h

main.cpp的定义如下:

#include 
#include "functions.h"
using namespace std;

int main () {
    printhello();
    cout << "This is main:" << endl;
    cout << "The result of factorial(5) is :" << factorial(5) << endl;
    return 0;
}

factorial.cpp定义:

#include "functions.h"

int factorial(int n) {
    if (n == 1) return 1;
    else return n * factorial(n - 1);
}

printhello.cpp定义:

#include 
#include "functions.h"
using namespace std;

void printhello() {
	int i;
    cout << "Hello World!" << endl;
}

functions.h定义:

#ifndef _FUNCTIONS_H_
#define _FUNCTIONS_H_
void printhello();
int factorial(int n);
#endif

上面的代码中,使用了宏定义防止头文件重复包含,以前没有系统总结过,下面总结一下。

2.2 避免头文件重复包含的方法

2.2.1 宏定义

上面的例子中,我们使用了宏定义方法防止头文件重复包含,具体而言:
在main.cpp中我们首先定义了#define _FUNCTIONS_H_,然后在functions.h中有如下语句:

#ifndef _FUNCTIONS_H_
#define _FUNCTIONS_H_
...
#endif

这是宏定义避免头文件的模板,其中ifndef的意思是:

如果这个宏定义了,就不会执行下面的语句

这样的话,如果因为某种原因,第二次调用了头文件,那么由于第一次已经定义了这个宏,第二次判断这个宏已经存在了,所以就不会执行下面的语句了,也就避免了头文件的重复包含。

2.2.2 #pargma once

宏定于有时候可能会出现变量名冲突,另一种方案就是在头文件中使用#pargma once

//.h文件
#pargma once
...

这个方法更简洁,但是老的编译器可能不支持。

2.3 使用Makefile编译文件

2.3.1 手动编译

对于上述三个文件,我们也可以不使用Makefile进行编译,编译的方法如下:

g++ main.cpp factorial.cpp printhello.cpp -o main

结果:
在这里插入图片描述
使用手动的方法是完全可以的,但是当项目的体量很大,文件很多的时候,每次改动一个文件,就需要对所有的文件编译,非常耗时。手动编译的方法还有一种:先只编译不链接,然后再将所有的编译文件链接到一起:

g++ main.cpp -c
g++ factorial.cpp -c
g++ printhello.cpp -c

使用了上述的语句,就会生成.o的文件:
Makefile及cmake学习_第1张图片
然后再链接到一起:

g++ *.o -o main

这个语句就是把所有的.o文件链接到一起,生成编译文件main。以上就是两种手动编译的方法,虽然有缺点,但是使用Makefile进行编译本质上就是把上面的语句写成了脚本而已,所以上面的两种方法就是基础。

2.3.2 Makefile编译-版本1

首先需要创建一个Makefile文件,使用下面的指令

touch Makefile

创建的文件名字最好就是Makefile,这样就可以直接使用make指令,如果是其他的名字,使用make的指令的时候还要指定文件名。下面就是使用Makefile的第一个版本:

main: main.cpp factorial.cpp printhello.cpp
	g++ -o main main.cpp printhello.cpp factorial.cpp

有几个需要注意的地方:

  • 第一句就表示main依赖于这些源文件,main就是目标
  • 第二句开头一定有一个TAB符号,第二行就是链接,来生成这个目标

写好了Makefile之后,就可以编译了。

make
// 如果文件的名字不是Makefile,就需要指定文件的名字
make -f 文件名

到这里看来,版本一的Makefile除了一开始就把几个文件的名字写好了,与手动的并没有什么区别。
其实不然,还是有一点区别的。我们使用Makefile进行编译,如果目标生成的时间比依赖的源文件生成的时间都新的话(就是说文件没有更改过),此时我们make的话,就会提示我们,这是最新的目标,就不会重复编译了。但是这个版本并没有很只能,如果文件太多了,更改一点点,每次都需要重新编译所有,很费时间

2.3.3 Makefile编译-版本2

CXX = g++
TARGET = main
OBJ = main.o factorial.o printhello.o 

$(TARGET): $(OBJ)
	$(CXX) -o $(TARGET) $(OBJ)

main.o: main.cpp
	$(CXX) -c main.cpp

factorial.o: factorial.cpp
	$(CXX) -c factorial.cpp

printhello.o: printhello.cpp
	$(CXX) -c printhello.cpp

这个其实看着复杂,整体的思路就是使用手动编译的第二个思路,先编译不链接,生成.o文件,然后链接。
首先CXX就是编译的方法,这里使用的g++,OBJ就是依赖,跟版本1不同的是,这里的依赖使用的是.o文件,而不是.cpp文件。
紧接着:

$(TARGET): $(OBJ)
	$(CXX) -o $(TARGET) $(OBJ)

这一句其实拆解开来,就是版本1的小小变动的版本了,这里的依赖就变成了.o文件。所以我们接下来就要得到.o文件,这里对应的就是手动编译的第二种方法了。
使用版本二可以继承版本一的优点,同时如果只有1个文件修改了,我们不用再编译所有的文件了,只需要编译修改的文件,大大提高了编译的效率。但面临很多文件的时候,还是需要写很多语句,非常麻烦,下面的版本三就用来解决这个问题。

2.3.4 Makefile编译-版本3

CXX = g++
TARGET = main
OBJ = main.o factorial.o printhello.o 

CXXFLAGS = -c -Wall

$(TARGET): $(OBJ)
	$(CXX) -o $@ $^

%.o: %.cpp
	$(CXX) $(CXXFLAGS) $< -o $@
	
.PHONY: clean
clean:
	rm -f *.o $(TARGET)

版本三前面和版本二是一样的,我们定义了一个变量CXXFLAGS表示g++之后的选项,这里我们增加了一个-Wall,表示在编译的时候把所有的警告也写出来。编译的代码:

$(TARGET): $(OBJ)
	$(CXX) -o $@ $^

的第一行其实就是main: main.o factorial.o printhello.o,第二行出现了$@以及$^,在Makefile文件中,$@就代表目标,也就是分号之前的内容,$^代表依赖列表中所有,具体而言,这里就是三个.o文件,下面还将介绍一个$<,这个表示依赖的第一个。很明显,如果依赖只有一个的话,二者是等价的。
所以上面的代码翻译古来就是下面的意思:

main: main.o factorial.o printhello.o
	g++ -o main.o factorial.o printhello.o

其实就非常好理解了。
下面解释:

%.o: %.cpp
	$(CXX) $(CXXFLAGS) $< -o $@

这个就是将每个cpp文件作为依赖,生成对应名称的.o文件,使用%来替代文件名。下面的$<在上面已经解释了,其实就是%.cpp。所以这句话翻译出来就是:g++ -c -Wall main.cpp -o main.o,此时就可以生成.o文件了。但是根据之前的经验,生成.o文件只需要gcc -c main.cpp即可,后面的要不要都无所谓,所以上面的内容可以简化为:

%.o: %.cpp
	$(CXX) $(CXXFLAGS) $<

最后介绍:

.PHONY: clean
clean:
	rm -f *.o $(TARGET)

我们先不关注第一句话,直接看第二句,第二句的目标是clean,没有依赖,执行的任务是删除全部的.o文件以及之前生成的目标。但是如果文件中正好有clean这个文件,会出现什么问题呢?我们在之前讲过,如果目标文件比依赖都晚的话,就不会再编译目标文件了,所以就会提示:clean已经是最新,这里就达不到我们的要求了。针对这个问题,我们使用了.PHONY: clean,其中.PHONY是伪目标,这个目标永远不会存在,即使你这一次编译了,下一次他还是不存在。我们这里可以这么理解:.PHONY将clean标记为伪目标,此时make clean的时候就不管有没有文件存在了,每一次都会执行。如果我们要使用make对某个目标编译的话,可以直接make 目标

下面一些运行的结果:
Makefile及cmake学习_第2张图片
使用了-Wall将这个警告揪出来了。

Makefile及cmake学习_第3张图片
目录里面存在clean文件,我们此时使用make clean:
Makefile及cmake学习_第4张图片
可以看到,所有的.o文件以及main目标都被删除了。
到这里,我们已经完成的差不多了,编译也比较简单。但是再思考一下,如果我们换一个工程,岂不是还需要依赖,还是有一点麻烦的,我们可以再改进一下。

2.3.5 Makefile编译-版本4

CXX = g++
TARGET = main
SRC = $(wildcard *.cpp)
OBJ = $(patsubst %.cpp, %.o, $(SRC))

CXXFLAGS = -c -Wall

$(TARGET): $(OBJ)
	$(CXX) -o $@ $^

%.o: %.cpp
	$(CXX) $(CXXFLAGS) $^

.PHONY: clean
clean:
	rm -f *.o $(TARGET)

版本四就是我们的终极版本,SRC就是当前路径下所有的cpp文件名,OBJ就是将cpp文件名改为.o文件,后面的就和版本三是一样的了。有个这个版本,即使我们的代码换了个工程,照样也是可以编译的。
以上就是使用Makefile文件对cpp工程进行编译的方法,如果要对c工程进行编译,上面的g++换成gcc即可。

2.4 使用CMake编译

你可能感兴趣的:(c++)