工程管理与编译工具-makefile/cmake

本文深入解析Makefile与CMake在工程管理中的应用,通过实例展示如何高效构建与链接复杂工程项目,包括动态库的编译与链接技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

随着工程规模的扩大,需要一个工具管理你的工程,但是很多时候,大家又不知道如何高效地管理工程文件。这里写一个关于工程管理与编译系统的博客,介绍如何快速、高效地管理工程。我就选择我最常见的makefilecmake两个工具。makefile适合管理规模不大的小工程,能够很方便的直接撰写编译规则。cmake会自动生成makefile规则,适合管理大规模的工程。

1. Makefile

MakeFile差不多是现在最流行的工程管理工具了,对于编译大型工程非常高效。这里,我们介绍makefile的大部分使用方法

1.1 工程视图

为了说明Makefile的效率,我们还是按照管理一个小型的工程来描述吧,构建一个这样的工程,各个模块如下:

./                     # 根目录
├── a.cpp              # class A的实现
├── a.h                # class A的申明
├── b.cpp              # class A的实现
└── b.h                # class B的申明
├── main.cpp           # main 函数
├── Makefile           # 主模块的makefile
├── submodule          # 子模块1,封装class D 的libmytest.so
│   ├── d.cpp          # class D的实现
│   ├── d.h            # class D的申明
│   └── Makefile       # 子模块1的makefile
│   └── submodule2     # 子模块2,封装class E 的libclasselib.so
│       ├── e.cpp      # class E的实现
│       ├── e.h        # class E的申明
│       └── Makefile   # 子模块2的makefile
├── together.h         # 头文件,include所有的class

1.2 实现各个class以及makefile

1 主目录下的Makfile


TARGET:= Hello#我们的目标文件
CC:= g++ #编译工具
SUB_MODULE_DIR=submodule#子模块目录

EXTRA_LIB:=libmytest.so #使用到的动态连接库

OBJECTS:= a.o b.o main.o #依赖的目标文件
HEAD:= a.h b.h together.h  #依赖的头文件
HEAD+= d.h  #添加依赖的头文件

#HIDE?=@  这里,添加@后,就不会打印所有的编译日志信息了
#一个例子解释说明一下使用的技巧
# target: relay1 relay2 relay3 ... relay*
#$< --> the first replay, i.e., relay1
#$^ --> all the relays, i.e., relay1 relay2 relay3 ... relay*
#$@ --> the output object, i.e., target

DEBUGINFO:=-Wall --g -O0 #调试选项
CXXFLAGS+= -Os $(DEBUGINFO) -I$(SUB_MODULE_DIR)  #编译选项
LINKFLAGS:=-L./ -lmytest -Wl,-rpath,$(PWD) #链接选项

all:$(TARGET)  #默认的make构建目标
	@echo "Make $(TARGET) Done"

%.o:%.cpp $(HEAD) #编译生成中间目标文件的依赖和规则,这里使用的是隐式推导规则
	$(HIDE)$(CC) -c $(CXXFLAGS) $< -o $@

$(TARGET): $(OBJECTS) #链接生成中间目标文件的依赖和规则
	make $(EXTRA_LIB)
	$(HIDE)$(CC) $^ -o $@ $(LINKFLAGS)

