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

版权声明:转载请标明出处 https://blog.csdn.net/Ms_yjk/article/details/82794595

如何正确编写 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;
	else
		return 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;
	else
		return 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()
{
	//此处为运行代码区域,输入自己的代码
}

猜你喜欢

转载自blog.csdn.net/Ms_yjk/article/details/82794595