笔记77:理解C++中头文件和源文件的作用【程序编译过程】

一、C++ 编译模式

通常,在一个 C++ 程序中,只包含两类文件—— .cpp 文件和 .h 文件。其中,.cpp 文件被称作 C++ 源文件,里面放的都是 C++ 的源代码;而 .h 文件则被称作 C++ 头文件,里面放的也是 C++ 的源代码。

C++ 语言支持"分别编译"(separatecompilation)。也就是说,一个程序所有的内容,可以分成不同的部分分别放在不同的 .cpp 文件里。.cpp 文件里的东西都是相对独立的,在编译(compile)时不需要与其他文件互通,只需要在编译成目标文件后再与其他的目标文件做一次链接(link)就行了。比如,在文件 a.cpp 中定义了一个全局函数 "void a(){}",而在文件 b.cpp 中需要调用这个函数。即使这样,文件 a.cpp 和文件 b.cpp 并不需要相互知道对方的存在,而是可以分别地对它们进行编译,编译成目标文件之后再链接,整个程序就可以运行了。

这是怎么实现的呢?从写程序的角度来讲,很简单。在文件 b.cpp 中,在调用 "void a()" 函数之前,先声明一下这个函数 "voida();",就可以了。这是因为编译器在编译 b.cpp 的时候会生成一个符号表(symbol table),像 "void a()" 这样的看不到定义的符号,就会被存放在这个表中。再进行链接的时候,编译器就会在别的目标文件中去寻找这个符号的定义。一旦找到了,程序也就可以顺利地生成了。

注意这里提到了两个概念,一个是"定义",一个是"声明"。简单地说,"定义"就是把一个符号完完整整地描述出来:它是变量还是函数,返回什么类型,需要什么参数等等。而"声明"则只是声明这个符号的存在,即告诉编译器,这个符号是在其他文件中定义的,我这里先用着,你链接的时候再到别的地方去找找看它到底是什么吧。定义的时候要按 C++ 语法完整地定义一个符号(变量或者函数),而声明的时候就只需要写出这个符号的原型了。需要注意的是,一个符号,在整个程序中可以被声明多次,但却要且仅要被定义一次。试想,如果一个符号出现了两种不同的定义,编译器该听谁的?

这种机制给 C++ 程序员们带来了很多好处,同时也引出了一种编写程序的方法。考虑一下,如果有一个很常用的函数 "void f() {}",在整个程序中的许多 .cpp 文件中都会被调用,那么,我们就只需要在一个文件中定义这个函数,而在其他的文件中声明这个函数就可以了。一个函数还好对付,声明起来也就一句话。但是,如果函数多了,比如是一大堆的数学函数,有好几百个,那怎么办?能保证每个程序员都可以完完全全地把所有函数的形式都准确地记下来并写出来吗?


二、什么是头文件

很显然,答案是不可能。但是有一个很简单地办法,可以帮助程序员们省去记住那么多函数原型的麻烦:我们可以把那几百个函数的声明语句全都先写好,放在一个文件里,等到程序员需要它们的时候,就把这些东西全部 copy 进他的源代码中。

这个方法固然可行,但还是太麻烦,而且还显得很笨拙。于是,头文件便可以发挥它的作用了。所谓的头文件,其实它的内容跟 .cpp 文件中的内容是一样的,都是 C++ 的源代码。但头文件不用被编译。我们把所有的函数声明全部放进一个头文件中,当某一个 .cpp 源文件需要它们时,它们就可以通过一个宏命令 "#include" 包含进这个 .cpp 文件中,从而把它们的内容合并到 .cpp 文件中去。当 .cpp 文件被编译时,这些被包含进去的 .h 文件的作用便发挥了。

举一个例子吧,假设所有的数学函数只有两个:f1 和 f2,那么我们把它们的定义放在 math.cpp 里:

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

并把"这些"函数的声明放在一个头文件 math.h 中:

/* math.h */
double f1();
double f2(double);
/* end of math.h */

在另一个文件main.cpp中,我要调用这两个函数,那么就只需要把头文件包含进来:

