Linux Makefile入门总结

文章介绍了Makefile的基础知识,包括目标文件、依赖关系、变量、隐含规则、通配符以及如何处理子目录和链接第三方库的编译。通过示例详细讲解了Makefile的编写和使用,适合初学者和需要进阶的Linux程序员参考。
摘要由CSDN通过智能技术生成

Makefile,Linux程序员必须要会的一项技能;当然,并不是说要“知根知底”,懂得一些常用用法即可!


目录

一、Makefile简介

二、高难度点的Makfile用法

1. 准备以下项目文件

2. 显示规则

3. 变量

4. 隐含规则

5. 通配符

6. 更加高级的用法

7. 可以简写命令

三、Makefile编译子目录的Makefile(多路径编译)

1. circle中的Makefile

2. cube中的Makefile

3. src中的Makefile(也就是主Makefile)

四、Makefile链接第三方库编译

五、Makefile企业项目用法编译

六、总结


一、Makefile简介

Makefile的注释:

# 这是一个注释

使用 “ # ” 即可;

Makefile的简单用法:

目标文件 : 依赖文件
[TAB]命令
  • [TAB]:是你键盘左边tab键;
  • 目标文件:要生成的文件;
  • 依赖文件:生成目标文件要依赖的文件;
  • 命令:执行的命令,生成目标文件。

准备一份代码,例如:

main.c 

#include <stdio.h>

int main(int argc, char **argv) {
	printf("hello world\n");
	return 0;
}

我们正常在Linux中编译,一般都是这样去编译:

 gcc main.c -o main

也可以新建一个文件,命名为Makefile,并写如下命令:

main: main.o
	gcc main.o -o main

main.o: main.c
	gcc -c main.c -o main.o 
# -o mian.o 可以省略

-c 是生成 .o 文件的必要参数! 

写好后,直接make就可以编译了,且生成main可执行文件!

Makfile执行顺序:

首先是mian目标文件,要生成目标文件得依赖main.o文件;此时还没有main.o文件,Makefile会往下找,找到了main.o目标文件,其依赖main.c文件;main.c文件刚好有,然后就会调用下面那条命令( gcc -c main.c -o main.o )去生成main.o文件;有了main.o文件,也就可以生成main目标文件了;Makefile紧接着会执行命令( gcc main.o -o main )去生成main目标文件!

也就是说,是先执行gcc -c main.c -o main.o,然后在执行gcc main.o -o main


二、高难度点的Makfile用法

1. 准备以下项目文件

circle.h

#include <stdio.h>

void test2();

circle.cpp

#include "circle.h"

void test2() {
	printf("test2()\n");
}

cube.h

#include <iostream>

void test22();

cube.cpp

#include "cube.h"

void test22() {
	std::cout << "test22()" << std::endl;
}

main.cpp

#include <iostream>

#include "cube.h"
#include "circle.h"

int main(int argc, char **argv) {
	std::cout << "hello world" << std::endl;
	test2();
	test22();
	
	return 0;
}

他们都是在同一个路径下!

2. 显示规则

跟上面举的例子差不多,只是这个例子多了几个文件,所以要多些几条命令。

什么是显示规则,就是命令都得正确明了的写在Makefile上。

test2: main.o circle.o cube.o 
	g++ main.o circle.o cube.o -o test2
	
main.o: circle.h cube.h main.cpp
	g++ -c main.cpp -o main.o
	
circle.o: circle.h circle.cpp
	g++ -c circle.cpp -o circle.o
	
cube.o: cube.h cube.cpp
	g++ -c cube.cpp -o cube.o
	

	
.PHONY: clean
clean: 
	rm -fr main.o circle.o cube.o
cleanall:
	rm -fr main.o circle.o cube.o test2

test2是要生成的目标可执行文件,它依赖main.o、circle.o、cube.o;

