一 .h .c 文件

先来看看这些文件的编译连接过程,然后引出一些具体的问题,如.h文件的相关作用、头文件编写规则、

(1)编译、链接

先来分析下下面这个小例子:

//a.h

class   A

{

pubic:

int   f(int   t);

};

//a.c

#include   "a.h"

int   A::f(int   t)

{

return   t;

}

//main.c

#include   "a.h"

void   main()

{

A   a;

a.f(3);

}

1 预处理阶段:预处理器看到#include "文件名"就把这个文件读进来,比如它编译main.c,看到#include"a.h",它就把a.h的内容读进来,它知道了,有一类A,包含一个成员函数f,这个函数接受一个int型的参数,返回一个int型的值。

2 编译/汇编阶段:再往下编译很容易就把A a这行读懂了,它知道是要拿A这个类在栈上生成一个对象。再往下,它知道了下面要调用A的成员函数f了,参数是3,由于它知道这个函数要一个整形数用参数,这个3正好匹配,那就正好把它放到栈上,生成一条调用f(int)函数的指令(一般可能是一句call),至于这个f(int)函数到底在哪里,它不知道,它留着空,链接时再解决。该阶段生成a.o和main.o文件

3 链接阶段:链接a.o和main.o文件,即明确了f(int)函数的实现所在的地址,把main.o中空着的这个地址位置填上正确的地址。最终生成了可执行文件main.out。

大致过程如下:

源码

预处理

编译、汇编

链接

Main.c

a.h

a.c

Main.i

a.i

Main.sàmain.o

a.sàa.o

Main.out

main.c文件中引用了#include”a.h”,根据C语言的特性,任何变量、函数在使用前都必须先声明,(方法一)包含相应的声明头文件是一种方法,当然若不怕麻烦,(方法二)可以将main中用到的每一个变量、函数都重新声明一遍。

小记:

1 在编译main.c,其包含了头文件#include“a.h“,但是此时根本不需要知道a.c中的实现内容。也就是说各个.c 文件的编译是相互独立的(C语言中一个.c文件对应一个模块)

2 根据方法二,说明.h文件并不是必须的,也就是说并不是每个.c文件都需要一个对应的.h文件,

二 头文件的由来

刚开始并没有.h文件,编译器也不认识头文件,大家的代码都是.c  .cpp,但是人们渐渐的发现了这样组织的缺点:1、很多.c(.cpp)文件中的声明语句就是相同的,但他们却不得不一个字一个字地重复地将这些内容敲入每个.c(.cpp)文件。如上面所说的方法二。2、当其中一个声明有变更时,就需要检查所有的.c(.cpp)文件,并修改其中的声明,啊~简直是世界末日降临!

形成.h文件:将重复的部分提取出来,放在一个新文件里,然后在需要的.c(.cpp)文件中敲入#includeXXXX这样的语句。具体叙述见【参考1】、具体的例子演变说明头文件的必要性【参考二】

三 头文件的构成

(1)一般的写法

//mymath.h

#ifndef _mymath_H

#define _mymath_H

extern int Global_A; //声明必要的全局变量

......

extern void fun(); //声明必要的外部函数

//根据下面的准则5,这里的extern最好不要,因为在顶层作用域中

//函数、变量的默认存储类说明符为extern

.....

#endif

在头文件中声明了全局变量和外部函数都添加了必要这两个字,说明这是提供给别的模块使用的函数即接口,对于那些只在自己本模块中用的函数不必放在头文件中进行声明,只需在实现文件中进行声明即可,见下面的实现代码。

#ifndef、#define、#endif的作用见【文章】。

//mymath.c

#include "mymath.h "

#include

-----------------------------------------------------------------

int Global_A ; //定义必要的全局变量和函数

void fun();

Static int a,b,c; //声明一些内部使用的全局变量及函数

//这里的内部指的是本编译模块如第一小节说的main.c 或者 a.c

//既然是本模块的变量和函数,应该声明为static

Static void somefun();

-----------------------------------------------------------------

//函数实现体

void fun()

{

}

