C++重温笔记(十二): C++多文件编程

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种:

  1. 使用宏定义避免重复引入
    实际多文件开发中,往往使用下面的宏定义来避免重复引入:

    #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 类也仅会定义一次。再次执行该项目会发现,其可以正常执行。

  2. 使用#pragma once避免重复引入
    除了第一种最常用方式之外,还可以使用#pragma one指令,将其附加到指定文件的最开头位置。则该文件只会被#include一次。

    #prama once
    class Student{
    	//.....
    };
    

    #ifndef是通过定义独一无二的宏来避免重复引入的, 这意味每次引入头文件都要进行识别,效率不高。 但考虑C和C++都支持宏定义,所以项目使用#ifndef规避可能出现的"头文件重复引入"问题,不会影响项目的可移植性。

    #pragma once不涉及宏定义,当编译器遇到它时就会立刻知道当前文件只引入一次,所以效率很高。 但值得一提的是,并不是每个版本编译器都能识别#pragma one指令,所以兼容性可能不是很好。 另外, #pragma once只能作用于某个具体文件, 无法像#ifndef那样仅作用于指定的一段代码。

  3. 使用_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常量, 才能在其他文件中使用呢? 有下面三种方法:

  1. 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头文件中,只需要引入头文件, 就可以使用。

  2. 借助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 常量的可见范围恢复至整个项目。

  3. 借助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++执行此项目。

  1. 预处理阶段, 生成.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 语言的宏命令,作用于程序执行的预处理阶段,其功能是将它后面所写文件中的内容,完完整整、一字不差地拷贝到当前文件中。

  2. 经历编译阶段,即对预处理阶段得到的 -i 文件做进一步的语法分析,生成相应的汇编代码文件。

    g++ -S main.i -o main.s
    g++ -S student.i -o student.s
    

    其中, -S选项用于限定g++编译器对指定文件编译,得到的汇编代码通常以".s"作为后缀名。我们这里依然是看下"cat main.s"。
    在这里插入图片描述
    这语言就不是我们能看懂的了,都是汇编语言了。

  3. 经历汇编阶段,即将汇编代码文件转换成可以执行的机器指令, 也就是二进制机器码 -- > 生成目标文件,二进制机器码

    g++ -c mian.s -o main.o
    g++ -c student.s -o student.o
    

    -c指令限定g++编译器只进行汇编操作, 最终生成目标文件(本质是二进制文件)以".o"作为后缀名。
    这个依然是看一眼:
    在这里插入图片描述
    这个更不是我们能看懂的了。

  4. 经历链接阶段,即将所有的目标文件组织成一个可以执行的二进制文件

    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文件中。

  1. 头文件中可以定义const对象
    全局的 const 对象默认是没有 extern 声明的,所以它只在当前文件中有效。把这样的对象写进头文件中,即使它被包含到其他多个 .cpp 文件中,这个对象也都只在包含它的那个文件中有效,对其他文件来说是不可见的,所以便不会导致多重定义。

    与此同时,由于这些 .cpp 文件中的 const 对象都是从一个头文件中包含进去的,也就保证了这些 .cpp 文件中的 const 对象的值是相同的,可谓一举两得。
    同理, static对象的定义也可以放进头文件
  2. 头文件中可以定义内联函数
    内联函数(inline 修饰)是需要编译器在编译阶段根据其定义将它内联展开的(类似宏展开),而并非像普通函数那样先声明再链接。这就意味着,编译器必须在编译时就找到内联函数的完整定义。
  3. 头文件中可以定义类
    在程序中创建一个类的对象时, 编译器只有在这个类的定义完全可见情况下,才能知道这个类对象应该如何布局。所以关于类的定义要求,和内联函数是一样的,即把类定义放进头文件, 然后再使用这个类的.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五个重要的东西: 显示规则, 隐晦规则,变量定义, 文件指示和注释

  1. 显示规则:
    通常在写makefile时使用的都是显式规则,这需要指明target和prerequisite文件。
    一条规则可以包含多个target,这意味着其中每个target的prerequisite都是相同的。
    当其中的一个target被修改后,整个规则中的其他target文件都会被重新编译或执行。

  2. 隐晦规则:
    make的自动推导功能所执行的规则

  3. 变量的定义:
    Makefile中定义的变量,一般是字符串

  4. 文件指示:
    Makefile中引用其他Makefile;指定Makefile中有效部分;定义一个多行命令

  5. 注释:
    Makefile只有行注释 “#”, 如果要使用或者输出"#"字符, 需要进行转义, "#

下面就拿上面那个student.cpp和main.cpp编译的例子来编写MakeFile文件。 LearningC目录下把其他的文件删除,只保留两个.cpp文件和一个.h文件。
在这里插入图片描述
编写MakeFile文件, 同样是建立一个文件, 然后在里面开写。

  1. 编写clean, 作用是删除所有的.o文件和可执行文件, 以防带来杂乱干扰

    clean:
    	rm *o student
    

    这里要是tab

  2. 编写目标文件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
    

    和上面格式要对应上

  3. 编写目标文件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_requiredcmake_minimum_required(VERSION major[.minor[.patch[.tweak]]][FATAL_ERROR])用于指定需要的CMake 的最低版本cmake_minimum_required(VERSION 3.10)
projectproject( [languageName1 languageName2 … ] )用于指定项目的名称,一般和项目的文件名称对应project(student)
add_executableadd_executable( [WIN32] [MACOSX_BUNDLE][EXCLUDE_FROM_ALL] source1 source2 … sourceN)用于指定从一组源文件 source1 source2 … sourceN 编译出一个可执行文件且命名为nameadd_executable(student student.cpp main.cpp)
include_directoriesinclude_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 😉

  • 106
    点赞
  • 465
    收藏
    觉得还不错? 一键收藏
  • 13
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值