c++中头文件与实现文件的关系

关于两者以前的关系,要从N年以前说起了~ long long ago,once aupon a time .......

那是一个被 遗忘的年代,在编译器只认识.c(.cpp))文件,而不知道.h是何物的年代。
那时的人们写了很多的.c(.cpp)文件,渐渐地,人们发现在 很多.c(.cpp)文件中的声明语句就是相同的,但他们却不得不一个字一个字地重复地将这些内容敲入每个.c(.cpp)文件。但更为恐怖的是,当其中 一个声明有变更时,就需要检查所有的.c(.cpp)文件,并修改其中的声明,啊~简直是世界末日降临!
终于,有人(或许是一些人)再不能忍受这 样的折磨,他(们)将重复的部分提取出来,放在一个新文件里,然后在需要的.c(.cpp)文件中敲入#include   XXXX这样的语句。这样即使某个声明发生了变更,也再不需要到处寻找与修改了---世界还是那么美好!
因为这个新文件,经常被放 在.c(.cpp)文件的头部,所以就给它起名叫做“头文件”,扩展名是.h.
从此,编译器(其实是预处理器)就知道世上除了.c(.cpp)文 件,还有个.h的文件,以及一个叫做#include命令。

虽然后来又发生很多的变化,但是这样的用法一直延续至今,只是时日久远了,人们便淡忘了当年的缘由罢了。
提到了头文件,就说说它的作用吧~
想 到了林锐GG写的高质量C/C++编程上头文件的作用的简短描述:
(1)通过头文件来调用库功能。在很多场合,源代码不便(或不准)向用户公布, 只要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口怎么实现的。编译器会从库中提取相应的代码。
(2) 头文件能加强类型安全检查。如果某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,这一简单的规则能大大减轻程序员调试、改 错的负担。


预处理是编译器的前驱,作用是把存储在不同文件里的程序模块集成为一个完整的源程序.
#include本身只是一个简单的文件包含 预处理命令,即为把include的后面文件放到这条命令这里,除此之外,没有其它的用处(至少我也样认为).


我对乾坤一笑兄的观点,十分赞同,基础的东东一定要弄明白.
我下面就乾坤一笑兄的例子做讲,完备他的一些让人迷惑不解的时候~

例子:
//a.h
void foo();


//a.c
#include "a.h"  //我的问题出来了:这句话是要,还是不要?
void foo()
{
    return;
}