所以main.o、circle.o、cube.o都得为其写上命令去生成,他们各自都有各自的依赖文件,依据依赖文件去生成;例如cube.o依赖cube.h和cube.cpp;

那么下面那些是什么?

伪目标

先不看 .PHONY: clean

先看 clean:

他也可以是一个目标,不过是伪目标;当触发clean后,会执行其下指定的命令:rm -fr main.o circle.o cube.o

一般常用来删除上面生成的.o文件和可执行文件!cleanall也是如此。

用法:

make clean 

make cleanall

 .PHONY: clean 是什么?

可以说是用来屏蔽同名clean的文件夹或文件;如果相同路径下有一个clean的文件夹或者文件,那么Makefile执行make clean会失败!

3. 变量

= :变量(可修改)

+= :追加

:= :常量(不可修改)

例如:

TARGET = test

CXX := g++

使用:

$(TARGET)        # 相当于C语言的宏定义

使用英文的美元$符号为开头,再用括号括起来即可!

那么现在可以使用变量对上面的Makefile做完善一下:

CXX := g++
TARGET = test2
OBJ = main.o cube.o circle.o

$(TARGET):  $(OBJ)
	$(CXX) $(OBJ) -o $(TARGET)
	
main.o: cube.h circle.h main.cpp
	$(CXX) -c main.cpp -o main.o
	
cube.o: cube.h cube.cpp
	$(CXX) -c cube.cpp -o cube.o
	
circle.o: circle.h circle.c
	$(CXX) -c circle.c -o circle.o
	
.PHONY: clean
clean: 
	rm -fr $(OBJ)

cleanall:
	rm -fr $(OBJ)  $(TARGET)

文件开头定义了三个变量,后续写的命令都是用这三个变量去替换掉了,执行make时,会自动识别替换回来,相当于C语言的宏替换!

4. 隐含规则

%c、%cpp、%o  : 任意的.c 或 任意的.cpp 或 任意的.o

*.c、*.cpp、*.o  :所有的.c 或 所有的.cpp 或 所有的.o

使用%c 或 %cpp 或 %o 或 %h 时,优先加点'.'使用,如:%.c、%.cpp、%.o、%.h;

如果使用某个编译时报错了,再把点'.'去掉即可!

知道了这些隐含规则,就可以使用他们再对Makefile做优化了:

CXX := g++
TARGET = test2
OBJ = main.o cube.o circle.o

$(TARGET):  $(OBJ)
	$(CXX) $(OBJ) -o $(TARGET)
	
%.o: %.h %cpp
	$(CXX) -c %.cpp -o %.o
	
.PHONY: clean
clean:
	rm -fr *.o
cleanall:
	rm -fr *.o $(TARGET)

5. 通配符

$^所有的依赖文件
$<所有依赖文件的第一个文件(应该也是最匹配的一个文件)
$@所有的目标文件

依赖文件:我要做这个操作,依赖哪些东西;

目标文件:我要做这个操作,要生成的东西;

使用通配符再对Makefile做最后的完善:

CXX := g++
TARGET = test2
OBJ = main.o cube.o circle.o
RMRF := rm -rf

$(TARGET):  $(OBJ)
	$(CXX) $^ -o $@
	
%.o: %.h %cpp
	$(CXX) -c $< -o $@
	
	
.PHONY: clean
clean:
	$(RMRF) *.o
cleanall:
	$(RMRF) *.o $(TARGET)

具体怎么使用,要放在哪个位置上呢?

如果还不懂,那就这样去想,一条命令,哪个位置是写依赖文件的,就放$^;哪个位置是写生成的目标文件的,就放$@;至于$<,可以-c后面去放!

6. 更加高级的用法

CXX := g++
TARGET = test2
#OBJ = main.o cube.o circle.o
RMRF := rm -rf
SRC = $(wildcard *.cpp)    # 获取当前路径下的所有.cpp文件            
OBJ = $(patsubst %.cpp, %.o, $(SRC))    # 把$(SRC)中符合.cpp的所有文件转换成.o文件

