Bootstrap

10-C语言结构体(上篇)

一、结构体的概述

前面的学习过程中,了解了一些普通的数据类型,还有数组等都是只能存储一个或多个同种类型的数据,无法存储不同类型的数据。但在实际需求中我们往往需要存储不同种类型的数据,如一个学生的基本信息:姓名、班级、年龄、家庭地址等,为了满足这样的需求,引出了结构体的概念。

  • 类型的分类
    1. 基本类型:char short int long float double;
    2. 构造类型:由基本类型封装打包而来,数组、结构体 、共用体、枚举等。

而结构体:由关键字 struct 修饰,是一种或多种基本类型或构造类型的数据的集合。

二、结构体的定义

1.结构体类型定义

定义的结构体类型不占用空间,它只是一个类型,相当于一个模板,通过这个模板创建的一个个结构体变量才是实实在在占用空间的。

1.1类型和变量分开定义

  • 代码演示
// 定义结构体类型
struct stu
{
    char name[32];
    int age;
};
// 定义结构体变量
struct stu jack;
  • 说明
    1. 在C语言里struct stu看成一个整体叫做结构体类型,定义结构体变量的时候struct也不能省,c++可以省略;
    2. {}里面的变量叫做结构体的成员,jack叫做结构体变量。

1.2定义类型的同时定义变量

  • 代码演示
// 定义结构体类型
struct stu
{
    char name[32];
    int age;
}jack,rose;
  • 说明:这种写法能省略部分代码,但不太推荐,因为在实际项目中,结构体类型一般定义在头文件里,如果定义类型的时候定义结构体变量,那其它文件在包含该头文件的时候就相当于都定义了同一个结构体变量,在同一个工程里重复定义了。

1.3一次性结构体

  • 代码演示
struct
{
    char name[32];
    int age;
}jack;
  • 以上三种定义结构体类型的方式,推荐第一种,其它两种根据需求在特定场景下使用。

1.4结构体的内存分布

  1. 结构体变量中的成员都拥有独立的内存空间;
  2. 结构体变量保存的只是结构体的成员变量名,每个成员变量名又指向一个独立的内存空间;
  3. 结构体成员的操作遵循成员自身类型的操作,即成员为数组就按数组操作它,成员是整型就按整型操作它。

2.结构体变量定义及初始化

2.1定义结构体变量

  • 代码演示
// 定义结构体类型
struct stu
{
    char name[32];
    //int age = 18; // error: expected ':', ',', ';', '}' or '__attribute__' before '=' token
    int age;
    float score;
};

int main()
{
    // 定义结构体变量
    struct stu jack;
    // 访问成员内容
    printf("%s %d %f\n", jack.name, jack.age, jack.score);
    return 0;
}
  • 运行结果
 850071552 0.000000
  • 说明:
    1. 定义结构体类型的时候成员不要赋值,因为本质上结构体类型不占用空间,赋值就得开辟空间存储数据;
    2. 结构体变量成员通过结构体变量.成员访问和操作;
    3. 结构体变量成员未赋值的时候存储的是随机值。

2.2结构体变量初始化

结构体变量初始化时,必须遵循成员自身类型以及成员顺序。

  • 初始化为具体值
// 定义结构体类型
struct stu
{
    char name[32];
    int age;
    float score;
};

int main()
{
    // 定义结构体变量
    struct stu jack = {"jack", 18, 88.8f};
    // 访问成员内容
    printf("%s %d %.2f\n", jack.name, jack.age, jack.score);
    return 0;
}
  • 运行结果
jack 18 88.80
  • 全部初始化为0
// 定义结构体类型
struct stu
{
    char name[32];
    int age;
    float score;
};

int main()
{
    // 定义结构体变量
    struct stu jack;

    memset(&jack, 0, sizeof(jack));
    // 访问成员内容
    printf("%s %d %f\n", jack.name, jack.age, jack.score);
    return 0;
}
  • 运行结果
 0 0.000000

3.结构体成员操作

操作结构体成员的时候,必须遵循成员自身的类型。

3.1将一个结构体赋值给另外一个

  • 逐个成员赋值
int main()
{
    // 定义结构体变量
    struct stu jack = {"jack", 18, 88.8f};
    struct stu rose;
    memset(&rose, 0, sizeof(rose));

    // rose.name = jack.name; // error: assignment to expression with array type
    // 数组名不允许赋值
    strcpy(rose.name, jack.name);
    rose.age = jack.age;
    rose.score = rose.score;

    // 访问成员内容
    printf("%s %d %.2f\n", rose.name, rose.age, rose.score); // jack 18 88.80
    return 0;
}

逐个成员赋值的时候要尤其注意,操作结构体成员的时候,必须遵循成员自身的类型,如案例中的数组成员就不能直接 = 赋值。

  • 整体赋值
int main()
{
    // 定义结构体变量
    struct stu jack = {"jack", 18, 88.8f};
    struct stu rose;
    memset(&rose, 0, sizeof(rose));
    
    rose = jack;
    // 访问成员内容
    printf("%s %d %.2f\n", rose.name, rose.age, rose.score); // jack 18 88.80
    return 0;
}

