• 每日一问15:C++中的.h,cpp以及.hpp文件


    每日一问15:C++中的.h,cpp以及.hpp文件

    1. 编译器角度的头文件(.h)和源文件(.cpp)

      先从编译器角度,来看一下头文件(.h)和源文件(.cpp):

      对于头文件(.h),在预处理阶段,头文件被包含到源文件后,它的使命就基本结束了。头文件包含了程序运行中可能需要用到的变量和函数等的声明,在编译过程中,编译器只检查所使用的函数和变量的声明是否存在,对于源文件中的实现并不关心。源文件编译后成生成目标文件(obj文件),目标文件中,这些函数和变量就视作一个个符号。链接器会将所有的目标文件链接起来,组成一个exe程序。在link的时候,需要在makefile里面说明需要连接哪个obj文件,此时,链接器去.obj文件中找在.cpp中实现的函数,再把他们build到makefile中指定的那个可以执行文件中。

    ​  一个.cpp对应一个.obj,然后链接器将所有的.obj链接起来,组成一个.exe程序。如果一个.cpp要使用另一个.cpp定义的函数,只需在这个.cpp中写上它的函数声明即可。链接器将所有的obj链接起来,但是如果碰巧有相同的函数或外部变量怎么办?C++可以通过一种叫做链接属性的关键字来限定,某个函数是属于整个程序公用的,还是只在一个编译单元obj里面使用,这些关键字就是extern(外部链接)和static(内部链接)。

    2. 为什么需要头文件(.h)和源文件(.cpp)

    1. 如果在h文件中实现一个函数体,那么如果在多个C文件中引用它,而且又同时编译多个C文件,将其生成的目标文件连接成一个可执行文件,在每个引用此头文件的C文件所生成的目标文件中,都有一份这个函数的代码,如果这段函数又没有定义成局部函数,那么在连接时,就会发现多个相同的函数,就会报错。

    2. 如果在h文件中定义全局变量,并且将此全局变量赋初值,那么在多个引用此头文件的C文件中同样存在相同变量名的拷贝,关键是此变量被赋了初值,所以编译器就会将此变量放入DATA段,最终在连接阶段,会在DATA段中存在多个相同的变量,它无法将这些变量统一成一个变量,也就是仅为此变量分配一个空间,而不是多份空间,假定这个变量在头文件没有赋初值,编译器就会将之放入 BSS段,连接器会对BSS段的多个同名变量仅分配一个存储空间 。

    3. 如果在C文件中声明宏,结构体,函数等,那么我要在另一个C文件中引用相应的宏,结构体,就必须再做一次重复的工作,如果我改了一个C文件中的一个声明,那么又忘了改其它C文件中的声明,这不就出了大问题了,如果把这些公共的东西放在一个头文件中,想用它的C文件就只需要引用一个就OK了!!!这样岂不方便,要改某个声明的时候,只需要动一 下头文件就行了

    4. 在头文件中声明结构体,函数等,当你需要将你的代码封装成一个库,让别人来用你的代码,你又不想公布源码,那么人家如何利用你的库中的各个函数呢?一种方法是公布源码,别人想怎么用就怎么用,另一种是提供头文件,别人从头文件中看你的函数原型,这样人家才知道如何调用你写的函数。

    3. 头文件(.h)和源文件(.cpp)中该放些什么

    头文件(.h)

      写类的声明(包括类里面的成员和方法的声明)、函数原型、#define常数等,但一般来说不写出具体的实现。在写头文件时需要注意,在开头和结尾处必须按照如下样式加上预编译语句(如下),目的是为了防止重复编译:

    #ifndef {Filename} 
    #define {Filename} 
    
    //{Content of head file} 
    
    #endif
    

    源文件(.cpp)

      源文件主要写实现头文件中已经声明的那些函数的具体代码。需要注意的是,开头必须#include一下实现的头文件,以及要用到的头文件。

      具体头文件和源文件中该放什么,分类如下:

    非模板类型(non-template) 模板类型(template)
    头文件 1.全局变量声明(带extern限定符)
    2.全局函数的声明
    3. 带inline限定符的全局函数的定义
    带inline 限定符的全局模板函数的声明和定义
    1. 类的定义
    2. 类函数成员和数据成员的声明
    3. 类定义内的函数定义(相当于inline)
    4. 带static const 限定符的数据成员在类内部的初始化
    5. 带inline限定符的类定义外的函数定义
    1. 模板类的定义
    2. 模板类成员的定义和声明(定义可以放在类内或者类外,类外不需要写inline)
    源文件 1. 全局变量的定义(及初始化)
    2. 全局函数的定义
    1. 类函数成员的定义
    2. 类带static限定符的数据成员的初始化

    ps:

    • 为什么inline函数的定义要放在头文件中?

      因为在大多数建置环境(build environments)中,inlining是在编译过程中进行的,而为了将一个函数调用替换为被调用函数的本体,编译器必须知道函数的具体实现。程序编译的时候,并不会去找.cpp文件中的函数实现,只有在link的时候才进行这个工作。我们在b.cpp中用#include “a.h”(这里的a.h和b.cpp是指这不是一对对应的头文件和源文件)实际上是引入相关声明,使得编译可以通过,程序并不关心实现是在哪里,是怎么实现的。

    4. 为什么引入.hpp文件

    ​  hpp文件,其实质就是将.cpp的实现代码混入.h头文件当中,定义与实现都包含在同一文件,则该类的调用者只需要include该cpp文件即可,无需再将cpp加入到project中进行编译。而实现代码将直接编译到调用者的obj文件中,不再生成单独的obj,采用hpp将大幅度减少调用 project中的cpp文件数与编译次数,也不用再发布烦人的lib与dll,因此非常适合用来编写公用的开源库。

    ​  同时,引入hpp文件很大一个原因就是模板。

    ​  对于模板,最重要的一点,就是在定义它的时候,编译器并不会对它进行编译,因为它没有一个实体可用。只有模板被具体化(specialization)之后(用在特定的类型上),编译器才会根据具体的类型对模板进行编译。所以才定义模板的时候,会发现编译器基本不会报错,也做不出智能提示。但是当它被具体用在一个类上之后,错误就会大片大片的出现,却往往无法准确定位。因为模板的这种特殊性,它并没有自己的准确定义,因此我们不能把它放在.cpp文件中,而要把他们全部放在.h文件中进行书写。这也是为了在模板具体化的时候,能够让编译器可以找到模板的所有定义在哪里,以便真正的定义方法。
      模板从本质上来讲很像宏,当模板在一个编译单元里被实例化时,它并不知道自己是否在别的编译单元也被实例化了。为了解决这个问题,目前的主流编译器,将每个模板的实例代码都单独存放在一个段里,每个段只包含一个模板实例。比如有个模板函数时add(),某个编译单元以int类型和float类型实例化了该模板函数,那么该编译单元的目标文件中就包含了两个该模板实例的段。在实例化的过程中,必须要知道模板函数的定义,所以模板函数的定义必须放在头文件中。(《程序员的自我修养》P113)

    这里的具体分析可以看下面参考博客中的4,5两篇。

    5. 使用hpp的注意事项

    ​  hpp文件也是一个.h文件,所以很多头文件(.h)的注意事项,对.hpp文件同样适用。适用hpp文件的注意事项如下:

    1. 不可包含全局对象和全局函数

      由于hpp本质上是作为.h被调用者include,所以当hpp文件中存在全局对象或者全局函数,而该hpp被多个调用者include时,将在链接时导致符号重定义错误。要避免这种情况,需要去除全局对象,将全局函数封装为类的静态方法。

    1. 类之间不可循环调用

      在.h和.cpp的场景中,当两个类或者多个类之间有循环调用关系时,只要预先在头文件做被调用类的声明即可,如下:

    class B;
    
    class A{
        public:
        	void someMethod(B b);
    };
    
    Class B{
        public:
        	void someMethod(A a);
    };
    

      在使用hpp的场景中,由于定义与实现都已经存在于一个文件,调用者必需明确知道被调用者的所有定义,而不能等到cpp中去编译。因此hpp中必须整理类之间调用关系,不可产生循环调用。同理,对于当两个类A和B分别定义在各自的hpp文件中,形如以下的循环调用也将导致编译错误:

    //a.hpp
    #include "b.hpp"
    class A{
        public:
        	void someMethod(B b);
    };
    
    //b.hpp
    #include "a.hpp"
    class B{
        public:
        	void someMethod(A a);
    };
    
    1. 不可使用静态成员

    ​  静态成员的使用限制在于如果类含有静态成员,则在hpp中必需加入静态成员初始化代码,当该hpp被多个文档include时,将产生符号重定义错误。唯 一的例外是const static整型成员,因为在vs2003中,该类型允许在定义时初始化,如:

    class A{
        public:
        	const static int a = 100;
    }
    

      如果需要在hpp中使用静态成员,可以考虑用其他方式来迂回实现:

    • 使用局部静态变量来模拟
    • 采用单例设计模式模拟实现

    参考博客:

    1. https://www.cnblogs.com/fenghuan/p/4794514.html
    2. https://blog.csdn.net/qq_30815237/article/details/88948632
    3. http://blog.chinaunix.net/uid-24118190-id-75239.html
    4. https://blog.csdn.net/u010608296/article/details/102483031
    5. https://www.cnblogs.com/Braveliu/p/12687632.html
  • 相关阅读:
    如果在写代码中遇到了问题,自己常用解决的网站
    软件过程与管理的发展历程
    软件项目论证
    软件项目管理概述
    软件工作量估计
    关键路径法(CMP)
    软件质量管理
    软件质量
    2022.3.3
    识别软件项目的活动
  • 原文地址:https://www.cnblogs.com/honernan/p/14622605.html
Copyright © 2020-2023  润新知