查资料的过程中有一句话印象深刻,这里记录下来:
教科书上的条条款款和真正的工程实践是有出入的,所以不要纸上谈兵
问题引入
为什么会有这个问题的出现呢?编译器需要在编译期间需要知晓类型的大小!
在大工程中,.cpp代码很多,而如果不管它的依赖性问题,那么很可能更改一个.h文件,就会导致众多包含这个.h文件的.cpp文件一起被重新编译,即使其他文件什么也没有改动,这个问题的本质在于C++并没有把"接口从实现中分离"做得很好, 举个例子:
// person.h
class Person {
public:
Person(const std::string& name,const Date& birthday,const Address& addr);
std::string name() const;
std::string birthDate() const;
std::string address() const;
private:
std::string theName;
Date theBirthDate;
Address theAddress;
};
// person.cpp
person.h 这个头文件中包含了整个Person的实现细节,
// test.h
#include "person.h"
class Test {
private:
Person person;
}
// test.cpp
test.h包含了这个.h文件
// main.cpp
#include "test.h"
main.cpp 包含了test.h
这个时候我对person.h做了一些改动,无论是什么改动,哪怕其他地方没有使用, main.cpp,person.cpp,test.cpp全部得重编,在大项目中,编译时间是相当长的,所以我们要减少编译依赖。
.h文件中包含了十足的细节上的实现,只要实现被改变了,其他include 它的 .h文件的.cpp文件都得进行重编
把所有cpp代码包含在一起
先讲简单的方法 all.cpp !!!
#include 指令的本质就是把那个文件的东西全部盖过来,那么我创建一个 all.cpp, 然后如下操作
// all.cpp
#include "main.cpp"
#include "person.cpp"
#include "test.cpp"
我们的编译只需要编译一个all.cpp文件,其他文件都不用编译!虽然不管改个什么东西都需要进行重编,但是只有一个文件啊!快了不止一个数量级,cmake这样写,
cmake_minimum_required(VERSION 3.15)
project(make_depend)
include_directories(${CMAKE_BINARY_DIR})
add_executable(test_make "all.cpp")
可能这个比较粗糙,但是在工程实践中有相当成功的例子,Opera 浏览器使用这种技巧将编译时间从半小时降到 5 分钟!并且没有什么缺陷了,要硬说缺陷,我也可以例几条:
- 增量编译失效,随便改个代码都需要重编(但是好像问题不大)
- 并行编译失效,无论是分布式编译还是利用本地的多核都没用了,因为只编译一个文件了(这好像是好事)
- 内存占用高,一次性加载了整个项目的代码
pimpl idom(类和类的实现分开)
我在.h文件中隐藏真实的实现,而只提供接口不就好了
接口如下:
// person.h
#pragma once
class Person {
public:
Person();
~Person();
void get_name();
private:
class PersonImpl *imp; // 定义实现类的指针,指针的大小是确定的!
};
// person.cpp
#include "person.h"
#include "person_impl.h"
Person::Person() {
imp = new PersonImpl;
}
Person::~Person() {
delete imp;
}
void Person::get_name() {
imp->get_name();
}
实现如下:
// person_impl.h
#pragma once
#include <string>
class PersonImpl {
public:
void get_name();
private:
std::string name = "testa";
};
// person_impl.cpp
#include "person_impl.h"
#include <iostream>
void PersonImpl::get_name() {
std::cout << name << std::endl;
}
main 文件:
// main.cpp
#include <bits/stdc++.h>
#include "person.h"
#include "test.h"
using namespace std;
int main() {
Person person;
person.get_name();
}
现在的编译依赖变成了这样:
main.cpp -> person.h
person.cpp -> person.h, person_impl.h
person_impl.cpp -> person_impl.h
现在我更改person 的实现部分,也就是 person_impl.h person_impl.cpp 就再也不会影响到main了,
但是这种方式的缺陷也很明显:
- 在多年以后,再看整个代码时你确定你不会被你自己的代码恶心到吗?找个实现跳转不知道要多少层,就是代码不清晰
- 对于关键.h, 被大多数文件所引用的,使用这种方法有效。但是很多.h没有被太多文件所引用
- 就因为它是指针,你没办法在栈上。。。还得写个辅助的工厂类