C语言中的自定义类型: 结构体、联合体和枚举

news2024/12/23 14:47:17

1.结构体类型

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

1.1结构体类型的声明

        上述的variable-list可以有也可以没有,有的意思是直接在这就创建了结构体变量,这里创建的变量可以直接初始化,如下面一段代码:

struct
{
    char c;
    int i;
    double d;
}s = {'x', 100, 3.14};

        例如描述一个学生: 

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

1.1.1结构体变量的创建和初始化

#include <stdio.h>
struct Stu
{
    char name[20];//名字
    int age;//年龄
    char sex[5];//性别
    char id[20];//学号
};
int main()
{
    //按照结构体成员的顺序初始化
    struct Stu s = { "张三", 20, "男", "20230818001" };
    printf("name: %s\n", s.name);
    printf("age : %d\n", s.age);
    printf("sex : %s\n", s.sex);
    printf("id : %s\n", s.id);
 
    //按照指定的顺序初始化
    struct Stu s2 = { .age = 18, .name = "lisi", .id = "20230818002", .sex = "⼥" };
    printf("name: %s\n", s2.name);
    printf("age : %d\n", s2.age);
    printf("sex : %s\n", s2.sex);
    printf("id : %s\n", s2.id);
    return 0;
}

 1.2结构的特殊声明

        在声明结构的时候,可以不完全的声明.比如:

//匿名结构体类型
struct
{
    int a;
    char b;
    float c;
}x;
struct
{
    int a;
    char b;
    float c;
}a[20], *p;

        上面的两个结构在声明的时候省略掉了结构体标签(tag).匿名的结构体类型只能在定义的时候创建变量,不能在后序的程序中单独创建结构体变量,如果没有对结构体类型重命名的话,基本上只能使用一次. 编译器会把上面的两个声明当成完全不同的两个类型。

1.3结构的自引用

        比如定义一个链表的节点:

        上述的代码是错误的,如果这样引用sizeof(struct Node)将会变为无穷大.

        正确的自引用方式是通过指针进行自引用:

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

         在结构体⾃引⽤使⽤的过程中,夹杂了 typedef 对匿名结构体类型重命名,也容易引⼊问题,看看下⾯的代码:

        因为Node是对前面的匿名结构体类型的重命名产生的,但是在匿名结构体内部提前使用Node类型来创建成员变量,这样是不行的.

        注:为了避免不必要的错误,定义结构体的时候不要使用匿名结构体.

1.4 结构体内存对齐

        这一节主要讨论的是如何计算结构体的大小,这就要涉及到结构体内存的对齐了.

1.4.1对齐规则

(1)结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处

(2)其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处.

        对齐数 = 编译器默认的一个对齐数与该成员变量大小的较小值.

        vs中默认的值为8.

        Linux中gcc没有默认对齐数,对齐数就是成员自身的大小

(3)结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍.

(4)如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所以最大对齐数(含嵌套结构体中成员的对齐数)的整数倍.

        以下面这段代码为例:

#include <stdio.h>

struct S
{
    char c1;  //char类型变量大小1, VS默认对齐数8, 故该成员变量对齐数为1
    int i;    //int 类型变量大小4, VS默认对齐数8, 故该成员变量对齐数为4
    char c2;  //char类型变量大小1, VS默认对齐数8, 故该成员变量对齐数为1
};
//结构体总大小要为所以成员变量中最大对齐数的整数倍,上述结构体中成员变量最大对齐数是4,但是成员
//变量已经占了9个字节,为了对齐,故总大小为12个字节。
int main()
{
    struct S s;
    printf("%zd\n", sizeof(s));
    return 0;
}

        当遇到嵌入结构体的情况:

#include <stdio.h>

struct S3 
{
    double d; // 8 8 8
    char c;   // 1 8 1
    int i;    // 4 8 4
};

//如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构
//体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。

