3.2联合体和枚举enum,还有动态内存malloc,free,calloc,realloc

内容:联合体,联合体类型的声明,联合体的特点, 联合体⼤⼩的计算 ,enum枚举类型的声明 枚举类型的优点, 枚举类型的使⽤,动态内存管理,动态内存分配 ,malloc和free ,calloc和realloc ,常⻅的动态内存的错误

联合体

联合体类型的声明像结构体一样,联合体也是由一个或者多个成员构成,这些成员可以是不同的类型。联合体的关键字是 union。但是编译器只为最大的成员分配足够的存储空间。

联合体的特点是所有成员共用同一块内存空间。所以联合体也叫:共用体。给联合体其中一个成员赋值,其他成员的值也跟着变化

我们看到联合体首先空间要满足成员的最大内存就是int4个字节,然后按照之前的空间对齐,刚好也是4,所以输出的结果就是4

不论我们打印地址的是un,还是un.i 还是un.a,结果都是一样的,因为联合体的成员是公用一块内存的,按照大的内存开辟,然后小的和大的一起放

还有因为成员都是公用一块内存。所以这两个成员不可以一起使用,因为你如果修改其中的一个成员的值,另一个成员也会被修改。

例题:

我们当初有写一道题关于判断大小端的问题

那个时候我们是用(char*)&n给(char*)p接受,然后打印出来

cs 复制代码
#include <stdio.h>

// 返回1:小端;返回0:大端
int check_sys() {
    int num = 1;  // 4字节int,十六进制:0x00000001
    // 将int*强制转为char*,char*仅访问1个字节(低地址字节)
    char* p = (char*)&num;
    return *p;  // 小端:低地址存0x01 → 返回1;大端:低地址存0x00 → 返回0
}

int main() {
    if (check_sys() == 1) {
        printf("当前系统是小端字节序\n");
    } else {
        printf("当前系统是大端字节序\n");
    }
    return 0;
}

现在我们学习到了联合体。就可以用联合体的知识

如果我们不想信的话,可以用调试看看内存,发现确实是01 00 00 00,是没错的

我们现在看一个实际问题:

假如现在我们有3件商品,而且每一件商品都需要下面的数据,我们如果直接使用结构体,就会发现我们要创建三个空间给下面的数据存储,这样就会一直有三个空间被浪费

图书:库存量、价格、商品类型、书名、作者、页数

杯子:库存量、价格、商品类型、设计

衬衫:库存量、价格、商品类型、设计、可选颜色、可选尺寸

但是如果我们用的是联合体嵌套在结构体里面就会完美解决这个问题

cs 复制代码
struct gift_list
{
    int stock_number; // 库存量
    double price;     // 定价
    int item_type;    // 商品类型

    union {
        struct
        {
            char title[20];    // 书名
            char author[20];   // 作者
            int num_pages;     // 页数
        } book;
        struct
        {
            char design[30];   // 设计
        } mug;
        struct
        {
            char design[30];   // 设计
            int colors;        // 颜色
            int sizes;         // 尺寸
        } shirt;
    } item;
};

我们首先把必要的三个数据表示出来,然后用联合体把下面三个成员公用一个空间地址,如果要用,就直接输出,一直都是占用一个空间,不会浪费空间

这里补充一下匿名结构体 :其实是上面的三个匿名结构体组建一个联合体

枚举:enum

枚举中的数据都是常量,定义了就不可以被修改

我们在设置好了以后,枚举中的数据就会默认以0开始以1为d开始递增,是不可以被修改的

除非你在初始化的时候就给赋值

如何输出枚举

枚举类型的优点为什么使用枚举?我们可以使用 #define 定义常量,为什么非要使用枚举?枚举的优点:

  1. 增加代码的可读性和可维护性
  2. 和 #define 定义的标识符比较枚举有类型检查,更加严谨。
  3. 便于调试,预处理阶段会删除 #define 定义的符号
  4. 使用方便,一次可以定义多个常量
  5. 枚举常量是遵循作用域规则的,枚举声明在函数内,只能在函数内使用

我们来看第一点增加代码的可读性和可维护性怎么体现

我们看这两个代码:

cs 复制代码
#include <stdio.h>

// 菜单打印函数(与原代码一致)
void menu()
{
    printf("=========================\n");
    printf("  1. 加法        2. 减法  \n");
    printf("  3. 乘法        4. 除法  \n");
    printf("  0. 退出                \n");
    printf("=========================\n");
    printf("请选择功能:");
}

