>>>> 1.1.1 嵌套结构体
        1. 重构
        随着添加一个又一个功能,处理一个又一个错误,代码的结构会逐渐退化。如果对此置之不理,这种退化最终会导致纠结不清,难以维护的混乱代码,因此需要经常性地重构代码扭转这种退化。
        重构就是在不改变代码行为的前提下,对其进行一系列小的改进,旨在改进系统结构的实践活动。虽然每个改进都是微不足道的,甚至几乎不值得去做,但如果将所有的改造叠加在一起时,对系统设计和架构的改进效果是十分明显的。
        在每次细微改进后,通过运行单元测试以确保改进没有造成任何破坏,然后才去做下一次改进。如此往复周而复始,每次改进后都要运行,通过这种方式保证在改进系统设计的同时系统能够正常工作。
        重构是持续进行的,而不是在项目结束时、发布版本时、迭代结束时、甚至每天下班时才进行。重构是每隔一个小时或半个小时就要去做的事情,通过重构可以持续地保持尽可能干净、简单且有表现力的代码。
        大量的实践证明,重复可能是软件中一切邪恶的根源,许多原则和实践规则都是为了控制与消除重复而创建的。消除重复最好的方法就是抽象,即将所有公共的函数指针移到一个单独的结构体中,创建一个通用的Validator类型校验器。也就是说,如果两种事物相似的话,必定存在某种抽象能够统一它们,因此消除重复的行为会迫使团队提炼出许多的抽象,进一步减少代码之间的耦合。
        自从发明子程序以来,软件开发领域的所有创新都是在不断尝试从源代码中消灭重复,即DRY(Don't Repeat Yourself)原则——别重复自己,因为重复黏贴会带来很多的问题,所以无论在哪里发现重复的代码,都必须消除它们。
        2. 类型与变量
        实际上,不管是范围值校验器还是奇偶校验器,其本质上都是校验器,其相同的属性是校验参数和待校验的值,其相同的行为可以共用一个函数指针调用不同的校验器。根据依赖倒置原则,将它们相同的属性和行为抽象为一个结构体类型Validator。比如:
        typedef struct _Validator{
              bool (*const validate)(struct _Validator *pThis, int value);
        }Validator;
        在这里,还是以范围值校验为例,在RangeValidatro结构体中嵌套一个Validator类型的结构体,即将Validator类型的变量isa作为RangeValidator结构体的成员。比如:
        typedef struct{
              Validator isa;
              const int min;
              const int max;
        }RangeValidator;
        RangeValidator rangeValidator;
        由于&rangeValidator与&rangeValidator.isa的值相等,因此以下关系恒成立。比如:
        Validator *pThis = &rangeValidator.isa;
        Validator *pThis = (Validator *)&rangeValidator
        即可将validateRange()函数原型:
        bool validateRange(void *pThis, int value);
        中的“void *pThis”转换为“Validator *pThis”,validatrRange()函数原型进化为:
        bool validateRange(Validator *pThis, int value);
        3. 初始化
        当将Validator类型的isa作为RangeValidator结构体成员时,显然rangeValidator.isa是一个结构体变量名,可以象任何普通结构体变量一样使用。使用Validator类型表达式:
        rangeValidator.isa
        即可引用rangeValidator变量的结构体成员isa的成员validate,即将rangeValidator.isa作为另一个点操作符的左操作符。比如:
        (rangeValidator.isa).valadate
        由于点操作符的结合性是从左向右的,因此可以省略括号。其等价于:
        rangeValidator.isa.valadate
        只要将rangeValidator.isa看作一个Validator类型的变量即可。   
        使用名为newRangeValidator的宏将结构体初始化:
        #define newRangeValidator(min, max)   {{validateRange}, (min), (max)}
        其中,validateRange为范围值校验器函数名,使用方法如下:
        RangeValidator  rangeValidator = newRangeValidator(0, 9);
        宏展开后如下:
        RangeValidator  rangeValidator = {{validateRange}, (0), (9) };
        其中,外面的{}为RangeValidator结构体赋值,内部的{}为RangeValidator结构体的成员变量isa赋值。即:
        rangeValidator.isa.validate = validateRange;
        rangeValidator.min = 0;
        rangeValidator.max = 9;
        如果有以下定义:
        Validator * pValidator = (Validator *)&rangeValidator;
        即可用pValidator引用RangeValidator的min和max。
        由于pValidator与&rangeValidator.isa不仅类型相同且值相等,则以下关系同样成立:
        Validator * pValidator = &rangeValidator.isa;
        因此可以利用这一特性获取validateRange()函数的地址,即pValidator->validate指向validateRange()。其调用形式如下:
        pValidator -> validate(pValidator, 8);
        4. 接口与实现
        以范围值校验器为例,validatorCheck()函数的调用形式如下:
        validatorCheck(&rangeValidator.isa, 8);
        当然,也可以采取以下调用形式:
        validatorCheck((Validator *)&rangeValidator, 8);
        其效果是一样的。
        为了便于阅读,如程序清单 2.24所示详细地展示了通用校验器的接口。
        程序清单 2.24 通用校验器接口(validator.h)
        1    #pragma once;
        2    #include<stdbool.h>
        3
        4    typedef struct _Validator{
        5          bool (*const validate)(struct _Validator *pThis, int value);
        6    }Validator;
        7
        8    typedef struct{
        9          Validator isa;
        10        const int min;
        11        const int max;
        12  } RangeValidator;
        13
        14  typedef struct{
        15        Validator isa;
        16        bool isEven;
        17  }OddEvenValidator;
        18
        19  bool validateRange(Validator *pThis, int value);        // 范围校验器函数
        20  bool validateOddEven(Validator *pThis, int value);         // 奇偶校验器函数
        21  #define newRangeValidator(min, max)  {{validateRange}, (min), (max)}
        22  #define newOddEvenValidator(isEven)  {{validateOddEven}, (isEven)}
        以范围值校验器为例,调用validateRange()的validatorCheck()函数的实现如下:
        1    bool validatorCheck(void *pValidator, int value)
        2    {
        3          Validate validate = *((Validate *)pValidator);
        4          return validate(pValidator, value);
        5    }
        由此可见,validatorCheck()函数的实现不依赖任何具体校验器,通用校验器接口的实现详见程序清单 2.25。
        程序清单 2.25  通用校验器接口的实现(validator.c)
        1    #include "validator.h"
        2
        3    bool validateRange(Validator *pThis, int value)
        4    {
        5          RangeValidator *pRangeValidator = (RangeValidator *)pThis;
        6          return pRangeValidator -> min <= value && value <= pRangeValidator -> max;
        7    }
        8
        9    bool validateOddEven(Validator *pThis, int value)
        10  {
        11        OddEvenValidator *pOddEvenValidator = (OddEvenValidator *)pThis;
        12        return (!pOddEvenValidator -> isEven && (value % 2)) ||
        13             (pOddEvenValidator -> isEven && !(value % 2));
        14  }
        在这里,作者并没有提供完整的代码,请读者补充完善。
        >>>> 1.1.2 结构体数组
        下面将以控制台菜单选项为例,介绍多分支选择结构程序设计的思想与实现方法。一般来说,菜单栏至少包括新建文件、打开文件、保存文件和退出4项基本功能。比如:
        1    void CreateFile()
        2    {
        3         printf("新建文件\n");
        4    }
        5
        6    void OpenFile()
        7    {
        8         printf("打开文件\n");
        9    }
        10
        11  void SaveFile()
        12  {
        13        printf("保存文件\n");
        14  }
        15
        16  void Exit()
        17  {
        18        printf("谢谢使用,再见!\n");
        19        exit(0);
        20  }
        如果使用函数指针,这4个函数的调用形式如下:
        void (*pfuncmd)();
        而新建文件、打开文件、保存文件和退出都可以作为字符串"新建文件"、"打开文件"、"保存文件"和"退出"存储在char数组中。比如:
        char cHelp[64];
        基于此,可以先声明一个结构体类型CmdEntry,其声明如下:
        1    typedef struct CmdEntry{
        2          void (*pfuncmd)();
        3          char cHelp[64];
        4    }CmdEntry;
        接着定义一个结构体数组作为函数表,分别用于存储菜单函数的入口地址和菜单信息。其声明如下:
        1    static CmdEntry cmdArray[10] = {
        2         {&CreateFile, "新建文件"},
        3         {&OpenFile, "打开文件"},
        4         {&SaveFile, "保存文件"},
        5         {&Exit, "退出"},
        6         // <标注1>可以在这里添加函数
        7          {0, 0}                                     // 退出
        8    };
        在这里,将cmdArray声明为一个内含10个元素的数组,数组的每个元素都是一个CmdEntry类型的数组,因此cmdArray[0]是第一个CmdEntry类型的结构体变量,cmdArray[1]是第2个CmdEntry类型的结构体变量,以此类推。cmyArray是数组名,该数组中的每个元素都是CmdEntry类型的结构体变量。
        为了标识结构体数组中的成员,可以采用访问单独结构体的规则:在结构体名后面加一个点运算符,再在点运算符后面加上成员名。比如:
        cmdArray[0].cHelp                                       // 第1个数组元素与cHelp相关联
        cmdArray[3].cHelp                                       // 第4个数组元素与cHelp相关联
        注意,数组下标紧跟在cmyArray后面,不是成员名后面。比如:
        cmdArray.cHelp[0]                                       // 错误
        cmdArray[2].cHelp                                       // 正确
        使用cmdArray[2].cHelp的原因是:cmdArray[2]是结构体变量名,正如cmdArray[1]是一个结构体变量名。使用cmdArray[3].cHelp的原因是cmdArray[3]是结构体变量名,如同cmdArray[0]是另一个变量名。由于数组变量名代表数组首元素的地址,因此下面两个语句是等价的:
        CmdEntry *pCmdEntry = &cmdArray[0];                 // pCmdEntry->cHelp即是cmdArray[0].cHelp
        CmdEntry *pCmdEntry = cmdArray;
        那么*pCmdEntry=cmdArray[0],因为&和*是一对逆运算符,所以可以做以下替换:
        cmdArray[0].cHelp = (*pCmdEntry).cHelp
        由于.运算符比*运算符的优先级高,因此必须使用圆括号。顺带提一下,下面的表达式代表什么?
        cmdArray[0].cHelp[1]
        这是cmdArray数组第1个结构体变量(cmdArray[0]部分)中的第2个字符(cHelp[1]部分),这个字符为“建”。这个示例指出,点运算符右侧的下标作用于各个成员,点运算符左侧的下标作用于结构体数组。最后总结一下:
        cmdArray                                               // 一个CmdEntry结构体的数组
        cmdArray[0]                                           // 一个数组元素,该元素是CmdEntru结构体
        cmdArray[0].cHelp                                       // 一个char数组(cmdArray[0]的cHelp成员)
        cmdArray[0].cHelp[1]                                   // cmdArray[0]元素的cHelp成员的一个字符
        根据上面的定义,即可用以下方式获得相应函数的入口地址。比如:
        cmdArray[0].pfuncmd = &CreateFile;
        即pfuncmd函数指针指向CreateFile()函数,其调用形式如下:
        cmdArray[0].pfuncmd();
        由此可见,采用回调函数动态绑定的方式,程序的可扩展性得到了很大的提升。只需在“<标注>1”处添加自定义的函数,无需多处修改代码,不仅可以很好地解决程序的可扩展性问题,而且还大大地降低程序的出错几率,详见程序清单2.26。
        程序清单2.26  控制台菜单选项程序
        1    #include <stdio.h>
        2    #include <stdlib.h>
        3    // 将上面的代码拷贝在这里
        4    void showHelp()
        5    {
        6
        7          for (int i = 0; (i < 10) && cmdArray.pfuncmd; i++){
        8               printf("%d\t%s\n", i, cmdArray.cHelp);
        9          }
        10  }
        11
        12  int main(void)
        13  {
        14        int iCmdNum;
        15        char cTmp1[256];
        16
        17        while (1){
        18             showHelp();
        19             printf("请选择!\n");
        20             iCmdNum = getchar() - '0';                    // 将字符转换为数字,转换失败也可以
        21             gets(cTmp1);                               // 清空缓冲区
        22              if (iCmdNum >= 0 && iCmdNum < 10 && cmdArray[iCmdNum].pfuncmd){
        23                   cmdArray[iCmdNum].pfuncmd();
        24             }else{
        25                   printf("对不起,你选择的数字不存在,请重新选择!\n");
        26             }
        27        }
        28        return 0;
        29  }
        ✍ 请用bubbleSort()算法完成这个练习,将employeeArray结构体数组分别按下列要求排序并输出:(1)按id从小到大排序;(2)按weight、age、height从小到大排序,相同时按id从小到大排序;(3)bloodType按A、B、O、AB顺序排序,相同时按id从小到大排序。