在C++中随着程序越来越复杂,我们希望把程序的各个部分分别储存在不同的文件中。C++支持的分离式编译(separate compilation)允许我们把程序分割到几个文件中去,每个文件独立编译。

头文件以.h为后缀,主要包含类和函数的声明;实现文件以.cpp为后缀。可以这样理解,头文件中包含就是一些接口声明,而实现文件就是对这些接口进行定义。
例如:

文件:Num.h

class Num{
private:
int num;
public:
Num();
Num(int);
int getNum();
};

文件:Num.cpp

#include "Num.h"

Num::Num() : num(){}
Num::Num(int n) : num(n){}
int Num::getNum(){
return num;
}

文件:NumTest.cpp

#include <iostream>
#include "Num.h" using namespace std; int main(int argc,char **argv){
Num n();
cout << n.getNum() << endl;
return ;
}

然后使用如下命令进行编译:

g++ NumTest.cpp Num.cpp -o NumTest

注意:必须在任何使用Num类的地方添加上 #include "Num.h" ,上面的案例中 NumTest.cpp 和 Num.cpp 文件都使用到了Num类,应此都必须在文件顶部引入 Num.h 文件。

#ifndef

有时我们可能会多次包含同一个文件,在C++中出现重复声明是不允许的。在上面的案例中 Num.cpp 和 NumTest.cpp 文件都包含了Num.h文件,但Num.cpp和NumTest.cpp是分开编译的,所以不会出现重复定义。
为了演示该错误,我们重新定义一个文件。

文件:Foo.h

#include "Num.h"

class Foo{
public:
Num n;
};

注意:这里没有Foo.cpp文件,因为Foo.h头文件没有什么需要被实现的。

接下来进行测试

文件:NumFooTest.cpp

#include <iostream>
#include "Num.h"
#include "Foo.h" using namespace std; int main(int argc,char **argv){
Num n();
cout << n.getNum() << endl; Foo f;
cout << f.n.getNum() << endl;
return ;
}

当笔者试图使用 g++ NumFooTest.cpp Num.cpp -o NumFooTest 命令进行编译时,出现如下的错误信息:

In file included from Foo.h::,
from NumFooTest.cpp::
Num.h::: error: redefinition of ‘class Num’
In file included from NumFooTest.cpp:::
Num.h::: error: previous definition of ‘class Num’
main.cpp: In function ‘int main()’:
main.cpp::: error: ‘class Foo’ has no member named ‘num’

从错误信息中可以看出错误原因是Num类重复定义了,为了解决这种问题可以使用#ifndef,#ifndef的功能可描述为如下:“如果宏语句未定义语句1则执行程序2,否则执行程序3”。
例如:

#ifndef NUM_H
#define NUM_H
<define class or whatever else>
#endif

当首次编译时 NUM_H 未被定义,所以 #ifndef NUM_H 条件为真,然后会执行 #define NUM_H 定义 NUM_H 并且执行我们定义的其他操作。若其后再次编译到该文件,由于 NUM_H 已经被定义了,所以 #ifndef NUM_H 条件为假,也就不会执行 #ifndef 到 #endif 间的任何代码。在知道了 #ifndef 的原理后,我们知道只需要将 #ifndef 语句应用到上面的 Num.h 文件,则会解决重复定义的问题。

文件:Num.h

#ifndef NUM_H
#define NUM_H
class Num{
private:
int num;
public:
Num();
Num(int);
int getNum();
};
#endif

#pragma once

除了是使用#ifndef语句避免重复定义,还可以将#pragma once添加到文件的开头,也可以完成同样的功能。Visual Studio默认使用的就是#pragma once。

文件:Num.h

#pragma once
class Num{
private:
int num;
public:
Num();
Num(int);
int getNum();
};

分离编译

完成分离式编译的步骤:
1.将.cpp文件编译为对象文件,该对象文件包含.cpp文件的机器码。
2.将对象文件链接到可执行文件。

将.cpp编译为对象文件,可以使用g++加上命令行选项 -c

g++ -c NumFooTest.cpp Num.cpp