int main()
{
    int input = 0;
    int a = 0, b = 0, ret = 0;

    do
    {
        menu();          // 显示菜单
        scanf("%d", &input);  // 接收用户选择

        // case后直接跟数字,无任何枚举/宏定义
        switch (input)
        {
        case 1:  // 加法(对应菜单1)
            printf("请输入两个操作数:");
            scanf("%d %d", &a, &b);
            ret = a + b;
            printf("结果:%d + %d = %d\n\n", a, b, ret);
            break;
        case 2:  // 减法(对应菜单2)
            printf("请输入两个操作数:");
            scanf("%d %d", &a, &b);
            ret = a - b;
            printf("结果:%d - %d = %d\n\n", a, b, ret);
            break;
        case 3:  // 乘法(对应菜单3)
            printf("请输入两个操作数:");
            scanf("%d %d", &a, &b);
            ret = a * b;
            printf("结果:%d * %d = %d\n\n", a, b, ret);
            break;
        case 4:  // 除法(对应菜单4)
            printf("请输入两个操作数:");
            scanf("%d %d", &a, &b);
            if (b != 0)  // 避免除零错误
            {
                ret = a / b;
                printf("结果:%d / %d = %d\n\n", a, b, ret);
            }
            else
            {
                printf("错误:除数不能为0!\n\n");
            }
            break;
        case 0:  // 退出(对应菜单0)
            printf("退出程序...\n");
            break;
        default:  // 输入错误
            printf("选择错误,请重新输入!\n\n");
            break;
        }
    } while (input != 0);  // 输入0时退出循环

    return 0;
}
cs 复制代码
#include <stdio.h>

// 定义枚举:用语义化常量表示菜单选项,替代魔法数字
enum option
{
    exit = 0,  // 退出选项(值为0)
    add,       // 加法(默认值1,枚举常量默认自增)
    sub,       // 减法(默认值2)
    mul,       // 乘法(默认值3)
    div        // 除法(默认值4)
};

// 菜单打印函数
void menu()
{
    printf("=========================\n");
    printf("  1. 加法        2. 减法  \n");
    printf("  3. 乘法        4. 除法  \n");
    printf("  0. 退出                \n");
    printf("=========================\n");
    printf("请选择功能:");
}

int main()
{
    int input = 0;
    int a = 0, b = 0, ret = 0;

    do
    {
        menu();          // 显示菜单
        scanf("%d", &input);  // 接收用户选择

        // 根据枚举常量判断功能,语义清晰
        switch (input)
        {
        case add:
            printf("请输入两个操作数:");
            scanf("%d %d", &a, &b);
            ret = a + b;
            printf("结果:%d + %d = %d\n\n", a, b, ret);
            break;
        case sub:
            printf("请输入两个操作数:");
            scanf("%d %d", &a, &b);
            ret = a - b;
            printf("结果:%d - %d = %d\n\n", a, b, ret);
            break;
        case mul:
            printf("请输入两个操作数:");
            scanf("%d %d", &a, &b);
            ret = a * b;
            printf("结果:%d * %d = %d\n\n", a, b, ret);
            break;
        case div:
            printf("请输入两个操作数:");
            scanf("%d %d", &a, &b);
            if (b != 0)  // 避免除零错误
            {
                ret = a / b;
                printf("结果:%d / %d = %d\n\n", a, b, ret);
            }
            else
            {
                printf("错误:除数不能为0!\n\n");
            }
            break;
        case exit:
            printf("退出程序...\n");
            break;
        default:
            printf("选择错误,请重新输入!\n\n");
            break;
        }
    } while (input != exit);  // 输入为exit(0)时退出循环

    return 0;
}

这两个代码逻辑都是一样的,但是我们会发现在case后面有enum的时候我们可以把1直接替换成对应的函数,这样子可能在现在看来没有什么用,但是如果把这个代码的数量乘以十或者乘以百,我们后期在维护和观看的时候都会发现下面的代码会比较友好,可以直观看到case后面的代码是关于什么内容的

然后我们在看第二点和 #define 定义的标识符比较枚举有类型检查,更加严谨是怎么体现的:

cs 复制代码
//define的时候会不出现报错
#define MON 1
#define TUE 2

// 用#define时,编译器不会检查类型,若误将字符串赋值给表示"周几"的变量,编译可能不报错(运行时才会出问题)
int day = "Monday";  // 逻辑错误,但#define无类型约束,编译器可能不提示

//但是enum就会出现报错
enum Week { MON, TUE, WED };
enum Week week;
week = "Monday";  // 编译报错:类型不匹配(枚举类型不能直接赋值字符串)

#define 定义的常量是无类型的文本替换,编译器不会对其进行类型校验;而枚举是有明确类型的自定义数据类型,编译器会检查变量赋值是否符合枚举类型的范围