CXXFLAGS = -c -Wall

$(TARGET):  $(OBJ)
	$(CXX) $^ -o $@
	
%.o: %cpp
	$(CXX) $(CXXFLAGS) $< -o $@
	
	
.PHONY: clean
clean:
	$(RMRF) *.o
cleanall:
	$(RMRF) *.o $(TARGET)

SRC = $(wildcard *.cpp)  : 获取项目路径下的所有.cpp源文件;

OBJ = $(patsubst %.cpp, %.o, $(SRC))  : 根据源文件链接成 .o 文件;

wildcard 和 patsubst 是Makefile函数的用法,具体我也不是很懂,反正就是这么使用就对了!

注释是我加上的,我想应该是这样解释的!

7. 可以简写命令

也就是不用写命令都可以,Makefile会自动识别生成命令!

CXX := g++
TARGET = test2
OBJ = main.o cube.o circle.o
RMRF := rm -rf

$(TARGET):  $(OBJ)
	#$(CXX) $^ -o $@
	
%.o: %.h %cpp
	#$(CXX) -c $< -o $@
	
	
.PHONY: clean
clean:
	$(RMRF) *.o
cleanall:
	$(RMRF) *.o $(TARGET)

三、Makefile编译子目录的Makefile(多路径编译)

在原项目的基础上,新建文件夹circle和cube和src;将circle.h和circle.cpp拷贝到circle;将cube.h和cube.cpp拷贝到cueb;将main.cpp拷贝src;并在每个文件夹内都创建一个Makefile。

 

我们现在的需求是,在src中的Makefile,执行make命令后,会自动编译circle和cube中的Makefile,并生成各自的.a静态库;然后再进行编译链接,最后生成可执行文件!

1. circle中的Makefile

libcircle.a: circle.o
	ar crv libcircle.a circle.o		# 编译链接成.a静态库
	
circle.o: circle.cpp
	gcc -c circle.cpp
	
.PHONY: clean
clean:
	rm libcircle.a circle.o

2. cube中的Makefile

libcube.a: cube.o
	ar crv libcube.a cube.o		# 编译链接成.a静态库
	
cube.o: cube.cpp
	gcc -c cube.cpp
	
.PHONY: clean
clean:
	rm libcube.a cube.o

3. src中的Makefile(也就是主Makefile)

GXX = g++
TARGET = test2
 
