C++中源文件和头文件的区别
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
C++的源代码文件分为两类:头文件(Header file)和源文件
(Source code file)。
头文件用于存放对类型定义、函数声明、全局变量声明等实体的声明,作为对外接口;而源程序文件存放类型的实现、函数体、全局变量定义.
C++的源代码文件分为两类:头文件(Header file)和源文件(Source code file)。
头文件用于存放对类型定义、函数声明、全局变量声明等实体的声明,作为对外接口;而源程序文件存放类型的实现、函数体、全局变量定义。
对于商业C++程序库,一般把头文件随二进制的库文件发布,而源代码保留。
一般情况下头文件常以.h或.hpp作为扩展名,而实现文件常以.cpp或.cc为扩展名。
头文件一般不直接编译,一个源文件代表一个“编译单元”。
在在编译一个源文件时,如果引用的类型、函数或其它实体不在本编译单元内,可以通过引用头文件将其它编译单元内实现的实体引入到本编译单元。
而从本质上讲,这些源代码文件都是纯文本文件,可以使用任何一款文本编译器进行源代码的编辑,并没有本质的区别,这些头文与实现文件的扩展名只是一种习惯。
而C++的标准库的头文件则不使用扩展名,例如string、 iostream、cstdio 等头文件。
对与源文件也一样,你完全可以使用.inl或.cplusplus作为文件的扩展名。
事实上,在一些C++的项目中.inl被用作源代码文件的扩展名,保存内联函数,直接包含在源文件中,如ACE(the Adaptive Communication Environment, /~schmidt/ACE.html)等。
gcc默认支持的C++源文件扩展名有.cc、.cp、.cpp、.cxx、.c++、.CPP、.C(注意后两项是大写,在Unix/Linux 上的文件名是区分大小写的)。
例如在gcc中你可以这样编译一个扩展名
为.cplusplus的C++程序:
g++ -x c++ demo.cplusplus
虽然文件名对程序没有任何影响,但.cpp和.cc这些扩展名是编译器默认支持的,使用这些扩展名您就不需要手动添加编译选项支持您使用的扩展名,如gcc 中的-x选项。
而实际上,头文件以什么为扩展名并没有什么影响,因为没有人会直接编译头文件,因为头文件里只有声明而没有定义,而在实际的编译过程中,#include预
编译指令用到的头文件是被直接插入到源代码文件中再进行编译的,这与直接将头文件的内容复制到#include行所在的位置是没有区别的,这样就很容易理解
#include可以出现在文件的什么位置,显然放到一个函数体或类的定义里是不
合适的。
1.1.1. 定义与声明有什么不同
一般来讲定义要放在源代码文件中,而声明要放在头文件中。
具体哪些内容应该放在源代码文件中,哪些内容应该放在头文件中,需要清楚地理解,哪些是定义,哪些是声明。
1.1.1.1. 类的定义与声明
类的定义是定义了类的完整结构,包括成员函数与成员变量,如例程[2-1]。
// 例程2-1: 类的定义
class Point
{
private:
int x_;
int y_;
public:
Point( int x, int y);
int X( void ) const;
int Y( void ) const;
};
而类的声明,只说明存在这一种类型,但并不定义它是什么样的类型,如例程[2-2]。
// 例程2-2: 类的声明
class Point;
类的说明与实现都可以放在头文件中,因为上层代码需要使用Point的类必须知道当前工程已经定义了这个类。
但应该使用定义还是声明呢?使用声明可以的地方使用定义都是可以的,但是,过多得使用定义会使项目编译时间加长,减慢编译速度,细节可参见(@see effective series,item 34)。
还有一种情况是必须使用声明的,就是当两个类在定义中出现互相引用的情况时,如例程[2-3]。
当然,这种情况出现的情况比较少,多数情况下也可以通过修改设计尽量避免,在不可避免的情况下只能使用这种方式。
// 例程2-3: 类定义的交叉引用
class B;
class A { public : B& GetB( void ) const; }
class B { public: A* CreateA( void ) const; }
类的定义只给出了类包含了哪些数据(成员变量)和接口(成员函数),但并没有给出实现,程序的实现应该放在原代码文件中。
如例程[2-1]中的Point类定义在Point.hpp头文件中,相应的源代码文件Point.cpp的内容如例程[2-4]所示。
// 例程2-4: 成员函数的实现
Point::Point(int x, inty)
:x_(x), y_(y)
{
}
int Point::X( void ) const
{
return x_;
}
int Point::Y( void ) const
{
return y_;
}
当然,类的成员函数的实现也可以放到头文件中,但编译时默认会为这些函数加上inline修饰符,当成内联函数处理。
像Point::X和PointY这样的简单的读值函数,比较适合放到头文件中作为内联函数,详见[??inline]一节。
1.1.1.
2. 函数的定义与声明
函数的声明只说明函数的外部接口,而不包含函数的实现函数体,如例程[2-5]所示。
// 例程2-5: 函数的声明
int SplitString(vector& fields
, const string& str
, const string& delimiter);
而函数定义则是包含函数声明和函数体在内的所有部分,如例程[2-6]所示,给出了一个拆分字符串的函数,虽然效率不高,但它的确是一个能工作的函数。
// 例程2-6: 函数的定义
int SplitString(vector& fields
, const string& str
, const string& delimiters)
{
string tmpstr = str;
fields.clear();
string::size_type pos1, pos2;
for(;;) {
pos1 = pos2 = 0;
if((pos1 = tmpstr.find_first_not_of(delimiters, pos2))
== string::npos)
break;
if((pos2 = tmpstr.find_first_of(delimiters, pos1))
!= string::npos){
fields.push_back(tmpstr.substr(pos1, pos2 - pos1));
}else {
fields.push_back(tmpstr.substr(pos1));
break;
}
tmpstr.erase(0, pos2);
}
return fields.size();
}
函数声明可以放在任何一个调用它的函数之前,而且在调用一个函数之前必须在调用者函数之前定义或声明被调函数。
函数的定义只能有一次,如果调用者与被调用者不在同一编译单元,只能在调用者之前添加函数的声明。
函数定义只能有一次,函数声明可以有无限次(理论上),这也是头文件的作用,将一批函数的声明放入一个头文件中,在任何需要这些函数声明的地方引用该头文件,以便于维护。
函数声明之前有一个可选的extern修饰符,表示该函数是在其它编译单元内定义的,或者在函数库里。
虽然它对于函数的声明来讲不是必须的,但可以在一个
源文件中直接声明其它编译单元内实现的函数时使用该关键词,从而提高可读性。
假如例程[2-6]中的函数SplitString定义在strutil.cpp文件中定义,而且在strutil.cpp还定义了很多字符串相关的函数,other.cpp只用到了strutil.cpp中SplitString这一个函数。
而您为了提高编译速度,可以直接在other.cpp中声明该函数,而不是直接引用头文件,此时最好使用extern标识,使程序的可读性更好。
1.1.1.3. 变量的定义与声明
变量的声明是带有extern标识,而且不能初始化;而变量的定义没有extern
标识,可以在定义时初始化,如例程[2-7]所示。
// 例程2-7:变量的定义与声明
// 声明
extern int global_int;
extern std::string global_string ;
// 定义
int global_int = 128;
std::string global_string = “global string”;
在形式上,与函数的声明不同的是,变量的声明中的extern是必须的,如果没有extern修饰,编译器将当作定义。
之所以要区分声明与变量,是在为对于变量定义编译器需要分配内存空间,而对于变量声明则不需要分配内存空间。
1.1.1.4. 小结
从理论上讲,声明与定义的区别就是:定义描述了内部内容,而声明不表露内部内容,只说明对外接口。
例如,类的定义包含了内部成员的声明,而类的声明不包含任何类的内部细节;函数的定义包含了函数体,而函数声明只包括函数的签名;变量的定义可以包含初始化,而变量的声明不可以包含初始化。
从语法表现上的共同点,声明可以重复,而定义不可以重复。
声明与定义的分离看似有些不方便,但是它可以使实现与接口分离,而且头文件本身就是很好的接口说明文档,具有较好的自描述性,加上现在较智能的集成开发环境(IDE),比起阅读其它类型的文档更方便。
C#在3.0中也加入了“部分方法(Partial method)”的概念,其作用与头文件基本相似,这也说明了头文件的优点。
从工程上讲,头文件的文件名应该与对应的源文件名相同便于维护,如果头文件中包含了多个源文件中的定义或声明,则应该按源文件分组布局头文件中的代码,并且通过注释注明每组所在的源文件。
当一个工程的文件较多时应该将源文件与头文件分开目录存放,一般头文件存放在include或inc目录下,而源文件存放在source或src目录下,根据经验,一个工程的文件数超过30个时应该将源文件与头文件分开存放,当文件较少时直接放到同一目录即可。
1.1.
2. 头文件中为什么有#ifndef/#define/#endif预编译指令
虽然函数、变量的声明都可以重复,所以同一个声明出现多次也不会影响程序的运行,但它会增加编译时间,所以重复引用头文件会使浪费编译时间;而且,当头文件中包含类的定义、模板定义、枚举定义等一些定义时,这些定义是不可以重复的,必须通过一定措施防止重复引用,这就是经常在头文件中看到的
#ifndef/#define/#endif的原因,一般形式如例程[2-8] 所示。
// 例程[2-8]
#ifndef HEADERFILE_H
#define HEADERFILE_H
// place defines and declarations here
#endif
一些编译器还支持一些编译器指令防止重复引用,例如Visual C++支持
#pragma once
指令,而且可以避免读磁盘文件,比#ifndef/endif效率更高。
1.1.3. #include与#include”filepath”有什么区别
在C++中有两种引用头文件的形式:
// 形式1
#include
// 形式2
#include “filename”
其实,C++标准中也没有确定这两种方式搜索文件filepath的顺序,而是由编译器的实现确定,其区别就是如果编译器按照第二种形式定义的顺序搜索文件filepath失败或者不支持这种方式时,将其替换为第一种顺序再进行搜索。
而实际上,一般来讲第一种方式都是先搜索编译器的系统目录,而第二种方式则是以被编译的头文件所在目录为当前目录进行搜索,如果搜索失败再在系统头文件里搜索。
这两种方式从本质上讲没有什么区别,但当我们自己的程序文件与系统头文件重名时,用后者就会先搜到我们的头文件而不是系统的。
但无论如何,与系统头文件重名都不是一个好习惯,一不小心就可能带来不必要的麻烦,当我们自己编写程序库时,最好把它放入一个目录里,不把这个目录直接添加到编译器的头文件搜索路径中(如gcc的-I, visual c++的/I选项等,其实在UNIX/Linux 平台的编译器一般都是-I选项),而是添加到上一级目录,而在我们的源文件中引用该头文件时就包含该目录名,这样不容易造成冲突。
例如,我们创建了一个程序库叫mylib,其中一个头文件是strutil.hpp,我们可以创建一个/home/user/project/src/mylib目录,然后把strutil.hpp放进去,然后把 /home/user/project/src添加到编译选项里:
gcc -I/home/user/project/src
这样,在我们的源程序中可以这样引用strutil.hpp文件:
#inclu de “mylib/strutil.hpp”
通过显示的目录名引用头文件就不容易产生冲突,不容易使我们自己的头文件与系统头文件产生混淆。
当然,从代码逻辑上我们还有另外一种解决冲突的方案,那就是命名空间,详见第[?]节。
1.1.4. #include 与#include有什么区别
这两个的区别是比较明显的,因为它们引用的不是同一个头文件,但其作用是不明显的,在功能上并没有任何区别。
不带扩展名,以字母c为前缀的一系列头文件只是C++将对应的C语言标准头文件引入到了std命名空间中,将标准库统一置入std命名空间中,另外如cstdlib、cmath等。
如果引用了后者,则需要在使用标准函数库时使用
using namespace std;
以引入std命名空间,或显示通过域作用符调用标准库函数,如
std::printf(“hello from noock”);
建议在C++项目中,特别是大中型项目中使用后者,尽可能避免标识符的冲突。