枚举常量遵循作用域规则,且编译器可隐式校验其取值是否在枚举定义的范围内(部分场景下);而#define的常量是全局替换,无作用域限制,也无法校验取值合法性

动态内存管理:

这个是什么呢?

我们先给一个背景,就是我们在创建一个数组之后就已经给他分配了地址,这个时候如果我想要扩展空间来满足更大的需求就不可以做到了,所以我们就需要用到动态内存来进行管理

这个时候我们就可以做到自由的分配和释放内存

首先我们看看malloc函数

malloc是C 语言提供了一个动态内存开辟的函数

功能:向内存的堆区申请一块连续可用的空间,并返回指向这块空间的起始地址

复制代码
void* malloc (size_t size);
  • 如果开辟成功,则返回这块空间的起始地址。
  • 如果开辟失败,则返回一个 NULL 指针,因此 malloc 的返回值一定要做检查
  • 返回值的类型是 void*,所以 malloc 函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定
  • 如果参数 size 为 0,malloc 的行为是标准是未定义的,取决于编译器

现在实现一下这个函数:

用perror就可以打印那里出现问题,返回0说明正常结束,但是我们是错误结束,所以返回非零数

我们来看一下错误的时候是怎么输出的

我们用INT_MAX乘以字节数,这样就超出了系统分配的空间范围

所以我们就输出一个没有足够空间来放下内存

free函数

复制代码
void free(void* ptr);

C 语言提供了另外一个函数 free,专门是用来做动态内存的释放和回收的,函数原型如下:free 函数用来释放动态开辟的内存

  • 如果参数 ptr 指向的空间不是动态开辟的,那 free 函数的行为是未定义的
  • 如果参数 ptr 是 NULL 指针,则函数什么事都不做
  • malloc 和 free 都声明在 stdlib.h 头文件中

举个例子:

cs 复制代码
#include <stdlib.h>
#include <stdio.h>

int main()
{
    //假设:申请20个字节的空间,存放5个整数
    int* p = (int*)malloc(5 * sizeof(int));
    //判断返回值
    if (p == NULL) //开辟失败了
    {
        perror("malloc");
        return 1;
    }
    //开辟成功了
    //使用这块空间了
    int i = 0;
    for (i = 0; i < 5; i++)
    {
        *(p + i) = i + 1;
    }
    for (i = 0; i < 5; i++)
    {
        printf("%d ", p[i]);
    }
    free(p);  //使用完就返回内存
    p = NULL; //然后把内存置位空
    return 0;
}

为什么要把返回的p置为NULL,我们先来看一下&p的地址在置为空的前面是什么?

可以看到,在置为空的前面指针p还有值,但是此时的p已经没有数据使用这块地址了,这就会导致野指针的出现,就相当于你有一条凶猛的野狗,然后当你离开后没有带走它,就会造成野狗脱壳,可能会危害社会

calloc函数

C 语言还提供了一个函数叫 calloc,calloc 函数也用来动态内存分配。原型如下

复制代码
void* calloc (size_t num, size_t size);

函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为 0

与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0

举个例子:

这个代码就很好看出来了,其实calloc函数在创建的时候会默认初始化为0 ,所以打印出来都是0

我们可以看到如果是malloc是没有初始化的,都是一些随机的垃圾值

所以malloc和calloc的区别是:

realloc函数

realloc 函数的出现让动态内存管理更加灵活

有时会发现过去申请的空间太小或过大,为合理使用内存,需对内存大小灵活调整,realloc 函数可实现动态开辟内存大小的调整,函数原型如下:

复制代码
void* realloc (void* ptr, size_t size);

ptr 是要调整的内存地址

size 调整之后新大小,单位是字节

返回值为调整之后的内存起始位置

该函数在调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间

realloc 在调整内存空间时存在两种情况:

情况 1:原有空间之后有足够大的空间

情况 2:原有空间之后没有足够大的空间

情况1:就是有足够大的空间可以直接在后面增加

情况2:就是后面被使用了,他就会重新找一块新的空间存放数据,还会把原始的数据拷贝过来,后面加上后续的内容返回新的地址

cs 复制代码
#include<stdio.h>
#include<stdlib.h>
int main()
{
    //申请一块空间,用来存放1~5的数字
    int* p = (int*)malloc(5 * sizeof(int));
    if (p == NULL)
    {
        perror("malloc");
        return 1;
    }
    //使用内存空间
    int i = 0;
    for (i = 0; i < 5; i++)
    {
        p[i] = i + 1;
    }
    //继续存放6~10
    //空间不够了,需要增容
    int* p2 = realloc(p, 10 * sizeof(int));  //可能换地址,不可以用p2
    if (p2 == NULL)      
    {
        perror("realloc");
        free(p);
        p = NULL;
        return 1;
    }
    p = p2;  //把新的地址给p
    free(p);
    p = NULL;
    return 0;
}