ALL_C = $(wildcard ./*.cpp)		# 获取当前路径下的所有.cpp文件
C_OBJ = $(notdir $(ALL_C))		# 去掉文件的绝对路径,只保留文件名
O_OBJ = $(patsubst %.cpp,%.o,$(C_OBJ))	# 把$(C_OBJ)中符合.cpp的所有文件转换成.o文件

CIRCLE := ../circle/
CUBE := ../cube/


.PHONE: all  					# 将all 设置成伪目标,all会第一个执行 ,但不会生成目标,依赖的目标会依次执行
all: libcircle.a libcube.a $(TARGET)   		#依赖的目标 libcircle.a 、libcube.a  、 test2

libcircle.a:
	$(MAKE) -C $(CIRCLE) 		# 进入指定路径,执行路径中的Makefile,然后回到当前目录。  $(MAKE) -C (路径)

libcube.a:
	$(MAKE) -C $(CUBE) 		

$(TARGET):$(C_OBJ)        		#编译当前目录下的.cpp 生成目标程序
	$(GXX) $^ -L $(CIRCLE) -L $(CUBE) -I $(CIRCLE) -I $(CUBE) -lcircle -lcube -o $@

.PHONY: clean cleanall
clean:
	rm -fr $(TARGET) $(CIRCLE)*.o $(CIRCLE)*.a $(CUBE)*.o $(CUBE)*.a   #清理子目录下的编译后产生的文件 ,当前目录下的目标文件
	
cleanall:
	rm -fr $(TARGET) $(CIRCLE)*.o $(CIRCLE)*.a $(CUBE)*.o $(CUBE)*.a $(TARGET)

MAKE是Makefile内部定义的变量,获取当前Makfile的路径;具体可以使用命令 make -p 去查看;

具体就不解释了,拷贝下来,再根据自己的项目去修改就好了!注释那里也介绍的很清楚了!

最终会生成一条命令去执行:

g++ main.cpp -L ../circle/ -L ../cube/ -I ../circle/ -I ../cube/ -lcircle -lcube -o test2


四、Makefile链接第三方库编译

在上面第三步的基础上,新建include文件夹和lib文件夹;将自己需要链接的第三方库的头文件拷贝到include;将需要链接的第三方库文件拷贝到lib文件夹;

例如,之前学习了log4cpp库的用法,我这里就以这个库为例:(具体根据自己需要的库来使用)

 然后修改main.cpp代码,增加日志的头文件等:(具体根据自己的库来使用)

#include <iostream>

#include "cube.h"
#include "circle.h"

#include "log4cpp/Category.hh"
#include "log4cpp/Appender.hh"
#include "log4cpp/FileAppender.hh"
#include "log4cpp/OstreamAppender.hh"
#include "log4cpp/Layout.hh"
#include "log4cpp/BasicLayout.hh"
#include "log4cpp/Priority.hh"
#include "log4cpp/PatternLayout.hh"
 
 
int main(int argc, char **argv) {
	
	
	std::cout << "hello world" << std::endl;
	test2();
	test22();
	
 
	/* 1.日志输出到控制台 */
	{
		log4cpp::Appender *appender1 = new log4cpp::OstreamAppender("console", &std::cout);
		appender1->setLayout(new log4cpp::BasicLayout());	// 默认配置
 
		log4cpp::Category& root = log4cpp::Category::getRoot();
		root.setPriority(log4cpp::Priority::DEBUG);
		root.addAppender(appender1);
 	
        /* 执行后,会在终端输出这句话*/
		root.debug("This is log4cpp output log message!\n");
	}
	
	// 关闭日志
	log4cpp::Category::shutdown();
 
	return 0;
}

修改主Makefile:

GXX 	= g++
TARGET 	= test2
 