这行命令会产生 NumFooTest.o 和 Num.o 对象文件。

然后将它们链接为可执行文件,我们再次使用g++命令:

g++ NumFooTest.o Num.o -o NumFooTest

这样就会产生可以执行文件NumFooTest。

如果改变了NumFooTest.cpp文件,我们只需要重新编译NumFooTest.cpp文件就行了

g++ -c NumFooTest.cpp

得到NumFooTest.o对象文件。

然后链接为可执行文件:

g++ NumFooTest.o Num.o -o NumFooTest

只编译变动过的文件可以为我们节约编译时间,大多数的IDE会自动帮助我们完成这一部分功能。

make命令

在一个很大的项目中很难去手动编译很多文件。如果你使用IDE的话,这些命令可以由IDE代劳。但如果未使用IDE的话,那么必须手动去完成。在大型项目中,使用 g++ 手动链接成百的文件,这是不可能的。
这时可以使用make命令,首先创建一个文件Makefile,然后在其中添加我们编译需要依赖的信息。

文件:Makefile

CFLAGS = -O
CC = g++
NumTest: NumTest.o Num.o
$(CC) $(CFLAGS) -o NumTest NumTest.o Num.o
NumTest.o: NumTest.cpp
$(CC) $(CFLAGS) -c NumTest.cpp
Num.o: Num.cpp
$(CC) $(CFLAGS) -c Num.cpp
clean:
rm -f core *.o

然后输入 make 命令,Linux就会自动去解析 Makefile 文件。

关于Makefile文件需要注意两点:

第一,Makefile 文件的名称是固定的(不能改变,没有后缀);

第二,缩进是Tab不是space。

接下来解析一下上面的命令:

NumTest: NumTest.o Num.o 

表示 NumTest 由 NumTest.o 和 Num.o 生成。

$(CC) $(CFLAGS) -o NumTest NumTest.o Num.o

其中 $(CC) 会被替换为 g++ , $(CFLAGS) 会被替换为 -0 ,替换后的命令会生成 NumTest 文件。文件中的其他命令也是类似的道理,最后的clean表示清理文件, rm -f core *.o 会删除所有以o结尾的文件(也就是生成 NumTest 的所有中间文件——对象文件)。

上面的文件内容依然比较复杂,如果换成如下的命令,除了可以使用上面那种格式,还可以在Makefile中使用下面这种格式:

CFLAGS = -O
CC = g++
SRC = NumTest.cpp Num.cpp
OBJ = $(SRC:.cpp = .o)
NumTest: $(OBJ)
  $(CC) $(CFLAGS) -o NumTest $(OBJ)
clean:  
  rm -f core *.o

参考文章:C++ Separate Header and Implementation Files

