当前位置: 首页 > news >正文

【C语言】自定义类型(附源码与图片分析)

前言


在讲解操作符时,已经简单了解过结构体了,这篇文章将带你深入了解结构体是如何运用的。

结构体和联合体和枚举

  • 前言
  • 一、结构体声明
    • 1、语法形式
    • 2、结构体的创建和初始化
      • 2.1按照结构体成员顺序初始化
      • 2.2按照指定的顺序初始化
    • 3、结构体的输出
      • 3.1单个输出和多个输出
    • 4、结构中的特殊声明
      • 4.1匿名结构体的使用
      • 4.2注意:
    • 5、结构的自引用
      • 5.1场景1:没有指针(错误方式)
      • 5.2场景2:使用指针(正确方式)
      • 5.3示例
  • 二、结构体的缩写
  • 三、结构体内存对齐
    • 6、对齐规则
    • 6.1示例1
    • 6.2示例2
    • 6.3示例3
    • 6.4示例4
  • 四、为什么存在内存对齐?
    • 7.1修改默认对齐数
      • 示例
  • 五、结构体传参
  • 六、结构体位段
    • 6.1什么是位段
    • 6.2位段的内存分配
    • 6.3位段的跨平台问题
    • 6.4位段的应用
    • 6.5位段使用的注意事项
  • 7、联合体的声明和创建
    • 7.1联合体的特点
    • 7.2联合体的地址变化
    • 7.3结构体和联合体内存图
    • 7.4联合体大小计算
      • 举例
        • 1>直接用结构体
        • 2>利用联合体
        • 3>练习(判断1是什么端存放)
  • 8、枚举类型
    • 8.1枚举类型的声明
    • 8.2枚举类型的优点
    • 8.3枚举类型的使用

一、结构体声明


1、语法形式


举例下面一个例子:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>struct book
{char name[20];char author[20];float price;char id[13];
}b3, b4;//全局变量struct book b2;//全局变量int main()
{struct book b1;//局部变量return 0;
}

2、结构体的创建和初始化


2.1按照结构体成员顺序初始化


#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>//创建结构体
struct book
{char name[20];char author[20];float price;char id[13];
};int main()
{//初始化1struct book b1 = { "PengGe_C Yuyan","PengGe",49.9, "QWE"};return 0;
}

2.2按照指定的顺序初始化

//初始化2
struct book b2 = { .id = "QWE", .price = 8.8, .author = "PengGe", .name = "PengGe_C" };

3、结构体的输出


3.1单个输出和多个输出


#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>struct book
{char name[20];char author[20];float price;char id[13];
};int main()
{//按照结构体成员的顺序初始化struct book b1 = { "PengGe_C Yuyan","PengGe",49.9, "QWE"};//单个打印printf("%s ", b1.name);printf("%s ", b1.author);printf("%.1f ", b1.price);printf("%s ", b1.id);printf("\n");//一次性打印printf("%s %s %.1f %s", b1.name, b1.author, b1.price, b1.id);return 0;
}

打印结果是一样的:
![[QQ20251007-213645.png]]

4、结构中的特殊声明


4.1匿名结构体的使用


在声明结构的时候,可以不完全的声明

struct
{char name;int age;int id;
}x={"zhangsan",22,11};

注意:匿名结构体只能使用一次

再比如:

struct
{int a;char b;float c;
}x;struct
{int a;char b;float c;
}a[20],*p;

上面的两个结构体在声明的时候省略掉了结构体标签。

4.2注意:

//在上⾯代码的基础上,下⾯的代码合法吗?
p = &x;
  1. 编译器会把上面的两个声明当成完全不同的两个类型,使用是非法的。
  2. 匿名的结构体类型,如果没有对结构体类型重命名的华为,基本上只能使用一次。

5、结构的自引用


当我们要通过1找到2,3,4时该怎么去做?
![[bit-2025-10-07-23-16-05.png]]

5.1场景1:没有指针(错误方式)


struct Node
{int data;//数据struct Node next;//错误!不能这样写
};

比喻:

  • 1号盒子(节点1)里要包含2号盒子(节点2)
  • 2号盒子里又要包含3号盒子(节点3)
  • 3号盒子里又要包含4号盒子(节点4)
  • …无限循环

实际结构:编译器会报错,因为结构体大小会变成无限大

5.2场景2:使用指针(正确方式)


