【维生素C语言】第十二章 - C语言自定义类型讲解(联合体、枚举、联合体)

 🔥 CSDN 累计订阅量破千的火爆 C/C++ 教程的 2023 重制版,C 语言入门到实践的精品级趣味教程。
了解更多: 👉 "不太正经" 的专栏介绍 试读第一章
订阅链接: 🔗《C语言趣味教程》 ← 猛戳订阅!

 原标题:自定义类型讲解?楼下保安大爷直呼内行!!【C语言】

前言:

本章将对C语言自定义类型进行讲解,前期我们讲过结构体,这章将会把前面结构体还没讲完的知识继续补充。


一、结构体(struct)

结构体我们在第七章已经讲过了,在本章里我们将先简略的复习,再做一些补充。

0x00 结构的基础知识

📚 结构体是一些值的集合,这些值称为成员变量。结构的每个成员以是不同类型的变量。

0x01 结构的声明

💬 代码演示:描述一个学生

struct Stu
{
 char name[20]; // 名字
 int age; // 年龄
 char sex[5]; // 性别
 char id[20]; // 学号
}; // 分号不能丢 

0x02 匿名结构体

📚 定义:在声明结构的时候,可以不完全声明。匿名结构体在声明时省略掉结构体标签(tag),因为没有结构体标签导致无法构成类型,所以匿名结构体自然只能用一次。

💬 代码演示:匿名结构体

struct
{
    int a;
    char b;
    float c;
    double d;
} s;

struct
{
    int a;
    char b;
    float c;
    double d;
} *ps;

📌 注意事项:

对于上面的代码如果进行如下操作,是非法的

int main()
{
    ps = &s; // error

    return 0;
}

❌ 此时编译器会报出如下警告:

在编译器看来,虽然成员是一模一样的,但是编译器仍然认为它们是两个完全不同的类型。 因为不相同,所以 *ps 不能存变量 s 的地址。

0x03 结构的自引用

📚 介绍:结构体中包含一个类型为该结构体本身的成员,包含同类型的结构体指针(不是包含同类型的结构体变量)

💬 代码演示:结构体自引用

struct A
{
    int i;
    char c;
};

struct B
{
    char c;
    struct A sa;
    double d;
};

📌 注意事项1:结构体不能自己包含自己,不能包含同类型的结构体变量

 ❌ 错误演示:

struct N
{
    int d;
    struct N n; // ❌ 结构体里不能存在结构体自己类型的成员
};

📚 为了加深理解,我们先引入一下数据结构的一些知识:

📌 注意事项2:结构体自引用时,不要用匿名结构体:

❌ 错误演示:

struct  // 如果省略结构体名字
{
    int data;
    struct Node* next; // 这里的 struct Node* 是哪里来的?
};

即使使用 typedef 重新取名为 Node,也是不行的。因为你要产生 Node 必须先有结构体类型之后才能重命名 Node,即先 Node* next 定义完成员之后才 typedef 才能对这个类型重命名为 Node。所以这种方式仍然是不行的:

typedef struct
{
    int data;
    Node* next; // 先有鸡还是先有蛋???
} Node;

🔑 解决方案:

typedef struct Node
{
    int data;
    struct Node* next;
} Node;

0x04 结构体变量的定义和初始化

💬 声明类型的同时直接创建变量:

struct S
{
    char c;
    int i;
} s1, s2; // 声明类型的同时创建变量

int main()
{
    struct S s3, s4;

    return 0;
}

💬 创建变量的同时赋值(初始化)

struct S
{
    char c;
    int i;
} s1, s2;

int main()
{
    struct S s3 = {'x', 20};
//                  c    i

    return 0;
}

💬 结构体包含结构体的初始化方法:

struct S
{
    char c;
    int i;
} s1, s2;

struct B
{
    double d;
    struct S s;
    char c;
};

int main()
{
    struct B sb = {3.14, {'w', 100}, 'q'};
    printf("%lf %c %d %c\n", sb.d, sb.s.c, sb.s.i, sb.c);
    
    return 0;
}

