美文网首页
减少c++编译时间

减少c++编译时间

作者: 七_8f69 | 来源:发表于2018-07-26 10:42 被阅读0次

减少c++编译时间

前言:

因为自己查阅文章时经常遇到,这些问题:因为环境不同而导致结果不一致、因为文章跳跃度太大而导致无法理解、因为文章代码运行结果不同而苦恼。

所以为了看我文章的人不会遭遇同样的问题,

以后我的文章将遵循以下原则:

1.会在文章开头标明相关配置与环境

2.尽可能循序渐进(还是看不懂可能是我没有这样的天赋),标出阅读的前提知识

3.列出的代码自己先跑一遍

////////////////////////////////////////

语言:c++

前提知识:

1.类

2.继承

3.虚函数

集成开发环境:Visual Studio 2017

////////////////////////////////////////

文章:

c++中,变量、类或是函数必须定义之后才能调用,如:

int a;

class b{}

void c(){}

然而,定义的对象必须在其定义之后的地方使用,如:

void a()

{

/*

函数实现

*/

b();//错误,因为编译器并没有找到b()

}

void b()

{

/*

函数实现

*/

}

此时除了将函数b的定义提至函数a的定义前,之外,还有一种解决方法,那就是声明,如:

void b();//仅仅是声明,不包含其定义

void a()

{

/*

函数实现

*/

b();//正确,因为b()已经声明

}

声明后如果不定义还是会报错,因为没有定义的声明只是个空壳,就像目录,只有目录而没有内容的书是无用的。

平时在定义的时候,其实也是声明了,定义也是声明,声明告诉编译器对象的类型与名字,而编译器会根据名字找到相应的定义,所以就不用依赖于定义的位置。

那么来谈下#include,#include只接受一个参数--头文件,在c++中,头文件是声明的集中存放处,而#include头文件,其实就是将头文件的内容加到调用文件中去,所以调用文件就声明了头文件中声明的对象,所以就能够使用它们(如果他们实现了的话)

c++习惯将声明与定义分开,因为这样比较高效,在调用对象时不需要定义,仅需要有声明,而定义只需要有一份,其他文件调用这些对象时,只需要通过声明就能找到相应定义并且使用它们,减少了非必要的代码量。

#include看似很便利,但其实有隐藏的缺陷。使用#include,只要#include的任意一个头文件遭到修改,甚至是无关痛痒的修改,都又可能造成编译地狱,当项目变大,而文件之间的依存关系又比较高时,可能一次编译就直接让你等到下班,你老板的脸色绝对不会好看。

打个比方,有a,b,c,d,三个头文件,而b,c,d都#include了a,当你#include了b,c,d,而a又需要改动时,你就遭罪了,因为a的改动,所以b,c,d,和#inlcude了他们的文件通通需要重新编译,这只是一个比方,你的项目中可能还有这不少这种情况,一旦底层头文件遭到修改,那么直接或间接使用了这个头文件的都需要重新编译,就像多米诺骨牌一样根本停不下来。

作为提供产品的一方,我们得尽量减少我们修改对客户的影响,比如说我们修改了public的成员变量,那么所有使用了该变量的客户代码都需要修改,相反如果该变量是private的话,那么需要做修改的只是我们而不会牵连到用户。(所以成员变量尽可能的设为私有,不过那已经是另一个话题了)

所以当我们提供给客户头文件时,尽可能的斩断其他文件与我们提供的头文件的依存关系,下面提供两种方法达成该目标。

1.pimpl idiom(指向实现风格):

客户不需要知道我们提供产品的细节,客户只需要知道如何使用即可,好的产品甚至能够预防客户的错误调用与低级错误,因此我们将产品分成一个声明类和实现类,客户只需要#inlude声明类的头文件即可使用,而无论我们怎么修改实现类,只要声明类不动客户就不需要重新编译,就算我们修改了声明类,也仅需要编译声明类及其调用即可。

那么怎么实现呢。

首先声明类是通过调用实现类的函数来实现自己的函数的,所以声明类和实现类的函数名最好一致。

那么怎么调用实体类方法呢,首当其冲肯定是定义实现类的对象,因为不能#include实现类的头文件,所以需要使用前置声明。

//Person.h

#ifndef PERSON_H_

#define PERSON_H_

#include //标准程序库组件不该被前置声明,想要正确声明它们有难度,

                                    //而且就费的功夫与成效而言这样做并不值得

class PersonImpl;

class Person

{

public:

         Person(conststd::string&);

         std::stringgetName()const;

         ~Person();

private:

         PersonImplpImpl;//报错,因为编译器必须在编译期间知道对象的大小,

                          //而PersonImpl只有声明,编译器无从得知其大小

};

#endif // PERSON_H_

下面才是正确的