$(EXTRA_LIB): #构建动态链接库的规则
	@echo "In making $(EXTRA_LIB)"
	@make -C $(SUB_MODULE_DIR) #编译子模块,使用-C指定,还可以通过-f指定为makefile文件名,默认搜索Makefile
	cp $(SUB_MODULE_DIR)/*.so ./

run:$(TARGET) #测试用例的规则
	./$(TARGET)

.PHONY:clean
clean: #清理规则
	rm -f $(TARGET) $(OBJECTS) *.so
	make clean -C $(SUB_MODULE_DIR)
	@echo "Clean done"

2编写主函数程序main.cpp

#include "together.h"
#include <iostream>
int main(int argc, char const *argv[])
{
	std::cout << "Hello, world!" << std::endl;
	A a;
	B b;
	D d;
	a.test();
	b.test();
	d.test();
	return 0;
}

3 编写主头文件together.h

#ifndef __TOGETHER_H__
#define __TOGETHER_H__

#include "a.h"
#include "b.h"
#include "d.h"
#endif

4 编写class A的实现文件a.cpp

#include "a.h"
#include <iostream>

A::A()
{
	std::cout << "Constructing the object of class A" << std::endl;
}
A::~A()
{
	std::cout << "Destroying the object of class A" << std::endl;
}
void A::test(void)
{
	std::cout << "Testing the object of class A" << std::endl;
}

5 编写class A的声明文件a.h

#ifndef __A_H__
#define __A_H__

class A
{
public:
	A();
	~A();
public:
	void test(void);

};

#endif

6 编写class B的实现文件b.cpp

#include "b.h"
#include <iostream>

B::B()
{
	std::cout << "Constructing the object of class B" << std::endl;
}
B::~B()
{
	std::cout << "Destroying the object of class B" << std::endl;
}
void B::test(void)
{
	std::cout << "Testing the object of class B" << std::endl;
}

7 编写class B的声明文件b.h

#ifndef __B_H__
#define __B_H__

class B
{
public:
	B();
	~B();
public:
	void test(void);
};

#endif

8 submodule的构建文件 Makefile

EXTRA_LIB:=libmytest.so
CC:=g++
OBJECTS:= d.o
HEAD:= d.h
#HIDE?=@ 
# target: relay1 relay2 relay3 ... relay*
#$< --> the first replay, i.e., relay1
#$^ --> all the relays, i.e., relay1 relay2 relay3 ... relay*
#$@ --> the output object, i.e., target
SUB_MODULE2:=submodule2
DEPEND_LIB:=libclasselib.so

DEBUGINFO:=-Wall
CXXFLAGS:= -Os $(DEBUGINFO) -fPIC -I$(SUB_MODULE2)
LINKFLAGS:=-shared -L./ -lclasselib -Wl,-rpath,$(PWD)

all:$(EXTRA_LIB) 
	@echo "Make $(EXTRA_LIB) Done"

%.o:%.cpp $(HEAD)
	$(HIDE)$(CC) -c $(CXXFLAGS) $< -o $@

$(EXTRA_LIB): $(OBJECTS) 
	make $(DEPEND_LIB)
	$(HIDE)$(CC) $^ -o $@ $(LINKFLAGS)


$(DEPEND_LIB):
	@echo "In making $(SUB_MODULE2)"
	@make -C $(SUB_MODULE2)
	cp $(SUB_MODULE2)/$(DEPEND_LIB) ./

.PHONY:clean
clean:
	rm -f $(OBJECTS) $(EXTRA_LIB) $(DEPEND_LIB)
	make clean -C $(SUB_MODULE2)
	@echo "Clean done"

9 编写class D的实现文件d.cpp

#include "d.h"
#include <iostream>
#include "e.h"
D::D()
{
	std::cout << "Constructing the object of class D" << std::endl;
}
D::~D()
{
	std::cout << "Destroying the object of class D" << std::endl;
}
void D::test(void)
{
	E e;
	e.test();
	std::cout << "Testing the object of class D" << std::endl;
}

10 编写class D的声明文件d.h

#ifndef __D_H__
#define __D_H__

class D
{
public:
	D();
	~D();
public:
	void test(void);
};

#endif

11 编写submodule2Makefile

EXTRA_LIB:=libclasselib.so
CC:=g++
OBJECTS:= e.o
HEAD:= e.h
#HIDE?=@ 
# target: relay1 relay2 relay3 ... relay*
#$< --> the first replay, i.e., relay1
#$^ --> all the relays, i.e., relay1 relay2 relay3 ... relay*
#$@ --> the output object, i.e., target


DEBUGINFO:=-Wall
CXXFLAGS:= -Os $(DEBUGINFO) -fPIC 
LINKFLAGS:=-shared 

all:$(EXTRA_LIB) 
	@echo "Make $(EXTRA_LIB) Done"

%.o:%.cpp $(HEAD)
	$(HIDE)$(CC) -c $(CXXFLAGS) $< -o $@

$(EXTRA_LIB): $(OBJECTS)
	$(HIDE)$(CC) $^ -o $@ $(LINKFLAGS)


.PHONY:clean
clean:
	rm -f $(OBJECTS) $(EXTRA_LIB)
	@echo "Clean done"

13 编写class E的声明文件e.h

#ifndef __E_H__
#define __E_H__

class E
{
public:
	E();
	~E();
public:
	void test(void);
};

#endif

13 编写class E的实现文件e.cpp

#include "e.h"
#include <iostream>

E::E()
{
	std::cout << "Constructing the object of class E" << std::endl;
}
E::~E()
{
	std::cout << "Destroying the object of class E" << std::endl;
}
void E::test(void)
{
	std::cout << "Testing the object of class E" << std::endl;
}

好了,现在我们就可以测试了,
执行make run,就可以看到基本的运行结果了。

newplan@g1-XXX:~/project/makefile$ make run
g++ -Os -Wall -Isubmodule/   -c -o a.o a.cpp
g++ -Os -Wall -Isubmodule/   -c -o b.o b.cpp
g++ -Os -Wall -Isubmodule/   -c -o main.o main.cpp
In making libmytest.so
make[1]: Entering directory '/home/newplan/project/makefile/submodule'
cc -c -Os -Wall -fPIC d.cpp -o d.o
cc d.o -o libmytest.so -shared
Make libmytest.so Done
make[1]: Leaving directory '/home/newplan/project/makefile/submodule'
cp submodule/libmytest.so ./
g++ a.o b.o main.o -o Hello -L. -lmytest -Wl,-rpath,/home/newplan/project/makefile
./Hello
Hello, world!
Constructing the object of class A
Constructing the object of class B
Constructing the object of class D
Testing the object of class A
Testing the object of class B
Constructing the object of class E
Testing the object of class E
Testing the object of class D
Destroying the object of class E
Destroying the object of class D
Destroying the object of class B
Destroying the object of class A

1.3 一些总结和说明

1.3.1 编译路径问题

细心的同学可能已经发现了,我的class D申明所在的文件d.h虽然在submodule目录下,但是我在together.hinclude的时候,还是直接使用了#include "d.h"这在正常情况下,肯定会报错,找不到d.h文件,如下:

In file included from main.cpp:1:0:
together.h:6:15: fatal error: d.h: No such file or directory
 #include "d.h"
               ^
compilation terminated.

但是由于我们使用了-I指定了include查找的路径(见CXXFLAGS:= -Os $(DEBUGINFO) -I$(SUB_MODULE_DIR)),因此就避免了这个错误问题。

1.3.1 动态链接库问题

很多时候,链接自己编写的动态链接库是一个特别头疼的事情,总是会报错,说找不到链接库,明明就是在当前路径下呀:

g++ a.o b.o main.o libmytest.so -o Hello -lmytest
/usr/bin/ld: cannot find -lmytest
collect2: error: ld returned 1 exit status
Makefile:27: recipe for target 'Hello' failed
make: *** [Hello] Error 1

1:针对链接时候找不到库问题,推荐使用-L指定链接路径。注意-L和路径之间不能有空格,后面跟上你的链接库(这里需要有空格)。如当前目录下就使用-L. -lmytest
2:经过上述方法,能够帮助你链接成功,但是现在还是没有解决问题:在运行的时候,你还会遇到找不到找不到动态连接库的问题。心态都快要爆炸了。

make run
./Hello
./Hello: error while loading shared libraries: libmytest.so: cannot open shared object file: No such file or directory
Makefile:35: recipe for target 'run' failed
make: *** [run] Error 127

这里面有一些复杂的原因,主要是因为相对路径和查找的关系,一种办法是可以把动态链接库直接状态/usr/lib里面去,我们这里使用另外一种稍微优雅的方法,来解决这个问题:通过rpath指定链接的绝对路径。这样,我们的链接选项就是这样子了:

LINKFLAGS:=-L. -lmytest -Wl,-rpath,$(PWD)

通过以上两个步骤,你的带有动态连接库的程序就可以运行了,注意,这里由于使用了绝对路径,你还是不能够移动你的动态连接库。为了一劳永逸地解决这个问题,推荐使用cmake工具。

2. cmake

2.1 工程视图

我们在上个例子中进一步扩展,添加各个模块的cmake编译规则,整体工程的目录如下:

├── a.cpp
├── a.h
├── b.cpp
├── b.h
├── CMakeLists.txt                # 新增加的主CMakeLists.txt文件
├── main.cpp
├── submodule                     # 动态链接库mytest.so
│   ├── CMakeLists.txt            # submodule的CMakeLists.txt文件
│   ├── d.cpp                     # 增加class D对于class E的调用
│   ├── d.h
│   └── submodule2                # 新增加的模块classelib.{so, a},封装class E
│       ├── CMakeLists.txt        # submodule2的CMakeLists.txt文件
│       ├── e.cpp                 # class E的实现
│       └── e.h                   # class E的申明
└── together.h

2.2 编写cmake相关文件

1: 主目录下的CMakeLists.txt

CMAKE_MINIMUM_REQUIRED(VERSION 3.0) #cmake版本要求
project(Hello)

message("Building make for Hello") #打印消息
message("add -DDYNAMIC_E=1 when build dynamic class E, the default is static")
set(CMAKE_BUILD_TYPE "Debug")

set(SUB_MODULE_PATH "./submodule")
set(CMAKE_CXX_STANDARD 11) # 设置std=c++11

# 设置debug或者release的编译参数
set(CMAKE_CXX_FLAGS_DEBUG "$ENV{CXXFLAGS} -O0 -Wall -g -ggdb -fPIC")
set(CMAKE_CXX_FLAGS_RELEASE "$ENV{CXXFLAGS} -O3 -Wall -fPIC")

# collect all message to "SRC_LIST" <-- ./*.cpp
aux_source_directory(. SRC_LIST)

#分别指定编译include和链接link的搜索目录
include_directories(${SUB_MODULE_PATH})
link_directories(${SUB_MODULE_PATH})

#添加一个子模块,编译到lib目录下去
add_subdirectory(${SUB_MODULE_PATH} lib) # add a submodule, and compile it to the "build/lib"
#设置编译目标
add_executable(Hello ${SRC_LIST})
#添加编译目标依赖
target_link_libraries(Hello mytest)

2:submodule目录下的CMakeLists.txt


### for sub module
add_subdirectory(submodule2 mo2_lib)  # add new mod2, compile it to the path: build/lib/mod2_lib
link_directories(mod2_lib)  # adding link search path:  build/lib/mod2_lib
include_directories(submodule2) # add including search path

aux_source_directory(. DIR_LIB_SRCS)
MESSAGE("building mytest with ${DIR_LIB_SRCS}")
add_library(mytest SHARED ${DIR_LIB_SRCS})

target_link_libraries(mytest classelib) # add libmod2.so to--> libmod1.so

3: submodule2目录下的CMakeLists.txt

aux_source_directory(. DIR_LIB_SRCS)
MESSAGE("building classelib with ${DIR_LIB_SRCS}")
add_library(classelib SHARED ${DIR_LIB_SRCS})

2.3 编译运行

1:执行mkdir build && cd build创建独立的编译环境,这样所有编译的内容就只会在build文件夹里面。
2:cmake -DDYNAMIC_E=1 ..生成makefile文件 DYNAMIC_E=1是我指定的宏,通过指定这个宏,我可以把submodule2,也就是classelib编译成动态连接库classelib.so,默认编译成静态链接库classelib.a
3:执行编译:make VERBOSE=1 -j8,其中VERBOSE=1打印详细的编译log信息,方便调试用;-j8指定编译器启用多线程(8个)加速编译。
4:使用ldd查看运行程序的依赖关系:

newplan@g1-nasp:~/project/makefile/build$ ldd Hello
        linux-vdso.so.1 =>  (0x00007ffd0a584000)
        libmytest.so => /home/newplan/project/makefile/build/lib/libmytest.so (0x00007fdfab66d000)
        libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007fdfab2c5000)
        libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007fdfab0ae000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fdfaace4000)
        libclasselib.so => /home/newplan/project/makefile/build/lib/mo2_lib/libclasselib.so (0x00007fdfaaae1000)
        libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fdfaa7d8000)
        /lib64/ld-linux-x86-64.so.2 (0x0000563a37051000)

5: 一些有用的选项
可以使用-DCMAKE_EXPORT_COMPILE_COMMANDS=on 把cmake的compile给dump下来。

2.4 参考资料:

3. 其他编译选项

其他一些编译工具如:autogen/bazel/waf/python setup等不太熟悉,就不赘述了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值