/* main.cpp */
#include "math.h"
main()
{int number1 = f1();int number2 = f2(number1);
}
/* end of main.cpp */

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


三、#include

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

/* ~main.cpp */
double f1();
double f2(double);
main()
{int number1 = f1();int number2 = f2(number1);
}
/* end of ~main.cpp */

不多不少,刚刚好。同理可知,如果我们除了 main.cpp 以外,还有其他的很多 .cpp 文件也用到了 f1 和 f2 函数的话,那么它们也通通只需要在使用这两个函数前写上一句 #include "math.h" 就行了。


四、头文件中应该写什么

通过上面的讨论,我们可以了解到,头文件的作用就是被其他的 .cpp 包含进去的。它们本身并不参与编译,但实际上,它们的内容却在多个 .cpp 文件中得到了编译。通过"定义只能有一次"的规则,我们很容易可以得出,头文件中应该只放变量和函数的声明,而不能放它们的定义。因为一个头文件的内容实际上是会被引入到多个不同的 .cpp 文件中的,并且它们都会被编译。放声明当然没事,如果放了定义,那么也就相当于在多个文件中出现了对于一个符号(变量或函数)的定义,纵然这些定义都是相同的,但对于编译器来说,这样做不合法。

所以,应该记住的一点就是,.h头文件中,只能存在变量或者函数的声明,而不要放定义。即,只能在头文件中写形如:extern int a; 和 void f(); 的句子。这些才是声明。如果写上 inta;或者 void f() {} 这样的句子,那么一旦这个头文件被两个或两个以上的 .cpp 文件包含的话,编译器会立马报错。(关于 extern,前面有讨论过,这里不再讨论定义跟声明的区别了。)

但是,这个规则是有三个例外的:

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

五、头文件中的保护措施

考虑一下,如果头文件中只包含声明语句的话,它被同一个 .cpp 文件包含再多次都没问题——因为声明语句的出现是不受限制的。然而,上面讨论到的头文件中的三个例外也是头文件很常用的一个用处。那么,一旦一个头文件中出现了上面三个例外中的任何一个,它再被一个 .cpp 包含多次的话,问题就大了。因为这三个例外中的语法元素虽然"可以定义在多个源文件中",但是"在一个源文件中只能出现一次"。设想一下,如果 a.h 中含有类 A 的定义,b.h 中含有类 B 的定义,由于类B的定义依赖了类 A,所以 b.h 中也 #include了a.h。现在有一个源文件,它同时用到了类A和类B,于是程序员在这个源文件中既把 a.h 包含进来了,也把 b.h 包含进来了。这时,问题就来了:类A的定义在这个源文件中出现了两次!于是整个程序就不能通过编译了。你也许会认为这是程序员的失误——他应该知道 b.h 包含了 a.h ——但事实上他不应该知道。

使用 "#define" 配合条件编译可以很好地解决这个问题。在一个头文件中,通过 #define 定义一个名字,并且通过条件编译 #ifndef...#endif 使得编译器可以根据这个名字是否被定义,再决定要不要继续编译该头文中后续的内容。这个方法虽然简单,但是写头文件时一定记得写进去。


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

一、源文件如何根据 #include 来关联头文件

  • 1、系统自带的头文件用尖括号括起来,这样编译器会在系统文件目录下查找。
  • 2、用户自定义的文件用双引号括起来,编译器首先会在用户目录下查找,然后在到 C++ 安装目录(比如 VC 中可以指定和修改库文件查找路径,Unix 和 Linux 中可以通过环境变量来设定)中查找,最后在系统文件中查找。

#include "xxx.h"(我一直以为 "" 和 <> 没什么区别,但是 tinyxml.h 是非系统下的都文件,所以要用 "")

二、头文件如何来关联源文件

这个问题实际上是说,已知头文件 "a.h" 声明了一系列函数,"b.cpp" 中实现了这些函数,那么如果我想在 "c.cpp" 中使用 "a.h" 中声明的这些在 "b.cpp"中实现的函数,通常都是在 "c.cpp" 中使用 #include "a.h",那么 c.cpp 是怎样找到 b.cpp 中的实现呢?

