如何正确编写 C 语言头文件和与之相关联的 c 源程序文件

查看此文章需要有一定的C语言编程基础

首先就要了解它们的各自功能。要理解C 文件与头文件(即.h)有什么 不同之处,首先需要弄明白编译器的工作过程。 一般说来编译器会做以下几个过程: 1.预处理阶段 2.编译阶段,首先编译成纯汇编语句,再将之汇编成跟 CPU 相关的二进 制码,生成各个目标文件 (.obj文件) 3.连接阶段,将各个目标文件中的各段代码进行编写 假定这个C 文件内容如下:

#include<stdio.h>#include "mytest.h"int main(int argc,char **argv) { test = 25; printf("test........... %d\n",test); } 

头文件"mytest.h"包含如下内容:

int test;

现在以这个例子来讲解编译器的工作: 1.预处理阶段:编译器以 C 文件作为一个单元,首先读这个 C 文件,发 现第一句与第二句是包含一个头文件,就会在所有搜索路径中寻找这两 个文件,找到之后,就会将相应头文件中的宏,变量,函数声明,嵌套 的头文件包含等,进行依赖关系检测,并进行宏替换,看是否有重复声 明与定义的情况发生,最后将那些文件中所有的东东全部扫描进这个当 前的 C 文件中,形成一个中间"C文件"

2.编译阶段,在上一步中相当于将第二个头文件中的test变量扫描进了一 个中间C 文件,那么 test 变量就变成了这个文件中的一个全局变量,此 时就将所有这个中间 C 文件的所有变量,函数分配空间,将各个函数编 译成二进制码,按照特定目标文件格式生成目标文件,在这种格式的目 标文件中进行各个全局变量,函数的符号描述,将这些二进制码按照一 定的标准组织成一个目标文件

3.连接阶段,将上一步成生的各个目标文件,根据一些参数,连接生成最 终的可执行文件,主要的工作就是重定位各个目标文件的函数,变量等, 相当于将个目标文件中的二进制码按一定的规范合到一个文件中。

再回到 C 文件与头文件各写什么内容的话题,一般都在头件中进行函数声明,变量声明,宏声明,结构体声明,而在C 文件中去进行变量定义,函数实现呢?原因如下:

1.如果在头文件中实现一个函数体,那么如果在多个C 文件中引用它,而 且又同时编译多个C 文件,将其生成的目标文件连接成一个可执行文件, 在每个引用此头文件的C 文件所生成的目标文件中,都有一份这个函数的代码,如果这段函数又没有定义成局部函数,那么在连接时,就会发现多个相同的函数,就会报错。

2.如果在头文件中定义全局变量,并且将此全局变量赋初值,那么在多个引用此头文件的 C 文件中同样存在相同变量名的拷贝,关键是此变量被赋了初值,所以编译器就会将此变量放入DATA段,最终在连接阶段, 会在DATA段中存在多个相同的变量,它无法将这些变量统一成一个变 量,也就是仅为此变量分配一个空间,而不是多份空间,假定这个变量在头文件没有赋初值,编译器就会将之放入 BSS 段,连接器会对BSS 段 的多个同名变量仅分配一个存储空间。

3.如果在 C 文件中声明宏,结构体,函数等,那么我要在另一个 C 文件 中引用相应的宏,结构体,就必须再做一次重复的工作,如果我改了一 个 C 文件中的一个声明,那么又忘了改其它C 文件中的声明,这不就出 了大问题了,程序的逻辑就变成了你不可想象的了,如果把这些公共的 东东放在一个头文件中,想用它的C 文件就只需要引用一个头文件就行 了,要改某个声明的时候,只需要动一下头文件就行了这样岂不方便。 再说头文件,头文件是一种文本文件,使用文本编辑器将代码编 写好之后,以扩展名.h 保存就行了。如上所述头文件中一般放一些重 复使用的代码,例如函数声明,变量声明,常数定义 ,宏的定义等等。 在实际编程中,我们在需调用该c 文件相对应的头文件用#include 语句 将头文件包含进来引用时,也就是相当于将头文件中所有内容复制到 #include 处。 为了避免因为重复引用而导致的编译错误,头文件常具有

