目录
1.自定义数据类型_结构体
1.1 结构体类型的声明
1.1.1 匿名结构体
1.2 结构体的自引用
1.2.1 Typedef结构体重命名:
1.3 结构体变量的定义和初始化
1.4 结构体内存对齐
1.4.1 结构体的对齐规则
1.4.2 为什么存在内存对齐?
1.4.3 设置默认对齐数
1.4.4 offsetof结构体偏移量计算函数
1.5 结构体传参
1.6 结构体实现位段(位段的填充&可移植性)
1.6.1 什么是位段?
1.6.2 位段的内存分配规则
1.6.3 位段存在的意义和具体数值的存放
2. 自定义数据类型_枚举
2.1 枚举类型的定义
2.2 枚举的优点
2.3 枚举的使用
2.4 枚举的大小
3. 自定义数据类型_联合
3.1 联合类型的定义
3.2 联合的特点
3.3 联合大小的计算
C语言内置类型(C语言自己的数据类型):char、short、int、long、float、double;
复杂类型(自定义类型):结构体、枚举、联合体;
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。比方说我们之前接触过的数组:数组找那个存放的必须是相同类型的变量,而结构体的强大也体现在其内部的成员变量可以是不同类型的变量;例如成员变量我可以设置为char[姓名]、int[年龄]等等;
声明一个结构体类型:struct 声明结构体;stu---结构体名字;结构体的成员变量可以是不同的类型;name名字其实就是一个字符串,用char 定义;年龄是一个整型数字,用 int 修饰;
struct stu
{
char name[20];//名字
char tel[10];//电话
char sex[10];//性别
int age;//年龄
};
int main()
{
return 0;
}
int main()
{
struct stu S1;//用结构体类型创建结构体变量S1、S2;
struct stu S2;
return 0;
}
这里需要注意:通过结构体创建的结构变量S1、S2、S3、S4、S5、S6是不同的;其中S1、S2为结构体局部变量,而S3、S4、S5、S6为结构体全局变量;
struct stu
{
char name[20];
char tel[10];
char sex[10];
int age;
}S4,S5,S6;
struct stu S3;
int main()
{
struct stu S1;//用结构体类型创建结构体变量S1、S2;
struct stu S2;
return 0;
}
匿名结构体类型:缺少结构体标签stu,必须在大括号外边定义结构体变量S1、S2、S3;否则缺少结构体名字,无法定义结构体变量;这种结构体称为匿名结构体;
struct
{
char name[20];
char tel[10];
char sex[10];
int age;
}S4,S5,S6;
如数据结构中的链表定义:1 2 3 4 5 ;我在内存中可以任意随机的存放;但是想要定义1之后,能找到2 ,接着能找到3,接着找到4,接着找到5;我需要定义一个数字1,同时在定义数字1的结构体中定义数字2的地址(用指针来指向数据2的地址);依次类推,在数据2中包含数字3的地址……其中:存放数据的地址叫数据域;存放地址的叫指针域;
结构体的自引用通过指针来指向下一个地址;
struct Node //结构体自引用
{
int data;
struct Node * Next;
};
typedef struct Node
{
int data;
struct Node * Next;
}Node;
int main()
{
struct Node N1;//以下两种定义方式均可
Node N2;
return 0;
}
给结构体struct重新命名为:Node;则主函数定义新变量:N1、N2既可以struct Node N1;也可以Node N2;在此建议:即使重命名,也不要把Node省略掉(typedef struct);
定义结构体变量:可以在结构体的大括号外边,分号前面定义结构体变量;也可以直接定义全局变量:struct stu S1;
结构体变量初始化:参照上述,定义一个结构体,定义结构体变量s,然后进行结构体变量初始化,结构体变量初始化需要用到大括号,结构体的成员变量是什么类型,初始化时就需要参照结构体成员变量的类型进行定义;结构体成员变量的访问:用结构体变量 + . 进行访问;
struct S
{
char c;
int a;
double d;
char arr[20];
};
int main()
{
struct S s = { 'c', 100, 3.14, "hello world" };
printf("%c %d %.2lf %s\n", s.c, s.a, s.d, s.arr);
return 0;
}//c 100 3.14 hello world
结构体嵌套结构体类型访问:铭记:结构体初始化用到大括号;什么样的类型对应什么样的打印方式;
struct T
{
double weight;
short age;
};
struct S
{
char c;
struct T st;
int a;
double d;
char arr[20];
};
int main()
{
//struct S s = { 'c', 100, 3.14, "hello world" };
struct S s = { 'c', { 55.6, 30 }, 100, 3.14, "hello world" };
printf("%c %.1lf %d %d %.2lf %s\n", s.c,s.st.weight,s.st.age, s.a, s.d, s.arr);
return 0;
}//c 55.6 30 100 3.14 hello world
这一部分我们来计算结构体变量的所占的字节大小(sizeof(s1));计算结构体的大小涉及到结构体内存对齐的规则;
1. 第一个成员在与结构体变量偏移量为0的地址处存放;
2. 其他的成员变量要对齐到某个数字(对齐数)的整数倍的地址处;
对齐数= 编译器默认的对齐数与该成员大小的较小值;VS中默认的值为8;(只是VS环境下设置的对齐数是8,gcc环境是没有设置默认对齐数的,没有对齐数就是较小值)
3. 结构体的总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍;
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有对齐数(含嵌套结构体的对齐数)的整数倍;
ag. 计算下述两个结构体的大小?输出答案:12 8
struct S1
{
char c1;
int a;
char c2;
};
struct S2
{
char c1;
char c2;
int a;
};
int main()
{
struct S1 s1 = { 0 };
printf("%d\n", sizeof(s1));
struct S2 s2 = { 0 };
printf("%d\n", sizeof(s2));
return 0;
}//12 8
在此:读者可能有很多的疑问,为什么只是改变了结构体成员变量的顺序,结构体的大小就会发生如此大的改变?原因要追溯到结构体的对齐规则。(其中char占一个字节,int占4个字节,double占16个字节)
首先:第一个成员要在与结构体变量偏移为0的地址处存放;计算struct S1时,第一个结构体变量为char c1,char占用一个字节,存放到左图所示第一个红色地址处;其他成员变量需要对齐到对齐数(编译器默认的对齐数和成员大小的较小值)的整数倍处;VS默认的对齐数为8;存放第二个成员变量int a时,int占用4个字节;对齐数为 4和VS默认的对齐数8 的最小值,也就是4,所以第二个成员变量int 存放到4的整数倍地址处,取4,存放到左图所示紫色地址处,int占用四个字节,所以依次向下占用四个内存;然后存放第三个成员变量char c2,char 所占字节个数为1,对齐数为1和VS默认的对齐数8的最小值,也就是1;所以char c2存放到左图的绿色地址处;至此,struct S1存放完毕,占用9个字节;但是结构体对齐规则定义:结构体的总大小为最大对齐数的整数倍;简单来说就是,S1中三个成员变量对应的对齐数分别为 1/8 4/8 1/8 中的最大值,也就是4,所以结构体的总大小为4的整数倍,但是至少也要保证存放9个字节,所以为3*4=12;也就是第一个输出结果12;
仿照上述struct S1计算结构体大小的方式来计算struct S2的大小:首先第一个成员变量char s1存放到与结构体偏移量为0的地址处,存放到右图的红色地址处;第二个成员变量char s2存放到对齐数1/8的最小值处,也就是1,即右图的黑色地址处;第三个成员变量int 占用四个字节,需要存放到4/8的最小值处,也就是4,从偏移量为0的地址处向下数4的地址来存放int的四个字节,即右图的绿色地址处;至此,struct S2存放完毕,占用8个字节;总的大小为最大对齐数的整数倍,最大对齐数为 1/8 1/8 4/8的最大值的整数倍,同时也要保证能够存放的下struct s2的8个字节,所以选取8;也就是第二个输出结果8;
ag. 嵌套结构体的计算规则:输出结果:12 8 16 32
struct S1
{
char c1;
int a;
char c2;
};
struct S2
{
char c1;
char c2;
int a;
};
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
struct S1 s1 = { 0 };
printf("%d\n", sizeof(s1));
struct S2 s2 = { 0 };
printf("%d\n", sizeof(s2));
struct S3 s3 = { 0 };
printf("%d\n", sizeof(s3));
struct S4 s4 = { 0 };
printf("%d\n", sizeof(s4));
return 0;
}//12 8 16 32
首先:struct S3的计算过程和上述s1、s2的计算过程相同,这里不作过多解释;S4中包含S3嵌套,如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有对齐数(含嵌套结构体的对齐数)的整数倍;struct S4中第一个成员变量char c1占一个字节,放在偏移量为0的地址处;嵌套结构S3放在对齐到自己最大对齐数的整数倍,s3中最大对齐数在double类型的8/8,对齐数为8,所以嵌套结构对齐到偏移量为8的地址处,本身嵌套结构占16个字节;char c1(1)+对齐的、保证偏移量为8(7)+嵌套结构本身占用的字节(16)+double占用的字节(8)=32;结构体整体的大小就是所有对齐数(含嵌套结构的对齐数)的整数倍;所有对齐数数中最大的就是8,恰巧32正好是8的整数倍;所以最终嵌套结构s4的结构体大小为32;
1.平台原因:不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某处地址处取某些特定类型的数据,否则抛出硬件异常;
2.性能原因:数据结构(栈)应该尽可能的在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要做两次内存访问,而对齐的内存访问仅仅需要一次即可;可大大节省时间;
总体来说,就是拿空间去换时间;
所以在设计结构体的时候,我们既要满足对齐,又要节省空间;根据上述的例子:struct s1的大小为12,struct s2的大小为8,所以为了最大程度的节省空间,在设计结构体时尽可能的将占空间小的成员放在一起,防止浪费空间;
C语言中可以通过 #pragma pack()进行主动修改对齐数;一般设置为2的次方数(2 4 8 16 ……);
以下程序,如果不使用#pragma pack(),则输出的结果是16;如果加上#pragma pack(),结果会发生改变;在程序之前加上#pragma pack(1)(程序之前的括号里加上x 就表示修改对齐数为x)表示修改默认的对齐数为1;在结构体之后加上#pragma pack()表示恢复修改的对齐数;
#pragma pack(1)
struct S
{
char c1;
double d;
};
#pragma pack(0)
int main()
{
struct S s;
printf("%d\n", sizeof(s));
return 0;
}
函数使用定义:size_t offsetof(structName,memberName)-----------(结构体名称,结构体成员名称);该函数需要引用头文件:#include
struct S
{
char c1;
double d;
int a;
};
int main()
{
printf("%d\n", offsetof(struct S,c1));
printf("%d\n", offsetof(struct S, d));
printf("%d\n", offsetof(struct S, a));
return 0;
}// 0 8 16
定义:Init初始化函数;切记:结构体初始化需要传参为 &结构体变量 ; 用指针来接收;因为定义的是结构体,所以接收的参数为
struct S* pc;指针访问为pc->结构体变量;
struct S
{
char c1;
double d;
int a;
};
Init(struct S* pc)
{
pc->c1 = 'w';
pc->d = 3.14;
pc->a = 30;
}
int main()
{
struct S s1 = { 0 };
Init(&s1);
printf("%c\n", s1.c1);
printf("%.2lf\n", s1.d);
printf("%d\n", s1.a);
return 0;
}
位段的声明和结构是类似的。位段也属于结构体的一种类型;位段的位表示的二进制数;也就是我们往位段里面放数,5=101;10=1010,放进位段的是二进制数;
1. 位段的成员必须是int 、unsigned int、signed int、short int (必须是是int 类型);
2. 位段的成员名后面有一个冒号和一个数字;
ag. A就是一个位段类型;其中_A _B _C _D均为位段的成员;位段成员冒号后面的数字表示位段成员所占的比特位:单位bit;1个字节等于8个bit;所以经过我们的计算,该位段总共47个比特位,是不是就是说该位段的大小(sizeof)就是6个字节,答案显然不是;位段也有自身的内存分配规则;
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
1. 位段的成员可以是int 、unsigned int 、signed int 或者是char (属于整型家族)类型;
2. 位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的;
3. 位段涉及很多不确定的因素,位段是跨平台的,注重可移植的程序应该避免使用位段;
ag. 以该例题具体讲解位段是如何为结构体变量开辟内存的,也就是位段的大小,也可以说打印sizeof(位段);
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
首先假设我们定义stuct A a ,结构体变量为a,位段为结构体变量开辟内存时,会根据位段的类型进行开辟,供所有位段成员使用,题中成员类型为int 型,我开辟一个整型供所有成员使用,int 占4个字节,32个比特位,a 占2个,b 占5个,c 占10个,32-17=15;是不够d 所使用的,位段规则定义15个bit舍弃,重新开辟一个int 型,32个比特位,供d 使用,舍弃2个bit位,所以该位段总共占8个字节;同时,位段设置时如果所占比特位大于该类型的字节大小,就会报错;也就是 int _e:33;系统就会报错,int型是无法承载33个比特位的;
如果定义的char类型,则占一个字节,8个比特位;
定义位段int _a:2;如果不使用位段,则_a 直接就会占用4个字节,32个比特位,而使用位段,只会占用2个比特位;所以从大局来考虑,位段的存在本身就是为了节省空间;
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
s.a = 10;
s.b = 20;
s.c = 3;
s.d = 4;
return 0;
}
初始化位段具体存放时:char-1个字节-8个比特位;第一个字节可以存放a b 占7个比特位,舍弃1个比特位;第二个字节放c,舍弃3个比特位;第三个字节放d,舍弃4个比特位;
a=10,二进制1010;但a只占3个比特位,所以存放的是010;b=20,二进制10100,b占4个比特位,所以存放的是0100;c=3,二进制为011,c占5个比特位,需要前面补0;所以放入地址的为00011;d=4,二进制为100,d占4个比特位,前面补0,所以放入地址的为0100;
所以该位段占(大小)3个字节; 0010 0010 0000 0011 0000 0100 转换16进制数为:2 2 0 3 0 4;总结:位段先使用低位,在使用高位,多余的舍弃,不够的从新开辟新的地址,开辟字节的大小根据存放的类型进行选择;
枚举顾名思义就是列举;枚举等同于结构体;枚举内部存放的是可能的情况(枚举常量);但需要区别于结构体;不是分号断开;是逗号断开,最后没有符号;
enum Sex
{
MALE,
FEMALE,
SECURE
};
枚举enum:打印结果为0 1 2 ;默认枚举常量是有值存在的;
enum Sex
{
MALE,
FEMALE,
SECURE
};
enum Color
{
RED,
GREEN,
BLUE
};
int main()
{
enum Sex s = MALE;
enum Color c = RED;
printf("%d %d %d\n", RED, GREEN, BLUE);
return 0;
}// 0 1 2
枚举初始化;初始化以后就不能在更改了;
enum Sex
{
MALE=2,
FEMALE=3,
SECURE=8
};
1. 增加代码的可读性和可维护性;
2. 和#define定义的标识符比较枚举有类型检查,更加严谨;简单来说就是#define LED P2^0 ,当程序使用到P2^0时,LED只是当做一个符号,没有具体类型;而枚举是有类型的;
3. 防止了命名污染(封装);枚举在大括号里,可以区别,防止重复;
4. 便于调试;
5. 使用方便,一次可以定义多个常量;
enum Color
{
RED=1,
GREEN=2,
BLUE=4
};
int main()
{
enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异;
clr = 5;
printf("%d\n", clr);
return 0;
}
因为枚举只能定义一个枚举成员变量,枚举成员变量初始化默认是0 1 2 ;所以枚举的大小为一个整型的大小,也就是4个字节 ;
enum S
{
RED,
BLUE,
GREEN
};
int main()
{
enum S s = BLUE;
printf("%d\n", sizeof(s));
return 0;
}//4
联合也称作联合体,也称作共用体;联合定义的变量包含一系列的成员,这些成员共用同一块空间,即他们的地址是相同的。联合体的格式同结构体;
union un
{
char c;
int i;
};
联合体的成员是共用同一块内存空间的,一个联合体变量的大小至少是最大成员的大小(简单来说就是本身需要有能力去保存最大成员的大小);
变相的来说就是定义的联合变量不能同时使用,因为在改变 c 的同时,会一并改变 i 的大小;
union un
{
char c;
int i;
};
int main()
{
union un u;
printf("%d\n", &(u.c));
printf("%d\n", &(u.i));
printf("%d\n", &u);
return 0;
}//7337912
7337912
7337912
大小端存储方式:低字节放在高地址上,叫做大端存储模式;高字节放在低地址上,叫做小端存储模式;
ag. 代码实现判断大小端存储模式?
int main()
{
int a = 1;// 00 00 00 01
if (*(char*)&a == 1)
{
printf("小端\n");
}
else
printf("大端\n");
return 0;
}
把 a 的地址取出来,强制类型转换为char类型;解引用如果是1,则表示低端是1,低地址放在低端就是小端存储模式;
联合的大小:联合大小至少是最大成员的大小;
当最大成员的大小不是最大对齐数的整数倍时,就要对齐到最大对齐数的整数倍;联合体的大小是最终对齐数的整数倍;
union un
{
int a;
char arr[5];
};
int main()
{
union un u;
printf("%d\n", sizeof(u));
return 0;
}// 8
int占用4个字节,对齐数是8;则 int 的对齐数是最小对齐数,也就是4;char占用 1 个字节,对齐数是8,则char 的对齐数是最小对齐数,也就是1;所以最大对齐数是4,联合体的大小是最大对齐数的整数倍;同时联合体的大小至少是最大成员的大小;所以最终联合体的大小是8;