C++:include:理解 C++ 中的头文件和源文件的作用

关于头文件和源文件我们主要围绕:

  • C++编译模式

  • 声明和定义区别,

  • 符号只能被定义一次,

  • 符号被定义在多个源文件,但是一个源文件只能定义一次

这四个方面来分析论述

 1:C++ 编译模式

  1. 在一个C++程序中,只包含两类文件,.cpp和.h 文件,其中,.cpp文件被称为C++源文件,里面放的是C++源代码;而.h 文件则被称为C++ 头文件,里面放的也是C++源代码。
  2. C++语言支持 “分别编译”(separatecompilation),也就是说,一个程序所有的内容,可以分成不同的部分分别放在不同的.cpp文件里。.cpp文件里的东西是相对独立的,在编译compile时不需要与其他文件互通,只需要在编译成目标文件后再其他目标文件做一次链接 Link即可。
  3. 比如在文件a.cpp 中定义了一个全局函数 “void a(){}” ,在b.cpp中需要调用这个函数,即便这样,文件a.cpp和文件b.cpp并不需要相互知道对方的存在,而是分别堆他们进行编译,编译成目标文件 obj 之后在链接,链接成功,整个程序就可以运行了。
  4. 那么上面是如何实现的?从写程序的角度来讲,很简单,在文件 b.cpp 中,在调用“void a()”函数之前,先声明一下这个函数就可以。这是因为编译器在编译 b.cpp的时候会生成一个 符号表(symbol table), 像 void a(){} 这样的看不到定义的符号,就会被存放在这个表中,在进行链接时,编译器就会在 其他的目标文件中去寻找这个符号的定义,一旦找到了,程序就可以顺利生成了。

2:定义和声明

  • 简单来说,定义就是把一个 “符号”完整的描述出来,它是变量还是函数,返回什么类型,需要什么参数等。
  • 声明:这是声明这个符号的存在,告诉编译器,这个符号是在其他文件中定义的,我在这里先用着,你在链接的时候去别的地方寻找它到底是什么样的
  • 定义的时候要按照C++语法完整的定义一个符号(变量或者函数),而声明的时候就需要写出这个符号的原型
  • 一个符号在程序中可以被声明多次,但是只能被定义一次
  • 这种声明定义机制,给C++程序员带来很多好处,同时引出一种编写程序的方法,考虑一下,如果有一个很常用的函数 “void f(){ }”  在程序中被 需要.cpp 文件调用,那么我们只需要在一个文件中定义这个函数,而在其他的文件中声明这个函数就可以了。如果有很多函数的话,不可能让每个需要用到这些函数的 .cpp 文件都去声明吧,所以这里为们就引出了 .h 头文件

3:什么是头文件

  • 为了解决如果有很多函数,需要被很多.cpp 源文件引用,我们可以将声明这些函数放在一个位置,等需要用时,直接复制过来就可以,这个位置就是头文件发挥作用了。
  • 所谓头文件其实它的内用跟.cpp文件时一样的,都是 C++的源代码,但是头文件不用被编译,我们吧所有的函数声明全部放进一个头文件中,当某一个.cpp 源文件需要他们时,他们就通过一个宏命令 “ #include” 包含进这个 .cpp 文件中,从而把他们的内用 复制合并到 .cpp文件中去。 当 .cpp文件被编译时,这些被包含进行的 .h 文件便发挥作用了。

比如我们有一个数学函数  fun1()  和 fun2() ,

  1. 我们可以把他们的定义放在  math.cpp 里面
  2. 函数的声明刚在 math.h 里
  3. 在另一个文件 main.cpp 中我们需要使用这两个函数,那么只需要把头文件包含进来就可以
math.cpp

double f1()
{
    //do something here....
    return;
}
double f2(double a)
{
    //do something here...
    return a * a;
}
math.h


double f1();
double f2(double);
main.cpp 

#include "math.h"
main()
{
    int number1 = f1();
    int number2 = f2(number1);
}

这样一个完整的程序就成功了,需要注意的是 .h头文件不用写在编译器的命令之后,但是必须要在编译器可以找到的地方(比如跟 main.cpp 在同一个目录下),那么 main.cpp 和 math.cpp就都可以分别通过编译,生成 main.o  和math.o ,然后再把这两个目标文件进行链接,程序就可以运行了