其实 .cpp 和 .h 文件名称没有任何直接关系,很多编译器都可以接受其他扩展名。比如偶现在看到偶们公司的源代码,.cpp 文件由 .cc 文件替代了。

在 Turbo C 中,采用命令行方式进行编译,命令行参数为文件的名称,默认的是 .cpp 和 .h,但是也可以自定义为 .xxx 等等。

谭浩强老师的《C 程序设计》一书中提到,编译器预处理时,要对 #include 命令进行"文件包含处理":将 file2.c 的全部内容复制到 #include "file2.c" 处。这也正说明了,为什么很多编译器并不 care 到底这个文件的后缀名是什么----因为 #include 预处理就是完成了一个"复制并插入代码"的工作。

编译的时候,并不会去找 b.cpp 文件中的函数实现,只有在 link 的时候才进行这个工作。我们在 b.cpp 或 c.cpp 中用 #include "a.h" 实际上是引入相关声明,使得编译可以通过,程序并不关心实现是在哪里,是怎么实现的。源文件编译后成生了目标文件(.o 或 .obj 文件),目标文件中,这些函数和变量就视作一个个符号。在 link 的时候,需要在 makefile 里面说明需要连接哪个 .o 或 .obj 文件(在这里是 b.cpp 生成的 .o 或 .obj 文件),此时,连接器会去这个 .o 或 .obj 文件中找在 b.cpp 中实现的函数,再把他们 build 到 makefile 中指定的那个可以执行文件中。

在 Unix下,甚至可以不在源文件中包括头文件,只需要在 makefile 中指名即可(不过这样大大降低了程序可读性,是个不好的习惯哦^_^)。在 VC 中,一帮情况下不需要自己写 makefile,只需要将需要的文件都包括在 project中,VC 会自动帮你把 makefile 写好。

通常,C++ 编译器会在每个 .o 或 .obj 文件中都去找一下所需要的符号,而不是只在某个文件中找或者说找到一个就不找了。因此,如果在几个不同文件中实现了同一个函数,或者定义了同一个全局变量,链接的时候就会提示 "redefined"。


总结:

#include <iostream>        // 注;系统自带的头文件用尖括号括起来,这样编译器会在系统文件目录下查找
using namespace std;
#include "swap.h";         // 注:用户自定义的文件用双引号括起来,编译器首先会在用户目录下查找,然后再到 C++ 安装目录//    (比如 VC 中可以指定和修改库文件查找路径,Unix 和 Linux 中可以通过环境变量来设定)中查找,最后在系统文件中查找int main()
{int x = 10;int y = 20;swap(x, y);system("pause");       return 0;
}

这个程序的实现过程:

  1. 执行“#include <iostream>”     :将系统自带头文件的内容全部预编译(就是复制粘贴)到当前源文件中,反正头文件里面全是声明而不是定义,随便添加都可以 【因为include就是一个宏命令,他没有是实质的作用,就是进行一个复制粘贴的操作】【头文件.h是不参与编译的,只有源文件.cpp才参与编译】
  2. 执行“#include "swap.h";”      :将用户自定义的头文件内容全部预编译进入源文件中
  3. 执行“swap(x, y);”             :其实c++并不是和python一样是一行一行的执行代码的,他是将源文件.cpp分别编译并产生.o/.obj文件,再利用makefile文件进行link【主函数所在源文件----编译通过----产生.o文件----里面包含所有涉及到的函数和变量组成的符号表(symbol table)】【源文件swap.cpp------编译通过----产生.o文件----里面包含所有涉及到的函数和变量组成的符号表(symbol table)】【VS自动产生makefile文件,将两个.o文件关联,这样在主源文件中声明但并未定义的函数会直接连接到swap.cpp这个源文件中执行相应的代码】
  4. 总结:所以并没那么复杂,保证让源文件都可以通过编译就好了,link工作会由VS自动完成,所以要通过编译就要保证该声明的函数声明到位了即可

笔记位置:

D:\work_file\(2)C++_Learning\01_C++入门\Learning_Leval1\06章_函数\6.7_函数的分文件编写\6.7_函数的分文件编写