#ifndef LABEL#define LABEL……….. //代码部分 #endif

的格式。其中,LABEL 为一个唯一的标号,命名规则跟变 量的命名规 则一样。常根据它所在的头文件名来命名,例如,如果头文件的文件 名叫做 hardware.h ,那么可以这样使用:

#ifndef    __HARDWARE_H__ #define   __HARDWARE_H__ ….....   //代码部分 #endif

这样写的意思就是,如果没有定义__HARDWARE_H__ ,则定义 __HARDWARE_H__ ,并编译下面的代码部分,直到遇到#endif。这样, 当重复引用时,由于__HARDWARE_H__ 已经被定义,则下面的代码部 分就不会被编译了,这样就避免了重复定义。 另外一个地方就是使用#include 时,使用引号与尖括号的意思 是不一样的。使用引号(“”)时,首先搜索工程文件所在目录,然后 再搜索编译器头文件所在目录。而使用尖括号(<>) 时,刚好是相反的搜索顺序。假设我们有两个文件名一样的头文件 hardware.h ,但 内容却是不一样的。一个保存在编译器指定的头文件目录下,我们把 它叫做文件 I ;另一个则保存在当前工程的目录下,我们把它叫做文件II。如果我们使用的是#include ,则我们引用到 的是文件 I。如果我们使用的是#include “hardware.h”,则我们 引用的将是文件II 。 其实头文件对计算机而言没什么作用,她只是在预编译 时#include 的地方展开一下,其它就没别的意义了,不管是C还是C++,你把你的函数,变量或者结构体,类啥的放在你的.c 文件里。 然后编lib,dll,obj等等,但对于我们程序员而言,他们怎么知道你 的 lib,dll...里面到底有什么东西?这就要看你的头文件。

你的头文件 就是对用户的说明。函数,参数,各种各样的接口的说明。那既然是 说明,那么头文件里面放的自然就是关于函数,变量,类的“声明” 了。记着,是“声明”,不是“定义”。那么,我假设大家知道声明 和定义的区别。所以,最好不要傻嘻嘻的在头文件里定义什么东西。 比如全局变量: #ifndef _XX_头文件.H #define _XX_头文件.H Int A; #endif 那么,很糟糕的是,这里的int A是个全局变量的定义,所以如果这个头文件被多次引用的话,你的 A会被重复定义显然语法上错了。 只不过有了这个#ifndef 的条件编译,所以能保证你的头文件只被引用一次,不过也许还是会岔子,但若多个c 文件包含这个头文件时还 是会出错的,因为宏名有效范围仅限于本 c 源文件,所以在这多个c文件编译时是不会出错的,但在链接时就会报错,说你多处定义了同一个变量。 . c 文件是程序文件,内含函数实现,变量定义等内容。这样我们将c 和 h 文件分开写成两个文件是一个良好的编程风格。

由上说明可知,头文件其实用来存放函数原型,函数原型是用来向编译器传递函数的一些特定信息的手段。通常情况下,如果在同一个源文 件中的前面(也就是在调用者的前面)已经出现了该函数的定义,编译 器就会记住这个被调用函数的参数数量和类型,以及该函数的返回值的类型。在这个源文件中,编译器会按照函数原型的声明检查后续调用的 参数和返回值,确保调用者正确地按照函数原型的声明向函数传递了正 确的参数数目和类型,并把返回值赋给类型匹配的变量。而函数原型则 又是用函数声明来完成的,函数原型用分号结束,这是函数原型与函数 定义不同的地方。考虑到代码的可读性,在书写函数原型时保留形式参 数变量名,如果被调函数的定义与调用者不在同一个源文件中,那么就需要在函数原型前加上extern 关键字通知编译器被调函数的参数情况和 返回值情况。

