doc/docs/Cpp-Notes-main/summary/1.头文件解析.md
sairate 4108f6c42d docs(book): 添加现代 C++教程及相关代码
- 新增现代 C++ 教程的 Preface 章节,包括英文和中文版本
- 添加 C++ Primer 练习代码
- 新增 Learn C++ 教程的 C++ 开发简介章节
- 添加头文件解析文档
- 更新 mkdocs.yml,包含新教程的目录结构
- 修改项目设置,使用 Python 3.10环境
2025-07-06 16:14:43 +08:00

64 lines
7.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
sort: 1
---
# 头文件解析
## 头文件中应该写什么
头文件的作用就是被其他的 .cpp 包含进去的。它们本身并不参与编译,但实际上,它们的内容却在多个 .cpp 文件中得到了编译。通过"定义只能有一次"的规则,我们很容易可以得出,头文件中应该只放变量和函数的声明,而不能放它们的定义。因为一个头文件的内容实际上是会被引入到多个不同的 .cpp 文件中的,并且它们都会被编译。放声明当然没事,如果放了定义,那么也就相当于在多个文件中出现了对于一个符号(变量或函数)的定义,纵然这些定义都是相同的,但对于编译器来说,这样做不合法。
所以,应该记住的一点就是,.h头文件中只能存在变量或者函数的声明而不要放定义。即只能在头文件中写形如extern int a; 和 void f(); 的句子。这些才是声明。如果写上 inta;或者 void f() {} 这样的句子,那么一旦这个头文件被两个或两个以上的 .cpp 文件包含的话,编译器会立马报错。
但是,这个规则是有三个例外的:
一,头文件中可以写 const 对象的定义。因为全局的 const 对象默认是没有 extern 的声明的,所以它只在当前文件中有效。把这样的对象写进头文件中,即使它被包含到其他多个 .cpp 文件中,这个对象也都只在包含它的那个文件中有效,对其他文件来说是不可见的,所以便不会导致多重定义。同时,因为这些 .cpp 文件中的该对象都是从一个头文件中包含进去的,这样也就保证了这些 .cpp 文件中的这个 const 对象的值是相同的可谓一举两得。同理static 对象的定义也可以放进头文件。
头文件中可以写内联函数inline的定义。因为inline函数是需要编译器在遇到它的地方根据它的定义把它内联展开的而并非是普通函数那样可以先声明再链接的内联函数不会链接所以编译器就需要在编译时看到内联函数的完整定义才行。如果内联函数像普通函数一样只能定义一次的话这事儿就难办了。因为在一个文件中还好我可以把内联函数的定义写在最开始这样可以保证后面使用的时候都可以见到定义但是如果我在其他的文件中还使用到了这个函数那怎么办呢这几乎没什么太好的解决办法因此 C++ 规定,内联函数可以在程序中定义多次,只要内联函数在一个 .cpp 文件中只出现一次,并且在所有的 .cpp 文件中,这个内联函数的定义是一样的,就能通过编译。那么显然,把内联函数的定义放进一个头文件中是非常明智的做法。
头文件中可以写类class的定义。因为在程序中创建一个类的对象时编译器只有在这个类的定义完全可见的情况下才能知道这个类的对象应该如何布局所以关于类的定义的要求跟内联函数是基本一样的。所以把类的定义放进头文件在使用到这个类的 .cpp 文件中去包含这个头文件,是一个很好的做法。在这里,值得一提的是,类的定义中包含着数据成员和函数成员。数据成员是要等到具体的对象被创建时才会被定义(分配空间),但函数成员却是需要在一开始就被定义的,这也就是我们通常所说的类的实现。一般,我们的做法是,把类的定义放在头文件中,而把函数成员的实现代码放在一个 .cpp 文件中。这是可以的,也是很好的办法。不过,还有另一种办法。那就是直接把函数成员的实现代码也写进类定义里面。在 C++ 的类中,如果函数成员在类的定义体中被定义,那么编译器会视这个函数为内联的。因此,把函数成员的定义写进类定义体,一起放进头文件中,是合法的。注意一下,如果把函数成员的定义写在类定义的头文件中,而没有写进类定义中,这是不合法的,因为这个函数成员此时就不是内联的了。一旦头文件被两个或两个以上的 .cpp 文件包含,这个函数成员就被重定义了。
## 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"。
.h文件中能包含
- 类成员数据的声明,但不能赋值
- 类静态数据成员的定义和赋值,但不建议,只是个声明就好。
- 类的成员函数的声明
- 非类成员函数的声明
- 常数的定义constint a=5;
- 静态函数的定义
- 类的内联函数的定义
不能包含:
- 所有非静态变量(不是类的数据成员)的声明
- 默认命名空间声明不要放在头文件using namespace std;等应放在.cpp中在 .h 文件中使用 std::string
## 参考资料
- <https://www.runoob.com/w3cnote/cpp-header.html>