参转载自文章:

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/728452.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

mac系统Idea登录codeium不跳转,报错faild download language server

问题描述 idea通过插件中心安装Codeium以后&#xff0c;登录无法正常跳转到登录页&#xff0c;等待一段时间&#xff0c;右下角图标报错**“faild download language server”** 解决方案 根据上面的报错&#xff0c;是没有成功下载“language_server_macos_x64“&#xff0…

CSAPP-程序的机器级表示

文章目录 概念扫盲思想理解经典好图安全事件 概念扫盲 1.汇编代码使用文本格式&#xff0c;相较于汇编的二进制可读性更好 2.程序内存包括&#xff1a;可执行的机器代码、操作系统需要的信息、管理过程调用和返回的运行时栈、用户分配的内存块 3.链接器为函数调用找到匹配的可…

P9889 [ICPC2018 Qingdao R] Plants vs. Zombies 题解 二分+贪心

[ICPC2018 Qingdao R] Plants vs. Zombies 传送门 题面翻译 给定 n n n 个植物和 m m m 的步数限制&#xff0c;每个植物在位置 1 … n 1\dots n 1…n 上。你初始时在位置 0 0 0&#xff0c;每次可以移动到相邻的位置上。 每次设你走完一步后到达的位置是 i i i&#…

数学建模【模糊综合评价分析】

一、模糊综合评价分析简介 提到模糊综合评价分析&#xff0c;就先得知道模糊数学。1965年美国控制论学家L.A.Zadeh发表的论文“Fuzzy sets”标志着模糊数学的诞生。 模糊数学又称Fuzzy数学&#xff0c;是研究和处理模糊性现象的一种数学理论和方法。模糊性数学发展的主流是在…

小程序API能力集成指南——配网能力API汇总(一)

ty.playnet.autoConnectToAp 自动连接wifi 需引入PlayNetKit&#xff0c;且在>1.1.0版本才可使用 请求参数 Object object 属性类型默认值必填说明ssidstring是配网之后&#xff0c;设备工作 Wi-Fi 的名称pwdstring是配网之后&#xff0c;设备工作 Wi-Fi 的密码completef…

git之系列

git之常用ignore 。 git之常用命令 。 git之reflog分析 。 git之添加和删除全局配置 。 git之如何恢复代码到之前版本 。 git之merge和rebase 。 git之如何合并部分提交 。 git之本地有未提交代码如何切换分支 。 Git通过tag创建分支并推送到远程 。

大语言模型系列-GPT-3

文章目录 前言一、GTP-3的改进二、GPT-3的表现总结 前言 《Language Models are Few-Shot Learners&#xff0c;2020》 前文提到GPT-2进一步提升了模型的zero shot能力&#xff0c;但是在一些任务中仍可能会“胡说”&#xff0c;GTP-3基于此提出了few shot&#xff0c;即预测…

7-22 试试手气(Python)

我们知道一个骰子有 6 个面&#xff0c;分别刻了 1 到 6 个点。下面给你 6 个骰子的初始状态&#xff0c;即它们朝上一面的点数&#xff0c;让你一把抓起摇出另一套结果。假设你摇骰子的手段特别精妙&#xff0c;每次摇出的结果都满足以下两个条件&#xff1a; 1、每个骰子摇出…

ZYNQ--AXI_DMA使用

文章目录 手册阅读典型连接图SG模式关闭时的寄存器地址SG模式开启时的寄存器地址BD设计PS端设计对于DMA寄存器的控制对DMA进行初始化 手册阅读 典型连接图 SG模式关闭时的寄存器地址 SG模式开启时的寄存器地址 关于各个bit的功能&#xff0c;具体看数据手册。 BD设计 通过PL侧…

sql高级

sql高级 SQL SELECT TOP 子句 SELECT TOP 子句用于规定要返回的记录的数目。 SELECT TOP 子句对于拥有数千条记录的大型表来说&#xff0c;是非常有用的。 **注意:**并非所有的数据库系统都支持 SELECT TOP 语句。 MySQL 支持 LIMIT 语句来选取指定的条数数据&#xff0c; O…