Static void somefun()

{

}

在【参考3】中,作者提出对全局变量的定义进行了重新的布局。一个模块与其他模块的数据传递最好通过专有的模块进行,而不要直接通过数据单元直接传递(这是VC++的思想),因此不建议在模块的头文件中声明全局变量;全局变量最好统一定义在一个固定的文件中,所以可以采用下面的方法:

//Globel_Var.c

/*******定义本工程中所用到的全局变量*******/

int speed ;

int torque ;

//Globel_Var.H

/*******声明本工程中所用到的全局变量*******/

extern int speed;

extern int torque;

定义一个Global_Var.C文件来放全局变量,然后在与之相对应的Globel_Var.H文件中来声明全局变量。

这样哪个文件用到这两个变量就可以在该文件的开头处写上文件包含命令;例如aa.C文件要用到speed,toque两个变量,可以在aa.H文件中包含Globel_Var.H文件。(这里不是很明白原作者为什么将Global_Var.H头文件包含在aa.H中,而不是包含在要用到它的实现模块中?求大神帮忙)

(2)头文件类型

1、头文件是为用户提供调用接口,这种头文件中声明了模块中需要给其他模块使用的函数和数据。更多规则见【参考3】

2、说明性头文件,这种头文件不需要有一个对应的代码文件,在这种文件里大多包含了大量的宏定义,没有暴露的数据变量和函数。

(3)一些准则

1头文件中不能有可执行代码,也不能有数据的定义,只能有宏、类型(typedef,struct,union,menu,class),数据和函数的声明。

(1)要么是纯纯的声明 用extern声明并无赋值或者函数体

(2)在程序执行时只有一份数据如const常量、全局静态变量

(3)唯一宗旨保持数据的一次定义规则

#define NAMESTRING “name”

typedef unsigned long word;

menu{ flag1,flag2};

typedef struct

{

int x;

int y;

}Piont;

extent Fun(void);

extent int a;

int a ;//全局变量这样的形式就是的定义,所以不能放在头文件中

2头文件中不能包本地数据(模块自己使用的数据或函数,不被其他模块使用)

这一点相当于面向对象程序设计里的私有成员,即只有模块自己使用的函数,数据,不要用extern在头文件里声明,只有模块自己使用的宏,常量,类型也不要在头文件里声明,应该在自己的*.c文件里声明。

3含一些需要使用的声明。在头文件里声明外部需要使用的数据,函数,宏,类型。

4 防止重复包含

5 在.h文件中声明的函数,如果在其对应的.c文件中有定义,那么我们在声明这个函数时,不使用extern修饰符,如果反之,则必须显示使用extern修饰符.

6 不要在.c文件中使用extern,这是在.h文件中使用的。

四 头文件的作用

(1)简化了声明的书写。

(2)通过头文件来调用库功能。在很多场合,源代码不便(或不准)向用户公布,只要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口怎么实现的。编译器会从库中提取相应的代码。

(3)头文件能加强类型安全检查。如果某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,这一简单的规则能大大减轻程序员调试、改错的负担。

五 结论

拼拼筹筹总算把这个写完,但是大部分的内容都是参考里的博文,感谢他们的分享。

头文件刚开始的主要作用是简化各个模块之间互相引用时声明的方便性,也就是头文件包含的是非本模块要用到的函数接口或变量或类型。再着有了小节四中的其他作用。

头文件的出现于存在与C语言本身的特性也息息相关。因为C语言中不管是变量还是函数,在使用前都需要声明;变量、函数的作用域也十分的重要【文章】

要想写好头文件,需明白一些知识点:1、C语言中声明和定义的区别【文章】 2 哪些声明该写到头文件中 3 头文件在程序模块中的作用

注:

1 extern 应用感觉比较混乱,一个是人为的一些规定,另外编译器默认认为顶层作用域的存储类说明符为extern

2 在C++中class A{};这是一个定义,而classA;这是一个声明。

关于类的声明和定义。

class A;  //类的声明

类的声明和普通变量声明一样,不产生目标代码,可以在同一,以及多个编译单元重复声明。

