指針在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.總結
指針及結構體在軟件開發項目中是很常見的,掌握它們的使用方法是軟件開發工程師的必修課。
本文用實例來描述了指針及結構體的具體用法。“冰凍三尺,非一日之寒”,要想熟練掌握它們的用法,還需要我們多多地實踐,還需要我們不斷地練習和總結。