1. 写在前面
c++在线编译工具,可快速进行实验: https://www.bejson.com/runcode/cpp920/
这段时间打算重新把c++捡起来, 实习给我的一个体会就是算法工程师是去解决实际问题的,所以呢,不能被算法或者工程局限住,应时刻提高解决问题的能力,在这个过程中,我发现cpp很重要, 正好这段时间也在接触些c++开发相关的任务,所有想借这个机会把c++重新学习一遍。 在推荐领域, 目前我接触到的算法模型方面主要是基于Python, 而线上的服务全是c++(算法侧, 业务那边基本上用go),我们所谓的模型,也一般是训练好部署上线然后提供接口而已。所以现在也终于知道,为啥只单纯熟悉Python不太行了, cpp,才是yyds。
和python一样, 这个系列是重温,依然不会整理太基础性的东西,更像是查缺补漏, 不过,c++对我来说, 已经5年没有用过了, 这个缺很大, 也差不多相当重学了, 所以接下来的时间, 重温一遍啦 😉
资料参考主要是C语言中文网和光城哥写的C++教程,然后再加自己的理解和编程实验作为辅助,加深印象,当然有些地方我也会通过其他资料(C++ Primer Plus)进行扩充。 关于更多的细节,还是建议看这两个教程。
今天这篇文章学习多文件编程, 对于C++项目, 往往都是很多个.h
和.cpp
文件组合在一起最后构成程序, 这里面其实有很多细节需要注意, 之前在实习的时候, 参与过C++项目开发, 经常在cpp里面找不到某个类的声明或者某个函数的定义等等报错问题, 当时还觉得奇怪,明明都写好了? 怎么还是报找不到呢? 其实像多文件编程里面, 头文件的引入,命名空间的使用, const常量的使用,以及过个文件如何在一起编译等,是有很多细节东西的, 这篇文章就把这几个地方整理下,这样就能从一个项目的角度再审视C++啦。
主要内容:
- C++多文件编程初步
- C++如何防止头文件被重复引入
- C++命名空间如何应用在多文件编程中
- C++ const常量如何在多文件编程中用
- C++多文件项目如何用g++命令执行
- C++多文件编程更深层原理
- g++, MakeFile和CMake小补
- 小总
Ok, let’s go!
2. C++多文件编程初步
为啥要用多文件编程呢? 我们不妨先拿个单文件编程看看,比如建立一个student.cpp, 然后写下面代码:
// Stuent.cpp
#include <iostream>
using namespace std;
class Stuent{
public:
char *name;
int age;
float score;
void say(){
cout << name << " " << age << endl;
}
};
int main()
{
Stuent *pStu = new Stuent;
pStu -> name = "wuzhongqiang";
pStu -> age = 25;
pStu -> score = 92.5f;
pStu -> say();
delete pStu;
return 0;
}
这个程序比较简单,就是定义了一个类,然后用类建了一个对象,把对象的基本信息通过成员函数的方式输出了一下。 就这么个简单的功能, 在一个文件里面,就用了20多行的代码, 如果我们想完成复杂一点的功能, 这个文件代码会非常多,几百行几千行都有可能。
这时候, 在程序中阅读代码是非常难受的,并且查找的时候也不方便, 很多代码再考虑到内聚和耦合, 这样写肯定是行不通的。 所以往往当程序非常复杂的时候,就考虑把代码写成多个文件, 然后放到不同的目录进行管理,这样维护代码的时候,就非常容易了。分散代码遵循的基本原则: 实现相同功能的代码存储在一个文件。
一个完整的c++项目常常是多个代码文件组成,根据后缀名不同,大致分为两类:
.h
文件: 头文件,用于存放常量,函数的声明部分,类的声明部分.cpp
文件: 源文件,用于存放变量,函数的定义部分,类的实现部分
虽然类内部的成员函数可以在声明的同时进行定义(自动成为内联), 但原则上不推荐这样使用。即使定义成员函数代码很少,定义也应该放在适当的.cpp文件中。
根据上面的规则,就可以把上面代码进行拆分。
// student.h
class Student{
public:
const char *name;
int age;
float score;
void say();
};
// student.cpp
#include <iostream>
#include "student.h"
void Student::say(){
std::cout << name << " " << age;
}
// main.cpp
#include "student.h"
int main(){
Student *pStu = new Student;
pStu -> name = "wuzhongqiang";
pStu -> age = 25;
pStu -> score = 92.5f;
pStu -> say();
delete pStu;
}
程序进行合理地划分时,需要额外将 "student.h"
分别引入到 student.cpp 文件和 main.cpp 文件中,因为这 2 个文件中需要使用 student.h
文件声明的 Student 类,就如同我们在使用 cin 和 cout 时需要提前引入 头文件一样。
注意,引入编译器自带的头文件(包括标准头文件)用尖括号,例如 ;引入自定义的头文件用 “” 双引号,例如 “student.h”
这样, 程序就显得非常清晰了。
3. C++如何防止头文件被重复引入
头文件包含命令#include
的效果与直接复制粘贴头文件内容效果是一样的, 预处理器实际上也是这样做的, 它会读取头文件的内容,然后输出到#include
命令所在的位置。
头文件包含是一个递归的过程,如果被包含的头文件中还包含了其他头文件,预处理器会继续将它们也包含进来,这个过程一直持续下去…
但这样会导致一个问题,就是重复引入同一个源文件。比如下面的程序:
//student.h
class Student {
//......
};
//school.h
#include "student.h"
class School {
//......
private:
Student stu[50];
};
//main.cpp
#include "student.h"
#include "school.h"
int main() {
//......
return 0;
}
运行此项目会发现,编译器报“Student 类型重定义”错误。这是因为在 school.h
文件中已经 #include
了一次 "student.h"
,而在 main.cpp 主程序又同时 #include
了 "school.h"
和 "student.h"
,即 Student 类的定义被引入了 2 次,C++不允许同一个类被重复定义。
那么,我们既然知道了在school.h文件中已经引入了Student类,那如果去掉main.cpp
中的引入student.h文件呢? 这个确实能解决这里的问题,但并不适合所有"重复引入"的场景。
比如,a.h中定义了个类型TYPE1, b.h中定义了个类型TYPE2, 而这俩里面都包含了stdio.h。 如果我main.cpp要同时使用TYPE1和TYPE2,就必须将a.h和b.h包含进来。 这时候stdio.h被重复包含, 而这个情况,我们无法用上面方法解决。
下面整理, C++多文件编程中, 处理"多次#include导致重复引入"问题的方式, 有下面3种:
-
使用宏定义避免重复引入
实际多文件开发中,往往使用下面的宏定义来避免重复引入:#ifndef _NAME_H #define _NAME_H // 头文件内容 #endif
其中,_NAME_H是宏的名称。需要注意的是,这里设置的宏名必须是独一无二的,不要和项目中其他宏的名称相同。
当程序第一次
#include
该文件时, 由于_NAME_H
尚未定义,所以会定义_NAME_H
并执行"头文件内容代码", 当发生多次#include
时, 因为前面定义好了_NAME_H
, 所以不会重复执行"头文件内容"部分的代码了。所以, 将上面项目中student.h文件做如下修改:
#ifndef _STUDENT_H #define _STUDENT_H class Student{ //.... }; #endif
虽然该项目
main.cpp
文件中仍#include
了 2 次"student.h"
,但鉴于_STUDENT_H
宏只能定义一次,所以 Student 类也仅会定义一次。再次执行该项目会发现,其可以正常执行。 -
使用
#pragma once
避免重复引入
除了第一种最常用方式之外,还可以使用#pragma one
指令,将其附加到指定文件的最开头位置。则该文件只会被#include
一次。#prama once class Student{ //..... };
#ifndef
是通过定义独一无二的宏来避免重复引入的, 这意味每次引入头文件都要进行识别,效率不高。 但考虑C和C++都支持宏定义,所以项目使用#ifndef
规避可能出现的"头文件重复引入"问题,不会影响项目的可移植性。#pragma once
不涉及宏定义,当编译器遇到它时就会立刻知道当前文件只引入一次,所以效率很高。 但值得一提的是,并不是每个版本编译器都能识别#pragma one
指令,所以兼容性可能不是很好。 另外,#pragma once
只能作用于某个具体文件, 无法像#ifndef那样仅作用于指定的一段代码。 -
使用_Pragma操作符
C99标准中新增加了一个和#pragma
指令类似的_Pragma
操作符,可以看做是#pragma
的plus版,不仅实现了#prama
所有功能,更重要的,_Pragma
还能和宏搭配使用。当处理头文件重复引入问题, 可以把下面的具体加到相应文件开头:
_Pragma("once") class Student { //...... };
事实上,无论是 C 语言还是 C++,为防止用户重复引入系统库文件,几乎所有库文件中都采用了以上 3 种结构中的一种,这也是为什么重复引入系统库文件编译器也不会报错的原因。
这三种方法的后两种其实是一类, 特点是编译效率高,但可移植性差(编译器不支持,会发出警告等), 而#ifndef
特点是可移植性高,编译效率差。 一般常用的是#ifndef / #define / #endif
组合解决头文件被重复引入的问题。
4. C++命名空间如何应用在多文件编程
C++引入命名空间是为了避免合作开发项目时产生的命名冲突, 当进行多文件编程的时候, 通常是将声明部分(变量、函数和类等)划分到.h
文件中, 将实现部分划分到.cpp
文件中。
这时候,如果是要给变量、函数或类指定命名空间, 则该命名空间至少包含它们的声明部分, 所以多文件编程时, 命名空间在.h头文件中。
// student_li.h
#ifndef _STUDENT_LI_H
#define _STUDENT_LI_H
namespace Li{
class Student{
public:
void display();
};
}
#endif
// student_li.cpp
#include "student_li.h"
#include<iostream>
void Li::Student::display(){
std::cout << "Li::display" << std::endl;
}
// student_han.h
#ifndef _STUDENT_HAN_H
#define _STUDENT_HAN_H
namespace Han{
class Student{
public:
void display();
};
}
#endif
// student_han.cpp
#include "student_han.h"
#include<iostream>
void Han::Student::display(){
std::cout << "Han::display" << std::endl;
}
// main.cpp
#include <iostream>
#include "student_li.h"
#include "student_han.h"
int main(){
Li::Student stu1;
stu1.display();
Han::Student stu2;
stu2.display();
}
当类的声明位于指定的命名空间中时,如果要在类的外部实现其成员方法,需同时注明所在命名空间名和类名(例如本项目中的 Li::Student::display()
)
不同的头文件中可以使用不同的命名空间, 也可以使用相同的命名空间, 但后者的前提是位于该命名空间中的成员必须保证互不相同,否则会发生"重定义"错误。
//demo1.h
#ifndef _DEMO1_H
#define _DEMO1_H
#include<iostream>
namespace demo {
void display() {
std::cout << "demo1::display" << std::endl;
}
int num=20;
}
#endif
//demo2.h
#ifndef _DEMO2_H
#define _DEMO2_H
#include <iostream>
namespace demo {
// 这个虽然名字也叫display, 但是与上面那个是重载函数,并不是同一个
void display(int a) {
std::cout << "demo2::display" << std::endl;
}
//int num; 因为 demo1.h 中已经声明有同名的变量,取消注释会造成重定义错误
}
#endif
5. C++ const常量如何在多文件编程中用
多文件编程中代码划分原则: 将变量、函数或者类的声明部分存放到.h
文件,对应的实现部分放在.cpp
文件。但const常量是个小例外。
const修饰的变量必须在定义的同时进行初始化操作(extern修饰除外), const修饰除了表明变量是常量, 还将所修饰的变量可见范围限制到了当前文件。这意味着, 除非const常量的定义和main主函数位于同一个
.cpp
文件,否则该const常量只能在其所在的.cpp
文件使用。
如何定义const常量, 才能在其他文件中使用呢? 有下面三种方法:
-
const常量定义在
.h
头文件中
这个最常用, 虽然有点违背上面的规律, C++多文件编程中,另外另个违背规律的(类的定义和内联函数定义,他们通常也都定义在.h
头文件)//demo.h #ifndef _DEMO_H #define _DEMO_H const int num = 10; #endif // mian.cpp #include <iostream> #include "demo.h" int main(){ std::cout << num << std::endl; return 0; }
将const常量定义在.h头文件中,只需要引入头文件, 就可以使用。
-
借助extern先声明再定义const常量
const常量的定义先可以遵循"声明在.h文件,定义在.cpp文件", 借助extern关键字。//demo.h #ifndef _DEMO_H #define _DEMO_H extern const int num; #endif // demo.cpp #include "demo.h" const int num = 10; // main.cpp #include <iostream> #include "demo.h" int main(){ std::cout << num << std::endl; }
C++ const 关键字会限定变量的可见范围为当前文件,即无法在其它文件中使用该常量。而 extern 关键字会 const 限定可见范围的功能,它可以使 const 常量的可见范围恢复至整个项目。
-
借助extern直接定义const常量
这个是第二种方式的简化, 直接在demo.cpp中定义和初始化即可。// demo.cpp extern const int num = 10;
第一种方式是最常用, 更简单的方式。
6. C++多文件项目如何用g++命令执行
Linux平台一般使用g++命令执行c++程序, 当然也会用makefile文件运行较大的C++项目, 想在后面整理这个, 这个东西可以看做是g++代码整理, 有了MakeFile, 执行程序会更加快速方便。 但前提, 我们必须先深入了解C++程序的执行过程。
这个上面的那个在线网站就不好使了,需要找一个Linux系统玩下面的案例了。 我这里是用的实验室服务器, 如果是自己本子,可以安装一个虚拟机。 关于这个我有很多篇文章整理。
// Linux上看是否安装了g++
which g++
// 如果没有 先装一下
yum install gcc-c++
C++或者C程序的执行过程分为4步, 依次是预处理,编译,汇编和链接。
C++多文件编程中有头文件(.h后缀)和源文件(.cpp后缀), 需要注意, 执行C++项目时, 头文件是不需要经历以上4个阶段的, 只有项目中所有源文件才必须经历
我们通过下面一个例子, 详细看下C++程序的执行过程。
首先, 建立一个c++项目目录, 我这里就直接建立"LearningC"目录, 然后在这里面新建三个文件student.h, stduent.cpp, main.cpp
, 代码如下:
//student.h
class Student {
public:
const char *name;
int age;
float score;
void say();
};
//student.cpp
#include <iostream> //std::cout、std::endl
#include "student.h" //Student
void Student::say() {
std::cout << name << "的年龄是" << age << ",成绩是" << score << std::endl;
}
//main.cpp
#include "student.h" //Student
int main() {
Student *pStu = new Student;
pStu->name = "wuzhongqiang";
pStu->age = 25;
pStu->score = 66.6f;
pStu->say();
delete pStu; //删除对象
return 0;
}
文件组织如下:
下面用g++执行此项目。
-
预处理阶段, 生成.i文件
-->
这一步,主要是做了宏替换, 注释删除工作等g++ -E main.cpp -o main.i g++ -E student.cpp -o student.i
这里的
-E
选项用于限定g++编译器只进行预处理,而不进行后续的3个阶段,-o
选项用于指定生成文件的名称, 通常".i
"作为C++程序预处理后的文件后缀名。
我们这里看一看下.i
文件cat main.i
。
比较清晰的是两点, 第一点是main.cpp的#include "student.h"
, 这里直接用studen.h
的内容替换了这句代码,同样#include <iostream>
这句话,也让原内容替换掉。第二点是注释都干掉了。当然做了很多工作,我这只显示了最后一小部分。#include
是一个来自 C 语言的宏命令,作用于程序执行的预处理阶段,其功能是将它后面所写文件中的内容,完完整整、一字不差地拷贝到当前文件中。 -
经历编译阶段,即对预处理阶段得到的 -i 文件做进一步的语法分析,生成相应的汇编代码文件。
g++ -S main.i -o main.s g++ -S student.i -o student.s
其中,
-S
选项用于限定g++编译器对指定文件编译,得到的汇编代码通常以".s"作为后缀名。我们这里依然是看下"cat main.s
"。
这语言就不是我们能看懂的了,都是汇编语言了。 -
经历汇编阶段,即将汇编代码文件转换成可以执行的机器指令, 也就是二进制机器码
-- >
生成目标文件,二进制机器码g++ -c mian.s -o main.o g++ -c student.s -o student.o
-c
指令限定g++编译器只进行汇编操作, 最终生成目标文件(本质是二进制文件)以".o
"作为后缀名。
这个依然是看一眼:
这个更不是我们能看懂的了。 -
经历链接阶段,即将所有的目标文件组织成一个可以执行的二进制文件
g++ main.o student.o -o student.exe
注意,如果不用
-o
指定可执行文件的名称,默认情况下会生成 a.out 可执行文件。Linux 系统并不以文件的扩展名开分区文件类型,所以 a.out 和 student.exe 都是可执行文件,只是文件名称有区别罢了。
此时大功告成:
这个编译过程非常重要。当然,上面的这四步过程也可以直接一步到位:
g++ main.cpp student.cpp -o student.exe
7. C++多文件编程更深层原理
7.1 更深一层
C++多文件编程中,一个完整C++项目可以包含2类文件, 头文件(.h)和源文件(.cpp)。
同属一个C++项目中所有代码是分别进行编译的,只需要在编译成目标文件后再与其他目标文件做一次链接即可。
例如,在 a.cpp 源文件中定义有一个全局函数
a()
,而在文件 b.cpp 中需要调用这个函数。即便如此,处于编译阶段的 a.cpp 和 b.cpp 并不需要知道对方的存在,它们各自是独立编译的是,只要最后将编译得到的目标文件进行链接,整个程序就可以运行。
那么,这个过程是怎么实现的呢? b.cpp怎么就能找到a.cpp里面的a()
函数了? 怎么知道a()函数就在a.cpp中了? 万一有多个a()
函数咋办? 下面一一来捋捋。
首先, 写程序的角度, 当文件b.cpp需要调用a()
函数时, 只需要先声明一下该函数即可, 这是因为, 编译器在编译b.cpp的时候,会生成一个符号表, 类似a()
这样看不到定义的符号就存到这个表中。 在链接阶段, 编译器就会在别的目标文件中寻找这个符号的定义, 一旦找到, 程序就可以顺利生成,否则,链接错误。
这里的两个概念: 声明和定义
- 定义: 某个符号完整的描述清楚, 是变量还是函数, 变量类型以及变量值是多少, 函数的参数有哪些以及返回值是什么等
- 声明: 仅仅告诉编译器该符号存在,至于该符号具体含义,等链接的时候才能知道
也就是说, 定义的时候需要遵循C++语法规则完整描述一个符号, 而声明时只需要给出该符号原型即可。 一个符号允许被声明多次,但只能被定义一次,否则编译器不知道应该用哪个?
基于声明和定义的不同,才有了 C++ 多文件编程的出现。试想如果有一个很常用的函数 f()
,其会被程序中的很多 .cpp 文件调用,那么我们只需要在一个文件中定义此函数,然后在需要调用的这些文件中声明这个函数就可以了。
那么, 如果有几百函数, 声明该怎么办呢? 一种简单的方法是将它们的声明全部放入一个文件中, 当需要时直接从文件中拷贝。 这种方法可行,但太麻烦, 于是乎头文件便可以发挥作用。
所谓头文件, 其实它内容和.cpp文件内容是一样的,都是C++源代码, 唯一区别是在于头文件不用被编译。 我们把所有的函数声明全部放进一个头文件, 当某个.cpp源文件需要,就#include只接将头文件内容引入到.cpp中。 这样, 当.cpp在预处理的时候, #include引入的
.h
文件就会替换该文件中的所有声明。
所以上面的三个问题, 其实就清楚了。 b.cpp首先会通过#include
操作, 把a()
函数声明内容复制过去,这样, 编译b.cpp的时候就知道有这么个符号了。 然后a.cpp和b.cpp都进行编译, 生成.o文件之后 进行链接, b.o就去a.o中找a()
函数的定义。 而有多个a()
函数的时候, 会进行命名空间的限制。
7.2 C++头文件内应该写啥
.h
头文件作用是被其他.cpp
包含进去,其本身并不参与编译, 但它的内容会在多个.cpp
中得到编译。
根据"符号的定义只有一次"的规则,就很容易理解,** 头文件中只能放变量和函数的声明而不能放他们定义**, 因为一个头文件内容实际上是会被其他多个不同.cpp
文件引入且编译。 如果把函数和变量定义放在里面,这时候会发生重定义错误。
比如:
// 声明 ok
extern int a;
void f();
// 定义 不OK
int a;
void f() {}
但, 有三种情况是例外, 他们虽然属于定义的范畴,但应该放在.h
文件中。
- 头文件中可以定义const对象
全局的 const 对象默认是没有 extern 声明的,所以它只在当前文件中有效。把这样的对象写进头文件中,即使它被包含到其他多个.cpp
文件中,这个对象也都只在包含它的那个文件中有效,对其他文件来说是不可见的,所以便不会导致多重定义。
与此同时,由于这些 .cpp 文件中的 const 对象都是从一个头文件中包含进去的,也就保证了这些 .cpp 文件中的 const 对象的值是相同的,可谓一举两得。
同理, static对象的定义也可以放进头文件 - 头文件中可以定义内联函数
内联函数(inline
修饰)是需要编译器在编译阶段根据其定义将它内联展开的(类似宏展开),而并非像普通函数那样先声明再链接。这就意味着,编译器必须在编译时就找到内联函数的完整定义。 - 头文件中可以定义类
在程序中创建一个类的对象时, 编译器只有在这个类的定义完全可见情况下,才能知道这个类对象应该如何布局。所以关于类的定义要求,和内联函数是一样的,即把类定义放进头文件, 然后再使用这个类的.cpp
文件中#include
。
类的内部通常包含成员变量和成员函数, 成员变量要等到具体对象被创建时才会被定义(分配空间), 但成员函数需要一开始就被定义。 通常做法是类定义放到头文件, 成员函数实现放在.cpp
中。 当然,还有一种方法是在类定义的时候,直接把成员函数实现写好,此时编译器会将其看为内联函数。但尽量不这么做。
7.3 再看头文件重复引入错误
C++多文件编程中, .h
头文件中只包含声明语句的话, 即便被同一个.cpp
文件引入多次也没有问题, 因为声明语句是可以重复的, 但上面有3种特例, 是可以把定义写入头文件, 并且这3种特例都是很常见的操作。
那么这个时候, 如果包含上面3种特例里面某一种的头文件,被多个.cpp
文件引入, 就会出现重定义错误了。 C++提供了3种处理机制, 最常用的就是#ifndef/#define/#endif
。
详细的看上面吧,到这里,就串起来了,也更加清晰啦。
8. g++,CMake和MakeFile小补
这里补充点知识,就是MakeFile和CMake,这两个是这两天学习CV项目时偶然接触到的,所以就产生了好奇, 想看看这俩东西是啥,既然有了g++了, 为啥还要有这俩东西?
8.1 MakeFile
上面已经梳理清楚了一个完成C++项目的执行过程, 主要是分为四步: 预处理、编译、汇编、链接。g++命令确实可以对一个C++项目通过上面四步转成可执行文件,但在中大型项目里面,这样玩还是太复杂。 于是乎就有了MakeFile。
Makefile 文件描述了 Linux 系统下 C/C++ 工程的编译规则,它用来自动化编译 C/C++ 项目。一旦写编写好 Makefile 文件,只需要一个 make 命令,整个工程就开始自动编译,不再需要手动执行 GCC 命令。
一个中大型 C/C++ 工程的源文件有成百上千个,它们按照功能、模块、类型分别放在不同的目录中,Makefile 文件定义了一系列规则,指明了源文件的编译顺序、依赖关系、是否需要重新编译等。
MakeFile文件是g++代码的整理, 有了MakeFile文件, 执行程序会更加快速方便, 而CMake是简化MakeFile编写,可以自动生成MakeFile文件
当然,这里不会整理非常详细,因为我现在也只学到了点皮毛,只是通过一个例子看看这玩意咋用,现在学习首先是从应用角度开始, 关于原理和理论可以先参考上面的C语言中文网相关内容,后面如果学习到更多,会继续补充。
首先, MakeFile文件的基本格式长下面这样:
target ... : prerequisites ...
command
...
这里参数说明:
- target: 目标文件, 可以是object file,也可以是可执行文件
- prerequisites - 生成target所需要的文件或目标
- command - make需要执行的命令(任意shell命令), Makefile命令必须tab开头
不用把这玩意想成多么高大上, 这其实就是个文件,更直接的说, 这玩意里面其实就是我们上面生成student.exe文件的全部指令,只不过只要有了这个文件, 我们就可以直接用make命令,直接生成可执行文件。 那你说, 那不还得把上面指令敲到MakeFile文件吗? 其实有了CMake, 就不需要了。
Makefile五个重要的东西: 显示规则, 隐晦规则,变量定义, 文件指示和注释
-
显示规则:
通常在写makefile时使用的都是显式规则,这需要指明target和prerequisite文件。
一条规则可以包含多个target,这意味着其中每个target的prerequisite都是相同的。
当其中的一个target被修改后,整个规则中的其他target文件都会被重新编译或执行。 -
隐晦规则:
make的自动推导功能所执行的规则 -
变量的定义:
Makefile中定义的变量,一般是字符串 -
文件指示:
Makefile中引用其他Makefile;指定Makefile中有效部分;定义一个多行命令 -
注释:
Makefile只有行注释 “#”, 如果要使用或者输出"#"字符, 需要进行转义, "#
下面就拿上面那个student.cpp和main.cpp编译的例子来编写MakeFile文件。 LearningC目录下把其他的文件删除,只保留两个.cpp
文件和一个.h
文件。
编写MakeFile文件, 同样是建立一个文件, 然后在里面开写。
-
编写clean, 作用是删除所有的.o文件和可执行文件, 以防带来杂乱干扰
clean: rm *o student
这里要是tab
-
编写目标文件1: 依赖文件1
目标文件就是想得到的文件, 依赖文件就是目前所拥有的东西。 目前只有student.cpp和main.cpp文件, 而目标文件就是得到对应的.o文件student.o:student.cpp g++ -c student.cpp -o student.o main.o:main.cpp g++ -c main.cpp -o main.o
和上面格式要对应上
-
编写目标文件2: 依赖文件2
这里和上面同理, 我们现在有了student.o和main.o文件了,接下来我们就是基于这两个,得到我们的可执行文件student:student.o main.o g++ main.o student.o -o student
这样就编写完了, 我们组织文件的时候, 是从下往上组织上面代码。
这样就编写好了makefil文件,然后我们只需要用make命令编译这个文件即可。
这样就会自动执行编译程序了, 每次我们想编译,只需要make这个文件, 而不需要之前手动一系列g++命令了。修改起来也非常方便。
当然,上面这个代码是一个最简单的版本了,下面我们尝试修改一下,让其稍微复杂一点, 主要是为了编译更高效。参考这篇文章
当然, 真实大项目要比这个复杂的多,编写这个文件就类似于shell脚本那样,还有很多其他高级用法,比如定义变量, 条件判断等, 这些目前没用到, 详细的可以看上面给的那个链接。
8.2 Cmake
说完了makefile文件,接下来, 整理下Cmake了
CMake是一个跨平台的编译(Build)工具,可以用简单的语句来描述所有平台的编译过程,其是在make基础上发展而来的,早期的make需要程序员写Makefile文件,进行编译,而现在CMake能够通过对cmakelists.txt的编辑,轻松实现对复杂工程的组织
首先, 需要先安装cmake
sudo apt-get install cmake
cmake --version
编译流程如下:
1. 编写Cmake配置文件CMakeLists.txt, 理解成Cmake所要处理的代码
2. 执行命令 cmake path生成MakeFile, path是CmakeList.txt所在目录
3. 使用make命令进行编译
这么说太抽象,还是拿上面例子实操下。
把之前编写的makefile以及生成的可执行文件删除掉, 然后建立一个build目录,以及建立CMakeLists.txt文件, 文件组织如下:
注意这个组织方式,因为不同组织方式可能指定路径会不同。
接下来,我们编写CMakeLists.txt文件, vim下
cmake_minimum_required(VERSION 3.10)
# set the project name
project(student)
# add the executable
add_executable(student student.cpp main.cpp)
这个是不是比上面编写MakeFile文件简单多了,并且这里面的大部分命令都是固定语法,相当于我们只需要指定一些参数即可。 首先, 先整理上面几个命令:
命令 | 命令语法 | 命令简述 | 使用范例 |
---|---|---|---|
cmake_minimum_required | cmake_minimum_required(VERSION major[.minor[.patch[.tweak]]][FATAL_ERROR]) | 用于指定需要的CMake 的最低版本 | cmake_minimum_required(VERSION 3.10) |
project | project( [languageName1 languageName2 … ] ) | 用于指定项目的名称,一般和项目的文件名称对应 | project(student) |
add_executable | add_executable( [WIN32] [MACOSX_BUNDLE][EXCLUDE_FROM_ALL] source1 source2 … sourceN) | 用于指定从一组源文件 source1 source2 … sourceN 编译出一个可执行文件且命名为name | add_executable(student student.cpp main.cpp) |
include_directories | include_directories([AFTER|BEFORE] [SYSTEM] dir1 dir2 …) | 用于设定目录,这些设定的目录将被编译器用来查找 include 文件 | include_directories(${PROJECT_SOURCE_DIR}/lib) |
这种命令太多了,需要平时多加积累, 由于我这里就用到了这几个, 所以先整理这几个,如果有其他命令需要用到,可以去官网查具体使用https://cmake.org/cmake/help/v2.8.8/cmake.html#section_Commands
把上面CMakeLists.txt写好之后, 我们去build目录, 在这里面输入cmake ../.
这里的../.
表示CMakeLists.txt所在目录,cmake找的就是这个文件。 这样执行完了之后, build目录下面自动出来一个Makefile文件。 我们直接在本目录下make
这样, 就搞定啦。所以, Cmake编译大项目的时候还是非常方便的。
9. 小总
首先,还是先来一张导图把这一篇文章内容拎起来:
到这里, C++重温基础就过完了一遍, 这次学习基本上是参考C语言中文网的C++基础教程走的,然后在学习过程中,遇到疑问或者不懂的, 再通过查阅其他资料进行补充, 其他资料主要参考是《C++ Primer Plus》书籍,无可非议, 比较权威。
这里不是打广告,跟了一遍C++教程,确实觉得站长内容写的非常详细且内容之间非常连贯,非常适合构建知识体系, 所以也非常感谢站长的教程, 当然我整理的这几篇博客算是笔记,更是对我之前C++知识的查缺补漏, 如果想学习更多细节, 还是建议去读站长的原教程。
C++语言一直给我的感觉就是非常有魅力,当然这种魅力我也说不出在哪,就是比较喜欢。当然, 离熟悉还差很远,离精通更是遥不可及,不过这一遍学习,我算是把知识框架搭建了起来, 我觉得这个东西对我学习来说非常重要, 不管学习什么知识,我觉得只要我有框架, 我就能持续学习,不断扩充, 这是一个非常有意思的事情。 所以,这十二篇博客,后面会根据所学持续扩充, 我也知道, 每一篇都非常长,但每次遇到问题,回来查阅的时候,我会发现非常的舒服,几乎相关内容都连在一块,查起来非常高效。也希望能帮助到更多的伙伴啦。
趁着在学校的这半年时间, 我博客会补充各种基础知识,形成框架体系,因为我觉得这个时候是建立知识框架最好的时间,通过实习,我发现一旦工作,我会变得非常浮躁, 那时候不适合静心学习,搭建框架, 只适合根据实际应用把新知识进行补充,来使得框架变丰满,只可惜,我是最后半年才悟到的,得抓紧了哈哈。
- 语言方面: python和C++继续重温,依然是参考站长编写的教程, C++目前基础过完,后面会补几篇C语言之前不知道的关键知识,然后就是C++11新特性,然后就是STL库了,这个才是实际中常用的东西,感觉得系统走一遍
- python数据分析的库后面得走一遍, 为后面工作打基础
- Linux以及几个数据库教程走一遍,同样为后面工作打基础
学校的时间是打基础最好的时间,所以继续Rush 😉