• C语言结构体中的柔性数组成员


    考虑如下问题,我们试图定义一个名为Student的结构,这个结构应包括学生的姓名,学生已修课程的数量以及已修课程各科的分数。实践中,每个学生已修课程的数目是不一样的,这使得我们在定义用于存储分数的结构成员时面临两难的局面:

    • 如果将该数组定义得比较小,会存在某学生所修课程数量较多,存不下的情况。
    • 如果将该数组定义得很大,比如10000,则对于大多数学生而言,内存空间浪费严重。而且,无论将该数组定义得再大,理论上都存在实际数据超量,存不下的可能。

    本文引用自作者编写的下述图书; 本文允许以个人学习、教学等目的引用、讲授或转载,但需要注明原作者"海洋饼干叔
    叔";本文不允许以纸质及电子出版为目的进行抄摘或改编。
    1.《Python编程基础及应用》,陈波,刘慧君,高等教育出版社。免费授课视频 Python编程基础及应用
    2.《Python编程基础及应用实验教程》, 陈波,熊心志,张全和,刘慧君,赵恒军,高等教育出版社Python编程基础及应用实验教程
    3. 《简明C及C++语言教程》,陈波,待出版书稿。免费授课视频

    解决方案之一是把分数数组成员定义为一个指向float的指针,如下述C语言代码所示:

    //Project - StudentScores
    #include 
    #include 
    
    typedef struct {
        char sName[20]; //学生姓名
        int  n;         //已修课程数量
        float* scores;  //指针作为结构成员,分数数组
    } Student;
    
    int main() {
        Student s = {"Dorothy Henry", 4, NULL};
        printf("sizeof(s) = %lld, sizeof(s.sName) = %lld, "
               "sizeof(s.n) = %lld, sizeof(s.scores) = %lld\n",
               sizeof(s),sizeof(s.sName),sizeof(s.n),sizeof(s.scores));
    
        s.scores = calloc(s.n,sizeof(float));
    
        s.scores[0] = 80;  s.scores[1] = 90; s.scores[2] = 90; s.scores[3] = 80;
        float fSum = 0;
        for (int i=0;i<s.n;i++)
            fSum += s.scores[i];
        printf("Average score of %s: %f",s.sName,fSum/s.n);
    
        free(s.scores);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    上述程序的执行结果为:

    sizeof(s) = 32, sizeof(s.sName) = 20, sizeof(s.n) = 4, sizeof(s.scores) = 8
    Average score of Dorothy Henry: 85.000000
    
    • 1
    • 2

    第5 ~ 9行:定义了Student结构,其包含3个数据成员,分别是20个字节的学生姓名sName,4个字节的已修课程数量n,8个字节的分数”数组“指针scores。其3个成员的字节数相加,等于一个Student对象的尺寸32个字节。

    对于Student结构而言,scores跟其它成员一样,只是数据成员,只不过类型特殊,是float*。

    Student s = {"Dorothy Henry", 4, NULL};
    
    • 1

    第12行:s对象的初始化中,将s.n初始化为4,s.scores初始化为空指针

    s.scores = calloc(s.n,sizeof(float));
    
    • 1

    第17行:s.scores只是一个指针,要往s.scores”数组“里存分数前,需要手动申请需要的内存空间。这行代码为其申请了s.n,即4个float的空间。必要时,如果希望往s.scores“数组”中存入超过4个的分数,可以通过realloc()函数重新调整其动态内存的大小。

    s.scores[0] = 80;  s.scores[1] = 90; s.scores[2] = 90; s.scores[3] = 80;
    float fSum = 0;
    for (int i=0;i<s.n;i++)
        fSum += s.scores[i];
    printf("Average score of %s: %f",s.sName,fSum/s.n);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    第19 ~ 23行:在分配了内存空间之后,s.scores指针便可以当成”数组“来使用。使用过程中,程序员会注意避免下标越界。这几行代码先把4个分数填入s.scores”数组”,然后再计算平均分并打印出来。

    free(s.scores);
    
    • 1

    第25行:释放calloc()申请的动态内存

    这种使用指针成员来管理不定尺寸空间的方法需要程序员手动申请及释放内存,程序会变得比较零散。另外一个解决方案是使用结构的柔性数组成员(flexible array member)。请阅读下述C语言程序:

    //Project - FlexMember
    #include 
    #include 
    
    typedef struct {
        char sName[20];  //学生姓名
        int  n;          //已修课程数量
        float scores[];  //柔性数组成员
    } Student;
    
    int main() {
        unsigned int nBytes = sizeof(Student) + 4*sizeof(float);
        Student* s = malloc(nBytes);
        printf("sizeof(*s) = %lld, sizeof(s->sName) = %lld, "
               "sizeof(s->n) = %lld, nBytes = %lld\n",
               sizeof(*s),sizeof(s->sName),sizeof(s->n), nBytes);
    
        printf("s = %p, s->scores = %p\n", s, s->scores);
    
        s->n = 4;
        s->scores[0] = 80;  s->scores[1] = 90; s->scores[2] = 90; s->scores[3] = 80;
        float fSum = 0;
        for (int i=0;i<s->n;i++)
            fSum += s->scores[i];
        printf("Average score: %f",fSum/s->n);
    
        free(s);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    上述程序的执行结果为:

    sizeof(*s) = 24, sizeof(s->sName) = 20, sizeof(s->n) = 4, nBytes = 40
    s = 0000000000711480, s->scores = 0000000000711498
    Average score: 85.000000
    
    • 1
    • 2
    • 3

    说明:在读者的计算机上,执行结果中的地址很可能与本书不同。

    typedef struct {
        char sName[20];  //学生姓名
        int  n;          //已修课程数量
        float scores[];  //柔性数组成员
    } Student;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    第5 ~ 9行:scores数组成员即为Student结构的柔性数组成员。柔性数组成员的定义要满足如下要求。

    • 该成员必须是结构的最后一个成员;
    • 该成员在语法上定义了一个不指定元素数量的“空”数组。
      事实上,对于一个Student类型的对象而言, 只有sName及n成员会被分配空间,scores成员是不占空间的。
    unsigned int nBytes = sizeof(Student) + 4*sizeof(float);
    Student* s = malloc(nBytes);
    
    • 1
    • 2

    第12 ~ 13行:现假设我们要存4门课程的分数,通过一个Student的对象大小加上4个float的对象大小得到需要的内存字节数nBytes。然后,通过malloc()函数分配nBytes的堆空间,并把地址传给指针s。

    printf("sizeof(*s) = %lld, sizeof(s->sName) = %lld, "
           "sizeof(s->n) = %lld, nBytes = %lld\n",
           sizeof(*s),sizeof(s->sName),sizeof(s->n), nBytes);
    
    • 1
    • 2
    • 3

    第14 ~ 16行:通过执行结果可以看到,sName成员占20个字节,n成员占4个字节。虽然我们事实上给s所指向的Student对象申请了nBytes = 40个字节的空间,但在编译器看来,*s,即s所指向的Student对象的大小只有24个字节。

    printf("s = %p, s->scores = %p\n", s, s->scores);
    
    • 1

    第18行:把s,s->scores按地址格式输出。根据执行结果,我们可以画出该Student对象的内存结构图。
    在这里插入图片描述
    如果把s->scores的地址值减去s的地址值,差为24 = sizeof(Student)。这说明,结构的柔性数组成员事实上是一个指针,它指向紧随该对象的内存地址,其值恒等于对象地址+sizeof(类型)。换句话说:如果我们实际分配给结构对象的空间大于sizeof(Student),那么多出来的内存可以通过其柔性数组成员来访问。

    Student s1;
    printf("\n%p - %p",&s1,s1.scores);
    
    • 1
    • 2

    如果我们直接定义类型为Student的变量s1,编译器会为s1分配sizeof(Student) = 24个字节的空间。但即便如此,s1.scores仍然会等于s1的地址+24。如果我们强行通过s1.scores进行数据访问,事实上访问的是不属于s1对象的空间,这是程序员需要小心避免的。

    s->n = 4;
    s->scores[0] = 80;  s->scores[1] = 90; s->scores[2] = 90; s->scores[3] = 80;
    float fSum = 0;
    for (int i=0;i<s->n;i++)
        fSum += s->scores[i];
    printf("Average score: %f",fSum/s->n);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    第20 ~ 25行:给s的柔性数组成员赋值,然后计算平均分并打印。由于我们确信s->scores所对应的内存空间属于s指向的结构对象,上述操作是安全的。

    free(s);
    
    • 1

    第27行:一定不要忘了释放动态分配的内存空间。

    请读者注意,将带有柔性数组成员的结构对象赋值给另外一个同类型对象是危险的:

    Student s1;
    s1 = *s;      //*s是存有4个分数的占40个字节空间的结构对象
    
    • 1
    • 2

    对于编译器而言,s1和s都只有sizeof(Student) = 24字节的空间。从s到s1的赋值,只会拷贝前24个字节。同样的危险也会发生在函数传值时,函数的传值,可以认为是从实际参数到形式参数的赋值。

    为了帮助更多的年轻朋友们学好编程,作者在B站上开了两门免费的网课,一门零基础讲Python,一门零基础C和C++一起学,拿走不谢!

    简洁的C及C++
    由编程界擅长教书,教书界特能编程的海洋饼干叔叔打造
    Python编程基础及应用
    由编程界擅长教书,教书界特能编程的海洋饼干叔叔打造

    如果你觉得纸质书看起来更顺手,目前Python有两本,C和C++在出版过程中。

    Python编程基础及应用

    Python编程基础及应用实验教程
    在这里插入图片描述

  • 相关阅读:
    一文1500字手把手教你Jmeter如何压测数据库【保姆级教程】
    顺序表存储一元多项式,并实现两个多项式相加运算(C++,无序输入)
    C/C++字符串和数组基本操作demo
    Python中的eval() & exec()
    【FAQ】应用内支付服务无法拉起支付页面常见原因分析和解决方法
    一个年薪20万软件测试工程师都具备的能力,你有吗?
    1024程序员狂欢节有好礼 | 前沿技术、人工智能、集成电路科学与芯片技术、新一代信息与通信技术、网络空间安全技术
    P5661 [CSP-J2019] 公交换乘
    一篇五分生信临床模型预测文章代码复现——Figure 2. 生存分析,箱线图表达改变分析(一)
    RL学习日志2-----Q-learning、Sarsa、DQN、Policy Gradients公式分析
  • 原文地址:https://blog.csdn.net/SeaBiscuitUncle/article/details/126719917