前言
本文对自定义类型的结构体创建、使用、结构体的存储方式和对齐方式,枚举的定义、使用方式以及联合体的定义、使用和存储方式展开叙述,如有错误,请各位指正。
目录
前言
1 结构体
1.1 结构体的声明
1.2 结构体的自引用
1.3 结构体变量的定义和初始化
1.4 结构体变量的内存对齐
1.5 修改默认对齐数
1.6 结构体传参
2.位段
2.1 位段的内存分配
2.2 位段的跨平台问题
3.枚举
3.1枚举类型的定义
3.2枚举的优点
3.3枚举的使用
4. 联合(共用体)
4.1 联合类型的定义
4.2 联合的特点
4.3 联合大小的计算
1 结构体
结构体是一些值的集合,这些值称为成员变量,结构的每一个成员可以是不同类型的变量。
1.1 结构体的声明
struct tag
{
member-list;
}variable-list;
tag 结构体标签
member-list 成员列表
variable-list 变量列表
注意:创建结构提变量的时候最后的 ; 不能丢掉
结构体就创建一个自定义的包含多种数据类型的数据类型
示例
创建一个描述学生的结构体数据类型 Stu
struct Stu
{char name[20];int age;char sex;char id[20];
};
特殊声明 :匿名结构体类型,没有结构体的标签,没有给结构体起名字。
示例1(创建没有标签的结构体)
struct
{int a;char b;float c;
}x;//x创建的结构体变量
这种结构的使用只能现创现用,在不同的位置是无法创建同一类型的结构体变量的,创建变量需要结构体标签,没有标签就是创建不了变量了。
示例2(创建结构体的指针)
struct
{
int a;
char b;
float c;
} *p;
示例1和示例2中下方等式成立吗?
p = &x;
1.2 结构体的自引用
结构体的自引用就是结构体中包含结构体的指针。
示例:在数据结构中数据的存放有链表性结构,链表中的每一处的节点都会存放一个数据,还可以根据节点找到下一个节点的地址,然后创建结构体;那么我结构体存放节点的数据和指向下一个节点的指针就可以了,这就是结构体的自引用了。
struct Node
{int data;// 节点的数据struct Node* next;// 下一个节点的地址
};
结构的创建变量的时候名字较为复杂还有 unsigned int 、unsigned char等等 都可以使用 typedef重新定义名字,这样在创建变量的时候就简单了,如下
//重新定义变量
typedef unsigned int uint;
typedef unsigned char uchar;
typedef char u8;
typedef struct Stu Stu;
//创建变量
unit a;
unchar b;
u8 c;
Stu x;
需要注意
在结构体创新命名的时候,需要结构体有标签。
1.3 结构体变量的定义和初始化
结构体定义就是创建结构体变量,以下是创建结构提变量的方式:
//方式1 创建结构体的时候创建全局变量
struct Point
{int x;int y;
}a1;
//方式2
struct Point a2;
//方式3
int main()
{struct Point a3;return 0;
}
初始化和和数组的初始化较为相似,是应用{ }来初始换结构体创建的变量,在后面附上值就可以了,完全初始化,
struct Point
{int x;int y;
}a1 = {10,20};
struct Point a2 = {5,9};int main()
{struct Point a3 = { 2,3 };return 0;
}
不完全初始化(一个一个的给变量赋值)
struct S s1= { .num=10,.ch='q',.p.x=6, .p.y=10 };
嵌套结构体的初始化,有几个结构体就用几个{ }来创建变量,示例如下:
struct Point1
{int x;int y;
};
struct Point2
{int z;char ch;struct Point1 a1;float d;
};
int main()
{struct Point2 s = { 1,'a',{1,2},6.14f };return 0;
}
结构体的访问方式,分为两种,一种是使用 . 符号来访问,一种是使用 -> 符号来访问
struct Point
{int x;int y;
};
struct S
{int num;char ch;struct Point p;float d;
};int main()
{// 初始化struct S s = { 3,'w',{1,2},3.15f };//访问printf("%d %c %d %d %0.1f\n", s.num,s.ch,s.p.x, s.p.y, s.d);return 0;
}
struct Stu
{char name[20];int age;char sex[20];float score;
};
int main()
{//打印结构体信息struct Stu s = { "张三", 20, "男", 95.0f };struct Stu *ps = &s;printf(" %s %d %s %.1f\n", ps->name, ps->age, ps->sex, ps->score);return 0;
}
1.4 结构体变量的内存对齐
结构体的内存对齐决定了结构体的在内存中所占用的空间大小。
引入
创建两个结构体,结构体中的变量类型相同,但是顺序不同,其内存大小一样吗?
struct S1
{char c1;int i;char c2;
};
struct S2
{char c1;char c2;int i;
};
int main()
{printf("%d\n", sizeof(struct S1));//12printf("%d\n", sizeof(struct S2));//8return 0;
}
1.结构体的顺序不一样,内存不一样;
2.内存的大小和成员的数据类型有关吗?
结构体的对齐方式按照以下几条
1.结构体的第一成员,对齐到结构体在内存中的存放位置的0偏移处;
2.从第二个成员开始,每个成员都要对齐到(一个对齐数)的整数倍处;
对齐数:结构体成员自身大小和默认对齐数的较小值;
VS:默认对齐数为8;
Linux gcc :没有默认对齐数,对齐数就是结构题成员的自身大小;
3.结构体的总大小,必须是所有成员的对齐数中最大对齐数的整数倍。
4.如果结构体中嵌套了结构体成员,要将嵌套的结构体成员的对齐到自己的成员中最大的对齐数的整数。(结构体的总大小必须是最大对齐数的整数倍,包含请嵌套结构体成员的对齐数,是所有对齐数的最大值)
S1的结构体存储的方式
struct S1
{char c1;int i;char c2;
};
(1)c1是第一个成员,从0偏移量存储,占用一个字节(灰色);
(2)i为整型变量,对齐数为4,默认对齐数为8,取较小值为4,偏移量为4,偏移量 1,2,3处内存浪费掉了(黄色);
(3)c2字符变量,对齐数为1,默认对齐数为8,取较小值为1,占用偏移量为8的位置(红色);
(4)确定结构体的大小,从c1到c2处,占用了9个字节的空间,取结构体成员的最大对齐数4,因此内存要占用12个字节,浪费6个字节(蓝色)。
S1的结构体存储的方式
struct S2
{char c1;char c2;int i;
};
(1)c1是第一个成员,从0偏移量存储,占用一个字节(灰色);
(2)c2字符变量,对齐数为1,默认对齐数为8,取较小值为1,偏移量是对齐数的倍数 2,占用偏移量为2的位置(红色);
(3)i为整型变量,对齐数为4,默认对齐数为8,取较小值为4,偏移量为4,去偏移量的倍数 4,偏移量 2,3处内存浪费掉了(黄色);
(4)确定结构体的大小,取结构体成员的最大对齐数4,因此内存要占用8个字节,浪费2个字节空间(蓝色)。
使用 offsetof函数来验证上述的偏移量是否正确,这个函数就是计算偏移量的,头文件是stddef,
验证程序
struct S1
{char c1;int i;char c2;
};
struct S2
{char c1;char c2;int i;
};
int main()
{printf("%d\n", sizeof(struct S1));//12printf("%d\n", offsetof(struct S1, c1));//0printf("%d\n", offsetof(struct S1,i));//4printf("%d\n", offsetof(struct S1,c2));//8return 0;
}
嵌套结构体内存对齐,
#include <stdio.h>
#include <stddef.h>
struct S2
{char c1;char c2;int i;
};
struct S3
{double d;struct S2 s2;int a;
};
int main()
{printf("%d\n", sizeof(struct S3));//24printf("%d\n", offsetof(struct S3,d));//0printf("%d\n", offsetof(struct S3,s2));//8printf("%d\n", offsetof(struct S3,a));//16return 0;
}
S3是如何对齐呢?
struct S3
{double d;struct S2 s2;int a;
};
(1)d是第一个成员,从0偏移量存储,占用8个字节(灰色);
(2)s3为结构体变量,结构体成员最大的对齐数为4,默认对齐数为8,取较小值为4,偏移量是对齐数的倍数 8,占用偏移量为8的位置(黄色);
(3)a为整型变量,对齐数为4,默认对齐数为8,取较小值为4,偏移量取16,取偏移量的倍数 16(黄色);
(4)确定结构体的大小,取结构体成员的最大对齐数8,因此内存要占用24个字节,浪费4个字节空间(蓝色)。
为什么要对齐呢?
1. 程序移植: 不是所有的平台数据的存储方式和访问方式是一样的。
2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总结:结构体的对齐就是利用内存换取运行时间的手段。
如何节省空间?
将占用空间小的成员集中到一起。
1.5 修改默认对齐数
#pragma pack()//恢复默认对齐数
#pragma pack(1)//设置对齐数为1
#include <stdio.h>
#include <stddef.h>
#pragma pack(1)//设置对齐数为1
struct S1
{char c1;int i;char c2;
};
#pragma pack()//恢复默认对齐数int main()
{printf("%d\n", sizeof(struct S1));//6printf("%d\n", offsetof(struct S1,c1));//0printf("%d\n", offsetof(struct S1,i));//1printf("%d\n", offsetof(struct S1,c2));//5return 0;
}
此时S1的空间对齐方式
(1)c1是第一个成员,从0偏移量存储,占用一个字节(灰色);
(2)i为整型变量,对齐数为1,偏移量为1,(黄色);
(3)c2字符变量,对齐数为1,占用偏移量为5的位置(红色);
(4)确定结构体的大小,从c1到c2处,占用了6个字节的空间。
1.6 结构体传参
结构体传参一般采用地址传参的方式,在传递值传参的时候,需要重新创建变量,浪费大量的内存空间,传址调用较为省空间。
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.位段的成员必须是 int 、unsigned int、或者是signed int;
2.位段的成员名后有一个冒号和一个数字。
struct A
{int _a:2;int _b:5;int _c:10;int _d:30;
};
A是一个位段类型,位段的大小是多少呢?
printf("%d", sizeof(struct A));//8
原本4个整型数据,占据16个字节,如何在内存中限制到8个字节的呢?
2.1 位段的内存分配
1. 位段的成员可以是 int unsigned、 int、 signed int 、 char 类型
2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
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;
位段开辟空间一次开辟一个字节,不够使用的话再开辟另一个字节;位段是从低地址到高地址存储的,在每个字节中也是从低位向高位存储;
2.2 位段的跨平台问题
1. int 位段被当成有符号数还是无符号数是不确定的。
2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机
器会出问题。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是
舍弃剩余的位还是利用,这是不确定的。
特点:位段和结构体相比,位段的存储空间为共用空间,位段更加节省空间,但是存在跨平台问题。
3.枚举
3.1枚举类型的定义
可以被一一列举的变量,语法形式和结构体类似,例如生活中的,
1.一周的星期一到星期日有限的7天,可以一一列举;
2.性别有:男、女分别;
3.一年有12个月,可以一一列举。
enum Day
{Mon,Tues,Wed,Thur,Fri,Sat,Sun
};
enum Sex
{MALE,FEMALE,SECRET
};
三原色
enum Color
{RED,GREEN,BLUE};
上述是有可能取到的值,默认从零开始一次递增,在定义的时候可以赋初值,示例
#include <stdio.h>
enum Day
{Mon = 1,Tues = 2,Wed = 3,Thur = 4 ,Fri = 5,Sat = 6,Sun = 7
};
int main()
{printf("%d\n", Mon);//1printf("%d\n", Tues);//2return 0;
}
枚举常量就是给特定的字符赋予一定的数值,在后续的使用中Mon和1等价。
3.2枚举的优点
和#define相比枚举创建的常量可以在调试中显示,而宏定义是不可以的。
1. 增加代码的可读性和可维护性;
2. 和#define定义的标识符比较枚举有类型检查,更加严谨;
3. 防止了命名污染(封装);
4. 便于调试;
5. 使用方便,一次可以定义多个常量。
3.3枚举的使用
下述代码是用于C51单片机的按键控制LED亮灭的程序,创建的枚举变量使用的时候程序较好理解,如果将KEY1_PRESS为1的话,并不是特别的直观的表达出按键1以将按下。
#include <REGX52.H>
#include <stdio.h>
//引脚定义
#define SMG_A_DP_PORT P1
sbit KEY1 = P0 ^ 0;
sbit KEY2 = P0 ^ 1;
sbit KEY3 = P0 ^ 2;
sbit KEY4 = P0 ^ 3;
sbit LED1 = P2 ^ 0;
sbit LED2 = P2 ^ 1;
sbit LED3 = P2 ^ 2;
sbit LED4 = P2 ^ 3;enum KEY
{KEY_UNPRESS = 0,KEY1_PRESS = 1,KEY2_PRESS = 2,KEY3_PRESS = 3,KEY4_PRESS = 4,
};//枚举
char key_scan(mode);//当mode=0的时候 单次扫描 mode=1 连扫
void main()
{unsigned char ret = 0;while (1){ret = key_scan(0);switch (ret){case KEY1_PRESS:{LED1 = !LED1;break;}case KEY2_PRESS:{LED2 = !LED2;break;}case KEY3_PRESS:{LED3 = !LED3;break;}case KEY4_PRESS:{LED4 = !LED4;break;}}}}//延时函数 当1=ten_us,延时10us
void delay_10us(unsigned int ten_us)
{while (ten_us--);
}
char key_scan(mode)//当mode=0的时候 单次扫描 mode=1 连扫
{static char key = 1;if (mode){key = 1;}if (key == 1 && (KEY1 == 0 || KEY2 == 0 || KEY3 == 0 || KEY4 == 0)){key = 0;delay_10us(1000);if (KEY1 == 0)return KEY1_PRESS;else if (KEY2 == 0)return KEY2_PRESS;else if (KEY3 == 0)return KEY3_PRESS;else if (KEY4 == 0)return KEY4_PRESS;}else if (KEY1 == 1 && KEY2 == 1 && KEY3 == 1 && KEY4 == 1){key = 1;}return KEY_UNPRESS;
}
4. 联合(共用体)
4.1 联合类型的定义
联合是一种特殊的自定义类型,定义的变量也包含一系列的成员这些成员会共用一块内存空间。
union UN
{char c;int i;
};
int main()
{union UN un;printf("%d\n", sizeof(un));//4printf("%p\n", &un);//00EFFB08printf("%p\n", &(un.c));//00EFFB08printf("%p\n", &(un.c));//00EFFB08return 0;
}
三次取地址的结果是一样的,说明c和i的起始地址的是一样,可以得出,两个变量占用得空间开始是一样得,当然两个变量是不可以同时使用得,c占用4个字节的第一个字节,i占用四个字节,如图
4.2 联合的特点
联合的成员是共用同一块内存空间的,联合变量的大小至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。
int main()
{union Un{int i;char c;};union Un un;//下面输出的结果是什么?un.i = 0x11223344;un.c = 0x55;printf("%x\n", un.i);//11223355return 0;
}
un的前四个字节为11223344,后有将44修改为了55,因为VS2017上位小端存储。
可以封装成函数判断大小端存储:
#include<stdio.h>
int check_sys()
{union Un{int i;char c;}un;un.i = 1;return un.c;//判断变量,低位是否为1或者0
}
int main()
{int ret = check_sys();if (ret){printf("小端存储");}elseprintf("大端存储");return 0;
}
4.3 联合大小的计算
1.联合的大小至少是最大成员的大小。
2.当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
int main()
{union Un1{char c[5];int i;};union Un2{short c[7];int i;};//下面输出的结果是什么?printf("%d\n", sizeof(union Un1));//8printf("%d\n", sizeof(union Un2));//16
}
Un1的内存大小为8
(1).c[5]的对齐数位1,占用5个字节的空间
(2).i的对齐数为4,占用内存4个字节的空间,
(3)最大对齐数为4,再取4的倍数,内存大小为8个字节;
Un2的内存大小为16
(1).c[7]的对齐数位2,占用14个字节的空间
(2).i的对齐数为4,占用内存4个字节的空间,
(3)最大对齐数为4,再取4的倍数,内存大小为16个字节;