其实realloc函数也可以申请内存,

int*p=(int*)realloc(NULL,40)

这个时候他的作用就相当于malloc(40)

动态内存常见错误:

1.对NULL指针的解引用操作 >>就是没有对malloc和realloc,calloc函数的返回值判断

没有判断是否为NULL

2.对动态空间的越界访问 >>就是本来创建是5个格子,却访问出去

3.free释放非动态内存空间 >>就是释放的不是动态内存函数,像栈区的数组那些内存都是自动生成自动释放的,不需要free释放

4.使用free释放动态内存的一部分 >>就是想要释放该区域,必须要传该区域的起始地址

5.对同一块内存连续释放空间

6.动态开辟空间却没有释放内存

第4点来说:使用free释放动态内存的一部分

cs 复制代码
int main()
{
    int* p = (int*)malloc(10*sizeof(int));
    if (p == NULL)
    {
        perror("malloc");
        return 1;
    }
    int i = 0;
    for (i = 0; i < 5; i++)
    {
        *p = i + 1;
        p++;
    }
    free(p);
    p = NULL;
    return 0;
}

这样的p指针就不是空间的起始地址,我们就不可以释放该内存

但是下面的就可以做到上面的操作并且可以返回起始地址

cs 复制代码
int main()
{
    int* p = (int*)malloc(10*sizeof(int));
    if (p == NULL)
    {
        perror("malloc");
        return 1;
    }
    int i = 0;
    for (i = 0; i < 5; i++)
    {
       * (p+i) = i + 1;
       
    }
    free(p);
    p = NULL;
    return 0;
}

第5点:对同一块内存连续释放空间

cs 复制代码
void test()
{
    int *p = (int *)malloc(100);
    free(p);
    free(p);//重复释放
}

这个时候当我们释放p空间后再次释放,系统找不到p,因为已经被释放了,就会出现未定义问题

第6点:动态开辟空间却没有释放内存

(1)写的函数却没有释放指针p,下面就会找不到p指针

cs 复制代码
int test()
{
    int* p = (int*)malloc(10 * sizeof(int));
    if (p == NULL)
    {
        perror("malloc");
        return 1;
     //没有释放内存
    }
    
}

int main()
{
    test();
   //如果下面想要调用p就不可以,因为p没有被释放,系统找不到
   //而且每一次调用都会占用地址,又不会释放,就会浪费性能和空间
    return 0;
}

(2)可以接受上面没释放的函数指针来释放

cs 复制代码
int* test()
{
    int* p = (int*)malloc(...); // 假设存在malloc申请内存的操作
    if (p == NULL)
    {
        perror("malloc");
        return 1;
    }
    //....
    // free(p);假如忘记释放
    return p;
}

int main()
{
    int *ptr = test(); //这里用ptr来接受上面的p
    free(ptr); // 就可以去释放
    ptr = NULL;
    return 0;
}

(3)释放指针的时候上面有代码return返回,就会不执行下面的释放代码

cs 复制代码
void test()
{
    int* p = (int*)malloc(10 * sizeof(int));
    if (p == NULL)
    {
        perror("malloc");
        return 1;
    }
    int n = 0;
    //使用
    //...
    if (n != 1)
        return;
    //释放
    free(p);
    p = NULL;
}

int main()
{
   
}
相关推荐
梵克之泪1 小时前
【号码分离】从Excel表格、文本、word文档混乱文字中提取分离11位手机号出来,基于WPF的实现方案
开发语言·ui·c#
charlie1145141911 小时前
面向C++程序员的JavaScript 语法实战学习4
开发语言·前端·javascript·学习·函数
夫唯不争,故无尤也1 小时前
Python广播机制:张量的影分身术
开发语言·python
自信150413057591 小时前
初学者小白复盘23之——联合与枚举
c语言·1024程序员节
Andy1 小时前
回文子串数目--动态规划算法
算法·动态规划
sin_hielo2 小时前
leetcode 1930
算法·leetcode
qq_479875432 小时前
X-Macros(3)
java·开发语言
塞北山巅2 小时前
相机自动曝光(AE)核心算法——从参数调节到亮度标定
数码相机·算法
聆风吟º2 小时前
【数据结构入门手札】算法核心概念与复杂度入门
数据结构·算法·复杂度·算法的特性·算法设计要求·事后统计方法·事前分析估算方法