struct Node
{int data;//数据struct Node* next;//正确!使用指针
};

比喻:
1号盒子 ->[数据:1 | 地址条:“2号盒子的位置”]
2号盒子 ->[数据:2 | 地址条:“3号盒子的位置”]
3号盒子 ->[数据:3 | 地址条:“4号盒子的位置”]
1号盒子 ->[数据:4 | 地址条:“NULL(没有下一个)”]

5.3示例


//没有指针:无限大小
struct Node {int data;           // 4字节struct Node next;   // 4字节 + 下一个Node的大小
}; //总大小 = 4 + (4 + (4 + (4 + ...))) = 无限大//使用指针:固定大小
struct Node {int data;           // 4字节struct Node* next;  // 8字节(64位系统)
}; //总大小 = 4 + 8 = 12字节(固定)

二、结构体的缩写


我们在数据结构的书上常常看到这么写结构体
![[bit-2025-10-07-22-57-05.png]]

其实很好理解,typedef就是重命名(简化代码),将写法复杂的struct Node改成了Node。

在结构体自引用的过程中,夹杂了typedef对匿名结构体类型重命名,也容易引入问题,如下列代码

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

这是错误的,因为Node时对前面的匿名结构体类型的重命名产生的,但是在匿名结构体内部提前使用Node类型来创建成员变量,这是不行的。正确的写法如下:

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

三、结构体内存对齐


前面讲了结构体的基本使用。接下来就是结构体内存对齐,这对于计算结构体的大小有着紧密关系。

6、对齐规则


首先得掌握结构体我得对齐规则:

  1. 结构体得第一个成员会放到结构体变量起始位置,也就是偏移量为0得地址处。
  2. 其他成员对齐到整数倍的地址处,对齐数=编译器默认对齐数与该成员变量大小的较小值
    • VS中对齐数默认是8
    • Linux中gcc没有默认对齐数,成员大小就是对齐数
  3. 结构体总大小 = 最大对齐数的整数倍
  4. 如果嵌套结构体,按照结构体与结构体变量所在的最大对齐数的整数倍处。将结构体所占的大小放入结构体变量中
    接下来用几个示例来讲这4点是如何用的

6.1示例1


![[QQ20251009-202454.png]]

6.2示例2


![[bit-2025-10-09-20-16-07.png]]

6.3示例3


![[bit-2025-10-09-20-17-38.png]]

6.4示例4


![[bit-2025-10-09-20-19-53.png]]

四、为什么存在内存对齐?

⼤部分的参考资料都是这样说的:

  1. 平台原因 (移植原因):
  • 不是所有的硬件平台都能访问任意地址上的任意数据的
  • 某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
  1. 性能原因:
    数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
    原因在于
  • 为了访问未对齐的内存,处理器需要作两次内存访问
  • 而对齐的内存访问仅需要⼀次访问
  • 假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用⼀个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。

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

那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
那就让占用空间小的成员尽量集中在⼀起

//例如:
struct S1 
{char c1;int i;char c2;
};struct S2{char c1;char c2;int i;};

s1和s2类型的成员一模一样,但是s1和s2所占空间的大小有了一些区别。

7.1修改默认对齐数


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

示例


#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>#pragma pack(1)//设置默认对⻬数为1
struct S
{char c1;int i;char c2;
};
#pragma pack()//取消设置的对齐数,还原为默认
int main()
{//输出的结果是什么?printf("%d\n", sizeof(struct S));return 0;
}

输出结果为:6
不用预处理指令输出结果:12

五、结构体传参


