【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;
}
打印结果是一样的:
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;
- 编译器会把上面的两个声明当成完全不同的两个类型,使用是非法的。
- 匿名的结构体类型,如果没有对结构体类型重命名的华为,基本上只能使用一次。
5、结构的自引用
当我们要通过1找到2,3,4时该怎么去做?
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字节(固定)
二、结构体的缩写
我们在数据结构的书上常常看到这么写结构体
其实很好理解,typedef就是重命名(简化代码),将写法复杂的struct Node改成了Node。
在结构体自引用的过程中,夹杂了typedef对匿名结构体类型重命名,也容易引入问题,如下列代码
typedef struct
{int data;Node* next;
}Node;
这是错误的,因为Node时对前面的匿名结构体类型的重命名产生的,但是在匿名结构体内部提前使用Node类型来创建成员变量,这是不行的。正确的写法如下:
typedef struct Node
{int data;struct Node* next;
}Node;
三、结构体内存对齐
前面讲了结构体的基本使用。接下来就是结构体内存对齐,这对于计算结构体的大小有着紧密关系。
6、对齐规则
首先得掌握结构体我得对齐规则:
- 结构体得第一个成员会放到结构体变量起始位置,也就是偏移量为0得地址处。
- 其他成员对齐到整数倍的地址处,对齐数=编译器默认对齐数与该成员变量大小的较小值
- VS中对齐数默认是8
- Linux中gcc没有默认对齐数,成员大小就是对齐数
- 结构体总大小 = 最大对齐数的整数倍
- 如果嵌套结构体,按照结构体与结构体变量所在的最大对齐数的整数倍处。将结构体所占的大小放入结构体变量中
接下来用几个示例来讲这4点是如何用的
6.1示例1
6.2示例2
6.3示例3
6.4示例4
四、为什么存在内存对齐?
⼤部分的参考资料都是这样说的:
- 平台原因 (移植原因):
- 不是所有的硬件平台都能访问任意地址上的任意数据的
- 某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
- 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于
- 为了访问未对齐的内存,处理器需要作两次内存访问
- 而对齐的内存访问仅需要⼀次访问
- 假设⼀个处理器总是从内存中取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;
}
第一个和第二个打印的结果相同
那它们分别有什么优缺点呢?
- 在结构体无指针传参时,需要创建栈区,会有空间和时间上的浪费。
- 结构体指针传参时,地址的大小无非4/8个字节,即节省空间,又节省时间。
能用指针传参时,就尽量用指针传参。
六、结构体位段
6.1什么是位段
- 位段的成员必须是int、unsigned、signed int,在C99中位段成员也可以是其他类型。
- 位段的成员名后边有个冒号和一个数字。
struct s
{int a:2;int a:5;int a:10;
}
6.2位段的内存分配
- 和内存对齐一样,可以用来节省内存
- 位段的成员可以是int,unsigned int,signed int,char等类型
- 位段的空间是按照4个字节(int)或1个字节(char)开辟的
- 标准C语言中,位段有很多不确定因素,要注意位段是不可以跨平台的使用
6.3位段的跨平台问题
- int 位段被当成有符号数还是无符号数是不确定的。
- 位段中最⼤位的数⽬不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
- 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。
- 当⼀个结构包含两个位段,第⼆个位段成员比较⼤,无法容纳于第⼀个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
总结:
跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。
6.4位段的应用
下图是⽹络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要几个bit位就能描述,这里使用位段,能够实现想要的效果,也节省了空间,这样⽹络传输的数据报大小也会较小一些,对网络的畅通是有帮助的。
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;
}
可以用这个图更好看到内存如何变化的
7.3结构体和联合体内存图
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;//尺⼨
};
上面结果虽然设计简单,用起来也方便,但是结构的设计中包含了所有的各种属性,这样使得结构体的大小就会偏大,会浪费内存。因为对于部分商品来说,只有部分属性是常用的。
比如:
图书就不需要:设计、颜色、尺寸
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;//尺⼨};};
};
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-12月
- 三原色
这些数据的表示就可以使用枚举
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定义常量,为什么非要用枚举呢?
枚举的优点有很多:
- 增加代码的可读性和可维护性
- 和define定义的标识符比较枚举有类型检查,更加严谨
- 便于调试,预处理阶段会删除define定义的符号
- 枚举常量是遵循作用域规则的,枚举声明在函数内,只能在函数内使用
8.3枚举类型的使用
enum Color
{RED = 1,GREEN = 2,BLUE = 4
};enum Color clr = GREEN;//使⽤枚举常量给枚举变量赋值
在C语言中可以那整数给枚举变量赋值,都是在C++中不行,C++的类型比较严格。