1. 程式人生 > >Mr.J--C語言頭函式的建立(附嚴薇敏《資料結構》線性表程式碼)

Mr.J--C語言頭函式的建立(附嚴薇敏《資料結構》線性表程式碼)

 

如何正確編寫 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()
{
	//此處為執行程式碼區域,輸入自己的程式碼
}