admin 管理员组

文章数量: 1086019


2024年3月20日发(作者:js幻灯片播放效果)

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/)等。gcc默认支持的C++源文件扩展名

有.cc、.cp、.cpp、.cxx、.c++、.CPP、.C(注意后两项是大写,在Unix/Linux上的文件

名是区分大小写的)。例如在gcc中你可以这样编译一个扩展名为.cplusplus的C++程序:

g++ -x c++ lus

虽然文件名对程序没有任何影响,但.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类定义在头

文件中,相应的源代码文件的内容如例程[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;

();

string::size_type pos1, pos2;

for(;;) {

pos1 = pos2 = 0;

if((pos1 = _first_not_of(delimiters, pos2))

== string::npos)

break;

if((pos2 = _first_of(delimiters, pos1))

!= string::npos){

_back((pos1, pos2 - pos1));

}else {

_back((pos1));

break;

}

(0, pos2);

}

return ();

}

函数声明可以放在任何一个调用它的函数之前,而且在调用一个函数之前必须在调用

者函数之前定义或声明被调函数。函数的定义只能有一次,如果调用者与被调用者不在同

一编译单元,只能在调用者之前添加函数的声明。函数定义只能有一次,函数声明可以有

无限次(理论上),这也是头文件的作用,将一批函数的声明放入一个头文件中,在任何需

要这些函数声明的地方引用该头文件,以便于维护。

函数声明之前有一个可选的extern修饰符,表示该函数是在其它编译单元内定义的,

或者在函数库里。虽然它对于函数的声明来讲不是必须的,但可以在一个源文件中直接声

明其它编译单元内实现的函数时使用该关键词,从而提高可读性。假如例程[2-6]中的函数

SplitString定义在文件中定义,而且在还定义了很多字符串相关

的函数,只用到了中SplitString这一个函数。而您为了提高编译速

度,可以直接在中声明该函数,而不是直接引用头文件,此时最好使用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,其中一个头文件是,我们可以创

建一个/home/user/project/src/mylib目录,然后把放进去,然后把

/home/user/project/src添加到编译选项里:

gcc -I/home/user/project/src

这样,在我们的源程序中可以这样引用文件:

#include “mylib/”

通过显示的目录名引用头文件就不容易产生冲突,不容易使我们自己的头文件与系统

头文件产生混淆。

当然,从代码逻辑上我们还有另外一种解决冲突的方案,那就是命名空间,详见第[?]

节。

1.1.4. #include 与#include有什么区别

这两个的区别是比较明显的,因为它们引用的不是同一个头文件,但其作用是不明显

的,在功能上并没有任何区别。不带扩展名,以字母c为前缀的一系列头文件只是C++将

对应的C语言标准头文件引入到了std命名空间中,将标准库统一置入std命名空间中,

另外如cstdlib、cmath等。

如果引用了后者,则需要在使用标准函数库时使用

using namespace std;

以引入std命名空间,或显示通过域作用符调用标准库函数,如

std::printf(“hello from noock”);

建议在C++项目中,特别是大中型项目中使用后者,尽可能避免标识符的冲突。


本文标签: 函数 头文件 声明 定义 文件