自定义类型:结构体,位段,枚举,联合
目录
结构体
1.结构体的声明
为什么我们要创建一个结构体呢?因为一个结构体内部可以放置很多类型的变量,包括数组,字符串,字符等等,那么怎么去声明一个结构体呢?
我们可以分为两种形式:例如:
struct Student
{
char name[10];
int age;
char sex[10];
}s1;
这样我们就声明了一个结构体,其中s1代表我们用这个结构体创建的变量,当然也可以在主函数内部创建结构体变量,变量类型为struct Student;
其次,我们还可以这样去声明:
typedef struct Student
{
char name[10];
int age;
char sex[10];
}Stu;
这种类型的声明用到了typedef重命名,这样如果我们在主函数内部去创建结构体变量,就可以直接使用我们这里重命名类型Stu;
2.结构体的特殊声明
在我们声明的结构体的时候可以不给这个结构体命名,就像这样:
struct
{
char name[10];
int age;
char sex[10];
};
此时,我们把这种省略了标签的结构体称为匿名结构体,注意!这样的结构体如果在函数内部使用那么只能使用一次,使用完后便会将占用的内存还给操作系统,结构体会销毁,因此,在我们写程序的时候应该避免使用这种匿名结构体;
3.结构体的自引用
当我们想把一个结构体放到一个结构体内部,然后来进行连锁访问时就叫结构体的自引用,但让我们仔细想想,如果一个结构体中嵌套了很多很多的结构体,这个结构体的大小我们是无法预测的,占用的空间也会很大,
struct Node
{
int data;
struct Node next;
};
因此,像这样自引用是错误的,正确的自引用方式为使用结构体类型的指针来对结构体进行连锁访问,自引用,如下:
struct Node
{
int data;
struct Node* next;
};
4.结构体变量的定义和初始化
我们了解了结构体类型和声明过后,对结构体变量进行定义和初始化很简单:
1.
struct Point
{
int x;
int y;
}p1;
//声明类型的同时定义变量p1
2.
struct Point p2; //定义结构体变量p2
3.
struct Point p3 = {x, y};//初始化:定义变量的同时赋初值。
4.
struct Stu //类型声明
{
char name[15];//名字
int age; //年龄
};
struct Stu s = {"zhangsan", 20};//初始化
5.
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化
5.结构体内存对齐
这里我们引入一个新的概念,结构体内存对齐:为什么要有这个东西呢?让我们先来了解他的用法:当我们想要去计算一个结构体内存大小时,就必须要用到内存对齐这一规则,让我们先来看两个结构体大小计算例子:
int main()
{
struct S1
{
char c1;
int i;
char c2;
}s1;
printf("%zd\n", sizeof(struct S1));
struct S2
{
char c1;
char c2;
int i;
}s2;
printf("%zd\n", sizeof(struct S2));
return 0;
}
下面是代码的结果,为什么两个结构体内部的元素大小和类型相同,大小却不一样呢?
这都是因为结构体的内存对齐规则:
1. 第一个成员在与结构体变量偏移量为 0 的地址处。 2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 对齐数 = 编译器默认的一个对齐数 与 该成员大小的 较小值 。 VS 中默认的值为 8 3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。 4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
在了解了结构体内存对齐规则之后,再让我们回头看上面两段代码: 1.首先,第一个成员是从结构体变量偏移量为0的地址处开始存放的,char类型为1个字节,int类型为4个字节,再来看每个成员的对齐数,默认对齐数为8,1小于8,char的对齐数为1,同理int的对齐数为4,那么int类型的成员 只能放到地址为4的倍数的地方,而char可以随意存放,因为任意数都是1的倍数,让我们来看看结构体在内存的存放图解: 如图:绿色代表char,红色代表int,你可能会认为最后一个char放到8的地址处就结束了,但别忘了结构体的大小必须是最大对齐数的整数倍,这个结构体中最大对齐数是4,如果在8地址处,占用的空间是9,不是4的倍数,那么 会继续占用空间,直到11处,这时占用12个空间才满足要求,黄色代表的是浪费的空间。 2.有了第一个结构体计算的实例,让我们直接看看第二个存放图解: 如图:绿色是char,红色是int,黄色则是代表浪费的空间,这种情况共占用8个空间,恰好是4的倍数,那么不再向下浪费空间,大小就是8。 然后再让我们了解一下为什么存在结构体内存对齐这种规则: 1. 平台原因 ( 移植原因 ) : 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特 定类型的数据,否则抛出硬件异常。 2. 性能原因 : 数据结构 ( 尤其是栈 ) 应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访 问。 总结:结构体内存对齐就是一种用空间来换取时间的做法。
6.修改默认对齐数
当我们不想使用编译器的默认对齐数,而是想要自己设定一个对齐数该怎么办呢?
我们可以使用#pragma()来自定义对齐数:
#pragma pack(8)//设置对齐数为8
struct Student
{
char name[10];
int age;
char sex[10];
}s1;
#pragma pack()//将对齐数设为默认值
#pragma pack(1)//设置对齐数为1
typedef struct Student
{
char name[10];
int age;
char sex[10];
}Stu;
#pragma pack()//将对齐数设为默认值
7.结构体传参
直接上代码:
typedef struct Student
{
char name[10];
int age;
char sex[10];
}Stu;//这里我们重命名结构体Student
void test1(Stu s1)//这里我们直接使用形参对结构体访问
{
printf("%s\n%d\n%s", s1.name, s1.age, s1.sex);
}
void test2(Stu* s2)//也可以使用指针进行访问
{
printf("%s\n%d\n%s", s2->name, s2->age, s2->sex);
}
int main()
{
Stu s2 = { "lisi",18,"nan" };
test1(s2);
test2(&s2);
return 0;
}
值得注意的是:我们在编写程序的时候尽量使用指针,因为形参是实参的一份临时拷贝,在我们创建形参的时候会开辟一块与实参一样大的空间,如果结构体很大,那么就会占用很多的空间,使代码的效率降低,因此,应该尽量多使用指针。
位段
1.什么是位段?
位段其实和结构体是类似的,但是存在两个区别:
1.位段的成员必须是int类型的,例如:int, unsigned int, signed int,char
2.位段的成员后面必须有一个冒号和一个数字
例如:
struct number
{
int a : 1;
int b : 2;
int c : 3;
int d : 4;
}s1;
冒号加数字的意思代表为这个变量开辟多少空间,单位比特位;
2.位段的内存分配
位段的内存分配和空间开辟到底是怎么样的?举个例子:
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
printf("%d", sizeof(struct S));
return 0;
}
这段代码结果是多少呢?如下:
为什么是3呢?那是因为位段在开辟空间时一次开辟一个char(一个字节)的空间或是一个int(四个字节)的空间,当剩下的比特位不足以放下下一个成员时,就会再开辟一个字节或是四个字节,
我们假设上面的例子以char为单位来开辟空间,图解如下:
那么至少要开辟三次也就是三个字节才可以放下位段中的这些成员,看起来位段相比结构体而言节省了不少空间,但位段也存在很多问题:
3.位段的跨平台问题
1. int 位段被当成有符号数还是无符号数是不确定的。 2. 位段中最大位的数目不能确定。( 16 位机器最大 16 , 32 位机器最大 32 ,写成 27 ,在 16 位机 器会出问题。 3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。 4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是 舍弃剩余的位还是利用,这是不确定的,上面的例子我们只是假设舍弃剩余空位,并不能代表实际情况! 总结: 跟结构相比,位段可以达到同样的效果但是有跨平台的问题存在。
枚举
说到枚举,顾名思义,我们想到的就是把可能的取值分别一一列举出来,没错,在C语言中,枚举也是这么使用的。
1.枚举类型的定义
我们列出以下例子,直接上代码:
enum Day//星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};
enum Color//颜色
{
RED,
GREEN,
BLUE
};
enum Month//月份
{
Jenuary,
Fabuary,
March,
April,
May,
June,
July,
August,
September,
Octorber,
November,
December
};
其中,enum XXX都是我们定义的枚举类型,而大括号中的内容都是枚举类型的可能取值,也叫枚举常量。
2.枚举常量的取值问题
以上我们列出的枚举常量都是默认有值的,默认从第一个成员为0开始,依次递增,例如:
当然,我们也可以自己给这些枚举常量赋值,例如:
当我们给tue赋值2时,第一个成员mon还是默认为0,但是tue后面的成员会由tue的赋值依次递增。
3.枚举的优点
为什么我们要使用枚举呢?
原因有如下几点:
1. 增加代码的可读性和可维护性 2. 和 #define 定义的标识符比较枚举有类型检查,更加严谨。 3. 防止了命名污染(封装) 4. 便于调试 5. 使用方便,一次可以定义多个常量
4.枚举的使用
enum Color//颜色
{
RED=1,
GREEN=2,
BLUE=4
};
enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
clr = 5; //ok??
如果我们直接对枚举常量赋值时,即便结果可能是正确的,但事实上存在问题,因为两边的类型并不匹配,左边是枚举变量,右边是整形,因此我们在赋值时只能拿枚举常量给枚举变量赋值,才不会出现类型的差异!
联合(共用体)
1.联合类型的定义
联合也是一种特殊的自定义类型,但与前面两种不同的是,在联合体内部,成员公用一块空间,
举例:
//联合类型的声明
union Un
{
char c;
int i;
};
//联合变量的定义
union Un un;
那么如果我们去计算这个联合体的大小,结果是多少呢?请往下看:
2.联合的特点
联合的成员共用一块内存空间,那么联合变量的大小,至少是联合体内部最大的成员大小,
union Un
{
int i;
char c;
};
union Un un;
int main()
{
// 下面输出的结果是一样的吗?
printf("%p\n", &(un.i));
printf("%p\n", &(un.c));
//下面输出的结果是什么?
un.i = 0x11223344;
un.c = 0x55;
printf("%x\n", un.i);
return 0;
}
哈哈,上面的结果就说明联合体内部,char 和 int的首元素访问地址是相同的,验证了联合体成员公用一块内存的结论,那下面为什么会有11223355呢?我们知道在小端机器上数据存储是从低地址处放到高地址处的,那么0x11223344在内存中就是44332211,同理0x55就是55000000,又因为他们共用一块内存,44就被c改成了55,内存中为55332211,打印出来就是11223355啦~
3.联合体大小的计算
联合变量的大小,至少是联合体内部最大的成员大小,那么最大呢?这点就与结构体相同了,如果联合体的大小不是最大对齐数的整数倍,就要对齐到最大对齐数的整数倍的地址处,剩下的空间浪费掉,这样才是真实大小。
union Un1
{
char c[5];
int i;
};
union Un2
{
short c[7];
int i;
};
//下面输出的结果是什么?
int main()
{
printf("%d\n", sizeof(union Un1));
printf("%d\n", sizeof(union Un2));
return 0;
}
两个例子,第一个是8,第二个是16,图解如下:
本篇文章就到这里了,如果觉得对你有帮助的话,别忘了点点关注多多支持作者~
标签:struct,自定义,int,枚举,char,那点,对齐,事儿,结构 From: https://blog.csdn.net/Dai_renwen/article/details/143595545