ALL_C = $(wildcard ./*.cpp)		# 获取当前路径下的所有.cpp文件
C_OBJ = $(notdir $(ALL_C))		# 去掉文件的绝对路径,只保留文件名
O_OBJ = $(patsubst %.cpp,%.o,$(C_OBJ))	# 把$(C_OBJ)中符合.cpp的所有文件转换成.o文件

CIRCLE := ../circle/
CUBE   := ../cube/

LOG4CPP_INCLUDE := ../include/			# 第三方库的头文件路径
LOG4CPP_LIB		:= ../lib/log4cpp		# 第三方库的库路径

LIBS 	= -lpthread
LOG4CPP = -llog4cpp				# 链接第三方库

.PHONE: all  					# 将all 设置成伪目标,all会第一个执行 ,但不会生成目标,依赖的目标会依次执行
all: libcircle.a libcube.a $(TARGET)   		#依赖的目标 libcircle.a 、libcube.a  、 test2

libcircle.a:
	$(MAKE) -C $(CIRCLE) 		# 进入指定路径,执行路径中的Makefile,然后回到当前目录。  $(MAKE) -C (路径)

libcube.a:
	$(MAKE) -C $(CUBE) 		

$(TARGET):$(C_OBJ)        		#编译当前目录下的.cpp 生成目标程序
	$(GXX) $^ -L $(CIRCLE) -L $(CUBE) -L $(LOG4CPP_LIB) -I $(CIRCLE) -I $(CUBE) -I $(LOG4CPP_INCLUDE) -lcircle -lcube $(LOG4CPP) $(LIBS) -o $@  

.PHONY: clean cleanall
clean:
	rm -fr $(TARGET) $(CIRCLE)*.o $(CIRCLE)*.a $(CUBE)*.o $(CUBE)*.a   #清理子目录下的编译后产生的文件 ,当前目录下的目标文件
	
cleanall:
	rm -fr $(TARGET) $(CIRCLE)*.o $(CIRCLE)*.a $(CUBE)*.o $(CUBE)*.a $(TARGET)

最终会生成一条命令去执行:

g++ main.cpp -L ../circle/ -L ../cube/ -L ../lib/log4cpp -I ../circle/ -I ../cube/ -I ../include/  -lcircle -lcube -llog4cpp  -lpthread -o test2


五、Makefile企业项目用法编译

个人觉得,企业项目中,会像下方这样去安排文件和编写Makefile去编译!

首先准备代码,编译成.so库

hello.h

#ifndef _HELLO_H_
#define _HELLO_H

void sayHello();

#endif 

hello.cpp

#include "hello.h"

#include <iostream>

void sayHello() {
    std::cout << "hello world!" << std::endl;
}

通过以下命令编译成动态库.so

g++ -c hello.cpp -o hello.o
ar -rsv libhello.so hello.o

将hello.h头文件拷贝到include文件夹中;将libhello.so拷贝到lib文件夹中;

文件目录如下:

.
├── include
│   ├── hello.h
│   └── log4cpp
│       ├── AbortAppender.hh
│       ├── Appender.hh
|       .
|       .    # 文件太多,省略了。。。
|       .
│       ├── TriggeringEventEvaluator.hh
│       └── Win32DebugAppender.hh
├── lib
│   ├── libhello.so
│   └── log4cpp
│       ├── liblog4cpp1.so
│       ├── liblog4cpp.a
│       ├── liblog4cpp.la
│       ├── liblog4cpp.so.5
│       └── liblog4cpp.so.5.0.6
└── src
    ├── circle
    │   ├── circle.cpp
    │   ├── circle.h
    │   └── Makefile
    ├── cube
    │   ├── cube.cpp
    │   ├── cube.h
    │   └── Makefile
    ├── main.cpp
    └── Makefile

8 directories, 68 files

circle.h和circle.cpp调用了libhello.so库,然后他们自己再编译成.so库!

 include/hello.h

#ifndef _HELLO_H_
#define _HELLO_H

void sayHello();

#endif 

src/circle/circle.h

#include <stdio.h>
#include "hello.h"

void test2();

src/circle/circle.cpp (这里调用了libhello.so库中的sayHello()方法)

#include "circle.h"

void test2() {
	printf("test2()\n");

	// 调用libhello.so库中的方法
	sayHello();
}

src/circle/Makefile

GXX := g++

HELLO_INCLUDE 	:= ../../include/			# libhello.so库的头文件路径
HELLO_LIB		:= ../../lib/				# libhello.so库的路径

libcircle.so: circle.o
	ar crv libcircle.so circle.o		# 编译链接成.so动态库
	
circle.o: circle.cpp
	$(GXX) -I $(HELLO_INCLUDE) -L $(HELLO_LIB) -lhello -c circle.cpp
	
.PHONY: clean
clean:
	rm libcircle.so circle.o

crc/cube/cube.h

#include <iostream>

void test22();

crc/cube/cube.cpp

#include "cube.h"

void test22() {
	std::cout << "test22()" << std::endl;
}

crc/cube/Makefile

GXX := g++

libcube.a: cube.o
	ar crv libcube.a cube.o		# 链接成.a静态库
	
cube.o: cube.cpp
	$(GXX) -c cube.cpp
	
.PHONY: clean
clean:
	rm libcube.a cube.o

src/main.cpp (这里调用了libhello.so库中的sayHello()方法)

#include <iostream>

#include "cube.h"
#include "circle.h"

#include "log4cpp/Category.hh"
#include "log4cpp/Appender.hh"
#include "log4cpp/FileAppender.hh"
#include "log4cpp/OstreamAppender.hh"
#include "log4cpp/Layout.hh"
#include "log4cpp/BasicLayout.hh"
#include "log4cpp/Priority.hh"
#include "log4cpp/PatternLayout.hh"
 
 
int main(int argc, char **argv) {
	
	
	std::cout << "hello world" << std::endl;
	test2();
	test22();
	
	// 注意,这里调用了libhello.so库的sayHello方法
	sayHello();
 
	/* 1.日志输出到控制台 */
	{
		log4cpp::Appender *appender1 = new log4cpp::OstreamAppender("console", &std::cout);
		appender1->setLayout(new log4cpp::BasicLayout());	// 默认配置
 
		log4cpp::Category& root = log4cpp::Category::getRoot();
		root.setPriority(log4cpp::Priority::DEBUG);
		root.addAppender(appender1);
 	
		root.debug("This is log4cpp output log message!\n");
	}
	
	// 关闭日志
	log4cpp::Category::shutdown();
 
	return 0;
}