来看下列代码

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>struct Stu 
{int arr[100];int i;double d;
};//非地址传参
void my_struct1(struct Stu x)
{for (int i = 0; i < 5; i++){printf("%d ", x.arr[i]);}printf("\n");printf("%lf\n", x.d);printf("%d\n", x.i);
}//地址传参
void my_struct2(const struct Stu* x)
{for (int i = 0; i < 5; i++){//x->(指向)arr[i]printf("%d ", x->arr[i]);}printf("\n");printf("%lf\n", x->d);printf("%d\n", x->i);
}int main()
{struct Stu s = { {1,2,3,4,5},100,23.33 };my_struct1(s);my_struct2(&s);return 0;
}

第一个和第二个打印的结果相同
![[Pasted image 20251009215033.png]]

那它们分别有什么优缺点呢?

  • 在结构体无指针传参时,需要创建栈区,会有空间和时间上的浪费。
  • 结构体指针传参时,地址的大小无非4/8个字节,即节省空间,又节省时间。
    能用指针传参时,就尽量用指针传参。

六、结构体位段


6.1什么是位段


  1. 位段的成员必须是int、unsigned、signed int,在C99中位段成员也可以是其他类型。
  2. 位段的成员名后边有个冒号和一个数字。
struct s
{int a:2;int a:5;int a:10;
}

6.2位段的内存分配


  1. 和内存对齐一样,可以用来节省内存
  2. 位段的成员可以是int,unsigned int,signed int,char等类型
  3. 位段的空间是按照4个字节(int)或1个字节(char)开辟的
  4. 标准C语言中,位段有很多不确定因素,要注意位段是不可以跨平台的使用
    ![[QQ20251009-223610.png]]

![[QQ20251009-233858.png]]

6.3位段的跨平台问题


  1. int 位段被当成有符号数还是无符号数是不确定的。
  2. 位段中最⼤位的数⽬不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
  3. 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。
  4. 当⼀个结构包含两个位段,第⼆个位段成员比较⼤,无法容纳于第⼀个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。

总结:
跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。

6.4位段的应用


下图是⽹络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要几个bit位就能描述,这里使用位段,能够实现想要的效果,也节省了空间,这样⽹络传输的数据报大小也会较小一些,对网络的畅通是有帮助的。
![[WPS图片(1).png]]

6.5位段使用的注意事项


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

struct A
{int _a : 2;int _b : 5;int _c : 10;int _d : 30;
};int main()
{struct A a = { 0 };scanf("%d", &a._b);//这是错误的//正确示范int b = 0;scanf("%d", &b);sa._b = b;return 0;
}

7、联合体的声明和创建


联合体和结构体区别
相同点:

  • 由多个成员组成
    不同点:
#include<stdio.h>
//联合体类型的声明
union Un
{char c;int i;
};int main()
{//联合体变量的定义union Un u = { 0 };//计算变量大小printf("%zd\n",sizeof(u));return 0;
}

输出结果:4

7.1联合体的特点


联合体共用一块内存空间,联合体大小是最大成员的大小(因为联合体至少有能力保存最大的那个成员)。

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>union Un
{int a;char b;
};int main()
{union Un u = { 0 };printf("%p\n", &u);printf("%p\n", &u.a);printf("%p\n", &u.b);return 0;
}

输出结果:

0000007549AFF784
0000007549AFF784
0000007549AFF784

7.2联合体的地址变化


我们想进一步查看内存是如何变化的,可以通过调试查看地址变化

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>union Un
{int a;char b;
};
int main()
{union Un u = { 0 };u.a = 0x11223344;u.b = 0x55;return 0;
}

![[Pasted image 20251010093816.png]]
![[Pasted image 20251010093837.png]]

可以用这个图更好看到内存如何变化的
![[bit-2025-10-10-09-42-45.png]]

7.3结构体和联合体内存图


![[bit-2025-10-10-09-46-11.png]]

7.4联合体大小计算


  • 联合体的大小至少是最大成员的大小
  • 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>union Un
{char a;int b;
};int main()
{union Un u = { 0 };printf("%zd\n", sizeof(u));return 0;

输出结果:4

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>union Un
{char a[5];//5个char类型int b;
};int main()
{union Un u = { 0 };printf("%zd\n", sizeof(u));return 0;
}

输出结果:8

union Un
{short a[7];int b;
};int main()
{union Un u = { 0 };printf("%zd\n", sizeof(u));return 0;
}

输出结果:16

举例


使用联合体是可以节省空间的

比如,我们要搞一个活动,要上线一个礼品兑换单,礼品兑换单中有三种商品:图书、杯子、衬衫。每一种商品都有:库存量、价格、商品类型和商品相关的其他信息。

  • 图书:书名、作者、页数
  • 杯子:设计
  • 衬衫:设计、可选颜色、可选尺寸
1>直接用结构体

struct gift_list
{//公共属性int stock_number;//库存量double price; //定价int item_type;//商品类型//特殊属性char title[20];//书名char author[20];//作者int num_pages;//页数char design[30];//设计int colors;//颜⾊int sizes;//尺⼨
};

上面结果虽然设计简单,用起来也方便,但是结构的设计中包含了所有的各种属性,这样使得结构体的大小就会偏大,会浪费内存。因为对于部分商品来说,只有部分属性是常用的。
![[bit-2025-10-10-10-14-33.png]]

比如:

图书就不需要:设计、颜色、尺寸

2>利用联合体

所有就可以把公共属性单独写出来,剩余属于各个商品本身的属性使用联合体,这样就可以介绍所需的内存空间,一定程度上节省了内存。

struct gift_list
{int stock_number;//库存量double price; //定价int item_type;//商品类型union item{struct book{char title[20];//书名char author[20];//作者int num_pages;//页数};struct mug{char design[30];//设计};struct shirt{char design[30];//设计int colors;//颜⾊int sizes;//尺⼨};};
};

![[bit-2025-10-10-11-04-27.png]]

3>练习(判断1是什么端存放)

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>int check()
{union Un{int x;char y;}s;s.x = 1;return s.y;
}int main()
{int a = 1;int ret = check();if (ret == 1)printf("小端存放\n");elseprintf("大端存放\n");return 0;
}

8、枚举类型


8.1枚举类型的声明


枚举的用处:

  1. 一周的星期一到星期日
  2. 性别:男、女、保密
  3. 月份:1-12月
  4. 三原色
    这些数据的表示就可以使用枚举
enum Day
{Mon,Tues,Wed,Thur,Fri,Sat,Sun
};enum Sex
{MALE,FEMALE,SECRET
};enum Color
{RED,GREEN,BLUE
};

以上定义的都是枚举类型,大括号{}中的内容是枚举类型的可能取值,也叫枚举常量。

这些可能取值都是有值的,默认从0开始,依次递增1,当然在声明枚举类型是也可以赋初值。

enum Color//颜⾊
{RED=2,GREEN=4,BLUE=8
};
enum Color//颜⾊
{RED, //0GREEN, //1BLUE //2
};
enum Color//颜⾊
{RED = 3, //3GREEN, //4BLUE //5
};
enum Color//颜⾊
{RED, //0GREEN = 4,//4BLUE //5
};

8.2枚举类型的优点


我们可以使用define定义常量,为什么非要用枚举呢?
枚举的优点有很多:

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

8.3枚举类型的使用


enum Color
{RED = 1,GREEN = 2,BLUE = 4
};enum Color clr = GREEN;//使⽤枚举常量给枚举变量赋值

在C语言中可以那整数给枚举变量赋值,都是在C++中不行,C++的类型比较严格。

http://www.dtcms.com/a/499290.html

相关文章:

  • 用户头像文件存储功能是如何实现的?
  • 网站设计大概在什么价位渠道销售
  • C++竞赛递推算法-斐波那契数列常见题型与例题详解
  • 单元测试-例子
  • 网站顶部素材山西制作网站
  • PHP 高效 JSON 库 JsonMachine
  • 网站建设内部因素百度站长平台有哪些功能
  • Linux内核IPoIB驱动深度解析:在InfiniBand上跑IP网络的高性能之道
  • 275TOPS算力边缘计算盒子的价值洞察与市场定位---视程空间
  • 对话 MoonBit 张宏波:为 AI 重构编程语言
  • QGIS制图专题4:缓冲区分析与服务半径专题图制作
  • IP 资源会枯竭吗?IPv6 能解决代理市场的矛盾吗?
  • 物联网运维中的边缘计算任务调度优化策略
  • TensorFlow2 Python深度学习 - 循环神经网络(LSTM)示例
  • C++第二十三课:猜数字游戏等练习
  • 河南省建设厅网站中州杯企业网站推广怎么做
  • 【数论】最大公因数 (gcd) 与最小公倍数 (lcm)
  • rocky linux MariaDB安装过程
  • git的 Rebase
  • 第8篇 QT联合halcon12在vs2019搭建环境开发图像处理
  • 【小白笔记】最大交换 (Maximum Swap)问题
  • CentOS安装Node.js
  • 深入解析MCP:从基础配置到高级应用指南
  • 佛山网站建设服务wordpress 不能更换主题
  • Process Monitor 学习笔记(5.13):从 0 到 1 的排障剧本清单(可复用模板)
  • Fluent 重叠网格+UDF NACA0012翼型摆动气动仿真
  • 深圳网站建设 设计卓越迈wordpress一键采集文章
  • 理想汽车Java后台开发面试题及参考答案(下)
  • python|if判断语法对比
  • 全链路智能运维中的实时流处理架构与状态管理技术