相同类型的结构体变量才能整体赋值。

  • 内存拷贝
int main()
{
    // 定义结构体变量
    struct stu jack = {"jack", 18, 88.8f};
    struct stu rose;
    memset(&rose, 0, sizeof(rose));

    memcpy(&rose, &jack, sizeof(struct stu));
    // 访问成员内容
    printf("%s %d %.2f\n", rose.name, rose.age, rose.score); // jack 18 88.80
    return 0;
}

这种直接内存层面的拷贝是最直接的方法,能减少代码层面出错的概率。

3.2键盘给结构体成员赋值

  • 代码演示
int main()
{
    // 定义结构体变量
    struct stu jack = {"jack", 18, 88.8f};
    struct stu rose;
    memset(&rose, 0, sizeof(rose));

    printf("请输入姓名 年龄 成绩:");
    scanf("%s %d %f", rose.name, &rose.age, &rose.score);
    // 访问成员内容
    printf("%s %d %.2f\n", rose.name, rose.age, rose.score);
    return 0;
}
  • 运行结果
请输入姓名 年龄 成绩:rose 20 85.5
rose 20 85.50

3.3结构体嵌套

结构体嵌套:定义结构体类型的时候,结构体的某些成员为结构体变量。

  • 代码演示
struct A
{
    int a1;
    int a2;
};

struct B
{
    int b;
    struct A obj_a;
};

int main()
{
    // 嵌套结构体初始化
    struct B obj_b = {11, {22, 33}};
    // 嵌套结构体访问
    printf("%d %d %d\n", obj_b.b, obj_b.obj_a.a1, obj_b.obj_a.a2); // 11 22 33
    return 0;
}
  • 说明:
    1. 嵌套结构体初始化的时候,{11, {22, 33}} 中 {22, 33}的 {} 可以去掉,但建议保留,会使代码逻辑清晰;
    2. 访问嵌套结构体成员的时候一定要访问到最底层;
    3. 其内存分布:结构体变量 obj_b 里面保存了变量 b 和 结构体变量 obj_a 的变量名,b 指向了一个独立的空间保存了一个整型数据,obj_a 指向了一个独立的空间保存了变量名 a1和a2,a1和a2又分别指向独立的空间保存的整型数据;
    4. 内层结构体最好先定义。

三、结构体的应用

1.结构体数组

结构体数组:本质上是一个数组,只不过数组的成员为一个个结构体变量。

  • 代码演示
struct stu
{
    char name[32];
    int age;
    float score;
};

void set_stus_info(struct stu *stus, int n)
{
    printf("请输入%d个学生的信息(姓名 年龄 分数):");
    int i = 0;
    for (i = 0; i < n; i++)
    {
        scanf("%s %d %f", stus[i].name, &stus[i].age, &stus[i].score);
    }
}

void get_stus_info(struct stu *stus, int n)
{
    int i = 0;
    for (i = 0; i < n; i++)
    {
        printf("姓名:%-6s 年龄:%-4d 分数%.2f\n", stus[i].name, stus[i].age, stus[i].score);
    }
}

int main()
{
    // 定义结构体数组
    struct stu stus[5];
    int n = sizeof(stus) / sizeof(stus[0]);

    // 为5个学生赋值
    set_stus_info(stus, n);
    // 遍历学生的信息
    printf("----------------------------------\n");
    get_stus_info(stus, n);
    return 0;
}
  • 运行结果
请输入5个学生的信息(姓名 年龄 分数):小明 18 88.8
小红 19 87.7
王刚 18 83.6
李华 20 95  
王翠花 28 60
----------------------------------
姓名:小明   年龄:18   分数88.80 
姓名:小红   年龄:19   分数87.70 
姓名:王刚   年龄:18   分数83.60 
姓名:李华   年龄:20   分数95.00 
姓名:王翠花 年龄:28   分数60.00 

和数组的操作基本上没啥区别,只不过这里数组中的每个元素换成了结构体变量罢了,但是对数据的存储更加灵活丰富了,现在通过数组也能间接实现存储不同类型的数据了。

2.指针成员

指针成员:即结构体的成员里面有指针变量,指针成员一定要记得将指针成员指向合法的空间(栈区、全局区、文字常量区、堆区)。

如果指针成员没有初始化,那指针成员为野指针,一定不要访问野指针。

2.1指针成员指向栈区

  • 代码演示
typedef struct stu
{
    char name[32];
    int age;
    char *addr;
}STU;

int main()
{
    char address[32] = "湖北省";
    STU jack = {"jack", 18, address};

    printf("姓名:%-6s 年龄:%-4d 地址:%s\n", jack.name, jack.age, jack.addr);
    return 0;
}
  • 运行结果
姓名:jack   年龄:18   地址:湖北省
  1. 指针成员指向栈区,即指针成员指向局部变量;
  2. 可以对结构体类型起别名,方便定义结构体变量,减少代码量。

2.2指针成员指向全局区

和上面指针成员指向栈区操作类似,只不过指针变量指向的全局变量,这里不做演示。