src/Makefile

GXX 	= g++
TARGET 	= test3
 
ALL_C = $(wildcard ./*.cpp)		# 获取当前路径下的所有.cpp文件
C_OBJ = $(notdir $(ALL_C))		# 去掉文件的绝对路径,只保留文件名
O_OBJ = $(patsubst %.cpp,%.o,$(C_OBJ))	# 把$(C_OBJ)中符合.cpp的所有文件转换成.o文件

CIRCLE := ./circle/
CUBE   := ./cube/

HELLO_LIB := ../lib/	# 自定义库 libhello.so库的路径

LOG4CPP_INCLUDE := ../include/			# 第三方库的头文件路径
LOG4CPP_LIB		:= ../lib/log4cpp		# 第三方库的库路径


LIBS 	= -lpthread
LOG4CPP = -llog4cpp				# 链接第三方库

.PHONE: all  					# 将all 设置成伪目标,all会第一个执行 ,但不会生成目标,依赖的目标会依次执行
all: libcircle.so libcube.a $(TARGET)   		#依赖的目标 libcircle.so 、libcube.a  、 test2

libcircle.so:
	$(MAKE) -C $(CIRCLE) 		# 进入指定路径,执行路径中的Makefile,然后回到当前目录。  $(MAKE) -C (路径)

libcube.a:
	$(MAKE) -C $(CUBE) 			# 进入指定路径,执行路径中的Makefile,然后回到当前目录。  $(MAKE) -C (路径)

$(TARGET):$(C_OBJ)        		#编译当前目录下的.cpp 生成目标程序
	$(GXX) $^ -L $(CIRCLE) -L $(CUBE) -L $(LOG4CPP_LIB) -L $(HELLO_LIB) -I $(CIRCLE) -I $(CUBE) -I $(LOG4CPP_INCLUDE) -lcircle -lcube -lhello $(LOG4CPP) $(LIBS) -o $@  

.PHONY: clean cleanall
clean:
	rm -fr $(TARGET) $(CIRCLE)*.o $(CIRCLE)*.so $(CUBE)*.o $(CUBE)*.a   #清理子目录下的编译后产生的文件 ,当前目录下的目标文件
	
cleanall:
	rm -fr $(TARGET) $(CIRCLE)*.o $(CIRCLE)*.so $(CUBE)*.o $(CUBE)*.a $(TARGET)

最终会生成一条命令去执行:

g++ main.cpp -L ./circle/ -L ./cube/ -L ../lib/log4cpp -L ../lib/ -I ./circle/ -I ./cube/ -I ../include/ -lcircle -lcube -lhello -llog4cpp  -lpthread -o test3


六、总结

Makefile的基本用法已经介绍完了,我想的是,如果作为一名Linux程序员,懂得上面所介绍的用法也差不多了;如果是架构师或者项目经理级别的人物,当然得去更深入的去学习! 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

cpp_learners

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值