让你提前认识软件开发(13):指针及结构体的使用

第1部分 重新认识C语言

指针及结构体的使用

 

【文章摘要】

        指针在C语言中占有很重要的地位,同时也是学习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.程序代码

/**********************************************************************

*版权所有 (C)2014, Zhou Zhaoxiong。

*

*文件名称: Pointer.c

*内容摘要:用于演示指针和结构体操作

*其它说明:无

*当前版本: V1.0

*作    者: Zhou Zhaoxiong

*完成日期: 20140416

*

*  版本       修改时间     修改人           修改内容

********************************************************************

*   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.总结

        指针及结构体在软件开发项目中是很常见的,掌握它们的使用方法是软件开发工程师的必修课。

       本文用实例来描述了指针及结构体的具体用法。“冰冻三尺,非一日之寒”,要想熟练掌握它们的用法,还需要我们多多地实践,还需要我们不断地练习和总结。

 

 

(欢迎访问南邮BBS:http://bbs.njupt.edu.cn/)
(欢迎访问重邮BBS:http://bbs.cqupt.edu.cn/nForum/index)

(本系列文章每周更新两篇,敬请期待!本人新浪微博:http://weibo.com/zhouzxi?topnav=1&wvr=5,微信号:245924426,欢迎关注!)

 

时间: 2024-07-31 11:43:20

让你提前认识软件开发(13):指针及结构体的使用的相关文章

提前认识软件开发(1) 序言

序言 正式踏上软件开发岗位已经将近两年了,经历了很多,也学到了很多."万事开头难",学生转变为职业人的过程,恰似茧化成蝶的过程. 1.为什么要写作本系列? 在工作的过程中,我发现我们在学校里面学到的很多东西片面肤浅,根本达不到岗位的要求.像编码规范.研发流程等等学校里几乎不提及的知识,在实际的软件开发项目中却恰恰是异常的重要. 在学校里面,我们欠缺的教育主要包括以下方面: 第一,对编码规范的要求.计算机编程课上,老师只要求学生能够编写程序实现既定的功能即可,几乎不会对编码规范提出要求,

让你提前认识软件开发(1):序言

序言           正式踏上软件开发岗位已经将近两年了,经历了很多,也学到了很多."万事开头难",学生转变为职业人的过程,恰似茧化成蝶的过程.          1.为什么要写作本系列?        在工作的过程中,我发现我们在学校里面学到的很多东西片面肤浅,根本达不到岗位的要求.像编码规范.研发流程等等学校里几乎不提及的知识,在实际的软件开发项目中却恰恰是异常的重要.        在学校里面,我们欠缺的教育主要包括以下方面:        第一,对编码规范的要求.计算机编程

提前认识软件开发(4) 破除几个有关软件开发的错误观念

我们做事情的能力能够有所提升,观念的转变是关键. 从学生转变为职业人的过程是很艰难的,因为我们要与自己积累了多年的"老毛病"作斗争,这些"老毛病"包括:做事拖拉.不守时.不遵守规则.怕吃苦等.就像发射火箭卫星一样,摆脱重力的束缚所花费的燃料是最多的,一旦成功,那么以后的流程就会比较轻松了.所谓"万事开头难",也就是这个道理. 那么,要想掌握工作中C语言的基本技能,我们需要破除哪些错误观念呢? 开发(4) 破除几个有关软件开发的错误观念-在破除旧观

让你提前认识软件开发(4):破除几个有关软件开发的错误观念

第1部分 重新认识C语言 破除几个有关软件开发的错误观念           我们做事情的能力能够有所提升,观念的转变是关键.        从学生转变为职业人的过程是很艰难的,因为我们要与自己积累了多年的"老毛病"作斗争,这些"老毛病"包括:做事拖拉.不守时.不遵守规则.怕吃苦等.就像发射火箭卫星一样,摆脱重力的束缚所花费的燃料是最多的,一旦成功,那么以后的流程就会比较轻松了.所谓"万事开头难",也就是这个道理.        那么,要想掌握工

Android For JNI(五)——C语言多级指针,结构体,联合体,枚举,自定义类型

Android For JNI(五)--C语言多级指针,结构体,联合体,枚举,自定义类型 我们的C已经渐渐的步入正轨了,基础过去之后,就是我们的NDK和JNI实战了 一.多级指针 指针的概念我们在前面也讲了许多,也提到了多级指针的概念,那具体我们怎么去操作呢 #include <stdio.h> #include <stdlib.h> main(){ int i = 3; //一级 int* p = &i; //二级,保存一级指针 int** w = &p; //三

struct-关于结构体指针与结构体数组

问题描述 关于结构体指针与结构体数组 目前开发遇到一个问题,有一个动态库dll,需要传进去一个结构体数组,函数为 opt_api_init(InfoInitMarket* InitMarketInfo): 这个是结构体 typedef struct tagInfoInitMarket { char commID[COMMODITY_ID + 1]; //合约编码 char classID[CLASS_ID + 1]; //品种代码 char instrumentType; //合约类型:期货:F

C语言 socket 如何发送带指针的结构体?

问题描述 C语言 socket 如何发送带指针的结构体? 发送一个带指针的结构体,我知道怎么发送? 先把结构体转换成字符串,然后发出.但是接收的时候,怎么接收到结构体中?thanks! 解决方案 发送: struct f;char *ffff[sizeof(f)];memcpy(ffff &f sizeof(f)); 接收: struct f; ffff为收到的数据 memcpy(&f ffff sizeof(ffff)); 解决方案二: C语言---结构体指针C语言--结构体中的指针C语

Go语言指针访问结构体的方法_Golang

本文实例讲述了Go语言指针访问结构体的方法.分享给大家供大家参考.具体分析如下: Go有指针,但是没有指针运算. 结构体字段可以通过结构体指针来访问.通过指针间接的访问是透明的. 复制代码 代码如下: package main import "fmt" type Vertex struct {     X int     Y int } func main() {     p := Vertex{1, 2}     q := &p     q.X = 1e9     fmt.P

c-将含有指针的结构体写入文件,关闭程序后如何再次读写

问题描述 将含有指针的结构体写入文件,关闭程序后如何再次读写 从文件中读出一篇英文文章,若干行,每行最多不超过80个字符.首次运行都不会出现错误,关闭后再次读写文件就会出现错误 #include #include #include #include #include #define N 100 typedef struct lnode { char data; struct lnode *next; }Linklist; Linklist *Par[N]; struct paragraph {