//Person.h

#ifndef PERSON_H_

#define PERSON_H_

#include //为了使用智能指针管理指针

#include

class PersonImpl;

class Person

{

public:

         Person(conststd::string&);

         std::stringgetName()const;

         ~Person();

private:

         std::shared_ptrpImpl;//正确,因为指针大小都是一致的,且编译器可以提前得知

};

#endif // PERSON_H_

所以我们将所有实现细节隐藏在一个指针背后,通过指针调用实现函数,以下给出完整代码

//Person.h

#ifndef PERSON_H_

#define PERSON_H_

#include //为了使用智能指针管理指针

#include

class PersonImpl;//前置声明

class Person

{

public:

         Person(conststd::string&);

         std::stringgetName()const;

         ~Person();

private:

         std::shared_ptrpImpl;//正确,因为指针大小都是一致的,且编译器可以提前得知

};

#endif // PERSON_H_

//

//Person.cpp

#include "Person.h"

#include "PersonImpl.h"//必须类定义式才能调用其成员函数

Person::Person(const std::string &name):pImpl(new PersonImpl(name))

{

}

std::string Person::getName() const

{

         returnpImpl.get()->getName();

}

Person::~Person()

{

}

//

//PersonImpl.h

#ifndef PERSONIMPL_H_

#define PERSONIMPL_H_

#include

class PersonImpl

{

public:

         PersonImpl(conststd::string&);

         ~PersonImpl();

         std::stringgetName()const { return this->Name; }

private:

         std::stringName;

};

#endif // PERSONIMPL_H_

//

//PersonImpl.cpp

#include "PersonImpl.h"

PersonImpl::PersonImpl(const std::string& name):Name(name)

{

}

PersonImpl::~PersonImpl()

{

}

//

1.接口:

使用java和c#的朋友对接口应该不会陌生,这是一种惯用的隐藏细节、实现多态的手法,而c++并没有相应的关键字,但是我们可以根据原理实现它通过抽象类。

抽象类及不可以生成对象的类,在java和c#中也有抽象类,但是它们两者都是奉行单继承,多接口的主义,与之相比c++奉行多继承,虽然可能造成很多问题,但毫无疑问给了程序员更多的自由。c++中抽象类即是带有纯虚函数的类,使用抽象类充当接口甚至能在接口中实现成员变量与成员函数,这提供的巨大弹性有其独特的作用。

具体接口代码如下:

//human.h

#ifndef HUMAN_H

#include

#include

class human

{

public:

         staticstd::shared_ptr create(const std::string&);//通过工厂方法,隐藏实现类

         virtual~human();

         virtualstd::string getName()const=0;//与pimpl相比,接口的函数名必须与实现类接口完全一样

private:

};

#endif // !HUMAN_H_

//

//HumanImpl.h

#pragma once

#ifndef HUMANIMPL_H_

#define HUMANIMPL_H_

#include "human.h"

class HumanImpl:public human

{

public:

         HumanImpl(conststd::string & name);

         virtual~HumanImpl();

         std::stringgetName()const { return this->Name; }

private:

         std::stringName;

};

#endif // !HUMANIMPL_H_

//

//HumanImpl.cpp

#include "HumanImpl.h"

HumanImpl::HumanImpl(const std::string& name):Name(name)

{

}

HumanImpl::~HumanImpl()

{

}

std::shared_ptrhuman::create(const std::string & name)

{

         returnstd::shared_ptr(new HumanImpl(name));

}

//

在上述代码中,我们使用了工厂方法提供指针给客户,客户通过指针调用相应方法,从而隐藏实现类,至此除了更改接口,否则客户不需要重新编译。

至此,我们两种方法都介绍完毕,当然任何方法都有其缺点。

pimpl中我们通过指针调用实现方法,每次访问都是间接访问,每个对象都需要增加实现指针的内存大小,并且实现指针必须初始化指向一个动态分配的实现对象,因而我们需要承受因动态内存分配带来的额外开销,及其bad_alloc异常的可能性。

而接口类中,我们每个函数都是virtual,因此每次调用函数都有一个间接跳跃的成本。同时virtual函数隐藏了一个vptr指针,这个指针显然会增加我们的内存消耗。

所以我们在编写代码的过程其实就是一个不断择优的过程,也许我们可以承受更多的编译时间,却无法承受为了缩短编译时间所带来的消耗。没有最好的方法,学习不止,学无止境。

结尾

这是本人第二篇文章,对于编译器如何处理头文件(.h文件)和实现文件(.cpp)这个问题本人理解的还不太清晰,有错希望能够指出,枯燥还有请多多担待。

相关文章

网友评论

      本文标题:减少c++编译时间

      本文链接:https://www.haomeiwen.com/subject/crwvmftx.html