(一)深入了解typedef
typedef作為類型定義關鍵字,用于在原有資料類型(包括基本類型、構造類型和指針等)的基礎上,由使用者自定義新的類型名稱。
在程式設計中使用typedef的好處,除了為變量取一個簡單易記且意義明确的新名稱之外,還可以簡化一些比較複雜的類型聲明。比如:
typedef int INT32;
将INT32定義為與int具有相同意義的名字,這樣類型INT32就可用于類型聲明和類型轉換了,它和類型int完全相同。比如:
INT32 a; // 定義整型變量a
(INT32) b; // 将其它的類型b轉換為整型
既然已經有了int這個名稱,為什麼還要再取一個名稱呢?主要是為了提高程式的可移植性。比如,某種微處理器的int為16位,long為32位。如果要将該程式移植到另一種體系結構的微處理器,假設編譯器的int為32位,long為64位,而隻有short才是16位的,是以必須将程式中的int全部替換為short,long全部替換為int,如此這樣修改勢必工作量巨大且容易出錯。如果将它取一個新的名稱,然後在程式中全部用新取的名稱,那麼要移植的工作僅僅隻是修改定義這些新名稱即可。也就是說,隻需要将以前的:
typedef int INT16;
typedef long INT32;
替換成:
typedef short INT16;
typedef int INT32;
由此可見,typedef聲明并沒有建立一個新類型,而是為某個已經存在的類型增加一個新的名字而已。用這種方式聲明的變量與通過聲明方式聲明的變量具有完全相同的屬性。
至于typedef如何簡化複雜的類型聲明,将在後續的章節中詳細闡述。
綜上所述,如果在變量定義的前面加上typedef,即可定義該變量的類型。比如:
int size;
這裡定義了一個整型變量size,當加上typedef後:
typedef int size;
那麼,size就成為了上面的size變量的類型,即int類型。既然size是一個類型,當然可以用它來定義另外一個變量。即:
size a;
類似于變量的類型定義,也可以用typedef聲明新的類型,比如:
char *ptr_to_char; // 聲明ptr_to_char為一個指向字元的指針
typedef char ptr_to_char; // 聲明ptr_to_char為指向char的指針類型
ptr_to_char pch; // 聲明pch是一個指向字元的指針
對于初學者來說,也許會産生一個這樣的疑問,為什麼不使用#define建立新的類型名?比如:
#define ptr_to_char char*
ptr_to_char pch1, pch2;
由于有了“#define ptr_to_char char*”,是以“ptr_to_char pch1, pch2”可以展開為
char *pch1, pch2;
是以pch2為char型變量。如果用typedef來定義的話,其代碼如下:
typedef char* ptr_to_char;
ptr_to_char pch1, pch2;
則“ptr_to_char pch1, pch2”等價于
char *pch1;
char *pch2;
是以,pch1、pch2都是指針。
雖然#define語句看起來象typedef,但實際上卻有本質上的差别。對于#define來說,僅在編譯前對源代碼進行了字元串替換處理;而對于typedef來說,它建立了一個新的資料類型别名。由此可見,隻是将pch1定義為指針變量,卻并沒有實作程式員的意圖,而是将pch2定義成了char型變量。
在指針函數中,有這樣一類函數,它們也傳回指針,但是這個指針不是指向int、char之類的基本類型,而是指向函數。對于初學者,别說寫出這樣的函數聲明,就是看到這樣的寫法也是一頭霧水。比如,下面的語句:
int (*ff(int))(int *, int);
我們用上面介紹的方法分析一下,ff首先與後面的“()”結合,即:
int (*(ff(int)))(int *, int); // 用括号将ff(int)再括起來
也就意味着,ff是一個函數。
接着與前面的“*”結合,說明ff函數的傳回值是一個指針。然後再與後面的“()”結合,也就是說,該指針指向的是一個函數。
這種寫法确實讓人非常難懂,以至于一些初學者産生誤解,認為寫出别人看不懂的代碼才能顯示自己水準高。而事實上恰好相反,能否寫出通俗易懂的代碼是衡量程式員是否優秀的标準。一般來說,用typedef關鍵字會使該聲明更簡單易懂。在前面我們已經見過:
int (*PF)(int *, int);
也就是說,PF是一個函數指針“變量”。當使用typedef聲明後,則PF就成為了一個函數指針“類型”,即:
typedef int (*PF)(int *, int);
這樣就定義了傳回值的類型。然後,再用PF作為傳回值來聲明函數:
PF ff(int);
傳回函數指針會用在什麼地方呢?且聽下文分解。
(二)深入了解函數指針與指針函數
1.指針函數的定義
顧名思義,指針函數即傳回指針的函數。其一般定義形式如下:
類型名 *函數名(函數參數表列);
其中,字尾運算符括号“()”表示這是一個函數,其字首運算符星号“*”表示此函數為指針型函數,其函數值為指針,即它帶回來的值的類型為指針,當調用這個函數後,将得到一個“指向傳回值為…的指針(位址),“類型名”表示函數傳回的指針指向的類型”。
“(函數參數表列)”中的括号為函數調用運算符,在調用語句中,即使函數不帶參數,其參數表的一對括号也不能省略。其示例如下:
int *pfun(int, int);
由于“*”的優先級低于“()”的優先級,因而pfun首先和後面的“()”結合,也就意味着,pfun是一個函數。即:
int *(pfun(int, int));
接着再和前面的“*”結合,說明這個函數的傳回值是一個指針。由于前面還有一個int,也就是說,pfun是一個傳回值為整型指針的函數。
我們不妨來再看一看,指針函數與函數指針有什麼差別?
int (*pfun)(int, int);
通過括号強行将pfun首先與“*”結合,也就意味着,pfun是一個指針,接着與後面的“()”結合,說明該指針指向的是一個函數,然後再與前面的int結合,也就是說,該函數的傳回值是int。由此可見,pfun是一個指向傳回值為int的函數的指針。
雖然它們隻有一個括号的差别,但是表示的意義卻截然不同。函數指針的本身是一個指針,指針指向的是一個函數。指針函數的本身是一個函數,其函數的傳回值是一個指針。
2. 用函數指針作為函數的傳回值
在上面提到的指針函數裡面,有這樣一類函數,它們也傳回指針型資料(位址),但是這個指針不是指向int、char之類的基本類型,而是指向函數。對于初學者,别說寫出這樣的函數聲明,就是看到這樣的寫法也是一頭霧水。比如,下面的語句:
int (*ff(int))(int *, int);
我們用上面介紹的方法分析一下,ff首先與後面的“()”結合,即:
int (*(ff(int)))(int *, int); // 用括号将ff(int)再括起來
也就意味着,ff是一個函數。
接着與前面的“*”結合,說明ff函數的傳回值是一個指針。然後再與後面的“()”結合,也就是說,該指針指向的是一個函數。
這種寫法确實讓人非常難懂,以至于一些初學者産生誤解,認為寫出别人看不懂的代碼才能顯示自己水準高。而事實上恰好相反,能否寫出通俗易懂的代碼是衡量程式員是否優秀的标準。一般來說,用typedef關鍵字會使該聲明更簡單易懂。在前面我們已經見過:
int (*PF)(int *, int);
也就是說,PF是一個函數指針“變量”。當使用typedef聲明後,則PF就成為了一個函數指針“類型”,即:
typedef int (*PF)(int *, int);
這樣就定義了傳回值的類型。然後,再用PF作為傳回值來聲明函數:
PF ff(int);
下面将以程式清單1為例,說明用函數指針作為函數的傳回值的用法。當程式接收使用者輸入時,如果使用者輸入d,則求數組的最大值,如果輸入x,則求數組的最小值,如果輸入p,則求數組的平均值。
程式清單 1 求最值與平均值示例
1 #include<stdio.h>
2 #include <assert.h>
3 double GetMin(double *dbData, int iSize) // 求最小值
4 {
5 double dbMin;
6 int i;
7
8 assert(iSize>0);
9 dbMin=dbData[0];
10 for (i=1; i<iSize; i++){
11 if (dbMin>dbData[i]) {
12 dbMin=dbData[i];
13 }
14 }
15 return dbMin;
16 }
17
18 double GetMax(double *dbData, int iSize) // 求最大值
19 {
20 double dbMax;
21 int i;
22
23 assert(iSize>0);
24 dbMax=dbData[0];
25 for (i=1; i<iSize; i++){
26 if (dbMax< dbData[i]) {
27 dbMax=dbData[i];
28 }
29 }
30 return dbMax;
31 }
32
33 double GetAverage(double *dbData, int iSize) // 求平均值
34 {
35 double dbSum=0;
36 int i;
37
38 assert(iSize>0);
39 for (i=0; i<iSize; i++)
40 {
41 dbSum+=dbData[i];
42 }
43 return dbSum/iSize;
44 }
45
46 double UnKnown(double *dbData, int iSize) // 未知算法
47 {
48 return 0;
49 }
50
51 typedef double (*PF)(double *dbData, int iSize); // 定義函數指針類型
52 PF GetOperation(char c) // 根據字元得到操作類型,傳回函數指針
53 {
54 switch (c)
55 {
56 case 'd':
57 return GetMax;
58 case 'x':
59 return GetMin;
60 case 'p':
61 return GetAverage;
62 default:
63 return UnKnown;
64 }
65 }
66
67 int main(void)
68 {
69 double dbData[]={3.1415926, 1.4142, -0.5,999, -313, 365};
70 int iSize=sizeof(dbData)/sizeof(dbData[0]);
71 char c;
72
73 printf("Please input the Operation :\n");
74 c=getchar();
75 printf("result is %lf\n", GetOperation(c)(dbData,iSize)); // 通過函數指針調用函數
76 }
上述程式中前面4個函數分别實作求最大值、最小值、平均值和未知算法,然後實作了GetOperation函數。這個函數根據字元的傳回值實作上面4個函數。它是以函數指針的形式傳回的,從後面的main函數的GetOperation(c)(dbData, iSize)可以看出,通過這個指針可以調用函數。
(三)深入了解void類型
1.空指針
一般來說,程式的起始位址是從“代碼區”的0位址開始存放的(注:如果插入一個記憶體分布圖,則更能說明問題,此處省略),但實際上現代作業系統并非如此,卻保留了從0開始的一塊記憶體。至于這塊記憶體到底有有多大,與具體的作業系統有關。如果程式試圖通路這塊記憶體,則系統提示異常。
為什麼作業系統不是保留一個位元組呢?由于記憶體管理是按頁來進行的,是以無法做到單獨保留一個位元組。盡管如此,但還是有極少數系統設定RAM區從0位址開始,但指向有效變量的指針不會指向0位址。即使“代碼區”從0位址開始,但在任何情況下,0位址都不是C語言中任何函數的起始位址,是以指向有效函數位址的指針也不會指向0位址。
☛ 課外知識延伸
雖然 80C51微控制器XDATA區(外部RAM)是從0位址開始的,但隻要對儲存在0位址中的變量不進行取位址操作(&操作),即可有效地保證指針不會指向0位址。
與此同時,雖然32位ARM7微控制器也是從0位址開始的,但這塊記憶體僅用于存放中斷向量代碼,而不是程式中的有效變量位址,是以即便用空指針來判斷指針的有效性,其仍然是可行的。
基于此,于是将空指針定義為指向0位址的指針。毫無疑問,任何一種指針類型都有一個特殊的指針值,即空指針。它既不會指向任何對象或函數,也不是任何對象或函數的位址。而未初始化的指針,則完全可能指向任何地方。
由此可見,空指針與未初始化的指針是完全不同的兩個概念。那麼,将如何在程式中獲得一個空指針呢?
2. 空指針常量與NULL
标準C規定,在初始化、指派或比較時,如果一邊是變量或指針類型的表達式,則編譯器可以确定另一邊的常數0為空指針,并生成正确的空指針值。即在指針上下文中“值為0的整型常量表達式”在編譯時轉換為空指針。
為了讓程式中的空指針使用更加明确,标準C專門定義了一個标準預處理宏NULL,其值為“空指針常量”,通常為0或(void *)0,即在指針上下文中NULL與0是等價的,而未加修飾的0也是完全可以接受的。由于void *指針的特殊指派屬性,比如:
#define NULL ((void *)0)
當NULL定義為((void *)0)時,即NULL是可以指派給任何類型指針的值,它的類型為void*,而不是整數0,是以初始化“FILE *fp = NULL;”是完全合法的。
而為了區分整數0和空指針0,當需要其它類型的0的時候,即使可能工作,但也不能使用NULL,如果這樣處理其格式是錯誤的,這在非指針上下文中是不能工作的。特别地,不能在需要ASCII空字元(NUL)的地方使用NULL。如果确實需要,則可以自定義為:
#define NUL '\0'
由此可見,常數0是一個空指針常量,而NULL僅僅是它的一個别名。
3. 空指針的用途
一般來說,未初始化是不能使用的非法指針,因為它完全有可能指向任何地方,進而導緻程式無法判斷它為非法指針。是以,不管指針變量是全局的還是局部的、靜态的還是非靜态的,都應該在聲明它的同時進行初始化,要麼賦予一個有效的位址,要麼賦予NULL。
标準C規定,全局指針變量的預設值為NULL,而對于局部指針變量則必須明确地指定其初值。是以,void通常用于指針變量的初始化,用來判斷一個指針的有效性。比如:
unsigned char *pucBuf=(void *)0; // 定義pucBuf為unsigned char類型指針并初始化為空指針
如果後續的代碼忘記初始化指針而直接使用的話,則可能造成程式失敗。雖然空指針也是非法指針,但可以通過程式判斷并告訴程式員代碼可能有問題。也就是說,如果一開始就将指針初始化為空指針,則可避免程式異常。比如:
if(pucBuf==0){
return error; // 如果pucBuf為空指針,則傳回參數錯誤
}
由于void類型指針的不确定性,是以它可以指向任意類型的資料,那麼隻要在使用時做一個簡單的強制類型轉換就可以了。比如:
unsignned char *pcData = NULL; // 定義pcData為unsigned char類型指針
void *pvData; // 定義pvData為void類型指針
pvData = pcData; // 無需進行強制類型轉換
pcData = (unsigned char*) pvData; // 将pvData強制轉換為unsigned char類型指針
顯然不存在void類型的對象,也就是說,當對象為空類型時,其大小為0位元組;當對象未确定類型時,那麼它的大小也是未确定的,是以不能聲明void類型變量。比如:
void a; // 非法聲明
既然上述聲明是非法的,那麼,也就不能将sizeof運算符用于void類型。也就意味着,編譯器不知道所指對象的大小,由于指針的算術運算總是基于所指對象的大小的,是以不允許對void指針進行算術運算。
總之,在指針聲明中,void *表示通用指針的類型。如果void作為函數的傳回類型,則表示不傳回任何值。如果void位于參數清單中,則表示沒有參數。
4. 用無類型指針作為函數參數
由于C語言中最小長度的變量為char類型(包括unsigned char、signed char等),其sizeof(char)的結果為1,而其它任何變量的長度都是它的整數倍。比如,如果使用SDCC51編譯器,其sizeof(int)為2。因為通用swap函數函數不知道需要交換的變量的類型,是以需要一個參數給出相應的訓示。由于C語言的變量類型多種多樣,是以不可能為每一種變量類型編号,而且swap并不關心變量的真正類型,是以可以用變量的長度代替變量類型。通用swap函數的原型為:
void swap(void *pvData1, void *pvData2, int iDataSize)
将a,b兩個變量(變量類型必須一樣)的值交換的代碼如下:
swap(&a, &b, sizeof(a));
通用swap排序函數的參考代碼見程式清單1.1。
程式清單1.1 通用swap排序函數
1 void swap (void *pvData1, void *pvData2, int iDataSize)
2 {
3 unsigned char *pcData1 = NULL;
4 unsigned char *pcData2 = NULL;
5 unsigned char ucTmp1;
6
7 pcData1 = (unsigned char *)pvData1;
8 pcData2 = (unsigned char *)pvData2;
9
10 do {
11 ucTmp1 = *pcData1;
12 *pcData1 = *pcData2;
13 *pcData2 = ucTmp1;
14 pcData1++;
15 pcData2++;
16 } while (--iDataSize > 0);
17 }