C 程序采用模块化的编程思想,需合理地将一个很大的软件划分 为一系列功能独立的部分合作完成系统的需求,在模块的划分上主要依据功能。模块由头文件和实现文件组成,对头文件和实现文件的正确使用方法是: 规则1:头文件(.h)中是对于该模块接口的声明,接口包括该模块提供给 其它模块调用的外部函数及外部全局变量,对这些变量和函数都需 在.h 中文件中冠以extern 关键字声明; 规则2:模块内的函数和全局变量需在.c文件开头冠以static关键字声 明; 规则 3:永远不要在.h 文件中定义变量;许多程序员对定义变量和声明变量混淆不清,定义变量和声明变量的区别在于定义会产生内存分配的操作, 是汇编阶段的概念;而声明则只是告诉包含该声明的模块在连接阶段 从其它模块寻找外部函数和变量。规则4:如果要用其它模块定义的变量和函数,直接包含其头文件即可。 某模块要访问其它模块中定义的全局变量时,只要包含该模块的头文 件即可。 共享变量声明就像在函数间共享变量的方式一样,变量可以在文件中共享。为了共享函数,要把函数的定义放在一个源文件中,然后在需要调用此函数的其他文件中放置声明。共享变量的方法和此方式非常类似。在此之前,不需要区别变量的声明和它的定义。为了声明变量 i,写成如下形式: int i; 这样不仅声明i是int型的变量, 而且也i 进行了定义,从而使编译器为i留出了空间。为了声明没有定义的变量i,需要在变量声明的开始处放置关键字extern: Extern int i; extern 提示编译器变量 i 是在程序中的其他位置定义的(大多数可能是在不同的源文件中),因此不需要为i分配空间。然而,由于关键字extern,使得编译器不会在每次编译其中某个文件时为变量 i 分配额外的内存空间。 当在文件中共享变量时,会面临和共享函数时相似的挑战:确保变量的所有声明和变量的定义一致。为了避免矛盾,通常把共享变量的声 明放置在头文件中。需要访问特殊变量的源文件可以稍后包含适当的 头文件。此外,含有变量定义的源文件包含每一个含有变量声明的头文件,这样使编译器可以检查两者是否匹配。

规则 5 . 作为一般规则,应该把下面所列的内容放入头(.h)文件中:

