hello,大家好!我是柚子,今天给大家分享的内容是C语言中的自定义类型结构体、联合体以及枚举,有什么疑问或建议可以在评论区留言,会顺评论区回访哦~
不同于数组的是,结构体中的每个成员可以是不同类型的变量;而数组是一组相同元素的集合。(一)结构的声明
例如:
- //结构体声明
- struct Str
- {
- char name[20];
- char sex[20];
- int num;
- int age;
- };//注意分号不能丢!!!
(二)结构体变量的创建和初始化
- #include
- //结构体声明
- struct Str
- {
- char name[20];
- char sex[20];
- int num;
- int age;
- };//注意分号不能丢!!!
- int main()
- {
- struct Str s = { "丁鸣","女",2024001,18 }; //注意成员的顺序,不要乱!
- printf("name:%s\n", s.name);
- printf("sex:%s\n", s.sex);
- printf("num:%d\n", s.num);
- printf("age:%d\n", s.age);
- return 0;
- }
(三)结构体的特殊声明
先看代码:
- //匿名结构体
- struct
- {
- int a;
- int b;
- int c;
- }s;
- struct
- {
- int a;
- int b;
- int c;
- }a[100],*p;
上述两种结构体在声明的时候省略了结构体标签,那如果将第一个结构体s的地址直接给p,能不能行的通呢??
- //匿名结构体
- struct
- {
- int a;
- int b;
- int c;
- }s;
- struct
- {
- int a;
- int b;
- int c;
- }*p;
- int mian()
- {
- p = &s;//是否合法?
- return 0;
- }
如果你将代码放在VS上你就会发现它会直接报错;第一,编译器会把上边两个声明当成完全不同的两个类型,所以是非法的;第二,匿名结构体类型,如果没有对结构体类型重命名的话,基本上只能用一次。
(四)结构体的自引用
在结构体中包含一个类型为该结构体本身行不行???
- struct Node
- {
- int data;
- struct Node next;
- };
如果可以的话,那么sizeof(struct Node)的大小为多少呢?所以,这么写是错误的。
为什么?
因为一个结构体中再包含一个同类型的结构体变量,这样的话结构体变量的大小就会无穷大,是不合理的。
正确的自引用方式如下:
- //正确的自引用
- struct Node
- {
- int data;
- struct Node* next;//存放节点地址
- };
我们再来看一段代码:
- typedef struct
- {
- int data;
- Node* next;
- }Node;
这样的代码是否可行?
当然是大错特错啦,你这个Node命名还没完成,你就提前开始使用,肯定是错的。
正确的应该这么用:
- //正确的typedef
- typedef struct Node
- {
- int data;
- struct Node* next;
- }Node;
(一)对齐原则:
1.结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数=编译器默认的一个对齐数与该成员变量大小的较小值。
-Vs中默认的值为8
-Linux中gcc没有默认对齐数,对齐数就是成员自身的大小
3.结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍。
4.如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
(二)为什么存在内存对齐?
1.平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数; 否则抛出硬件异常。
2.性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问(为了访问效率更高)。
假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。
总体来说:结构体的内存对齐是拿空间来换取时间的做法。
所以,为了让我们既满足对齐,又满足节省空间,我们要尽可能的让占用空间小的成员集中在一起。
(三) 修改默认对齐数
#pragma 这个预处理指令,可以改变编译器的默认对齐数。
- //#pragma
- #pragma pack(1) //设置默认对齐数1、4、8....
- struct s
- {
- char c1;
- int i;
- char c2;
- };
- #pragma pack() //取消默认对齐数
- int main()
- {
- printf("%d\n", sizeof(s));
- return 0;
- }
(四)结构体传参
- //结构体传参
- struct S
- {
- int data[1000];
- int num;
- };
- //①
- void print1(struct S t)
- {
- printf("%d %d\n", t.data[3], t.num);
- }
- //②
- void print2(const struct S* pt)
- {
- printf("%d %d\n", pt->data[3], pt->num);
- }
- int main()
- {
- struct S s = { {1,2,3,4,5,6},10};
- print1(s);
- print2(&s);
- return 0;
- }
一般我们选择第二种方法print2传参,不管是在空间上还是在性能上都是比较好的选择!!但是为了安全起见还是会加上const。
原因:函数传参的时候,参数时需要压栈的,会有时间和空间上的系统开销。
如果传递一个过大的结构体变量,参数压栈的系统开销较大导致性能下降。
结论:结构体传参的时候,要传结构体的地址。
(五)结构体实现位段
什么是位段?(位表示:二进制位)
位段是基于结构体的;
位段的声明和结构是类似的,有两个不同:
1.位段的成员必须是int、unsigned int 或signed int,在C99中位段成员的类型也可以选择其他类型。
2.位段的成员名后边有一个冒号和一个数字。
- //位段
- struct A
- {
- int a : 2;
- int b : 5;
- int c : 10;
- int d : 30; //一共47个比特位,两个int型
- };
- int main()
- {
- printf("%d\n", sizeof(A)); //8
- return 0;
- }
位段的出现其实就是为了节省空间的。
位段的内存分配:
1.位段的成员可以是int、unsigned int、signed int或者是char等类型
2.位段的空间上是按照需要以4个字节(int)或者1个字节((char)的方式来开辟的。
3.位段涉及很多不确定因素,位段是不可跨平台的,注重可移植性的程序应该避免使用位段。
结构体成员依据结构体变量类型的不同,一般有2种访问方式,一种为直接访问,一种为间接访问,相同的成员名称依靠不同的变量前缀区分。
直接访问应用于普通的结构体变量,直接访问使用结构体变量名.成员名。
间接访问应用于指向结构体变量的指针,间接访问使用(*结构体指针名).成员名或者使用结构体指针名->成员名。
- //联合体
- union u
- {
- char c;
- int u;
- };
- int main()
- {
- union u uu;
- printf("%zd\n", sizeof(uu));
- printf("%zd\n", &uu);
- printf("%zd\n", &(uu.c));
- printf("%zd\n", &(uu.u));
-
- return 0;
- }
联合体的成员共用一块空间 ,两个成员不同时使用。
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小。
联合提的大小至少是最大成员的大小。(X)
当最大成员的大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐的整数倍。(√)
- //计算
- union U
- {
- char c[5]; //5
- //按数组中的元素来算:1 8 1
- int a; //4
- //4 8 4
- };
- int main()
- {
- printf("%zd\n", sizeof(union U)); //8
- return 0;
- }
联合体也是存在对齐的。
使用联合体是可以节省空间的,比如,我们要搞一个活动,要上线一个礼品兑换单,礼品兑换单中有三种商品 : 图书、杯子、衬衫。
每一种商品都有:库存量、价格、商品类型和商品类型相关的其他信息。
图书:书名、作者、页数
杯子:设计
衬衫:设计、可选颜色、可选尺寸
- struct gift_1ist
- {
- //库存量、价格、商品类型
- int stock_num;
- 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 color;
- int sizes;
- }shirt;
- }item;
- };
举例二:
- union U
- {
- int n;//4
- struct S
- {
- char c1;
- char c2;
- char c3;
- char c4;
- }s;//4
- };
- int main()
- {
- union U u = { 0 };
- u.n = 0x11223344;
- //拿出每个字节里的内容,巧妙利用联合体成员占用同一个空间
- printf("%x %x %x %x\n", u.s.c1, u.s.c2, u.s.c3, u.s.c4);
- return 0;
- }
枚举就是可以一一例举,把可能的取值一一列举。
- //枚举 enum
- enum Sex
- {
- //性别
- MALE,
- FEMALE,
- SECRET
- };
- int main()
- {
- printf("%d\n", MALE);
- printf("%d\n", FEMALE);
- printf("%d\n", SECRET);
- return 0;
- }
我们可以使用#define定义常量,为什么非要使用枚举?
枚举的优点:
1.增加代码的可读性和可维护性
2.和#define定义的标识符比较枚举有类型检查,更加严谨。
3.便于调试,预处理阶段会删除#define定义的符号
4.使用方便,一次可以定义多个常量
5.枚举常量是遵循作用域规则的,枚举声明在函数内,只能在函数内使用。
今天的分享就先到这里,我们下期不见不散!!!拜拜~