//main.c
#include "a.h"
int main(int argc, char *argv[])
{
   foo(); 
  return 0;

针对上面的代码,请回答三个问题: 
a.c 中的 #include "a.h" 这句话是不是多余的? 
1.为什么经常见 xx.c 里面 include 对应的 xx.h? 
2.如果 a.c 中不写,那么编译器是不是会自动把 .h 文件里面的东西跟同名的 .c 文件绑定在一起? 
3.第三个问题我给他改了一下:如果 a.c 中不写include<>,那么编译器是不是会自动把 .h 文件里面的东西跟同名的.c文件绑定在一起?

下面是乾坤一笑的原话:

从C编译器角度看,.h和.c皆是浮云,就是改名为.txt、.doc也没有大的分别。换句话说,就是.h和.c没啥必然联系。.h中一般放的是同 名.c文件中定义的变量、数组、函数的声明,需要让.c外部使用的声明。这个声明有啥用?只是让需要用这些声明的地方方便引用。因为 #include "xx.h" 这个宏其实际意思就是把当前这一行删掉,把 xx.h 中的内容原封不动的插入在当前行的位置。由于想写这些函数声明的地方非常多(每一个调用 xx.c 中函数的地方,都要在使用前声明一下子),所以用 #include "xx.h" 这个宏就简化了许多行代码——让预处理器自己替换好了。也就是说,xx.h 其实只是让需要写 xx.c 中函数声明的地方调用(可以少写几行字),至于 include 这个 .h 文件是谁,是 .h 还是 .c,还是与这个 .h 同名的 .c,都没有任何必然关系。
    这样你可能会说:啊?那我平时只想调用 xx.c 中的某个函数,却 include了 xx.h 文件,岂不是宏替换后出现了很多无用的声明?没错,确实引入了很多垃圾 ,但是它却省了你不少笔墨,并且整个版面也看起来清爽的多。鱼与熊掌不可得兼,就是这个道理。反正多些声明(.h一般只用来放声明,而放不定义,参见拙著 “过马路,左右看”)也无害处,又不会影响编译,何乐而不为呢?
翻回头再看上面的3个问题,很好解答了吧?
它的解答如下:

答:1.不一定。这个例子中显然是多余的。但是如果.c中的函数也需要调用同个.c中的其它函数,那么这个.c往往会include同名的.h,这 样就不需要为声明和调用顺序而发愁了(C语言要求使用之前必须声明,而include同名.h一般会放在.c的开头)。有很多工程甚至把这种写法约定为代 码规范,以规范出清晰的代码来。 
2.答:1中已经回答过了。 
3.答:不会。问这个问题的人绝对是概念不清,要不就是想混水摸鱼。非常 讨厌的是中国的很多考试出的都是这种烂题,生怕别人有个清楚的概念了,绝对要把考生搞晕。

over!

在此里要明确一点,编译器是按照编译单元进行编译的,所谓的编译单元,是指一个.c文件以及它所include的所有.h文件.最直观的理解就是一 个文件,一个工程中可以包含很多文件,其中有一个程序的入口点,即我们通常所说的main()函数(当然也可以没有这个函数,程序照样能启动,详细见我的 blog中).在没有这个程序入口点的情况下,编译单元只生成目标文件object file(.o文件,windows下叫做.obj).

这个例子中总共包含了二个编译单元,分别是a.c,main.c,按照我所说的,在编译阶段只是生成各自的.o文件.这个阶段不和其它的文件发生任 何的关系.
而include这个预处理指令发生在预处理阶段(早先编译阶段,只是编译器的一个前驱处理程序).


.h .c不见得是浮云,脱离了编译器谈这些没有任何的意义,抛开更深层次的这些,比如说,OS如何启动这个文件,PE结构(linux 下为elf)等等
编译器首先要识别这个文件才可能去编译它,这是前提.如果你改了它的扩展名那么你的编译器还能认识它吗~上升到一个更高的层次上 看待这个问题,XX兄说的也不错~我想XX兄说的意思就是两者不可因为名字相同就认为两者有什么关系,名字是可以随便的~
两者之间的联系,我在前 面说过了,是由于历史的原因造成的,再加上人的习惯,我想谁也不想多去记那么多文件名吧.(拿我举个例子,一个数
据表如果多于30个字段,我就觉 得头大了,现在弄的表有的多达上百个字段,真希望那位高人研究出什么好的方法来~,也让我们的世界美好一些~)

乾坤一笑的第三个问题很有代表性,多次在网上看到,现在的编译器绝对没有那么智能,而且也没有必须那么做.下面我们主要聊聊编译器的处理过程.(我 想初学者有疑问的正在于此,即是对于编译过程.h .c(.cpp)的变化不太了解,)

下面我说举个简单的例子来聊聊~
例子如下:
//a.h
class   A
{
pubic:
      int   f(int   t);
};

//a.cpp
#include   "a.h"
int   A::f(int   t)
{
    return   t;
}

//main.cpp
#include   "a.h"
void   main()
{
      A   a;
      a.f(3);
}
在预处理阶段,预处理器看到#include "文件名"就把这个文件读进来,比如它编译main.cpp,看到#include   "a.h",它就把a.h的内容读进来,它知道了,有一类A,包含一个成员函数f,这个函数接受一个int型的参数,返回一个int型的值。再往下编译很 容易就把A   a这行读懂了,它知道是要拿A这个类在栈上生成一个对象。再往下,它知道了下面要调用A的成员函数f了,参数是3,由于它知道这个函数要一个整形数用参 数,这个3正好匹配,那就正好把它放到栈上,生成一条调用f(int)函数的指令(一般可能是一句call),至于这个f(int)函数到底在哪里,它不 知道,它留着空,链接时再解决。它还知道f(int)函数要返回一个int,所以也许它也为这一点做好了准备(在例子中,我们没用这个返回值,也许它就不 处理)。再往下到文件末尾了main.cpp编译好了,生成了main.obj。整个编译过程中根本就不需要知道a.cpp的内容。
同理,编译器 再编译a.cpp,把f()函数编译好,编译a.cpp时,它也不用管别的,把f()编译好就行了。生成了a.obj。
最后一步就是链接的阶段 了,链接器把项目中所有.cpp生成的所有.obj链接起来,
在这一步中,它就明确了f(int)函数的实现所在的地址,把main.obj中空 着的这个地址位置填上正确的地址。最终生成了可执行文件main.exe。

明白了吗?不明白那就多说几句了,我们在学编译原理的时候都知道,编译器是分阶段进行的,每一个阶段将源程序从一种表示转换成另一种表示,一般情况 下都进行如下顺序:源程序->词法分器->语法分析器->语义分析器->中间代码生成器->代码优化器->代码生成 器->目标程序.
其中这中间6项活动都要涉及的两项主要活动是:符号管理器与错误处理器.
归根原因,这里有一个叫做符号表的东东在 里面让你着魔一样不明白,其实符号表是一个数据结构.编译器的基本一项功能就是要记录源程序中使用的标识符并收集与每个标识符相关的各种属性信息.属性信 息表明了该标识符的存储位置/类型/作用域(在那个阶段有效)等信息,通俗的说一下就是,当编译器看到一个符号声明时,例如你的函数名它就会把它放到这个 符号表中去登记一下~符号表里存放着你的函数的入口地址,参数个数,返回信息等等一堆东西~而在联接阶段主要是处理工程中的符号表与调用对应处理关系,即 我们通常所说的解引用.
经过前面的,不知明白与否?

最后引用一下XXX兄的结尾三点:
搞清楚语法和概念说易也易,说难也难。窍门有三点:
1.不要晕着头工作,要抽空多思考思考,多看 看书; 
2.看书要看好书,问人要问强人。烂书和烂人都会给你一个错误的概念,误导你; 
3.勤能补拙是良训,一分辛苦一分才;



  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
reprint 使用说明 本人长期使用delphi做数据库的开发,报表控件使用Quickrpt,在打印上经常遇到一些问题,于是自己经常编写一部分打印的程序,经过总结开发了这个控件。 本控件可打印 datasource,dbgrid,stringgrid. 一 、控件属性: 1、colstitle 设置报表的列标题属性 (1)alignment 列标题对齐方式。 (2)font 列标题字体 (3)print 设置是否打印列标题 (4)rowspace 列标题行的高度 2、datasource 选择要打印的datasource 3、dbgrid 选择要打印的dbgrid 4、stringgrid 选择要打印的stringgrid 5、detail 设置要打印的明细数据属性 (1)arrange 设置明细字段排列方向 Horizontal 横向打印(默认的一般打印) Vertical 竖向打印(字段竖向排列适合打印字段内容较长的报表如“会议纪录”) (2)aotureturn 打印明细字段时,如果字段超过列的宽度是否允许自动换行 (3)colsline 设置竖线属性引用tpen类 (4)footer 设置明细尾 (5)frame 设置明细边框属性引用tpen类 (6)head 设置明细头同footer (7)rowsline 设置横线属性引用tpen类 (8)rowspace 设置明细行的高度 (9)top 明细与标题的距离 6、page 设置纸张的上下左右边距 7、pagefooter 设置页脚同明细尾 8、pageheader 设置页眉同页脚 9、printobject 选择要打印的对象 (datasource 、dbgrid、 stringgrid ) 10、title 设置标题类页眉、页脚 二 、控件方法: (1) preview 预览 (2) print 打印 (3) SaveToFile() 保存为报表文件 (4) SaveToStream() 保存为流(然后就可把整个报表存到数据库了很方便哦! ~_~) (5) PrintFile() 打印报表文件 (6) PrintStream() 打印流(流的数据库存取就不用多说了吧) (7) PreviewFile() 预览文件 (8) PreviewStream() 预览流 (9) OptionToStream() 报表的属性保存为流 (10) OptionFromStream() 从流得到报表属性 三、 控件事件: AfterPrint 打印后 BeforePrint 打印前 (本来很多事件,但没有预览,后来加上预览以及一些方法后实现很困难就去掉了!) 四、注意事项 (1) text 属性,输入“¦;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;#¦;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;”打印页码值,如果想打印页码可输入“第¦;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;#¦;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;页” 输入“¦;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;RecordCount¦;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;”打印 记录数 (2) 不同的打印机,打印和预览效果可能有一点点不同(保证数据打印完全正确一致)。本人用一台针式和一台激光打印机做测试,开始差别较大后经过调试基本一致,不知其他打印机如何。 就这些个吧基本满足了我的软件开发需要,解决了很多问题,关键是做报表省了很大力气,当然了很复杂的报表还是要其它报表控件来解决。如果有其他问题或好的建议可与我联系。 程序设计:吴进昊 E-mail :jinhaowu@hotmail.com qq :54254770 2003.01.03
C++ 头文件和源文件是程序模块化编程的基本单位,它们分别承担了不同的作用。 头文件C++ 程序用于存放函数、类、宏定义等声明的文件,它的主要作用是为源文件提供需要的函数、类等定义信息。头文件通常使用 `.h` 或 `.hpp` 作为文件后缀名,常见的标准库头文件有 `iostream`、`cstdio`、`cstring` 等。头文件通常包含了以下内容: - 宏定义 - 函数声明 - 类声明和定义 - 变量声明 源文件C++ 程序用于存放实现代码的文件,它的主要作用是实现头文件声明的函数、类等内容。源文件通常使用 `.cpp` 或 `.cc` 作为文件后缀名,常见的源文件有 `main.cpp`、`myclass.cpp` 等。 头文件和源文件之间有着密切的关系头文件包含了函数、类等的声明信息,而源文件则包含了函数、类等的具体实现头文件和源文件之间可以通过 `#include` 指令进行关联。在使用头文件时,需要使用 `#include` 将头文件包含到源文件,从而可以在源文件使用头文件的函数、类等定义信息。 在编写 C++头文件和源文件时,需要注意以下事项: 1. 头文件要有保护措施 头文件可能被多个源文件引用,如果头文件定义的内容被重复包含,就会导致编译错误。为了避免这种情况,头文件一般需要添加保护措施,例如: ```c++ #ifndef MYHEADER_H #define MYHEADER_H // 头文件内容 #endif ``` 2. 头文件和源文件的命名要清晰 头文件和源文件的命名应该简洁明了,能够清晰地表达文件的作用和内容。通常情况下,头文件的命名应该以 `.h` 或 `.hpp` 结尾,源文件的命名应该以 `.cpp` 或 `.cc` 结尾。 3. 头文件只包含必要的内容 头文件只应该包含必要的内容,例如函数、类、宏定义等声明信息,不应该包含具体的实现代码。具体的实现代码应该放在源文件。 总之,头文件和源文件C++ 程序模块化编程的基本单位,头文件提供了声明信息,源文件提供了具体的实现代码,它们之间可以通过 `#include` 指令进行关联。在编写头文件和源文件时,需要注意头文件的保护措施、文件命名的清晰和头文件只包含必要的内容等问题,以避免出现编译错误和代码混乱的情况。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值