2.3指针成员指向文字常量区

  • 代码演示
int main()
{
    // 定义结构体变量并初始化
    STU jack = {"jack", 18, "湖北省武汉市武昌区"};

    printf("姓名:%-6s 年龄:%-4d 地址:%s\n", jack.name, jack.age, jack.addr);
    return 0;
}
  • 运行结果
姓名:jack   年龄:18   地址:湖北省武汉市武昌区
  • 说明
  • 指针成员指向文字常量区,此处指针成员保存的是外部字符串的首元素的地址,不能对字符串内容修改;
  • 这种定义方式比较常见,这样定义,如果某个成员指向的数据长度不确定,如果使用字符数组成员,如char name[32],内存分配小了可能某些数据放不下,大了又浪费资源。所以定义指针成员,指向文字常量区就可以解决上面的问题。

2.4指针成员指向堆区

  • 代码演示
int main()
{
    // 定义结构体变量并初始化
    STU jack = {"jack", 18, NULL};

    jack.addr = (char *)malloc(32);
    strcpy(jack.addr, "河南郑州");

    printf("姓名:%-6s 年龄:%-4d 地址:%s\n", jack.name, jack.age, jack.addr);
    free(jack.addr);
    return 0;
}
  • 运行结果
姓名:jack   年龄:18   地址:河南郑州
  • 说明:指向堆区空间需要注意,按照堆区空间的操作方式操作,空间记得用完释放;

2.5结构体变量在堆区且指向堆区

  • 代码演示
int main()
{
    // 定义结构体变量并初始化
    STU *rose = (STU *)malloc(sizeof(STU));
    strcpy((*rose).name, "rose");
    (*rose).age = 25;
    (*rose).addr = (char *)malloc(32);
    strcpy((*rose).addr, "河北省石家庄");

    printf("姓名:%-6s 年龄:%-4d 地址:%s\n", (*rose).name, (*rose).age, (*rose).addr);
    free((*rose).addr);
    free(rose);
    return 0;
}
  • 运行结果
姓名:rose   年龄:25   地址:河北省石家庄
  • 说明:结构体变量在堆区,且指针成员指向堆区时,释放堆区空间先释放结构体指针成员指向的堆区空间,再释放结构体变量指向的堆区空间。

3.结构体深浅拷贝

3.1结构体潜拷贝

浅拷贝:结构体变量的空间内容直接赋值给另一个结构体变量的空间,比如前面将相同类型的结构体变量等号赋值给另外一个结构体变量。

  • 代码演示
int main()
{
    // 定义结构体变量并初始化
    STU jack = {"jack", 18, NULL};
    jack.addr = (char *)malloc(32);
    strcpy(jack.addr, "安徽省合肥市肥西县");

    STU rose;
    rose = jack;
    printf("姓名:%-6s 年龄:%-4d 地址:%s\n", rose.name, rose.age, rose.addr);
    if (jack.addr != NULL)
    {
        free(jack.addr);
        jack.addr = NULL;
    }
    if (rose.addr != NULL)
    {
        free(rose.addr);
        rose.addr = NULL;
    }
    return 0;
}
  • 运行结果
姓名:jack   年龄:18   地址:安徽省合肥市肥西县
进程已结束,退出代码-1073740940 (0xC0000374) // 非正常结束
  • 说明:
    1. 如果结构体中有指针成员,且指针成员指向了堆区,这样浅拷贝有可能造成内存多次释放的问题;
    2. 指针成员指向了堆区,浅拷贝的时候,只是将指针成员变量的值(即所指向空间的内存地址)拷贝了,两个结构体变量的指针成员指向了同一个内存空间。if (rose.addr != NULL)判断是否为空的时候只是判断指针成员是否有指向,并不知道空间是否释放,因此条件满足再次释放就造成了多次释放问题。

3.2结构体深拷贝

为了解决上面结构体浅拷贝多次释放的问题,需要把指针成员指向各种独立的堆区空间。

  • 代码演示
int main()
{
    // 定义结构体变量并初始化
    STU jack = {"jack", 18, NULL};
    jack.addr = (char *)malloc(32);
    strcpy(jack.addr, "安徽省合肥市肥西县");

    STU rose;
    // 先将非指针成员拷贝
    rose = jack;
    // 再单独为指针成员申请堆区空间
    rose.addr = (char *)malloc(32);
    strcpy(rose.addr, jack.addr);

    printf("姓名:%-6s 年龄:%-4d 地址:%s\n", rose.name, rose.age, rose.addr);

    // 释放堆区空间
    if (jack.addr != NULL)
    {
        free(jack.addr);
        jack.addr = NULL;
    }
    if (rose.addr != NULL)
    {
        free(rose.addr);
        rose.addr = NULL;
    }
    return 0;
}
  • 说明:
    1. 深拷贝本质上就是为结构体变量的指针成员独立申请一个堆区空间,然后将原结构体变量指针成员指向的空间内容拷贝给目的结构体指针成员;
    2. 除了指向堆区空间的指针成员,其它成员可以直接浅拷贝,减少代码量。

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;