|
由于课题需要,最近在利用《C++ Primer》这本书补习C++知识。当前我遇到了这样一个问题:该如何正确的编译一个别人写的C++项目(即Lammps里所谓的"UserPackage")。
其实这属于一类问题,我们可以自然而然地将其表述为:一个中(甚至大)型的实用C++项目,到底是如何被开发者组织起来的?
对类似我这种非科班同学来说,相信大家都曾有过这种疑问。因为非科班生在使用编程语言时,往往特别关心于语法的正确与否,或者某个算法该如何实现——这些小问题,很多用一个源文件的代码量就能解决(比如C++的一个.cpp或者Python的一个.py)。然而身边那些实用中、大型软件,打开文件夹一看,就知道肯定不是一个文件就能搞定的。下面我就结合自己的理解,以一个“杀鸡用牛刀”的例子,探讨一下该如何组织一个真正的项目。
<hr/>这个例子要实现的需求非常简单:交换两个数。平台就选择科学计算人员都熟悉的Linux系统(Ubuntu-20.04),编译器选最常用的g++。
&#34;这还不简单!&#34;你啪的一下新建了一个叫Main.cpp的文件,很快啊!然后上来就是,一个main函数,一个指针式交换:
# include <iostream>
int main()
{
//要交换的两个数
int para1 = 1;
int para2 = 2;
//这两个数的指针
int *p1 = &para1;
int *p2 = &para2;
//执行交换
int tmp;
tmp = *p1;
*p1 = *p2;
*p2 = tmp;
//输出结果,显然是已经交换了
std::cout << para1 << &#34;,&#34; << para2 << std::endl;
}
全部编,编译通过了啊,运行以后,自然是以点到为止——

慢着,这样的话本文还有什么意义......小伙子你要讲码德,代码怎么能搞窝里斗呢?
没错,组织一个项目与仅仅写一个算法,最大的区别之一就是分离式编译。项目越大越是如此,代码一旦搞起窝里斗,也就是相互耦合起来,将会给后期的debug和维护工作带来难以想象的困难。为此,我们不妨灵活的使用#include命令,专门定义一个叫transfer的函数(其实应该叫swap,懒得改了....),单独放进另一个.cpp文件里。这样,后期一旦发现函数本身有bug,就不用回到主函数所在的Main.cpp里,去一行一行找了。
新建一个叫way1的文件夹,作为我们的项目文件夹(注意这里的措辞)。然后在里面新建两个.cpp文件:Main.cpp和transfer.cpp:
Main.cpp:
//方式1 : 通过include一个cpp文件来实现
# include <iostream>
# include &#34;transfer.cpp&#34;
int main()
{
int para1 = 1;
int para2 = 2;
transfer(&para1,&para2); //调用transfer函数
std::cout << para1 <<&#34;,&#34;<< para2 <<std::endl;
}transfer.cpp:
//transfer.cpp
void transfer(int *a,int *b) //形参a,b都是指针
{
int tmp;
tmp = *a;
*a = *b;
*b = tmp;
}然后编译(注意必须进入到项目文件夹去)、运行,依然是顺利通过:

你若有所思的点点头,突然大叫到:&#34;诶诶诶,丁老师你也不讲码德,怎么把.cpp文件给include进去了?!&#34;
哈哈,这个反应很正常。因为我们平时见到的include,包含的一般是头文件,如Main里面的标准库<iostream>。其实,include命令做的是一种文本上的宏替换。你可以简单(但不严谨)的理解为复制粘贴:编译器在预编译阶段将transfer里面的内容粘进Main,替你把Main的代码补全。所以include一个.cpp还能编译通过,其实不足为奇。(而且注意,那行g++命令里并没有出现transfer.cpp,但编译器也没有报错,这就更加印证了本观点)
不过你的想法也没错,因为一般没有哪个开发者会这么写。实践上,人们通常利用头文件,把函数和类等的声明和实现分开写:声明写在.h里,具体实现写在.cpp里。这种做法有巨大优势:头文件实际上扮演了“接口”的角色。一个大型项目的代码之间可能出现相互调用的行为,譬如你写了transfer函数,但有另一个人想调用它,那他无需知道你.cpp里的代码,而是直接#include你的transfer函数的头文件就行了。
所以,我们新建一个项目文件夹way2。这个新项目里,应该包含3个文件:
Main.cpp
//方式2:使用头文件组织项目
# include <iostream>
# include &#34;transfer.h&#34; //引号用于include非标准库,比如你写的transfer.h
int main()
{
int para1 = 1;
int para2 = 2;
transfer(&para1,&para2); //调用transfer函数
std::cout << para1 <<&#34;,&#34;<< para2 <<std::endl;
}transfer.cpp
//transfer.cpp:transfer函数的具体实现
void transfer(int *a,int *b) //形参a,b都是指针
{
int tmp;
tmp = *a;
*a = *b;
*b = tmp;
}transfer.h,也就是头文件。注意,头文件名虽无强制约束,但应该与函数名或类名一致,这也是一种“码德”哦。
# pragma once //头文件保护符,防止重复编译。一般都要加上。
void transfer(int *a,int *b); //头文件里,声明有一个叫transfer的函数编译就有讲究了:由于Main里用到了transfer函数,我们得先把transfer和Main编译成两个.o文件,才能链接(link)起来,形成最终输出文件:

至此,我们似乎就获得了通过头文件,组织起一个项目的思路。问题看似解决了——
且慢,如果一个大项目里包含很多这种头文件,它们要求的编译顺序又不尽相同,那我们该怎么管理这个“编译流程”呢?总不能一个一个用命令行编译吧。。
实际上,有专门的软件替我们完成这个工作,那就是大名鼎鼎的makefile。在Linux下,该软件叫make,你可以通过以下命令查看有没有安装它:

如果命令行提示找不到该程序,可以用以下命令安装:
sudo apt-get install make关于make怎么用,文末放上了一个很详细的教程供参考。现在只需记住:我们要在文件夹下创建一个叫Makefile的文本文件,这文件里面定义了编译顺序:
Main.exe : Main.o transfer.o
g++ -o Main.exe Main.o transfer.o
Main.o : Main.cpp transfer.h
g++ -c Main.cpp
transfer.o : transfer.cpp
g++ -c transfer.cpp然后在命令行下敲击make,一键搞定!

<hr/>makefile教程 |
|