class A {

}; //类的定义

类的定义就特殊一点了,可能会有疑问,为什么不能把int x;这样的变量定义放到.h中(见4)但是可以把

类的定义放在头文件中重复引用呢?同时类的函数非inline定义(写在类定义里面的函数是inline,除外)不能写在

头文件中呢。

这是因为类的定义,只是告诉编译器,类的数据格式是如何的,实例话后对象该占多大空间。

类的定义也不产生目标代码。因此它和普通变量的声明唯一的区别是不能在同一编译单元内出现多次。

//source1.cc

class A;

class A;  //类重复声明,OK

class A{

};

class A{

};

class A{

int x;

};    //同一编译单元内,类重复定义,会编译时报错,因为编译器不知道在该编译单元,A a;的话要生产怎样的a.

//如果class A{};定义在head.h ,而head.h 没有

//#ifndef  #endif 就很可能在同一编译单元出现类重复定义的编译错误情况。

但是在不同编译单元内,类可以重复定义,因为类的定义未产生实际代码。

//source1.cc

class A{

}

//source2.cc

class A{

}                      //不同编译单元,类重复定义,OK。所以类的定义可以写在头文件中!

//source1.cc

class A{

}

//source2.cc

class A{

int x;

}                      //不同编译单元,OK

3//head.h

int x;

//source1.cc

#include “head.h”

//source2.cc

#include “head.h”

头文件不被编译,.cc中的引用 include “ head.h”其实就是在预编译的时候将head.h中的内容插入到.cc中。

所以上面的例子如果

g++ –o test source1.cc source2.cc, 同样会链时发现重复定义的全局变量x。

因此变量定义,包括函数的定义不要写到头文件中,因为头文件很可能要被多个.cc引用。

那么如果我的head.h如下这么写呢,是否防止了x的链接时重定义出错呢?

//head.h

#ifndef _HEAD_H_

#define _HEAD_H_

int x;

#endif

//source1.cc

#include “head.h”

//source2.cc

#include “head.h”

现在是否g++ –o test source1.cc source2.cc就没有问题了呢,答案是否定的。

所有的头文件都是应该如上加#ifndef    #endif的,但它的作用是防止头文件在同一编译单元被重复引用。

就是说防止可能的

//source1.cc

#include “head.h”

#include “head.h”

这种情况,当然我们不会主动写成上面的形式但是,下面的情况很可能发送

//source1.cc

#include “head.h”

#inlcude “a.h”

//a.h

#include “head.h”

这样就在不经意见产生了同一编译单元的头文件重复引用,于是soruc1.cc 就出现了两个int x;定义。

但是对于不同的编译单元source1.cc,source2.cc他们都是还会引用head.h的,即使#ifndef #endif的存在。【参考5】

六 参考资料:

