指針在C語言中占有很重要的地位,同時也是學習C語言的難點所在。結構體屬於用戶自己建立的數據類型,在實際的軟件開發項目中應用很廣泛。
本文以實際的例子介紹了C語言中指針和結構體的使用方法,為進一步的學習和應用提供了有益的參考。
1.指針和結構體簡介
在C語言中,將地址形象化地稱為指針,意即通過它能夠找到以它為地址的內存單元。實際上,使用指針是對一個內存單元的間接訪問。例如,有一個變量Var的值為1,使用一個變量Var_Pointer存放變量Var在內存中的地址3000,通過該地址能夠找到變量Var在內存中的值,那麼這種間接訪問操作的示意圖如圖1所示。
圖1指針操作示意圖
在諸如數組這樣的數據結構中,所有的數據都是同一種類型,即不能存放不同類型(如整型和字符型)的數據。結構體(structure)的出現解決了這個問題,它允許用戶自己建立由不同類型數據組成的組合型的數據結構。
在實際的軟件開發項目中,指針和結構體都有很重要的應用,要成為一名合格的軟件開發工程師,一定要學會靈活運用指針和結構體來編寫C語言程序。
2.本文中使用的程序流程說明
本文中程序實現的功能為:從本地文件中讀取以約定格式組成的員工的信息記錄(包括工號、姓名和年齡,字段之間以“|”分隔),解析後將每個字段的內容輸出到屏幕上。流程圖如圖2所示。
圖2本程序流程圖
本程序文件命名為“Pointer.c”,使用的本地文件命名為“EmployeeInfo.ini”,文件裡面的內容為形如“工號|姓名|年齡”這樣的記錄,內容存放示例如圖3所示。
圖3文件內容存放示例圖
注意,在程序編譯運行的時候,要將本地文件存放到與“Pointer.c”同級目錄下,這樣才能夠讀取到記錄信息。
3.程序代碼
* 版本 修改時間 修改人 修改內容 ******************************************************************** * V1.0 20140416 周兆熊 創建 **********************************************************************/ #include <stdio.h> #include <stdlib.h> #include <string.h> //字段最大長度 #define MAX_RET_BUF_LEN (1024) //數據類型 typedef unsigned char UINT8; typedef unsigned short int UINT16; typedef unsigned int UINT32; typedef signed int INT32; typedef unsigned char BOOL; //參數類型 #define MML_INT8_TYPE 0 #define MML_INT16_TYPE 1 #define MML_INT32_TYPE 2 #define MML_STR_TYPE 3 #define TRUE (BOOL)1 #define FALSE (BOOL)0 //員工信息結構體 typedef struct { UINT8 szEmployeeID[1024]; //員工工號 UINT8 szEmployeeName[1024]; //員工姓名 UINT32 iEmployeeAge; //員工年齡 } T_EmployeeInfo; /********************************************************************** *功能描述:獲取字符串中某一個字段的數據 *輸入參數: iSerialNum-字段編號(為正整數) iContentType-需要獲取的內容的類型 pSourceStr-源字符串 pDstStr-目的字符串(提取的數據的存放位置) cIsolater-源字符串中字段的分隔符 iDstStrSize-目的字符串的長度 *輸出參數:無 *返回值: TRUE-成功 FALSE-失敗 *其它說明:無 *修改日期 版本號 修改人 修改內容 * -------------------------------------------------------------- * 20140416 V1.0 zzx 創建 ***********************************************************************/ BOOL GetValueFromStr(UINT16 iSerialNum, UINT8 iContentType, UINT8 *pSourceStr, UINT8 *pDstStr, UINT8 cIsolater, UINT32 iDstStrSize) { UINT8 *pStrBegin = NULL; UINT8 *pStrEnd = NULL; UINT8 szRetBuf[MAX_RET_BUF_LEN] = {0}; //截取出的字符串放入該數組中 UINT8 *pUINT8 = NULL; UINT16 *pUINT16 = NULL; UINT32 *pUINT32 = NULL; UINT32 iFieldLen = 0; //用於表示每個字段的實際長度 if (pSourceStr == NULL) //對輸入指針的異常情況進行判斷 { return FALSE; } //字段首 pStrBegin = pSourceStr; while (--iSerialNum != 0) { pStrBegin = strchr(pStrBegin, cIsolater); if (pStrBegin == NULL) { return FALSE; } pStrBegin ++; } //字段尾 pStrEnd = strchr(pStrBegin, cIsolater); if (pStrEnd == NULL) { return FALSE; } iFieldLen = (UINT16)(pStrEnd - pStrBegin); if(iFieldLen >= MAX_RET_BUF_LEN) //進行異常保護, 防止每個字段的值過長 { iFieldLen = MAX_RET_BUF_LEN - 1; } memcpy(szRetBuf, pStrBegin, iFieldLen); //將需要的字段值放到pDstStr中去 switch (iContentType) { case MML_STR_TYPE: //字符串類型 { strncpy(pDstStr, szRetBuf, iDstStrSize); break; } case MML_INT8_TYPE: //字符類型 { pUINT8 = (UINT8 *)pDstStr; *pDstStr = (UINT8)atoi(szRetBuf); break; } case MML_INT16_TYPE: // short int類型 { pUINT16 = (UINT16 *)pDstStr; *pUINT16 = (UINT16)atoi(szRetBuf); break; } case MML_INT32_TYPE: // int類型 { pUINT32 = (UINT32 *)pDstStr; *pUINT32 = (UINT32)atoi(szRetBuf); break; } default: //一定要有default分支 { return FALSE; } } return TRUE; }
*修改日期 版本號 修改人 修改內容 * ------------------------------------------------------------------------------- * 20140416 V1.0 zzx 創建 ****************************************************************/ INT32 main(void) { UINT32 iInfoCount = 0; //該變量用於計算記錄條數 UINT8 szContentLine[1024] = {0}; //用於存放從文件中獨到的每條記錄 FILE *hFile = NULL; //文件句柄指針 //打開文件 hFile = fopen("EmployeeInfo.ini", "r"); if (!hFile) //打開失敗 { printf("Open EmployeeInfo.ini failed!\n"); return -1; //異常退出 } while (NULL != fgets(szContentLine, sizeof(szContentLine), hFile)) { T_EmployeeInfo t_EmployeeInfo = {0}; iInfoCount ++; //每讀取到一條記錄, 則記錄條數加1 //獲取EmployeeID if (TRUE != GetValueFromStr(1, MML_STR_TYPE, szContentLine, t_EmployeeInfo.szEmployeeID, '|', sizeof(t_EmployeeInfo.szEmployeeID))) { printf("獲取第%d位員工的工號失敗.\n", iInfoCount); return -1; } //獲取EmployeeName if (TRUE != GetValueFromStr(2, MML_STR_TYPE, szContentLine, t_EmployeeInfo.szEmployeeName, '|', sizeof(t_EmployeeInfo.szEmployeeName))) { printf("獲取第%d位員工的姓名失敗.\n", iInfoCount); return -1; } //獲取EmployeeAge if (TRUE != GetValueFromStr(3, MML_INT32_TYPE, szContentLine, (UINT8 *)&(t_EmployeeInfo.iEmployeeAge), '|', sizeof(t_EmployeeInfo.iEmployeeAge))) { printf("獲取第%d位員工的年齡失敗.\n", iInfoCount); return -1; } //逐條打印每個員工的信息 printf("第%d位員工的信息為:工號=%s, 姓名=%s,年齡=%d.\n", iInfoCount, t_EmployeeInfo.szEmployeeID, t_EmployeeInfo.szEmployeeName, t_EmployeeInfo.iEmployeeAge); } fclose(hFile); //最後一定要關閉文件句柄 return 0; }
4.程序內容詳解
4.1員工信息結構體T_EmployeeInfo
typedef struct { UINT8 szEmployeeID[1024]; //員工工號 UINT8 szEmployeeName[1024]; //員工姓名 UINT32 iEmployeeAge; //員工年齡 } T_EmployeeInfo;
說明:
(1)因為文件中每條記錄包括了工號、姓名和年齡,所以結構體中要定義三個成員變量,其中工號和姓名是字符串類型,年齡為整型。
(2)注意成員變量的命名規則,字符串類型以“sz”開頭,整型以“i”開頭,方便對變量進行識別。同時,為了防止每個字段的內容過長,定義字符數組的長度為1024(不要超過MAX_RET_BUF_LEN的大小)。
4.2字段數據獲取函數GetValueFromStr
該函數的工作原理為:根據輸入的參數來從pSourceStr中獲取第iSerialNum字段的內容,存放到pDstStr中,各個字段以cIsolater分隔開來。
注意,在執行函數的主要邏輯之前,要對指針進行保護,即對指針的異常情況進行判斷(判斷其是否為空,具體見程序代碼)。在實際的軟件開發項目中,這一點是非常重要的。
該函數的工作步驟為:
第一步:獲取每個字段的字段首和字段尾指針。strchr函數用於查詢兩個字段之間cIsolater的地址,字段的首位指針值相減就得到該字段的長度,並使用memcpy函數將該字段值拷貝到szRetBuf中。為了防止源串中字段值過長,還對解析出來的字段長度進行了異常保護。該方法在實際的軟件開發項目中經常用到。
第二步:將解析出的字段值放到pDstStr中去。根據不同的數據類型(如字符串、整型等),將第一步獲得的字段值存放到pDstStr中。由於第一步的szRetBuf為字符數組,而某些字段值要求為整數,因此在要求參數類型為整型的case分支中使用了atoi函數。注意,switch語句一定要有default分支。
4.3主函數中的文件操作函數
在主函數(main)中,使用了文件操作函數fopen、fgets和fclose。
(1) fopen函數
在使用文件之前,先要將其打開,本程序以只讀的方式(該函數第二個參數為r)操作文件,防止對文件的錯誤寫入。
(2) fgets函數
該函數用於從文件中讀取一個字符串,其描述如下:
函數定義:char *fgets(char *s, int size, FILE *stream);
函數說明:fgets()用來從參數stream所指的文件內讀入字符並存到參數s所指的內存空間,直到出現換行字符、讀到文件尾或是已讀了size-1個字符為止,最後會加上NULL作為字符串結束。
返回值:若成功則返回s指針,返回NULL則表示有錯誤發生或內容讀取完成。
在本程序中,將從文件中讀取到的內容存放到szContentLine中。
(3) fclose函數
該函數用於在操作完文件之後關閉文件指針,防止對該文件的錯誤操作。fclose函數一定要與fopen函數配對。在使用完文件之後,一定要調用fclose函數將文件關閉。
4.4 GetValueFromStr函數的調用
以獲取員工年齡的調用為例加以說明,調用代碼如下:
if (TRUE != GetValueFromStr(3, MML_INT32_TYPE, szContentLine, (UINT8 *)&(t_EmployeeInfo.iEmployeeAge), '|', sizeof(t_EmployeeInfo.iEmployeeAge)))
{
printf("獲取第%d位員工的年齡失敗.\n", iInfoCount);
return -1;
}
(1) GetValueFromStr函數的定義為:BOOL GetValueFromStr(UINT16 iSerialNum, UINT8 iContentType, UINT8 *pSourceStr, UINT8 *pDstStr, UINT8 cIsolater, UINT32 iDstStrSize),調用的時候,實參3對應形參iSerialNum,實參MML_INT32_TYPE對應形參iContentType,實參szContentLine對應形參pSourceStr,實參&(t_EmployeeInfo.iEmployeeAge)對應形參pDstStr,實參'|'對應形參cIsolater,實參sizeof(t_EmployeeInfo.iEmployeeAge)對應形參iDstStrSize。
(2)在函數調用的時候,實參和形參類型要完全匹配,如GetValueFromStr函數要求第3個參數為字符型指針,則傳入參數szContentLine也要為同樣類型的指針(因為字符數組名就代表該字符數組的首地址,即指針,所以滿足要求)。對於第4個參數,因為年齡為整型數據,而要求傳入的實參為字符型指針,因此要在t_EmployeeInfo.iEmployeeAge前面添加&來表示指針,同時還要在前面添加(UINT8 *)將該指針類型轉換為字符類型。第5個參數要求為一個字符,因此實參為'|',注意不要將單引號寫成了雙引號(雙引號表示字符串)。
(3)如果獲取字段失敗,那麼直接返回-1,不再走下面的流程。這樣可確保每條打印出的信息都是正確的。
4.5字段信息的輸出打印
為了查看程序解析是否正確,需要在終端打印相關信息。直接使用結構體成員變量來輸出對應字段的值。
5.程序測試
在實際的軟件開發項目中,將測試分為正常測試和異常測試。正常測試是嚴格按照程序的要求來設計測試流程,異常測試的目的是看在不滿足程序要求時,得到的結果會是怎樣的。
(1)正常測試
按照圖3的文件內容來編寫EmployeeInfo.ini文件,並將之放到與“Pointer.c”同級目錄下。運行程序,得到的結果如圖4所示。
圖4正常測試的輸出結果
從輸出結果可以看出,程序對信息內容的解析是正確的,因此,指針和結構體變量的使用也是正確的。
(2)異常測試
在實際的軟件開發項目中,一定要進行大量的異常測試,以檢查程序的正確性。
1)未正確放置EmployeeInfo.ini文件
刪除EmployeeInfo.ini文件,或將它放到其它目錄下,則程序輸出結果如圖5所示。
圖5文件不存在時的輸出結果
2) EmployeeInfo.ini文件中的記錄內容不符合要求
將第二條記錄的字段分隔符“|”去掉,則程序輸出結果如圖6所示。
圖6第二條記錄的字段分隔符“|”去掉時的輸出結果
3) EmployeeInfo.ini文件中無內容
將EmployeeInfo.ini文件中的內容全部刪除掉,則程序輸出結果如圖7所示。
圖7 EmployeeInfo.ini文件中無內容時的輸出結果
還有很多異常的情況,這裡就不一一列舉了。
一般而言,在產品發布之前,一定要經過充分的測試。
6.總結
指針及結構體在軟件開發項目中是很常見的,掌握它們的使用方法是軟件開發工程師的必修課。
本文用實例來描述了指針及結構體的具體用法。“冰凍三尺,非一日之寒”,要想熟練掌握它們的用法,還需要我們多多地實踐,還需要我們不斷地練習和總結。