🚩  3.140000 w 100 q

0x05 结构体内存对齐

📚 本段我们将讨论结构体占多大的内存空间,学习如何计算结构体的大小

💬 我们先来观察下面的代码:

#include <stdio.h>

struct S
{
    char c1; // 1
    int i; // 4
    char c2; // 1
};

int main()
{
    struct S s = { 0 };
    printf("%d\n", sizeof(s));

    return 0;
}

🚩  12

❓ 为什么是12呢?这就涉及到结构体内存对齐的问题了。

📚 结构体的对齐规则:

      ① 结构体的第一个成员放在结构体变量在内存中存储位置的0偏移处开始。

      ② 从第2个成员往后的所有成员,都要放在一个对齐数(成员的大小和默认对齐数的较小值)的整数的整数倍的地址处。VS中默认对齐数为8!

      ③ 结构体的总大小是结构体的所有成员的对齐数中最大的那个对齐数的整数倍。

      ④ 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

📌 注意事项:VS中默认对其数为8,Linux中没有默认对齐数概念!

💬 练习1:

#include <stdio.h>

struct S2
{
    char c;
    int i;
    double d;
};

int main()
{
    struct S2 s2 = {0};
    printf("%d\n", sizeof(s2));

    return 0;
}

💬 练习2:

#include <stdio.h>

struct S3
{
    char c1;
    char c2;
    int i;
};

int main()
{
    struct S3 s3 = { 0 };
    printf("%d\n", sizeof(s3));

    return 0;
}

 💬 练习3:

#include <stdio.h>

struct S4
{
    double d;
    char c;
    int i;
};

int main()
{
    struct S4 s4 = { 0 };
    printf("%d\n", sizeof(s4));

    return 0;
}

💬 结构体嵌套问题:

如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

#include <stdio.h>

struct S4
{
    double d;
    char c;
    int i;
};
struct S5
{
    char c1;
    struct S4 s4;
    double d;
};

int main()
{
    struct S5 s5 = {0};
    printf("%d\n", sizeof(s5));

    return 0;
}

❓ 为什么会存在内存对齐?

 1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特 定类型的数据,否则抛出硬件异常。

2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

结构体的内存对齐是拿空间来换取时间的做法。

⚡ 在设计结构体时,如何做到既满足对齐又能节省空间呢?

让空间小的成员尽量集中在一起。

💬 虽然S1和S2类型的成员一模一样,但是通过让空间小的成员尽量计中在一起,使所占空间的大小有了一些区别。

struct S1
{
    char c1;
    int i;
    char c2;
};

struct S2
{
    char c1;
    char c2;
    int i;
};

0x06 修改默认对齐数

📚 预处理指令 #pragma 可以改变我们的默认对齐数,#pragma pack(2)

#include <stdio.h>

// 默认对齐数是8
#pragma pack(2) // 把默认对齐数改为2
struct S
{
    char c1; //1
    int i; // 4
    char c2; // 1
};

#pragma pack() // 取消
int main()
{
    printf("%d\n", sizeof(struct S)); //12

    return 0;    
}

🔺 结论:结构体在对齐方式不合适的时候,我们可以通过使用 #pragma 自行修改默认对齐数

0x07 offsetof

📚 作用:该宏用于求结构体中一个成员在该结构体中的偏移量。

📜 头文件: stddef.h

💬 使用方法演示:

#include <stdio.h>
#include <stddef.h>

struct S
{
    char c1; //1
    int i; // 4
    char c2; // 1
};

int main()
{
    printf("%d\n", offsetof(struct S, c1));
    printf("%d\n", offsetof(struct S, i));
    printf("%d\n", offsetof(struct S, c2));

    return 0;    
}

🚩  0 4 8

💭 百度笔试题:写一个宏,计算结构体中某变量相对于首地址的偏移,并给出说明。

0x08 结构体传参

💬 观察下列代码:

#include <stdio.h>

struct S
{
 int data[1000];
 int num;
};

struct S s = {{1, 2, 3, 4}, 1000};

//结构体传参
void print1(struct S s)
{
    printf("%d\n", s.num);
}

//结构体地址传参
void print2(struct S* ps)
{
    printf("%d\n", ps->num);
}

int main()
{
    print1(s);  //传结构体
    print2(&s); //传地址

    return 0;
}

❓ print1(传结构体) 和 print2 (传地址)函数哪个更好?

💡 答案:首选 print2 (传地址)函数

🔑 解析:函数传参的时候是需要压栈的,会产生时间和空间上的系统开销。如果传递一个结构体对象时,结构体过大,参数压栈的系统开销就会很大,从而导致性能的下降。

 🔺 结论:结构体传参得时候,要传结构体的地址。

二、位段(bit field)

0x00 何为位段

📚 定义:位段,C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位域”( bit field) 。利用位段能够用较少的位数存储数据。

📚 位段的声明和结构体是类似的,但有两个不同点:

      ① 位段的成员只能是: int、unsigned int、signed int

      ② 位段的成员名后面有一个冒号和一个数字:member_name : number

💬 代码演示:

struct A
{
    int _a:2;
    int _b:5;
    int _c:10;
    int _d:30;
}

// A就是一个位段类型

❓ 那么问题来了,位段A的大小是多少?

#include <stdio.h>

struct A
{
    int _a:2;  // _a 成员占2个比特位
    int _b:5;  // _b 成员占5个比特位
    int _c:10; // _c 成员占10个比特位
    int _d:30; // _d 成员占30个比特位
};

int main()
{
    printf("%d\n", sizeof(struct A));

    return 0;
}

🚩  8

💡 运行结果居然是8,四个成员占47个比特位,而8个字节是64个比特位,为什么会这样呢?

0x01 位段的内存分配

📚 位段的意义:位段在一定程度上帮助我们节省空间。

📌 注意事项:

      ① 位段的成员可以是 int、unsigned int、signed int 或者是 char (属于整形家族)类型。

      ② 位段的空间上是 按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。

      ③ 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

❓ 空间是如何开辟的?

struct S
{
    char a : 3;
    char b : 4;
    char c : 5;
    char d : 4;
};

int main()
{
    struct S s = { 0 };
    s.a = 10;
    s.b = 12;
    s.c = 3;
    s.d = 4;
}

0x02 位段的跨平台问题

1. int 位段被当成有符号数还是无符号数是不确定的。

2. 位段中最大位的数目不能确定。比如16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。

3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。

4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。

🔺 总结:

跟结构相比,位段可以达到同样的效果,但位段可以更好地节省空间,缺陷是存在跨平台问题。

三、枚举(enumerate)

0x00 何为枚举

在数学和计算机科学理论中,一个集的枚举是列出某些有穷序列集的所有成员的程序,或者是一种特定类型对象的计数。这两种类型经常(但不总是)重叠。

是一个被命名的整型常数的集合,枚举在日常生活中很常见,例如表示星期的SUNDAY、MONDAY、TUESDAY、WEDNESDAY、THURSDAY、FRIDAY、SATURDAY就是一个枚举。 [ 百度百科 ]

📚 枚举,顾名思义就是壹壹列举,把可能的取值壹壹列举。

 eg. 性别有男、女和保密,我们就可以列举他们,或者一年有12个月,可以把每个月都壹壹列举。

0x01 枚举的定义

 💬 代码演示:

enum Day //星期
{
    // 枚举常量
    Mon,
    Tues,
    Wed,
    Thur,
    Fri,
    Sat,
    Sun
};

enum Sex //性别
{
    // 枚举常量
    MALE,
    FEMALE,
    SECRET
};

enum Color //颜色
{
    // 枚举常量
    RED,
    GREEN,
    BLUE
};

