C 中的 struct 是结构体,C++ 中的 struct 是一个类,二者还是有区别的。今天讲 C 中的结构体。

一. 结构体

数组是存储相同数据类型的集合,而结构可以用来存储不同数据类型,这些数据称为结构的成员。

1.0 结构声明

struct tag
{
    member-list
}variable-list;

1.1 使用标签

struct Person
{
    char name[8];
    int age;
    char gender[8];
};

1.2 初始化

结构的初始化方式和数组的初始化很相似。一个位于一对花括号内部、由逗号分隔的初始值列表可用于结构中各个成员的初始化。

struct Person person_1 = {"Alice", 13, "female"};

1.3 结构成员的直接访问

结构成员的变量是通过点操作符【 . 】访问的。点操作符接受两个参数,左操作数是结构变量的名字,右操作数是需要访问的成员的名字

printf("Name:%s, Age:%d, Gender:%s\n", person_1.name, person_1.age, person_1.gender);
#include <stdio.h>

struct Person
{
    char name[8];
    int age;
    char gender[8];
};

int main(void)
{
    struct Person person_1 = {"Alice", 13, "female"};
    printf("Name:%s, Age:%d, Gender:%s\n", person_1.name, person_1.age, person_1.gender);
    return 0;
}

1.4 typedef

利用 typedef 可以为某一类型自定义名称。

该定义的作用域取决于 typedef 定义所在的位置。如果定义在函数中,就具有局部作用域,受限于定义所在的函数。

typedef unsigned char Byte;
typedef struct
{
    char name[8];
    int age;
    char gender[8];
}Person;

1.5 结构成员的间接访问

如果你拥有一个指向结构的指针,该如何访问这个结构的成员呢?

使用箭头操作符 【 -> 】, 箭头操作符接受两个参数,左操作数必须是一个指向结构的指针,右操作数选择一个指定的结构成员

#include <stdio.h>

typedef struct
{
    char name[8];
    int age;
    char gender[8];
}Person;

int main(void)
{
    Person person_1 = {"Alice", 13, "female"};
    Person *person_p = &person_1;

    printf("Name:%s, Age:%d, Gender:%s\n", person_p->name, person_p->age, person_p->gender);
    return 0;
}

1.6 结构的自引用

struct Person
{
    char name[8];
    int age;
    char gender[8];
    struct Person mother;
};

这种自引用是非法的,因为 mother 是另一个完整的结构体,其内部还将包含自己的 mother,这又是一个完整的结构体,这样重复下去永无止境。

struct Person
{
    char name[8];
    int age;
    char gender[8];
    struct Person *mother;
};

这种自引用是合法的,因为 mother 现在是一个指针而不是结构。编译器在结构的长度确定之前就已经知道指针的长度。

如果你觉得一个结构内部包含一个指向该结构本身的指针有些奇怪,请记住它事实上所指向的是同一种类型不同结构

更加高级的数据结构,如链表和树,都是用这种技巧实现的

typedef struct{
    char name[8];
    int age;
    char gender[8];
    Person *mother;
}Person;

这是个陷阱,这个声明想为这个结构创建类型名 Person,但是它失败了。因为类型名直到声明的末尾才定义,所以在结构声明的内部它尚未定义。

解决方案是定义一个结构标签来声明 mother

typedef struct Person{
    char name[8];
    int age;
    char gender[8];
    struct Person *mother;
}Person;
#include <stdio.h>

typedef struct Person
{
    char name[8];
    int age;
    char gender[8];
    struct Person *mother;
}Person;

int main(void)
{
    Person mother = {"Eich", 40, "female", NULL};
    Person me = {"Alice", 13, "female", &mother};
    
    printf("This is me\n");    
    printf("Name:%s, Age:%d, Gender:%s\n", me.name, me.age, me.gender);
    printf("\n");
    printf("This is my mother\n");
    printf("Name:%s, Age:%d, Gender:%s\n", me.mother->name, me.mother->age, me.mother->gender);
    return 0;
}

1.7 结构的存储分配

#include <stdio.h>

typedef struct{
    char ch_1;
    int num_1;
    char ch_2;
}Test_1;