c语言在头文件后int a,C语言头文件 实现文件 工程文件组织相关推荐

  1. vim,编辑文件后最简单的消除~ 和 .un~后缀文件生成的操作 ,重点为红色字体部分

    关于在使用vim或Gvim编辑文件后,会自动生成的2个文件. ~后缀的是文件旧版本的备份文件. .un~后缀的文件是用于当你再次打开文件时也能进行撤销上次的更改. 禁止生成这两个文件的步骤 1. 打开 ...

  2. linux新建好文件后怎么编译,使用autoconf生成Makefile并编译工程的步骤

    前言 在Linux下,编译和链接需要用Makefile,而写好一个Makefile可不容易,写出一个规范的Makefile更是麻烦.我们可以利用autoconf来生成一个Makefile,尤其在大型工 ...

  3. php上传文件后定时删除,PHP根据条件定时删除文件代码

    这是一个根据文件的创建时间进行判断删除文件的,一般用于文件上传后定时删除文件. 是个很实用的东西(至少对于我来说是这样). 下面开始代码: /* 本文件用法:放到目录,在程序头部添加 include( ...

  4. python怎么读文件后删去空格以行为单位进行排序-文件操作

    day15回顾 迭代器 iter(iterable) 返回迭代器 next(iterator) 返回可迭代对象提供的数据,当没有数据时触发StopIteration异常通知 生成器: 两种: 生成器函 ...

  5. Python文件用pyinstaller打包成.exe文件后如何获取Python源码(Python文件反编译)

    此文章自己做个学习记录,也希望对跟我有一样困扰的同学一些帮助! 使用到的工具下载链接我都已经附上,点击下面蓝色字体可直接下载: 1.pyinstxtractor.py 2.wxMEDIT 3.在线编译 ...

  6. plsql导入dmp文件后服务器无数据,使用plsql导入dmp文件缺少imp*.exe

    md语法之行内代码和代码片续集 md语法之行内代码和代码片 一行之内嵌入一小段代码, 简称行内代码. 其方法为: 用撇号把代码围起来. 比如: import numpy as ny就可以了. 代码片的 ...

  7. php fopen 清空文件内容,如何在c语言中清空文件里的内容?

    对一个文件用读写方式打开 fopen("...", "r+");首先读出文件里面的(9php.com)内容,处理完成后需要重新写入文件中.在重新写入的(9php ...

  8. Java 检查文件后生成8位随机数

    Java 检查文件后生成8位随机数 先检查目标文件中是否有数据,如果有则不执行操作,没有就生成一个新的8位随机数. import java.io.*; import java.util.Objects ...

  9. linux修改文件后退出,LINUX vim 修改文件 退出

    vim 保存退出, 先按ESC ,然后:wq(保存退出)W:write,写入 Q:quit,退出, 也可以直接输入X,代表WQ,也是保存退出 或者 先按ESC,再按shift+ZZ 也是保存退出 正常 ...

  10. 删除U盘文件夹后自动生成不同位数字的新文件夹

    夹删除U盘文件后自动生成不同位数字的新文件夹解决方法 右键点击目录所在磁盘(C,D,E这种)的属性-选择工具-选择检查,系统自动修复文件系统错误. 如果U盘错误比较严重,修复的时间可能会比较久 然后在 ...

最新文章

  1. 数据结构_Search
  2. Lombok(1.14.8) - @Synchronized
  3. linux磁盘阵列oravote,Oracle在Linux下集群RAC的安装与启停
  4. 精通Spring Boot—— 第二十一篇:Spring Social OAuth 登录简介
  5. java抽象类实现接口可以不用实现方法
  6. 闲谈“个人核心竞争力”与“危机感” !!!
  7. Spring实战 MethodInvokingJobDetailFactoryBean使用与分析
  8. HTCondor下多台Linux计算集群的搭建
  9. 【快学springboot】2.Restful简介,SpringBoot构建Restful接口
  10. office 2010 安装
  11. 我的移动开发春季历程,大厂面试题汇总
  12. 顺序结构程序设计(顺序结构与选择结构)
  13. 手把手教你编译Flutter engine
  14. 用java代码检查sql语法错误_您的SQL语法有错误;检查与MariaDB服务器版本对应的手册,以便在第1行'?'附近使用正确的语法...
  15. IOS端微信小程序API播放视频无效,应该这样做
  16. Vue中使用的el-upload开启multiple属性,但onSuccess部分数据status:uploading状态,影响图片回显
  17. go postgresql 增删改查
  18. Java Web前端到后台常用框架介绍
  19. C语言最难学的四大内容是什么?
  20. springboot小区物业管理系统maven idea1562

热门文章

  1. JavaWeb01-HTML篇笔记(一)
  2. 动态控制C4C UI元素的显示和隐藏
  3. 分享:国外著名代码管理网站GitHub访问方式
  4. 小弟浅谈asp.net页面生成周期---上
  5. 自定义nagios监控mogilefs存储节点脚本
  6. 一起谈.NET技术,ASP.NET的状态管理
  7. 最近遇到个关于接口的奇怪的问题
  8. 【JavaAndroid开源库代码剖析】のandroid-smart-image-view
  9. expdp 字符集从ZHS16GBK到AL32UTF8
  10. 在Linux上显示某个进程的线程的几种方式