struct S4
{
    char c1;     // 1 8 1
    struct S3 s3; // 8 8 8    //s3大小为16个字节对齐数为8
    double d;    // 8 8 8
};

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

    return 0;
}

1.4.2为什么存在内存对齐(仅供参考)

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

        (2)性能原因:数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两个8字节内存块中。

        总结来说:结构体的内存对齐是拿空间换取时间的做法.

        设计结构体的时候,既要满足对齐,又要节省空间,则让占用空间小的成员尽量集中在一起:

        S1和S2类型的成员一模一样,但是S1占12个字节,S2占8个字节。 

1.4.3修改默认对齐数

        #pragma这个预处理指令,可以改变编译器的默认对齐数。

        结构体在对齐方式不适合的时候,我们可以自己更改默认对齐数。

1.5结构体传参 

#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;
}

        但传结构体和传结构体地址时我们选择传地址

        原因:函数传参的时候,参数是需要压栈的,会有时间和空间上的系统开销。如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能下降。

1.6 结构体实现位段

1.6.1什么是位段

        位段的实现是基于结构体的,位段的声明和结构是类似的,有两个不同:
1.位段的成员必须是int, unsigned int 或signed int, 在C99中位段成员的类型也可以选择其他类型。

2.位段的成员名后边有一个冒号和一个数字。

        比如:

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

        A就是一个位段类型,下面讨论一下位段A所占内存的大小。

1.6. 2位段的内存分配

         1. 位段的成员可以是 int, unsigned int, signed int 或者是 char 等类型
        2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的⽅式来开辟的。
        3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使⽤位段。
//⼀个例⼦
struct S
{
    char a:3;
    char b:4;
    char c:5;
    char d:4;
};

struct S s = {0};
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
//空间是如何开辟的?

        位段是专门设计出来节省内存的,成员变量冒号后的数字代表该变量占多少个bit位。如果成员是int类型,就一次开辟四个字节,不够用了之后再继续开辟,同理如果成员类型是char类型,一次开辟一个字节,不够用了之后再继续开辟。

