??前面的话??
大家好!在C语言中,有个叫“自定义类型”玩意,它究竟是什么呢?其实,就是字面意思,可以自己定义的类型就是自定义类型。具体说就是我们熟知的结构体,枚举,位段,联合体(共用体)。
👋Hi~ o( ̄▽ ̄)ブ这里是猪猪程序员 👀 很高兴见到你O(∩_∩)O! 🌱 现在正在发芽中… 🎉欢迎关注🔎点赞👍收藏??留言📝 📌本文由猪猪原创,CSDN首发!📆首发时间:🌼2021年10月1日🌼 💞? 博主水平有限,如果发现错误,一定要及时告知作者哦 o( ̄︶ ̄)o!感谢感谢! 📫博主的码云 gitee,平常博主写的程序代码都在里面。
🌱 1.结构体
🍀🍀1.1结构体概述
🌼🌼🌼1.1.1结构体概念
结构体(struct )是由一系列具有相同类型或不同类型的数据构成的数据集合,也叫结构,它就将不同类型的数据存放在一起,作为一个整体进行处理。
🌼🌼🌼1.1.2 结构体的声明与使用
🌼结构体的声明:
struct Book
{
char name[20];
char author[20];
int price;
};
这个声明描述了一个由两个字符数组和一个int变量组成的结构体。
它将这些变量封装成一个整体,代表了一本书(含有书名,作者名,价格)。
但是注意,它并没有创建一个实际的数据对象,而是描述了一个组成这类对象的元素。
因此,我们有时候也将结构体声明叫做模板,因为它勾勒出数据该如何存储,并没有实例化数据对象。
🌼结构体的使用(一共3种创建方法)
- 用结构体创建全局变量
struct Book
{
char name[20];
char author[20];
int price;
}b1,b2;
struct Book b1;
这里创建的b1,b2,b3 是完全等价的,都是全局变量。
- 用结构体创建局部变量
struct Book
{
char name[20];
char author[20];
int price;
};
int main()
{
struct Book b4;
return 0;
}
在main函数中创建的结构体变量就是局部变量
🌼结构体变量的定义和初始化
🌱1. 初始化:定义变量的同时赋初值。 🌱2. 结构体的初始化要使用大括号。
struct Stu
{
char name[15];
int age;
};
struct Stu s = {"zhangsan", 20};
🌱3. 结构体嵌套初始化:
在结构体中又包含了一个结构体
struct Point
{
int x;
int y;
}p1 = { 1,2 }, p2 = {3,4};
struct Point p3 = { 5,6 };
struct Node
{
int data;
struct Point p;
struct Node* next;
char name[20];
}n1 = {10, {4,5}, NULL};
struct Node n2 = {20, {5, 6}, NULL, "zhangsan"};
🌱4. 对于嵌套结构体的访问
struct Point
{
int x;
int y;
}p1 = { 1,2 }, p2 = { 3,4 };
struct Point p3 = { 5,6 };
struct S
{
int data;
struct Point p;
char name[20];
}n1 = { 10, {4,5}, };
struct S n2 = { 20, {5, 6}, "zhangsan" };
int main()
{
struct Point p4 = {1,2};
struct S s = { 20,{7,8} };
printf("%d", s.data);
printf("%d %d", s.p.x, s.p.y);
return 0;
}
🌱5. 对于结构体变量中的整型数组如何循环打印:
struct S
{
int a[10];
}n1 = { {1,2,3} };
int main()
{
int i = 0;
struct S s = { {7,8,9} };
for (i = 0; i < 10; i++)
{
printf("%d ", s.a[i]);
}
return 0;
}
🌼🌼🌼1.1.3匿名结构体的声明及使用
struct
{
int a;
char c;
double b;
}s1,s2;
匿名结构体可以无结构体的名字,要创建变量的时候只能在大括号的后面直接创建s1,s2 。 匿名结构体只能使用一次。
struct
{
int a;
char c;
double b;
}s1,s2;
struct
{
int a;
char c;
double b;
}*ps;
int main()
{
ps = &s1;
return 0;
}
以上创建了两个匿名的结构体类型,但编译器会认为他们是不同的,因此第二个结构体创建的匿名结构体指针无法指向第一个匿名结构体。
非法赋值使编译器报错。
🌼🌼🌼1.1.4 结构体的自引用
链表就如同车链子一样,head指向第一个元素:第一个元素又指向第二个元素;……,直到最后一个元素,该元素不再指向其它元素,它称为“表尾”,而链表的实现就需要用到结构体的自引用。
struct Node
{
int data;
struct Node* n;
};
上述创建了一个链表,data是数据域,n为指针域。 结构体自引用:能够找到通过地址找到自己同类型的下一个结点。
🌼🌼练习一:
struct Node
{
int data;
struct Node next;
};
如果可以,那sizeof(struct Node)是多少?
这样不行,会造成死循环,因为无法确定结构体的大小
🌼🌼练习二:
typedef struct
{
int data;
Node* next;
}Node;
分析如下:定义了一个匿名结构体,并且用typedef 将这个匿名结构体重命名为Node ,但Node的产生必须要先在内存中创建一个int型,和Node指针大小的内存空间,但是此时还未重命名,所有是错误的
正确写法:
typedef struct Node
{
int data;
struct Node* next;
}Node;
🍀🍀 1.2结构体对齐及其大小计算
🌼🌼🌼1.2.1偏移量
定义:把存储单元的实际地址与其所在段的段地址之间的距离称为段内偏移,也称为"有效地址或偏移量"。
先简单举个例子来说:
创建的s1 和s2 结构体变量的内存大小是多少呢:
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
int main()
{
struct S1 s1 = { 'x',100,'y'};
struct S2 s2 = { 'x','y'100};
printf("%d", sizeof(struct S1));
printf("%d", sizeof(struct S2));
return 0;
}
结果发现两者的内存大小不同,原因是因为涉及了内存对齐。
🌱内存对齐的规则
- 结构体第一个成员永远放在结构体变量偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 VS中默认的值为8 - 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
🌱为什么存在内存对齐?
-
平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特 定类型的数据,否则抛出硬件异常。 -
性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次 内存访问;而对齐的内存访问仅需要一次 访 问。 -
总体来说: 结构体的内存对齐是拿空间来换取时间的做法。
🌼🌼🌼1.2.2结构体大小计算
🌱对于S1来说:
struct S1
{
char c1;
int i;
char c2;
};
3个变量最大的占4个字节,而VS的默认对齐数是8,因此该结构体的默认对齐数是4 🌱对于S1来说:
struct S2
{
char c1;
char c2;
int i;
};
🌱练习一:
struct S3
{
double d;
char c;
int i;
};
printf("%d\n", sizeof(struct S3));
分析: 因此一共16个字节
🌼🌼🌼1.2.3修改默认对齐数
我们使用#pragma 修改默认对齐数
#include <stdio.h>
#pragma pack(8)
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()
#pragma pack(1)
struct S2
{
char c1;
int i;
char c2;
};
#pragma pack()
int main()
{
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
分析: 此时因为默认对齐数被设置成1,最小值,因此所有的对齐数都为1,相当于取消内存对齐,无空间浪费。 🌱百度笔试题: 写一个宏,计算结构体中某变量相对于首地址的偏移,并给出说明 考察: offsetof 宏的实现 在 <stddef.h> 中定义了个 offsetof(s,m) 宏,这个宏用来取得结构体中元素的偏移量很方便,下面是此宏的具体定义:
#define offsetof(s, m) (size_t)&(((s *)0)->m)
offsetof(s, m) 其中,s 是结构体名,m 是它的一个成员。s 和 m 同是宏 offsetof() 的形参,这个宏返回的是结构体 s 的成员 m 在结构体中的偏移地址。
(s *)0 : 这里的用法实际上是欺骗了编译器,使编译器认为 “0” 就是一个指向 s 结构体的指针(地址),即 s 结构体就是位于 0x0 这个地址处。
(s *)0-> m :指向这个结构体的 m 元素。
&((s *)0)->m : 表示 m 元素的地址。这里,如上面所说,因为编译器认为结构体 s 被认为是处于 0x0 地址处,所以 m 的地址自然的就是 m 在 s 中的偏移地址了。
最后将这个偏移值转化为 size_t 类型。
#include<stdio.h>
#include<stddef.h>
#define offsetof(s, m) (size_t)&(((s *)0)->m)
struct S
{
char c1;
int a;
char c2;
};
int main()
{
printf("%u\n", offsetof(struct S, c1));
printf("%u\n", offsetof(struct S, a));
printf("%u\n", offsetof(struct S, c2));
return 0;
}
🌼🌼🌼1.2.4结构体传参
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函数。 - 原因:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能 的下降。 - 结论:
结构体传参的时候,要传结构体的地址。
🍀🍀 1.3结构体与位段
🌼🌼🌼1.3.1位段
位段的声明和结构是类似的,有两个不同:
- 位段的成员必须是 int、unsigned int 或signed int 。
- 位段的成员名后边有一个冒号和一个数字。
- 位段可以节省空间。
int _a:2; 表示_a只需要2个比特位 比如:下方的A就是一个位段类型。那位段A的大小是多少?
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
printf("%d\n", sizeof(struct A));
🌱分析:
- 位段一次开辟一个整型(4字节==32比特位)
- _a + _b + _c一共用了17个比特位,之后的_c不够用了,于是又开辟了4个字节
- 因此一共8个字节
🌱总结
- 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
- 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
🌼🌼🌼1.3.2位段实现结构体
🌱练习: 以下的空间是如何开辟的?
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;
分析:
- 由于是char位段,因此是一个字节一个字节开辟的
🌱位段的跨平台问题
- int 位段被当成有符号数还是无符号数是不确定的。
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机
器会出问题。 - 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是
舍弃剩余的位还是利用,这是不确定的。
🌱总结: 跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
🌱2.枚举
🍀🍀 2.1枚举概述
🌼🌼🌼2.1.1枚举概念
枚举: 就是一一列举。 枚举常量 :{ }中的内容是枚举类型的可能取值,就叫枚举常量 。 枚举常量都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。
比如我们现实生活中:
- 一周的星期一到星期日是有限的7天,可以一一列举。
- 性别有:男、女、保密,也可以一一列举。
- 月份有12个月,也可以一一列举
🌼🌼🌼2.1.2枚举的声明与使用
enum Color
{
RED,
GREEN,
BLUE
};
int main()
{
printf("%d ", RED);
printf("%d ", GREEN);
printf("%d ", BLUE);
return 0;
}
以上定义的 enum Day , enum Sex , enum Color 都是枚举类型。
🌱亦可以自己对枚举初始化:
enum Color
{
RED=5,
GREEN=8,
BLUE
};
🌱枚举类型的使用
enum Color
{
RED=1,
GREEN=2,
BLUE=4
};
enum Color clr = GREEN;
clr = 5;
🍀🍀 2.2枚举大小计算
枚举变量的大小,即枚举类型所占内存的大小,枚举类型变量都占4字节。
enum A
{
QSW,
BSW,
CWS
}a;
int main()
{
printf("%d\n", sizeof(a));
return 0;
}
🍀🍀 2.3枚举与宏的区别
使用枚举定义的枚举常量是有类型的,为枚举类型,而使用#define 宏是替换,并没有枚举类型这种性质。
🌱 枚举的优点
- 增加代码的可读性和可维护性
- 和#define定义的标识符比较枚举有类型检查,更加严谨。
- 防止了命名污染(封装)
- 便于调试
- 使用方便,一次可以定义多个常量
🌱3.联合体
🍀🍀 3.1联合体概述
🌼🌼🌼3.1.1联合体概念
联合:也是一种特殊的自定义类型 这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。
union Un
{
char c;
int i;
};
union Un un;
🌼🌼🌼3.1.2联合体的声明与使用
union Un
{
char c;
int i;
};
union Un un;
printf("%d\n", sizeof(un));
🌼分析:打印的结果是4个字节
🌼联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小 (因为联合至少得有能力保存最大的那个成员)。
union Un
{
char c;
int i;
};
union Un u;
int main()
{
printf("%p ", &u);
printf("%p ", &(u.c));
printf("%p ", &(u.i));
return 0;
}
🌼🌼🌼3.1.3联合体判断大小端存储
🌱关于大小端: 内存中存储这两个字节有两种方法:
- 小端字节序:将低序字节存储在起始地址。
- 大端字节序:将高序字节存储在起始地址。
🌱方法一:未使用联合体
int main()
{
int a = 1;
char* pc = (char*)&a;
if (*pc == 1)
{
printf("小端");
}
else
printf("大端");
return 0;
}
🌱方法二:使用联合体
int main()
{
union U
{
char c;
int i;
};
u.i = 1;
if (u.c == 1)
{
printf("小端");
}
else
printf("大端");
return 0;
}
🍀🍀 3.2联合体大小计算
- 联合的大小至少是最大成员的大小。
- 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
union Un1
{
char c[5];
int i;
};
union Un2
{
short c[7];
int i;
};
printf("%d\n", sizeof(union Un1));
printf("%d\n", sizeof(union Un2));
🌱分析
- 对于Un1来说,其对齐数是4,而char[5]占据了5个字节,超过了4,因此结果为8
- Un2来说,其对齐数是4,而short[7]占据了14个字节,超过了12,因此结果为16
觉得文章写得不错的老铁们,点赞评论关注走一波!谢谢啦!
|