本文主要围绕,结构体,枚举,联合这三种日常代码编写过程中最常见的自定义类型进行叙述和详解。
目录
一、结构体
1.1结构体的创建与声明
通过结构体我们可以联想到数组,数组是一组相同类型元素的集合,而结构体可以包含不同类型的成员变量。我们可以自定义结构体的声明,比如学生是student我们可以自定义为stu,而大括号中所包含的成员就叫做成员列表,而大括号之后的variable-list就是变量列表,可以用来创建结构体变量。
例如用结构体来描述一个学生:
如果想要在创建结构体时就顺便创建几个相关的结构体变量,我们就可以在大括号后面创建变量列表。如下图,s1,s2,s3就是struct stu的三个变量。
1.2特殊结构体类型
在声明结构体时候,可以不完全声明。这种结构体类型就是匿名结构体类型。
当创建一个匿名结构体类型时,我们就无法在进行例如对main函数进行编写时来为其创建变量,只能在创建匿名结构体类型时直接进行变量创建,或者是直接在匿名结构体类型后面创建一个结构体指针指向它,比如上图的*p。注意,虽然上面创建了两个一样的结构体类型,但在编译器看来,这是两个不同的匿名结构体类型。如果我们在写代码时将&x也就是x的地址赋给*p时,编译器会进行报错处理:
不兼容就代表着它们不是一个类型,就好比将char*类型赋给int*,两种完全不同的类型,编译器肯定是会报错的。
在这里也只是简单介绍匿名结构体类型,在日常代码编写中,匿名结构体类型的使用也是较少的,没有必要,尽量不使用。
1.3结构体的自引用
接触过数据结构的应该都知道,数据结构描述的是数据在内存中的组织结构,有一种叫线性数据结构,线性数据结构分为顺序表和链表,顺序表是指在内存中开辟一块连续的空间进行存储。而链表则是一串数据在内存中的存放并不连续,但可以通过前一个数据找到下一个数据的具体位置,而结构体想要进行自引用找到下一个节点就需要采用这种链表的结构。
如上所示,如果想要通过类似于链表的形式来找到下一个节点,那么结构体内部组成就要分为两部分,即数据域与指针域,数据域用来存放数据,指针域用来链接下一个结构体变量的地址。
而相比于上面,下面这种编写方式就是错误的,在日常代码编写中也是尤其需要注意的
如果将第二个指针域直接写成结构体的变量,那么其是否可行呢?其实仔细观察就可以发现,其中是存在着很大的不合理性的,当struct node在内存中开辟空间时,int占了4个字节,往后进入struct node next,然后再开辟4个字节,再进入...是不是发现函数好像进入了类似于无限递归的形式,当我们用sizeof去计算这个结构体的大小时,我们会发现,它的大小是无法计算的。
1.4结构体变量的定义和初始化
与其他类型一样,也可以通过直接在结构体后面加变量名称来进行变量的定义:
而对结构体成员进行赋值时,可以采用默认顺序或者通过 . 操作符来进行赋值操作。
当然结构体变量中也可以包含结构体类型(需要用大括号将其括起来):
以下就是几种结构体变量定义和初始化的方式总结:
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
//初始化:定义变量的同时赋初值。
struct Point p3 = {x, y};
struct Stu //类型声明
{
char name[15];//名字
int age; //年龄
};
struct Stu s = {"zhangsan", 20};//初始化
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化
1.5结构体内存对齐
标准规定:
结构体内存对齐,直接来讲就是计算结构体大小。观察以下这一段代码:
按照正常的方式来计算的话,int占4个字节,char占1个字节,站在一般角度来看,s1和s2应该都是6个字节才对,但当程序运行起来,后我们却得到了两个截然不同的结果:
而这种结果正是由结构体内存对其方式决定的。我们可以通过引用#include <stddef.h>来使用offsetof,offsetof可以查找结构体成员与起始位置的偏移量。(注意offsetof是一个宏,不是函数)
它的第一个参数是type(类型)第二个参数是member(成员)。
拿刚刚的s1距离,我们可以看到三个变量c1 i c2 的偏移量分别是0 4 8,根据偏移量我们大概可以得出结构体类型在内存中的存储方式如下:
如果仅仅根据偏移量来看,应该也只有九个字节,可是刚刚打印出s1的大小却是12,所以在8的下面还有三个字节。
通过上面现象分析,我们发现结构成员不是按照顺序在内存中连续存放的,而是有一定对其规则的。
通过查看上面的标准规定,所以s1在存储时,c1存放在0的位置char占一个字节,和默认对齐数8比,1小所以放在0的位置占一个字节。i作为int型占4个字节,和默认对齐数8比4小,所以找4的倍数,所以从4开始往后存4个字节。c2作为char占一个字节任何一个整数都是1的倍数,所以直接放在8的位置,而因为结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍,s1中最大对齐数为4,而现在一共才9个字节,不是4的倍数,所以要凑够4的倍数,而往后最小的4的倍数就是12,所以再往后开辟3个字节。(注意:开辟时看倍数的是下标,最后根据最大对齐数来看倍数决定总大小时看的是此时内存所占空间)。
同样的s2第一个成员类型是int,所以从0开始往下开辟四个字节,c1占一个放在4的位置,c2放在5的位置,此时总共占了6个字节,此时最大对齐数是4,往后最小的4的倍数就是8,所以此时只需要8个字节就可以存放s2,所以再往后开辟两个字节的空间到7,此时刚好8个字节。
不同的情况,结构体开辟空间的大小就不一样,如果有double类型那最大对齐数和最终大小都会不一样:
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。如下图:
为什么会存在内存对齐?
定类型的数据,否则抛出硬件异常。
2. 性能原因:
1.7修改默认对齐数
#include <stdio.h>
#pragma pack(8)//设置默认对齐数为8
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)//设置默认对齐数为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));
return 0;
}
结构在对齐方式不合适的时候,我们可以自己更改默认对齐数。 例:当用pargma将对齐数设置成1时,就意味着没有对齐了。
1.8结构体传参
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;
}
二、位段
2.1位段
位段的声明和结构是类似的,有两个不同:
举例:
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
此时,运行程序,得到的结果是8个字节。
2.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;
//空间是如何开辟的?
所以先开辟1个字节即8个bit'位来存放,a有3个bit位的空间,10的二进制表示形式位01010,存三位进去存的就是010。b为12,二进制位为01100,存4位进去就是1100。
因为先使用高地址再使用低地址,此时开辟的第一个字节的8个bit位已经用了7个还有1个已经不够存放之后的数据了,所以此时需要再开辟一个字节,来存放后面的数据。
c为3,二进制位为011,有5个bit位的空间,不够的就用0补齐,所以存放进去就是00011,此时存放d,又不够了就再开辟一个字节,b的二进制位为00100,存放4个进去就是0100。
而数据在进行存放时,因为大多数电脑的存储方式都是小端存储,所以数据的低位就存放在低地址处,高位存放在高地址处,a最先创建是struct S的低位所以先在低地址处为其开辟1个字节即8个bit位的空间用来存储,而将a存放进这8个bit时,数据在使用内存时,先使用高位,再使用低位,然后将a放在这8个字节的高位,随后存放b,此时虽然ab所在空间还有1个字节,但是因为其处在低位,而后面开辟的空间则是在整个ab所在字节的更高位,所以这1个字节就无法进行使用了,然后不够后按照此步骤继续开辟空间进行存放(中间环节如有不懂可参考数据在内存中的存储(一))因为在查看内存时一般转换成16进制数字(每4个二进制位可转换成1个16进制位),所以最终struct S在内存中的布局大概就是这样:
我们在编译器中进行内存查看:
和我们推演出来的结果一样。
此测试以VS平台进行测试,综合测试数据如下:
2.3位段的跨平台问题
三、枚举
3.1枚举类型的定义
enum Day//星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};
enum Color//颜色
{
RED,
GREEN,
BLUE
};
enum Color//颜色
{
RED=1,
GREEN=2,
BLUE=4
};
如果只给其中一个赋值,那么前面的值依然是默认的值,后面的常量会在此基础上继续递增,例如:
enum Color//颜色
{
RED,
GREEN=5,
BLUE
};
那么RED就是0,而BLUE就是6。
3.2枚举的优点
在对常量进行定义时,我们往往第一个想到的是使用#define来定义常量,为什么非要使用枚举。
3.3枚举的使用
enum Color//颜色
{
RED=1,
GREEN=2,
BLUE=4
};
enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
clr = 5; //ok??
四、联合(共用体)
4.1联合类型的定义
//联合类型的声明
union Un
{
char c;
int i;
};
//联合变量的定义
union Un un;
//计算连个变量的大小
printf("%d\n", sizeof(un));
union Un
{
int i;
char c;
};
union Un un;
// 下面输出的结果是一样的吗?
printf("%d\n", &(un.i));
printf("%d\n", &(un.c));
程序运行之后,可以看到,显示的地址是一样的,由此可见,联合的成员是共用同一块内存空间的。
4.3联合体大小的计算
union Un1
{
char c[5];//5个字节
int i;//4个字节
};
//下面输出的结果是什么?
printf("%d\n", sizeof(union Un1));
虽然,char[5]所占的内存是最大的,是五个字节,char类型最大对齐数是1,int类型最大对齐数是4,而char[5]作为最大成员占5个字节,但整个联合的最大对齐数是4,5不是4的整数倍所以要对齐到最大整数倍,往后4最小的倍数就是8。所以最后结果应该是8。
本章内容就到此结束了,每一篇文章都是博主的精心打磨,耐心编排。更多好文关注博CSDN。一键三连不迷路。