Qt + mqtt对接阿里云平台(一)

一、阿里云平台 官网&#xff1a;点击跳转 二、创建产品与设备 1、“公共实例” 2、“设备管理”->“产品”->“创建产品” 3、“产品名称”->“自定义品类”->"确认" 4、“前往添加” 5、“添加设备” 6、摄入DeviceName和备注名称 7、"前往查…

每周一算法:A*(A Star)算法

八数码难题 题目描述 在 3 3 3\times 3 33 的棋盘上&#xff0c;摆有八个棋子&#xff0c;每个棋子上标有 1 1 1 至 8 8 8 的某一数字。棋盘中留有一个空格&#xff0c;空格用 0 0 0 来表示。空格周围的棋子可以移到空格中。要求解的问题是&#xff1a;给出一种初始布局…

文心一言 VS 讯飞星火 VS chatgpt (210)-- 算法导论16.1 1题

一、根据递归式(16.2)为活动选择问题设计一个动态规划算法。算法应该按前文定义计算最大兼容活动集的大小 c[i,j]并生成最大集本身。假定输入的活动已按公式(16.1)排好序。比较你的算法和GREEDY-ACTIVITY-SELECTOR的运行时间。如何要写代码&#xff0c;请用go语言。 文心一言&…

excel统计分析——裂区设计

参考资料&#xff1a;生物统计学 裂区设计&#xff08;split-plot design&#xff09;是安排多因素试验的一种方法&#xff0c;裂区设计对因素的安排有主次之分&#xff0c;适用于安排对不同因素试验精度要求不一的试验。 裂区设计时&#xff0c;先按第一因素的处理数划分主区&…

独立站营销新纪元:AI与大数据塑造个性化体验的未来

随着全球互联网的深入发展和数字化转型的不断推进&#xff0c;作为品牌建设和市场营销的重要载体&#xff0c;独立站将迎来新的发展机遇。新技术的涌现&#xff0c;特别是人工智能和大数据等技术的广泛应用&#xff0c;为独立站带来了前所未有的机遇与挑战。本文Nox聚星将和大家…

ios xcode 15 PrivacyInfo.xcprivacy 隐私清单

1.需要升级mac os系统到13 兼容 xcode 15.1 2.升级mac os系统到14 兼容 xcode 15.3 3.选择 New File 4.直接搜索 privacy 能看到有个App Privacy 5.右击Add Row 7.直接选 Label Types 8.选中继续添加就能添加你的隐私清单了 苹果官网文档Describing data use in privacy man…

Windows安装SSH教程:进阶配置选项详解

当我们谈论在Windows上安装SSH时,我们通常会关注基本的安装步骤和客户端/服务器的设置。然而,SSH的配置远不止于此。在本文中,我们将深入探讨一些高级的SSH配置选项,这些选项可以帮助您更好地定制SSH体验,提高安全性和性能。 一、SSH配置文件的详解 在Windows上,SSH的配…

Java二阶知识点总结(四)常见算法的基本结构

一、二叉树的DFS&#xff08;深度优先算法&#xff09; 1、递归 基本递归&#xff08;以中序遍历为例&#xff09; public List<Integer> inorderTraversal(TreeNode root) {List<Integer> resultnew ArrayList<>();order(root,result);return result;}pub…

从零开始学HCIA之IPv6基础05

1、IPv6地址支持无状态自动配置方式&#xff0c;主机通过某种机制获取网络前缀信息&#xff0c;然后主机自己生成地址的接口标识部分。 2、路由器发现功能是IPv6地址自动配置功能的基础&#xff0c;主要通过两种消息实现。 &#xff08;1&#xff09; 路由器通告&#xff08;…

springboot247人事管理系统

人事管理系统的设计与实现 摘 要 传统信息的管理大部分依赖于管理人员的手工登记与管理&#xff0c;然而&#xff0c;随着近些年信息技术的迅猛发展&#xff0c;让许多比较老套的信息管理模式进行了更新迭代&#xff0c;问卷信息因为其管理内容繁杂&#xff0c;管理数量繁多导…