1.6.3位段的跨平台问题

        1. int 位段被当成有符号数还是⽆符号数是不确定的。
        2. 位段中最⼤位的数⽬不能确定。(16位机器最⼤16,32位机器最⼤32,写成27,在16位机器会出问题。
        3. 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。
        4. 当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,是舍弃剩余的位还是利⽤,这是不确定的。
        总结:和结构体相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。

1.6.4位段的应用

        下图是⽹络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要⼏个bit位就能描述,这⾥使⽤位段,能够实现想要的效果,也节省了空间,这样⽹络传输的数据报⼤⼩也会较⼩⼀些,对⽹络的畅通是有帮助的。

1.6.5位段使用的注意事项 

        位段的几个成员共有同一个字节, 这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处没有地址的。内存中每个字节分配一个地址,一个字节内部的bit位是没有地址的。所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段的成员输入值,只能是先输入放在一个变量中,然后赋值给位段的成员。

#include <stdio.h>

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

int main()
{
    struct A sa = {0};
    scanf("%d", &sa._b); //这样是错误的,不能使用&操作符

    //正确的是通过变量赋值
    int b = 0;
    scanf("%d", &b);
    sa._b = b;
    
    return 0;
}

2.联合体类型

2.1 联合体类型的声明

        像结构体一样,联合体也是由一个或者多个成员构成,这些成员可以是不同的类型。但是编译器只为最大的成员分配足够的内存空间。联合体的特点是所有成员共用一块内存空间。所以联合体也叫:共用体。

#include <stdio.h>

//联合体类型的声明
union Un
{
    char c;
    int i;
};

int main()
{
    //联合变量的定义
    union Un un = {0};

    //计算联合体变量的大小
    printf("%d\n", sizeof(un));

    return 0;
}

2.2 联合体的特点

        联合的成员是共用同一块内存空间,这样一个联合变量的大小,至少是最大成员的大小。

//代码1
#include <stdio.h>
//联合类型的声明
union Un
{
    char c;
    int i;
};
int main()
{
    //联合变量的定义
    union Un un = {0};
    // 下⾯输出的结果是⼀样的吗?
    printf("%p\n", &(un.i));
    printf("%p\n", &(un.c));
    printf("%p\n", &un);
    return 0;
}

        上述代码打印出来的地址都是相同的,所以可以说明联合体变量和它的成员变量所占用的空间是同一块空间。

//代码2
#include <stdio.h>
//联合类型的声明
union Un
{
    char c;
    int i;
};
int main()
{
    //联合变量的定义
    union Un un = {0};
    un.i = 0x11223344;
    un.c = 0x55;
    printf("%x\n", un.i);
    return 0;
}

        我们可以看到通过十六进制的形式打印出来的值为11223355,因为在VS里面是小端存储,低位数据存放在低地址处,高位数据存放在高地址处,对超过一个字节的变量取地址都是取的低地址处的地址,但是c变量只占了一个字节,所以c变量的修改影响了i变量的低位。

 2.2.1相同成员的结构体和联合体对比

2.3 联合体⼤⼩的计算

        联合体的大小至少是最大成员的大小,当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。使用联合体是可以节省空间的。

#include <stdio.h>
union Un1
{
    char c[5]; // char变量的对齐数是1, vs默认为8, 所以对齐数为1
    int i;     // int变量的对齐数是 4, vs默认为8, 所以对齐数为4
};
union Un2
{
    short c[7]; // 2 8 2 大小为14个字节
    int i;      // 4 8 4
};
int main()
{
    //下⾯输出的结果是什么?
    printf("%zd\n", sizeof(union Un1)); //大小为8个字节
    printf("%zd\n", sizeof(union Un2)); //大小为16个字节
    return 0;
}

2.4联合体的一个应用

        写一个程序,运用联合体判断机器是大端还是小端:

#include <stdio.h>

int check_sys()
{
    union
    {
        int i;
        char c;
    }un;
    un.i = 1;
    return un.c;
}

int main()
{
    if (check_sys() == 1)
    {
        printf("小端");
    }
    else
    {
        printf("大端");
    }
}

3.枚举类型

3.1枚举类型的声明

        枚举顾名思义就是一一列举,把可能的取值一一列举出来。比如生活中一周的星期1到星期天:

        上述定义的enum Day就是枚举类型。{}中的内容是枚举类型的可能取值,也叫枚举常量。这些可能取值都是有值的,默认从0开始,依次递增1,当然在声明枚举类型的时候也可以赋初值。如果RED赋初值为3,GREEN和BLUE都没有赋初值,那么GREEN和BLUE的值为4和5,是基于RED的值依次增加1的。

        枚举其实和#define定义一个常量的效果是一样的。 

3.2枚举类型的优点

        我们可以使用#define定义常量为什么要使用枚举呢?因为枚举有以下一些优点:

1.增加代码的可读性和可维护性。(通过枚举可以把下面代码可能用到的数字对应成英语,增加可读性)

2.和#define定义的标识符比较枚举有类型检查,更加严谨。(在C++中枚举类型必须用枚举常量进行赋值,#define定义的符合在预处理阶段就会全部替换为定义的值

3.便于调试,预处理阶段会删除#define定义的符合,把他替换为#define定义所对应的值。

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

5.枚举常量是遵循作用域规则的,枚举声明在函数内,只能在函数内使用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1894372.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

时空预测+特征分解!高性能!EMD-Transformer和Transformer多变量交通流量时空预测对比

时空预测特征分解&#xff01;高性能&#xff01;EMD-Transformer和Transformer多变量交通流量时空预测对比 目录 时空预测特征分解&#xff01;高性能&#xff01;EMD-Transformer和Transformer多变量交通流量时空预测对比效果一览基本介绍程序设计参考资料 效果一览 基本介绍…

经典递归分析

在前面一篇中, 已经看过许多直观的递归的例子, 在这篇里, 将分析两个经典的递归问题, 阶乘与菲波那契数列数列, 在此过程中, 还将对比递归与循环(迭代)间的异同, 探讨递归与内存中的栈的关系, 以及递归的效率等问题. 如无特别说明, 示例使用的是 Java, IDE 则为 Eclipse. 阶乘(…

js学习--制作猜数字

猜数字制作 <!DOCTYPE html> <html><head><meta charset"utf-8"><title></title></head><body><script>function fun() {alert("1-100猜数字");let num Math.floor(Math.random() * 100) 1;for …

无法识别为 cmdlet、函数、脚本文件或可运行程序的名称

一、遇到问题 PS D:\software\nacos\nacos-server-2.3.1\bin> startup.cmd -m standalone startup.cmd : 无法将“startup.cmd”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写&#xff0c;如果包括路径&#xff0c; 请确保路径正确&#xff0c;然后…

Kamailio-Web管理页面Siremis的安装与部署

siremis 是针对于 Kamailio 的web管理接口&#xff0c;使用PHP书写&#xff0c;更新至2020年&#xff0c;相对不是太新但是是官方友链的 以下就采用 Ubuntu 22.04Siremis 5.8.0apache http server 2.4php7.0 如有疑问请参看官方指南 以下开始介绍操作步骤 安装apache2.4 we…

【JavaEE】多线程代码案例(2)

&#x1f38f;&#x1f38f;&#x1f38f;个人主页&#x1f38f;&#x1f38f;&#x1f38f; &#x1f38f;&#x1f38f;&#x1f38f;JavaEE专栏&#x1f38f;&#x1f38f;&#x1f38f; &#x1f38f;&#x1f38f;&#x1f38f;上一篇文章&#xff1a;多线程代码案例(1)&a…

【硬核科普】存算一体化系统(Processing-in-Memory, PIM)深入解析

文章目录 0. 前言1. 提出背景1.1 存储墙1.2 功耗墙 2. 架构方案2.1 核心特征2.2 技术实现2.2.1 电流模式2.2.2 电压模式2.2.3 模式选择 2.3 PIM方案优势 3. 应用场景4. 典型产品4.1 鸿图H304.2 三星HBM-PIM 5. 存算一体化缺点6. 总结 0. 前言 按照国际惯例&#xff0c;首先声明…

【C语言】auto 关键字

在C语言中&#xff0c;auto关键字用于声明局部变量&#xff0c;但它的使用已经变得很少见。事实上&#xff0c;从C99标准开始&#xff0c;auto关键字的默认行为就是隐含的&#xff0c;因此在大多数情况下无需显式使用它。 基本用法 在C语言中&#xff0c;auto关键字用于指定变…

视频监控平台web客户端的免密查看视频页:在PC浏览器上如何调试手机上的前端网页(PC上的手机浏览器的开发者工具)

目录 一、手机上做前端页面开发调试 1、背景 2、视频监控平台AS-V1000的视频分享页 3、调试手机前端页面代码的条件 二、手机端的准备工作 1、手机准备 2、手机的开发者模式 3、PC和手机的连接 &#xff08;1&#xff09;进入调试模式 &#xff08;2&#xff09;选择…

【大数据】—量化交易实战案例(海龟交易策略)

声明&#xff1a;股市有风险&#xff0c;投资需谨慎&#xff01;本人没有系统学过金融知识&#xff0c;对股票有敬畏之心没有踏入其大门&#xff0c;今天用另外一种方法模拟炒股&#xff0c;后面的模拟的实战全部用同样的数据&#xff0c;最后比较哪种方法赚的钱多。 海龟交易…

初试成绩占比百分之70!计算机专硕均分340+!华中师范大学计算机考研考情分析!

华中师范大学&#xff08;Central China Normal University&#xff09;简称“华中师大”或“华大”&#xff0c;位于湖北省会武汉&#xff0c;是中华人民共和国教育部直属重点综合性师范大学&#xff0c;国家“211工程”、“985工程优势学科创新平台”重点建设院校&#xff0c…

智慧消防视频监控烟火识别方案,筑牢安全防线

一、方案背景 在现代化城市中&#xff0c;各类小型场所&#xff08;简称“九小场所”&#xff09;如小餐馆、小商店、小网吧等遍布大街小巷&#xff0c;为市民生活提供了极大的便利。然而&#xff0c;由于这些场所往往规模较小、人员流动性大、消防安全意识相对薄弱&#xff0…

分布式计算、异构计算与算力共享

目录 算力 算力共享的技术支撑 云计算技术 边缘计算技术 区块链技术 分布式计算、异构计算与算力共享 分布式计算:计算力的“集团军作战” 异构计算:计算力的“多兵种协同” 算力共享:计算力的“共享经济” 深入融合,共创计算新纪元 算力共享对科研领域的影响 …

JavaScript懒加载图像

懒加载图像是一种优化网页性能的技术&#xff0c;它将页面中的图像延迟加载&#xff0c;即在用户需要查看它们之前不会立即加载。这种技术通常用于处理大量或大尺寸图像的网页&#xff0c;特别是那些包含长页面或大量媒体内容的网站。 好处 **1. 加快页面加载速度&#xff1a…

《昇思25天学习打卡营第9天|保存与加载》

文章目录 今日所学&#xff1a;一、构建与准备二、保存和加载模型权重三、保存和加载MindIR总结 今日所学&#xff1a; 在上一章节主要学习了如何调整超参数以进行网络模型训练。在这一过程中&#xff0c;我们通常会想要保存一些中间或最终的结果&#xff0c;以便进行后续的模…

在Windows 11上更新应用程序的几种方法,总有一种适合你

序言 让你安装的应用程序保持最新是很重要的,而Windows 11使更新Microsoft应用商店和非Microsoft应用商店的应用程序变得非常容易。我们将向你展示如何使用图形方法以及命令行方法来更新你的应用程序。 如何更新Microsoft Store应用程序 如果你的一个或多个应用程序是从Mic…

底层软件 | 十分详细,为了学习设备树,我写了5w字笔记!

0、设备树是什么&#xff1f;1、DTS 1.1 dts简介1.2 dts例子 2、DTC&#xff08;Device Tree Compiler&#xff09;3、DTB&#xff08;Device Tree Blob&#xff09;4、绑定&#xff08;Binding&#xff09;5、Bootloader compatible属性 7、 #address-cells和#size-cells属性8…

64.函数参数和指针变量

目录 一.函数参数 二.函数参数和指针变量 三.视频教程 一.函数参数 函数定义格式&#xff1a; 类型名 函数名(函数参数1,函数参数2...) {代码段 } 如&#xff1a; int sum(int x&#xff0c;int y) {return xy; } 函数参数的类型可以是普通类型&#xff0c;也可以是指针类…

基于 Windows Server 2019 部署域控服务器

文章目录 前言1. 域控服务器设计规划2. 安装部署域控服务器2.1. 添加 Active Directory 域服务2.2. 将服务器提升为域控制器2.3. 检查域控服务器配置信息 3. 管理域账号3.1. 新建域管理员账号3.2. 新建普通域账号 4. 服务器加域和退域4.1. 服务器加域操作4.2. 服务器退域操作 总…

实战教程:如何用JavaScript构建一个功能强大的音乐播放器,兼容本地与在线资源

项目地址&#xff1a;Music Player App 作者&#xff1a;Reza Mehdikhanlou 视频地址&#xff1a;youtube 我将向您展示如何使用 javascript 编写音乐播放器。我们创建一个项目&#xff0c;您可以使用 javascript 从本地文件夹或任何 url 播放音频文件。 项目目录 assets 1…