C++中的.h,cpp以及.hpp文件

2 篇文章 0 订阅

每日一问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文件中进行书写。这也是为了在模板具体化的时候,能够让编译器可以找到模板的所有定义在哪里,以便真正的定义方法。

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

5. 使用hpp的注意事项

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

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

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

  2. 类之间不可循环调用

    在.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);
    };
    
  3. 不可使用静态成员

​ 静态成员的使用限制在于如果类含有静态成员,则在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
  • 16
    点赞
  • 71
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值