typedef struct{
    char ch_1;
    char ch_2;
    int num_1;
}Test_2;


int main(void)
{
    printf("Test_1: %ld\n", sizeof(Test_1));
    printf("Test_2: %ld\n", sizeof(Test_2));
    return 0;
}

通常我们应根据结构成员的大小进行重排,减少因边界对齐而造成的内存损失。除非我们想把相关的结构成员存储在一起,提高程序的可维护性和可读性

但如果程序将创建几百个甚至几千个结构,减少内存浪费的要求就比程序的可读性更为急迫。在这种情况下,在声明中添加注释有助于减少可读性方面的损失

1.8 作为函数参数的结构

#include <stdio.h>

typedef struct Person
{
    char name[8];
    int age;
    char gender[8];
    struct Person *mother;
}Person;

void printPerson(Person person)
{
    printf("This is me\n");    
    printf("Name:%s, Age:%d, Gender:%s\n", person.name, person.age, person.gender);
    printf("\n");
    printf("This is my mother\n");
    printf("Name:%s, Age:%d, Gender:%s\n", person.mother->name, person.mother->age, person.mother->gender);
}

int main(void)
{
    Person mother = {"Eich", 40, "female", NULL};
    Person me = {"Alice", 13, "female", &mother}; 
    printPerson(me);
    return 0;
}

这种方法能够产生正确的结果,但是效率很低。因为 C 语言的参数按值传递要求把参数的一份副本传递给函数,如果这个结构体大小为 32 字节,则必须把 32 字节复制到栈区,以后再丢弃

#include <stdio.h>

typedef struct Person
{
    char name[8];
    int age;
    char gender[8];
    struct Person *mother;
}Person;

void printPerson(const Person *person)
{
    printf("This is me\n");    
    printf("Name:%s, Age:%d, Gender:%s\n", person->name, person->age, person->gender);
    printf("\n");
    printf("This is my mother\n");
    printf("Name:%s, Age:%d, Gender:%s\n", person->mother->name, person->mother->age, person->mother->gender);
}


int main(void)
{
    Person mother = {"Eich", 40, "female", NULL};
    Person me = {"Alice", 13, "female", &mother}; 
    printPerson(&me);
    return 0;
}

效率较高的方法是传递给函数一个指向结构的指针。结构越大,把指向它的指针传递给函数的效率越高

向函数传递指针的缺陷在于函数现在可以对调用程序的结构体变量进行修改。如果不希望如此,可以在函数中使用 const 关键字来防止这类修改

二. 联合

和结构相比,联合可以说是另一种动物了。联合的声明和结构类似,但它的行为方式却和结构不同。

联合的所有成员引用的是内存中的相同位置,联合的大小是它最大成员的大小,且它将保证对小成员的类型进行强制的对齐

2.1 联合声明

union Test
{
    int num_1;
    double num_2;
};
typedef union
{
    int num_1;
    double num_2;
}Test;
typedef union Test
{
    int num_1;
    double num_2;
}Test;

2.2 联合初始化

  1. 指定成员初始化
  2. 用另一个联合初始化
  3. 初始化联合的第一个成员
#include <stdio.h>

typedef union Test
{
    int num_1;
    double num_2;
}Test;

int main(void)
{
    Test test_1;
    test_1.num_2 = 3.14;
    printf("%lf\n", test_1.num_2);

    Test test_2 = {4};
    printf("%d\n", test_2.num_1);

    Test test_3 = test_1;
    printf("%lf\n", test_3.num_2);
    return 0;
}

作业

  1. 定义一个结构体变量(包括年、月、日)。计算该日在本年中是第几天,注意闰年问题。
  2. 现有 7 个学生的数据记录,每个记录包括学号、姓名、三科(语文、数学、英语)成绩。 编写一个函数input,用来输入一个学生的数据记录。 编写一个函数print,打印一个学生的数据记录。 在主函数调用这两个函数,读取 7 条记录输入,然后设计格式输出。
  3. 有 7 个学生,每个学生的数据包括学号、姓名、3门课(语文、数学、英语)的成绩,从键盘输入 7 个学生的数据,要求打印出3门课的总平均成绩,以及最高分的学生的数据(包括学号、姓名、3门课成绩)