📚 上述代码定义的 enum Day,enum Sex,enum Color 都是枚举类型。{ } 中的内容是枚举类型的可能取值,即枚举常量。这些可能取值都是有值的,默认从0开始,依次递增1。当然,可以在定义的时候也可以对其赋予初值,例如:

enum Color //颜色
{
    // 枚举常量
    RED = 1, // 赋初值
    GREEN = 2,
    BLUE = 4
};

0x02 枚举的优点

❓ 我们可以用 #define 定义常量,为什么非要使用枚举?

📚 枚举的优点:

      ① 增加代码的可读性和可维护性。

      ② 与 #define 定义的标识符相比,枚举有类型检查,更加严谨。

      ③ 有效防止命名污染(封装)。

      ④ 便于调试。

      ⑤ 使用方便,一次可以定义多个常量。

0x03 枚举的使用

💬 代码演示:

#include <stdio.h>

enum Color //颜色
{
    RED = 1,
    GREEN = 2,
    BLUE = 4
};

int main()
{
   enum Color c = GREEN;
   c = 5;
   printf("%d\n", c);

   return 0;
}

📌 注意事项:

     ① 默认从0开始,依次递增1。(可赋初值,上面赋值如果下面不赋,随上一个赋的值 +1 )

     ② 枚举常量是不能改变的。 (MALE = 3  error!)

#include <stdio.h>

enum Sex
{
    // 枚举常量
    MALE = 3, // 赋初值为3
    FEMALE, // 不赋初值,默认随上一个枚举常量,+1为4
    SECRET // +1为5
};

int main(void)
{
    enum Sex s = MALE;
    printf("%d\n", MALE);
    // MALE = 3 error ❌ 不可修改
    printf("%d\n", FEMALE);
    printf("%d\n", SECRET);

    return 0;
}

🚩  3  4  5

     

     ③ 枚举常量虽然是不能改变的,但是通过枚举常量创造出来的变量是可以改变的!

enum Color 
{
    // 枚举常量
    RED,
    YEELOW,
    BULE
};

int main(void)
{
    enum Color c = BULE; // 我们创建一个变量c,并将BULE赋给它
    c = YEELOW; // 这时将YEELOW赋给它,完全没有问题 ✅
    BULE = 6; // error!枚举常量是不能改变的  ❌

    return 0;
}

0x04 实际运用演示

💬 之前我们在实现计算器的时候是这么写代码的:(仅演示部分代码)

#include <stdio.h>

void menu()
{
    printf("*****************************\n");
    printf("**    1. add     2. sub    **\n");
    printf("**    3. mul     4. div    **\n");
    printf("**         0. exit         **\n");
    printf("*****************************\n");
}