【C++】C++中的分离式编译的更多相关文章

  1. C++中重定义的问题——问题的实质是声明和定义的关系以及分离式编译的原理

    这里的问题实质是我们在头文件中直接定义全局变量或者函数,却分别在主函数和对应的cpp文件中包含了两次,于是在编译的时候这个变量或者函数被定义了两次,问题就出现了,因此,我们应该形成一种编码风格,即: ...

  2. C++ 不支持模版的分离式编译

    1.C++不支持模版的分离式编译,为什么? C++是分别,单独编译,对于每个cpp文件,预编译为编译单元,这个编译单元是自包含文件,编译的时候,不需要其他的文件,编译好了,生成obj文件,然后连接成e ...

  3. 【转】为什么C++编译器不能支持对模板的分离式编译

    出处:刘未鹏(pongba) http://blog.csdn.net/pongba)   首先,一个编译单元(translation unit)是指一个.cpp文件以及它所#include的所有.h ...

  4. C++ —— 类模板的分离式编译

    目录 对于C++中类模板的分离式编译的认识 具体的实例 1.对于C++中类模板的分离式编译的认识 为什么C++编译器不能支持对模板的分离式编译(博文链接) 主要内容:编译器编译的一般工作原理.对模版的 ...

  5. 【c++ primer, 5e】函数声明 & 分离式编译

    p186~p188: 函数声明1.函数只能定义一次,但是可以声明多次. 2.函数的接口:返回类型 + 函数名 + 形参类型 3.为什么要在头文件中进行函数声明???在源文件中定义?暂时理解到,这么做可 ...

  6. 为什么C++编译器不能支持对模板的分离式编译

    首先,一个编译单元(translation unit)是指一个.cpp文件以及它所#include的所有.h文件,.h文件里的代码将会被扩展到包含它的.cpp文件里,然后编译器编译该.cpp文件为一个 ...

  7. DelphiXE10.1项目中增加预编译的方法

    操作: 菜单选择Proceject->Options->Delphi Compilerz在Conditional Defines(第一行)中添加预编译标识.例:VCL代码:uses{$IF ...

  8. iOS中的预编译指令的初步探究

    目录 文件包含 #include #include_next #import 宏定义 #define #undef 条件编译 #if #else #endif #if define #ifdef #i ...

  9. Spark入门实战系列--2.Spark编译与部署(中)--Hadoop编译安装

    [注]该系列文章以及使用到安装包/测试数据 可以在<倾情大奉送--Spark入门实战系列>获取 .编译Hadooop 1.1 搭建环境 1.1.1 安装并设置maven 1. 下载mave ...

随机推荐

  1. 002.MySQL高可用主从复制部署

    一 基础环境 主机名 系统版本 MySQL版本 主机IP master CentOS 6.8 MySQL 5.6 172.24.8.10 slave01 CentOS 6.8 MySQL 5.6 17 ...

  2. Django 学习第九天——请求与响应

    一.HttpRequest 对象: 服务器接收到http协议的请求后,会根据报文创建 HttpRequest 对象视图函数的第一个参数是HttpRequest 对象再django.http 模块中定义 ...

  3. SpringBoot返回json和xml

    有些情况接口需要返回的是xml数据,在springboot中并不需要每次都转换一下数据格式,只需做一些微调整即可. 新建一个springboot项目,加入依赖jackson-dataformat-xm ...

  4. Xamarin Essentials教程获取路径文件系统FileSystem

    Xamarin Essentials教程获取路径文件系统FileSystem 文件系统用于管理设备内的各类文件.通过文件系统,应用程序可以创建永久文件和临时文件,也可以获取预先打包的文件,如预设数据库 ...

  5. 2153 ACM 仙人球的残影 输出格式

    题目:http://acm.hdu.edu.cn/showproblem.php?pid=2153 中文题目,很简单,但是要注意输出格式,题目中三个字符长度 输出格式:%3d (整数) 思路:将输出看 ...

  6. Scrapy基础(十二)————异步导出Item数据到Mysql中

    异步导出数据到Mysql中 上次说过从Item中同步写入数据库,因为网络的下载速度和数据库的I/O速度是不一样的所以有可能会发生下载快,但是写入数据库速度慢,造成线程的堵塞:关于堵塞和非堵塞,同步和异 ...

  7. Linux下载软件

    .yum yum install tree yum install telnet 直接安装与更新 .查询软件是否装上 rpm -qa tree telnet .查询软件包的内容 rpm -ql tre ...

  8. ZOJ3967 : Card Game

    比赛的时候因为卡内存,在抠内存的时候改错了,导致赛内没有AC,赛后发现数组开的很小都可以AC. 分析题意我们发现,这题需要求出所有存在的直线形成的上凸壳,那么查询$[L,R]$时在凸壳上二分导数,找到 ...

  9. .net异常机制

    异常机制简介 当CPU运行到一些非法的指令,例如除零错误,访问内存页失败等指令,CPU会生成一个硬件异常,不同的异常有固定的异常代码作为标识符,异常产生以后CPU暂时不能继续执行后续的指令—因为后续的 ...

  10. JsonServer服务环境搭建

    在前后端分离的这种工作模式下,分工明确,各司其职.前端负责展示数据,后端提供数据.然而,在这种过程中对于接口的规范 需要提前制定好.例如根据规范提前模拟数据,这个时候就比较麻烦的.JsonServer ...