a.宏定义(预处理#define);

b.结构、联合和枚举声明;

c.typedef声明;

d.外部函数声明;

e.全局变量声明;

当声明或定义需要在多个文件中共享时,把他们放入一个头文件 中尤其重要。不要在两个或多个源文件的顶部重复声明或定义宏。应 该把它们放入一个头文件中,然后在需要的时候用#include 包含进来。 这样做的原因并不仅仅是减少打字输入——这样可以保证在声明或 定义变化的时候,只需要修改一处即可将结果一致地传播到各个源文件。(特别是,永远不要把外部函数原型放到.c 文件中) 最后,不能把实际的代码(如函数体)或全局变量定义(即定义和初始化实例)放入头文件中。

以上文字借鉴:百度文库


为了方便大家理解,我把严薇敏的《数据结构》一书中的线性表方法作为例子:

头函数创建:

#ifndef _LIST_H_
#define _LIST_H_
#include<stdlib.h>
#define LIST_INIT_SIZE 100 /* 线性表存储空间的初始分配量 */
#define LISTINCREMENT 10 /* 线性表存储空间的分配增量 */typedef int ElemType;
typedef struct {ElemType *elem;/*存储基址 */int length;/*当前长度 */int listsize;/*当前存储容量 */
}SqList;int InitList(SqList &L);/*初始化线性表*/
void DestoryList(SqList &L);/*销毁线性表*/
void ClearList(SqList &L); /*清空线性表*/
bool ListEmpty(SqList L);/*线性表是否为空*/
int ListLength(SqList L);/*返回L中的元素个数*/
void GetElem(SqList L, int i, ElemType *e);/*用e返回第i个元素的值*/
int LocateElem(SqList &L, ElemType e, int compare(ElemType p, ElemType e));/*返回L中第一个与满足关系的元素位置,不存在则返回0*/
int PriorElem(SqList L, ElemType cur_e, ElemType *pre_e); /*若cur_e是L的数据元素,且不是第一个,则用pre_e返回它的前驱,*//*           否则操作失败,pre_e无定义 */
int NextElem(SqList L, ElemType cur_e, ElemType *next_e); /* 操作结果:若cur_e是L的数据元素,且不是最后一个,则用next_e返回它的后继, *//*           否则操作失败,next_e无定义 */
int ListInsert(SqList *L, int i, ElemType e);/*在第i个位置插入元素e*/
int ListDelete(SqList *L, int i, ElemType *e); /*删除第i个元素,并用e返回其值*/
int ListTraverse(SqList L, void(*vi)(ElemType*)); /* 操作结果:依次对L的每个数据元素调用函数vi()。一旦vi()失败,则操作失败 *//*           vi()的形参加'&',表明可通过调用vi()改变元素的值 */
void print(ElemType *c);/*输出函数,输出线性表的数据*/#endif

各个模块方法的实现:

// 数据结构.cpp: 定义控制台应用程序的入口点。
//#include "stdafx.h"#include"List.h"/*  要么使用*和.  要么使用& 和 -> 不能同时使用 & 和 ->
*/
int InitList(SqList &L) {/* 构造一个空的顺序线性表 */(&L)->elem = (ElemType*)malloc(LIST_INIT_SIZE * sizeof(ElemType));/*分配空间大小*/if (!(&L)->elem)exit(-1);//0-正常中止,非0-非正常中止  <stdlib.h>(&L)->length = 0; /* 空表长度为0 */(&L)->listsize = LIST_INIT_SIZE; /* 初始存储容量 */return 1;
}void DestoryList(SqList &L) {/* 销毁顺序线性表L */free((&L)->elem);(&L)->elem = NULL;(&L)->length = 0;(&L)->listsize = 0;
}void  ClearList(SqList &L) {/*:将L重置为空表 */(&L)->length = 0;
}bool ListEmpty(SqList L)
{ /* 若L为空表,则返回TRUE,否则返回FALSE */if (L.length == 0)return true;elsereturn false;
}int ListLength(SqList L) {/*返回L中的元素个数*/return L.length;
}void  GetElem(SqList L, int i, ElemType *e) {/*用e返回L中的第i个数据元素的值*/*e = *(L.elem + i - 1);
}int LocateElem(SqList &L, ElemType e, int compare(ElemType p,ElemType e)) {/*返回L中第1个与e满足关系compare的数据元素的位序。若这样的数据元素不存在,则返回0*/ElemType *p;int i = 1; /* i的初值为第1个元素的位序 */p = L.elem; /* p的初值为第1个元素的存储位置 */while (i <= L.length && !compare(*p++, e))++i;if (i <= L.length)return i;elsereturn 0;
}int PriorElem(SqList L, ElemType cur_e, ElemType *pre_e) {/*若cur_e是L的数据元素,且不是第一个,则用pre_e返回它的前驱,*//*           否则操作失败,pre_e无定义 */int i = 2;ElemType *p = L.elem + 1;while (i <= L.length&&*p != cur_e){p++;i++;}if (i>L.length)return -1;else{*pre_e = *--p;return 1;}}int NextElem(SqList L, ElemType cur_e, ElemType *next_e){/* 操作结果:若cur_e是L的数据元素,且不是最后一个,则用next_e返回它的后继, *//*           否则操作失败,next_e无定义 */int i = 1;ElemType *p = L.elem;while (i<L.length&&*p != cur_e){i++;p++;}if (i == L.length)return -1;else{*next_e = *++p;return 1;}
}int ListInsert(SqList *L, int i, ElemType e) {/*在第i个位置插入元素e*/ElemType *newdata, *q, *p;if (i<1 || i>(*L).length + 1) /* i值不合法 */return -1;if ((*L).length >= (*L).listsize) /* 当前存储空间已满,增加分配 */{newdata = (ElemType *)realloc((*L).elem, ((*L).listsize + LISTINCREMENT) * sizeof(ElemType));if (!newdata)exit(-1); /* 存储分配失败 */(*L).elem = newdata; (*L).listsize += LISTINCREMENT; /* 增加存储容量 */}q = (*L).elem + i - 1; /* q为插入位置 */for (p = (*L).elem + (*L).length - 1; p >= q; --p) /* 插入位置及之后的元素右移 */*(p + 1) = *p;*q = e; /* 插入e */++(*L).length; /* 表长增1 */return 1;
}int ListDelete(SqList *L, int i, ElemType *e) {/*删除第i个元素,并用e返回其值*/ElemType *p, *q;if (i<1 || i>(*L).length) /* i值不合法 */return 0;p = (*L).elem + i - 1; /* p为被删除元素的位置 */*e = *p; /* 被删除元素的值赋给e */q = (*L).elem + (*L).length - 1; /* 表尾元素的位置 */for (++p; p <= q; ++p) /* 被删除元素之后的元素左移 */*(p - 1) = *p;(*L).length--; /* 表长减1 */return 1;
}int ListTraverse(SqList L, void(*vi)(ElemType*))
{ /* 操作结果:依次对L的每个数据元素调用函数vi()。一旦vi()失败,则操作失败 *//*           vi()的形参加'&',表明可通过调用vi()改变元素的值 */ElemType *p;int i;p = L.elem;for (i = 1; i <= L.length; i++)vi(p++);printf("\n");return 1;
}void print(ElemType *c)  /*输出函数*/
{printf("%d ", *c);
}int main()
{//此处为运行代码区域,输入自己的代码
}

Mr.J--C语言头函数的建立(附严薇敏《数据结构》线性表代码)相关推荐

  1. 数据结构(C语言)课设1——单位员工通讯录管理系统(线性表应用)

    数据结构(C语言)课设1--单位员工通讯录管理系统(线性表应用) 题目要求: 为某个单位建立一个员工通讯录管理系统,可以方便查询每一个员工的手机号.及电子邮箱.其功能包括通讯录链表的建立.员工通讯信息 ...

  2. 建立带表头结构的单链线性表,归并La和Lb得到新的单链线性表Lc

    逆位序(插在表头)输入n个元素的值,建立带表头结构的单链线性表L:正位序(插在表尾)输入n个元素的值,建立带表头结构的单链线性表: 已知单链线性表La和Lb的元素按值非递减排列,归并La和Lb得到新的 ...

  3. 《数据结构上机实验(C语言实现)》笔记(2 / 12):线性表

    文章目录 验证性实验 实现顺序表各种基本运算的算法 放码 sqlist.h sqlist.cpp exp2-1.cpp 结果 实现单链表各种基本运算的算法 放码 linklist.h linklist ...

  4. 数据结构摧毁线性表用c语言,[简述]数据结构-线性表(c语言实现)

    [简述]数据结构-线性表(c语言实现)second60 20180422 1. 线性表的定义 线性表是具有相同特性的数据元素的一个有限序列. 2. 线性表抽象数据类型描述 ADT  List { 数据 ...

  5. C语言/C++常见习题问答集锦[八十三]之数据结构顺序表(operand types are error: no match for “operator==“)

    C语言/C++常见习题问答集锦[八十三]之数据结构顺序表{operand types are error: no match for "operator=="} 程序之美 前言 主 ...

  6. c语言构造一个空线性表l,数据结构线性表顺序结构的定义与实现C语言-Go语言中文社区...

    大家好,今天给大家总结了一下数据结构里面的线性表的顺序结构,顺序表表示的是用一组地址连续的存储单元依次存储线性表的数据元素,所以顺序结构的实现一般采用数组的方式来实现,存储空间也采用动态分配的方式.在 ...

  7. C语言数据结构线性表上机实验报告,数据结构实验报告实验一线性表_图文

    数据结构实验报告实验一线性表_图文 更新时间:2017/2/11 1:23:00  浏览量:763  手机版 数据结构实验报告 实验名称: 实验一 线性表 学生姓名: 班 级: 班内序号: 学 号: ...

  8. c语言通讯录程序线性表,数据结构(C语言)课设1——单位员工通讯录管理系统(线性表应用)...

    数据结构(C语言)课设1--单位员工通讯录管理系统(线性表应用) 题目要求: 为某个单位建立一个员工通讯录管理系统,可以方便查询每一个员工的手机号.及电子邮箱.其功能包括通讯录链表的建立.员工通讯信息 ...

  9. 数据结构——线性表的C语言实现

    文章目录 1.什么是线性表? 2.线性表的抽象数据类型 3.线性表之顺序表的C语言实现 4.线性表之链式表的C语言实现 1.什么是线性表? 2.线性表的抽象数据类型 3.线性表之顺序表的C语言实现 # ...

最新文章

  1. matlab中的expotest,提高Fortran矩阵指数性能(Expokit比Matlab、Python慢)
  2. Microsoft Dynamics CRM4.0 Data Auditing and Restore (数据审核和恢复)
  3. java创建oracle序列_java学习:Hibernate学习-用oracle sequence序列生成ID的配置示例
  4. 使用Scikit-Learn轻松实现数据缩放
  5. linux中检测用户信息的命令是,在Linux系统上检查用户所属组
  6. android sse 人脸识别,基于Android Camera2之openCamera 流程
  7. 软件測试之独步武林系列(一)
  8. Omi v1.0震撼发布 - 令人窒息的Web组件化框架
  9. centos7 svn自动更新至web目录
  10. snakeyaml jyaml 哪个好_lol手游哪个英雄可玩性高 英雄联盟手游英雄强度排行
  11. android 照片拼接长图_齐了!照片排版最好看的App都在这!文末有福利
  12. 不使用随机数的洗牌算法
  13. hdfs中与file数组类似的数组_Java中的数组
  14. Python语言程序设计课程论文——Python小游戏
  15. 游侠小秘书基本智能框架完成!!!
  16. THINKPHP6 运行出现Malformed UTF-8 characters, possibly incorrectly encoded
  17. 虚拟同步发电机_学术简报基于分散式微电网的虚拟同步发电机无通信预同步并网方案...
  18. 云产品测试软件,云测试工具平台介绍
  19. 计算机配置介绍英语作文,需一篇用英文介绍笔记本电脑的小文章!~
  20. 史上最全最详细2014年初mac air 128G硬盘 4G内存 更换512G硬盘及更新最新操作系统macOS Big Sur操作手册

热门文章

  1. android keyguard,Android8.1 SystemUI Keyguard之指纹解锁流程
  2. 《创造奇迹的编程语言也有黑历史!原来他还有这副面孔?!》
  3. 2021年,作为算法工程师的你们会在CV业务上用Transformer吗?
  4. 重磅汇总!2015-2019学硕国家线走势情况!
  5. 前沿 | 加州理工大学什么是Imitation Learning(模仿学习)
  6. 《Python编程从入门到实践》记录之列表解析
  7. java编码技巧_编码小技巧 让java编程更便捷
  8. 如何建立个人博客网站
  9. java oop入门_java入门(十六) | OOP(三)之构造方法
  10. gc机制 php7_PHP7垃圾回收机制详解(附GC处理完整流程图)