4: #include

  1. #include 是来自C语言的宏命令,他在编译器进行编译之前,即在预编译的时候就会起作用。
  2. #include 的作用就是把它后面后面的那个文件的内容,完完整整的,一字不改地包含到当前的文件中来。它本身是没有其他任何作用与副功能的,它的作用就是把每一个它出现的地方,替换成 它后面文件的内容,就是简单的文本替换,别无其他,仅此而已。
  3. 所以main.cpp 文件中的第一句 (#include "math.h")在预先=编译时就会被替换成math.h 文件的内容,所以在编译的过程即将要开始,main.cpp的内容已经发生改变。
main.cpp

double f1();
double f2(double);
main()
{
    int number1 = f1();
    int number2 = f2(number1);
}

5: 头文件应该写些什么

  • 通过上面的论述,我们知道,头文件的作用就是被其他的.cpp文件包含进去的,它们本身并不参与编译,但实际上,他们的内容却在多个 .cpp文件中得到了编译,通过 “”定义只能有一次的规则” , 我们很容易得出,头文件应该只放变量和函数的声明。而不能放他们的定义。
  • 因为一个头文件实际上会被多个不同的 .cpp 文件使用,并且他们都会参与编译,如果放了定义,那么就会出现这个情况:在多个文件中出现了对于一个符号(变量或者函数)的定义,虽然这些符号是相同的,但是对于编译器来说,这就是不合法的。如果放声明,那就没事了。
  • 所以,我们应该记住一点: .h 头文件中,只能存在变量或者函数的声明,而不要放定义。所以只能写形如 : extern int a 和 void function() 这样的语句, 这些才是声明。 如果这样写 : int  a  或者  void  function(){}  ,一旦头文件被两个或者两个以上的 .cpp 文件包含的话,编译器就会立马报错。

例外项:下面三种情况可以在头文件中定义符号

  1. 头文件中可以写 const 对象的定义 和 static 对象的定义。 因为全局的 const对象默认没有 extern声明,所以在只在当前文件有效,把这样的对象写在头文件中, 即使它后来被其他多个 .cpp 文件包含进去,那么这个对象也只在包含它的那个文件中有效, 对其他文件来说是不可见的,所以 便不会出现多重定义的情况 。同时,这些 .cpp 文件中的该对象都是从一个头文件中包含进去的,这样也可以保证 这些 .cpp 文件中的这个 const对象的值是相同的,可谓一举两得。
  2. 头文件中写  内联函数 (inline)的定义 。因为 inline 函数 是需要编译器在遇到它的地方根据定义把它内联展开的。 并不是普通函数:先声明在链接(内联函数不会链接),既然需要内联展开,编译器就需要在编译时看到内联函数完整的定义,所以内联函数 就需要直接定义。那么问题来了 是在 头文件还是.cpp 文件中定义了 ? 是否想普通函数一样只能定义一次了 ?  试想一下:如果内联函数和其他普通函数一样只能定义一次的话,那就这种情况有点难办,因为内联函数 在一个文件还好,我们可以把内联函数的定义写在最开始的地方,这样可以保证后面使用的时候都可以见到定义。 但是,如果我在其他文件中还使用到了 这个函数该怎么办了 ?这好像也没什么太好的方法。 所以 C++ 规定 ,内联函数可以在程序中定义多次。 只要内联函数在一个 .cpp文件中只出现一次即可。 而且在所有的.cpp 文件中,这个内联函数的定义都是一样的,C++ 就认为能通过编译。 那么如何达到上面两个要求了,显然将内敛函数定义在 头文件中,是非常明智的。
  3. 头文件写类(class)定义 。 因为在程序中创建一个类的对象时,编译器只有在这个类的定义完全可见的情况下,才能知道这个类的对象该如何布局,所以关于类的定义要求,跟内联函数是一样的。所以把类的定义放在头文件中,在使用这个类的.cpp文件中包含这个头文件,是一个很好的办法。 注意:类的定义中包含着数据成员和函数成员,数据成员需要等到具体对象被创建才会被定义(分配空间)但是函数成员需要在一开始就被定义,这也是我们所说的类的实现。 
  •  一般而言:我们把类的定义放在头文件中,把函数成员的实现代码放在一个.cpp文件中。
  • 或者直接把成员函数的实现代码写进类定义里面,一起放进头文件中(在C++中,如果函数成员在类的定义体中被定义,那么编译器慧认为这个函数是内联函数) 注意:如果成员函数没有写进 类体中,指是写在类的头文件中,此时编译器不认为此函数是内联函数,那么一旦头文件被两个或者两个以上的.cpp 文件包含,这个函数就被重定义了。

6: 头文件的保护措施

  • 考虑一下:如果头文件中只包含声明的语句,那么它被同一个.cpp文件包含再多次都没问题,因为声明语句的出现是不受限制的
  • 但是如果出现上面说的 头文件例外情况中的任何一种,如果它再被一个.cpp文件包含多次的话,就会出现大问题。 因为这个违背了:“可以定义在多个源文件中,但是在一个源文件中只能出现一次”的原则。
  • 比如这个场景:如果在 a.h 中含有 类A的定义,b.h中含有类B的定义 ,由于类B 依赖类A , 所以 b.h 中也 #include 'a.h' 。  现在一个源文件,它同时用到了 类A 和类B ,于是程序员在这个源文件中既把 a.h 包含进来了  ,同时也 b.h 包含进来了。 这个就有一个问题:类A 的定义在这个源文件中出现了两次 (一次是a.h 头文件中的定义,另一次是 b.h 头文件中的定义)于是整个程序就会编译失败。 
  • 为了解决这个问题 “#define” 这个配合条件就可以很好解决这个问题,在一个 头文件中,通过"#define" 定义一个名字,并且通过条件编译 #ifndef  ........ #endif  使得编译器可以根据这个名字是否被定义,再决定是否继续编译该头件中后续内容。

7: C++ 头文件和源文件的区别

7.1 :源文件是如何根据 #include 来关联头文件

  • 系统自带的头文件用尖括号<> 括起来, 这样编译器就会在系统文件目录下查找
  • 用户自定义的文件用" " 双引号括起来,编译器首先会在用户目录下查找,然后再C++ 按照目录下查找,最后在系统文件中查找  

7.2 :头文件如何关联源文件

  • 其实这个问题实际是说: 已知头文件 "a.h" 声明了一系列函数,“b.cpp” 实现了定义了这些函数。 那么如果我想在 “c.cpp ”中使用 在“a.h” 声明的这些  , “b.cpp” 中实现定义的函数,我改如何做了 ?
  • 通常情况下:在“c.cpp” 中 #include "a.h" 就可以了,那么c.cpp 是怎样找到 b.cpp 中的实现的了 ?
  1. 其实 .cpp 和.h 文件名称没有任何直接关系,很多编译器都可以接受其他扩展名。 比如有的公司源代码中  .cpp 文件由 .cc 文件代替了 。在 Turbo C 中,采用命令行的方式进行编译,命令行参数为文件名称,默认的是 .cpp和.h ,但是也可以自定义为 .xxx等等 。
  2. 谭浩强老师在《C程序设计》中提到, 编译器预处理时,要对 #include命令行 进行“文件包含处理”:将 file.c 的全部内容复制到 #include “file2.c” 处。这也正说明 很多编译器并不关系后缀名是什么,因为 #include 预处理器就是完成了一个 “复制并插入代码”的工作。
  3. c.cpp 在编译的时候并不会去找 b.cpp 文件中函数的实现,只有在 link 链接的时候才会进行这个工作。 我们在 b.cpp 或者c.cpp 中 用 #include ‘a.h’ 实际上是引入相关声明,是的编译可以通过,程序并不关心是在哪里实现的,怎么实现的。 在源文件在编译后生成了 目标文件.o 或者.obj (这里是指 .cpp 文件编译后的产物)  ,目标文件中,这些函数和变量就视作一个个符号,在 Link的时候,需要在 makefile 里面说明需要 连接那个 .o或者.obj 文件,此时连接器会去这个 .o 或者.obj 文件中 找在 b.cpp 中实现的函数,再把他们 build 到 makefile中指定的那个可执行的文件中。
  4. 在 Unix系统下,甚至可以不在源文件中 #include 头文件,只需要在 makefile中指名即可(不过这个程序的可读性比较差), 在VC 中,一般情况下不需要自己写 makefile文件,只需要将需要的文件都包括在 project中, VC 会自动帮你把 makefile写好 。
  5. 通常情况下,C++ 编译器会在每个 .o或者 .obj 文件中都去找一下所需要的符号,而不是只在某个文件中找或者说找到一个就不找了。 因此,如果在几个不同文件中实现了同一个函数,或者定义了同一个全局变量,链接的时候就会出现 “redefined” 错误。

8: 头文件可以包含哪些内容

  1. 类成员数据的声明,但不能赋值
  2. 类的静态数据成员的定义和赋值(但不建议,只是个声明是最好的)
  3. 类的成员函数的声明
  4. 非类成员函数的声明
  5. 常数的定义  const int i = 0;  (const 可以限制变量仅在本.cpp 文件中的可见性)
  6. 静态数据的定义  static int j = 2;   (static 修饰普通函数或者变量时,限制了变量和函数的仅本文件可见性) 比如  :  void funcA(){ int a =0; a++; printf(a) },如果要连续记录调用了多少次,就得使用全局变量,但是全局变量暴露的太多了,其他文件中也能可见,所以,static a 用于仅本文件可见。
  7. 静态函数的定义  static  void function(){  }
  8. 类的内联函数的定义

不能包括

  •     所有非静态变量的声明
  •     静态修饰的成员函数和成员变量( static 修饰类的成员变量时,该成员是属于类本身,所有类的实例对象共享,造成成员变量和成员函数 对其他.cpp文件也可见,所以不能在头文件中包含)
  • 默认命名空间声明不要放在头文件,using namespace std;等应放在.cpp中,在 .h 文件中使用 std::string

参考文献:理解 C++ 中的头文件和源文件的作用 | 菜鸟教程

理解C++中的头文件和源文件的作用_放肆青春的博客的博客-CSDN博客_c++ 中的头文件和源文件的作用 csdn 

  • 15
    点赞
  • 80
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值