C++ 中头文件和源文件的关系
date
Feb 5, 2023
slug
cpp-header-source
status
Published
tags
CPP
summary
这涉及到编译、链接、声明和定义之间的关系
type
Post
一、C++ 编译模式
C++ 支持“分别编译”。一个程序的内容可以分成不同的部分放在不同的 .cpp 文件里。.cpp 文件里的东西都是相对独立的,在编译(compile)时不需要其他文件,编译后会生成目标文件。这些目标文件再互相做一次链接(link),整个程序就可以运行了。
不同 .cpp 文件如何使用对方的函数?例如在 a.cpp 定义了一个
void a(){},在 b.cpp 中需要使用这个 a 函数。它们在编译的时候不需要对方存在,但是 b.cpp 在调用函数 a 之前,需要对函数进行声明。b.cpp 编译后就会生成一个符号表(symbol table),像 void a() 这样声明的符号就会被保存在这个表中。后续进行链接时,编译器会尝试在别的目标文件中去寻找 void a() 的定义。声明:只是声明这个符号存在,如果本文件没有定义,链接的时候再去别的地方寻找。
定义:把一个符号完完整整地实现描述出来。
一个符号可以在整个程序中声明多次,但是只能被定义一次。如果程序中出现了两种不同的定义,编译器也不知道哪个定义才是期望的。
二、头文件的用途
如果有一大堆的数学函数需要引用,很难保证程序员都可以完完整整地把所有函数的形式声明在自己的 .cpp 的文件中。有一个简单的办法:把这许多的数学函数声明语句全部写在一个文件里,程序员需要用到的时候就从这个文件里复制到 .cpp 文件中。这显然很蠢。
头文件(.h)设计是为了解决这个问题。头文件它的内容和 .cpp 文件中内容相似,都是 c++ 的源代码。我们可以把所有的函数声明写到一个头文件中,当某个 .cpp 文件声明它们时,可以使用
#include 宏命令包含近 .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);- run.cpp
#include "math.h"
int main()
{
int number1 = f1();
int number2 = f2(number1);
return 0;
}#include:它作用是把它后面所写的那个文件的内容,完完整整地、一字不改地包含到当前的文件中来。在编译过程即将开始的时候,.cpp 内容的#include部分,就会被对应 .h 文件的声明替代。
实际上 run.cpp 在编译前就被简单的文本替换成:
double f1();
double f2(double);
int main()
{
int number1 = f1();
int number2 = f2(number1);
return 0;
}三、头文件的正确内容
通过上面知道,头文件的作用就是被其他 .cpp 包含进去,它本身不会参与编译。它实际上它们的内容会在多个 .cpp 文件中得到了编译。
应该记住的是,.h 文件中只能存变量或者函数的声明。例如
extern int a; 和 void f(); 的声明语句。如果存在 int a; 或者 void f() {} 的定义语句,一旦这个头文件被两个或两个以上的 .cpp 文件包含,编译器就会报重复定义错误。但是有三个例外:- .h 文件中可以写
extern const对象的定义:const 对象默认没有 extern 修饰,它只在当前文件中有效。及时它被包含到了多个 .cpp 文件中,这个对象也只在包含它的那个文件中有效,对于其他文件来说不可见。但可以写但不代表推荐!最好在 .h 的 const 前面加 extern 修饰声明,在 .cpp 中定义。
- .h 文件中可以写内联函数(
inline)的定义:1)inline 函数并不是必须定义在头文件中,但是一个好的工程习惯是将其定义在头文件中。2)inline 函数在链接时仅仅在单个 .cpp 文件中“可见”,并不是全局“可见”,是因为 inline 函数没有被编译成汇编码,无法用于链接。3)inline 函数仅仅是一个对编译器的建议,最后能否真正内联,还要看编译器。4)C++ 中在类中实现的成员函数会被编译器自动默认判定为 inline 函数。
- .h 文件中可以写类(
class)的定义:在程序中创建一个类的对象时,编译器只有在这个类的定义完全可见的情况下,才能知道这个类的对象应该如何布局。推荐的做法:可以把类的定义放在头文件中,而把函数成员的实现代码放在一个 .cpp 文件中。不过,还有另一种办法:直接把函数成员的实现代码也写进类定义里面是之成为内敛的。
四、头文件的保护措施
设想一下,如果 a.h 含有类 A 的定义,b.h 中含有类 B 的定义。b.h 需要中包含 a.h。如果有一个新的源文件 c.cpp 需要用到类 A 和类 B,它就需要包含 a.h 和 b.h。这时,类 A 的定义就在 c.cpp 中出现了两次!所以一般使用
ifndef 和 define 进行配合,避免头文件嵌套包含。#ifndef __TEST_H
#define __TEST_H
// ......
#endif#pragma once
// ......五、关于 #include
1、系统自带的头文件用尖括号括起来,这样编译器会在系统文件目录下查找。
2、用户自定义的文件用双引号括起来,编译器首先会在用户目录下查找,然后再到 C++ 安装目录(比如 VC 中可以指定和修改库文件查找路径,Unix 和 Linux 中可以通过环境变量来设定)查找,最后在系统文件中查找。
六、总结
只要知道 #include 的作用是简单的文本替换,再去思考是否会有重复定义问题。