int main()
{
    int input = 0;
    do {
        menu();
        printf("请选择:> ");
        scanf("%d", &input);
        switch {
            case 1:
                break;
            case 2:
                break;
            case 3:
                break;
            case 4:
                break;
            case 0:
                break;
            default:
                break;
    } while (input);
    
    return 0;
}

❓ 思考:阅读代码的时候如果不看上面的 menu,是很难知道 case 中的 12340 分别是什么的。1 为什么是加?2 为什么是减?看到数字的时候联想不到它的到底是干什么的。

⚡ 为了提高代码的可读性,我们可以使用枚举来解决:

#include <stdio.h>

void menu() {...}

enum Option
{
    EXIT, // 0
    ADD,  // 1
    SUB,  // 2
    MUL,  // 3
    DIV,  // 4
};

int main()
{
    int input = 0;
    do {
        menu();
        printf("请选择:> ");
        scanf("%d", &input);
        switch {
            case ADD: // 替换后就好多了,代码的可读性大大增加
                break;
            case SUB:
                break;
            case MUL:
                break;
            case DIV:
                break;
            case EXIT:
                break;
            default:
                break;
    } while (input);
    
    return 0;
}

四、联合体(union)

0x00 何为联合体

📚 定义:联合体又称共用体,是一种特殊的自定义类型。可以在相同的内存位置存储不同的数据类型。可以定义一个带有多成员的联合体,但是任何时候只能有一个成员带有值。

0x01 联合体的定义

💬 代码演示:

#include <stdio.h>

union Un
{
    char c; // 1
    int i; // 4
};

int main()
{
    union Un u; // 创建一个联合体变量
    printf("%d\n", sizeof(u)); // 计算联合体变量的大小

    return 0;
}

 🚩  4

❓ 为什么是4个字节呢?我们来试着观察下它的内存:

#include <stdio.h>

union Un
{
    char c; // 1
    int i; // 4
};

int main()
{
    union Un u;
    printf("%p\n", &u);
    printf("%p\n", &(u.c));
    printf("%p\n", &(u.i));

    return 0;
}

🚩 运行结果如下:

🔺 结论:联合体的成员是共用同一块内存空间的。因为联合至少要有保存最大的那个成员的能力,所以一个联合变量的大小至少是最大成员的大小。

0x02 联合体的初始化

💬 代码演示:

#include <stdio.h>

union Un
{
    char c; // 1
    int i; // 4
};

int main()
{
    union Un u = {10};

    return 0;
}

🐞 调试:打开监视后,我们可以看到 i 和 c 是是共用一个10的:

❓ 如果想在每个成员里放上独立的值呢?

#include <stdio.h>

union Un
{
    char c; // 1
    int i; // 4
};

int main()
{
    union Un u = {10}; 
    u.i = 1000;   
    u.c = 100;
    
    return 0;
}

🐞 观察调试过程:

 🔺 结论:在同一时间内你只可以使用联合体中的一个成员。

0x03 联合体大小的计算

💬 看代码:

#include <stdio.h>

union Un
{
    char a[5]; // 5
    int i; // 4
};

int main()
{
    union Un u;
    printf("%d\n", sizeof(u));

    return 0;
}

🚩  8

❓ 为什么又是8个字节了?

🔑 其实联合体也是存在对齐的,我们来更加系统地、详细的探究下联合体的大小规则:

📚 联合体大小的计算:

      ① 联合的大小至少是最大成员的大小。

      ② 当最大成员的大小不是最大对齐数的整数倍时,对要对齐到最大对齐数的整数倍。

union Un
{
    char a[5]; // 对齐数是1
    int i; // 对齐数是4
};

// 所以最后取了8个字节为该联合体的大小

0x04 实际运用演示

通过联合体判断当前机器大小端

🔗 复习链接: 【维生素C语言】第九章 - 数据的存储 (四、大小端)

💡 实现思路:

 💬 之前学的方法:

#include <stdio.h>

int main()
{
    int a = 1;
    if ( ( *(char*)&a ) == 1 )
        printf("小端\n");
    else
        printf("大端\n");
    
    return 0;
}

 ⚡ 将其封装成函数:

#include <stdio.h>

int check_sys() 
{
    int a = 1;
    return *(char*)&a;
}
int main()
{
    int ret = check_sys();
    if(ret == 1)
        printf("小端\n");
    else
        printf("大端\n");
    
    return 0;
}

💬 通过联合体的方式判断: (通过深刻理解联合体特点写出来的代码)

#include <stdio.h>

int check_sys() 
{
    union U {
        char c;
        int i;
    } u;

    u.i = 1;
    return u.c;
    // 返回1 就是小端
    // 返回0 就是大端
}
int main()
{
    int ret = check_sys();
    if(ret == 1)
        printf("小端\n");
    else
        printf("大端\n");
    
    return 0;
}


参考资料:

Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .

比特科技. C语言进阶[EB/OL]. 2021[2021.8.31]. .

📌 本文作者: 王亦优

📃 更新记录: 2021.7.31

勘误记录:

📜 本文声明: 由于作者水平有限,本文有错误和不准确之处在所难免,本人也很想知道这些错误,恳望读者批评指正!

本章完。

  • 14
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 11
    评论
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

平渊道人

喜欢的话可以支持下我的付费专栏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值