前言:本篇文章用一些例子介绍了结构体、位段、枚举、联合的一些基本知识,文章的重点是一定要会结构体的内存对齐那部分知识,因为这个知识很重要。如果文章那里写的有错误,还请各位朋友指出。
结构体
什么是结构体
结构体是一些值的集合,这些值称为成员变量。结构体中的每个成员都可以是不同类型的变量
结构体的声明
一般声明
struct tag
{
member-list;
}var-list;
tag为结构体标签,member-list为成员列表,var-list是变量列表。
例如:用结构体描述一个学生
struct Stu//结构体标签
{
char name[20];
short age;
char sex[5];
char id[20];
//成员列表
}s,*ps;//变量列表
特殊声明(匿名结构体声明)
匿名结构体声明就是省略结构体标签的声明,但是要直接加上变量名,而且只能使用一次
例如:
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}*p;
这里要注意:对于上边两个代码不能使用代码p=&x
,因为他们没有结构体标签,编译器认为上边的两个类型不同,所以这个操作是非法的。
结构体成员的访问
假设我们现在声明以下结构体并创建结构体变量
struct Stu
{
char name[20];
short age;
};//声明一个结构体
struct Stu s;//创建结构体类型
struct Stu *ps
要访问结构体成员有两种办法:
变量.成员
例如:上边的代码访问name成员就是s.name,访问age就是s.age
注意:这里如果要给s.name赋值不能直接赋值,因为name是数组名,代表首元素地址,s.name其实是数组首元素的地址,要想给它赋值,必须用strcpy
函数,例如strcpy(s.name,"zhangsan")
指针->成员
例如:上例中用指针访问name成员就是ps->name
,访问age成员就是ps->age
,这里的ps->name
和ps->age
相当于(*ps).name
和(*ps).age
结构体的自引用
结构体的自引用就是在结构中包含一个类型为该结构体本身的成员
例如:
struct Node
{
int data;
struct Node* next;
};
但是这里要注意的是不能像下面这么写,因为是先有结构体成员,才进行类型重命名,有一个先后顺序
typedef struct Node
{
int data;
Node* next;
}Node;//一定不能这么写
结构体的不完整声明
结构体的不完整声明就是在A结构体成员中包含B结构体,在B结构体成员中包含A结构体,但是总是得有一个在前面声明,所以就有了不完整声明
例如:
struct B;//不完整声明
struct A
{
int a;
struct B* pb;
};
struct B
{
int b;
struct A* pa;
};
结构体变量的初始化
例如:
struct Stu
{
char name[20];
short age;
};
struct Stu s={"zhangsan",20};
//定义变量的同时初始化
struct Stu
{
char name[20];
short age;
}s2={"lisi",30};
结构体的内存对齐(计算结构体大小)
结构体的内存对齐是一个很重要的知识,要想计算结构体的大小就必须学懂内存对齐
内存对齐的规则
- 第一个成员放在与结构体变量偏移量为0的地址处
- 剩下的其他成员对齐到对齐数的整数倍地址处。对齐数=编译器默认对齐数与该成员大小的较小值(vs默认值是8,Linux的gcc编译器是4,当然这个默认值是可以改变的,后边会讲到)
- 结构体的总大小为最大对齐数的整数倍
- 如果有嵌套了结构体的情况,嵌套的结构体对齐到自身的最大对齐数的整数倍处,结构体的总大小就是所有对齐数中最大对齐数的整数倍
例如:下面几个结构体的大小
例1:
struct S
{
char c1;
//第一个成员放在0偏移处 1
int i;
//i的大小为4,默认对齐数为8,实际对齐数为4,放在4-7偏移处
char c2;
//c2的大小为1,实际对齐数为1,8偏移为1的倍数,放在8偏移处
//结构体的总大小为最大对齐数4的倍数,既浪费3个,总大小为12
};
例二:
struct S3
{
double d;
//对齐数为8,放在0-7偏移
char c;
//对齐数为1,8偏移是1的倍数,可以放在8偏移
int i;
//对齐数为4,12是4的倍数,放在12-15偏移处
//总大小为最大对齐数8的倍数,16刚好是8的倍数,则结构体大小为16
};
struct S4
{
char c1;
//放在1偏移处
struct S3 s3;
//嵌套结构体最大对齐数是8,放在8-23偏移
double d;
//对齐数为8,放在24-31,32刚好为最大对齐数8的倍数,则32位结构体的总大小
};
结构体为什么会存在内存对齐
- 移植原因
不是所有的硬件平台都能访问任意地址的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,内存对齐可以增强程序的可移植性
- 性能原因
访问未对齐的内存,处理器需要做两次访问,而已经对齐的内存只要做一次访问,所以说结构体的内存对齐是在用空间换取时间
结构体的传参
struct stu
{
int data[10];
int count;
};
struct stu s = { "1,2,3,4", 100 };
printf(s);//直接传结构体
printf(&s);//创结构体的地址
注意:在结构体传参的时候,尽量传它的地址。因为如果结构体过大,函数传参的过程中是要压栈的,在参数压栈的过程中系统的开销比较大,如果直接传地址过去,会很大的提高效率。
默认对齐数的修改方法
#pragma pack(4)
...
代码区
...
#pragma pack(4)
设置中间代码区的默认对齐数为4
位段
什么是位段
- 1.位段声明和结构体类似
- 2.位段的成员必须是int、unsigned int、signed int
- 3.位段的成员名后边有一个冒号和一个数字
例如:
struct A
{
int a : 2;
int b : 5;
int c : 10;
int d : 30;
//2、5、10、30代表的是比特位而不是字节
};
位段的内存分配
位段的空间上是按需要以4个字节(int)或者是以1个字节(char)的方式来开辟的
那么上边的位段A的大小应该是多少呢?
struct A
{
int a : 2;
//先开辟4个字节的空间,也就是32个比特位
//a占掉2个比特位,32-2=30
int b : 5;
//b占掉5个比特位,30-5=25
int c : 10;
//c占掉10个比特位,25-10=15
int d : 30;
//d占30个比特位,前边开辟的4个字节已经不够用了,因此在开辟四个字节
};
所以sizeof(struct A)
的结果为8
位段的跨平台问题
- 1.int位段被当成是有符号还是无符号是不确定的
- 2.位段中最大位的数目不能确定
- 3.位段中的成员在内存中是从右向左还是从左向右分配的不确定
- 4.当一个结构包含两个位段,第二个位段成员比较大,放不下在第一个位段剩余的为时,舍弃还是利用第二个位段成员是不确定的
位段和结构体相比,可以达到相同的效果,但是可以很好的节省空间,但是可移植性太差,不存在内存对齐。
枚举
枚举就是把所有可能的值一一列举出来
枚举类型的定义
例如:
enum day
{
mon,
tues,
wed,
thur,
sat,
sun
};
上边的enum day
就是一个枚举类型,括号里的内容是枚举类型的可能取值,这些可能的取值都是有值得,默认是从0开始,依次递增1,当然也可以在定义的时候自己赋值,所以又把它叫做枚举常量
枚举的优点
- 1.增强代码的可读性和可为维护性
- 2.和#define定义的标识符常量比较枚举有类型的检查,更加严谨
- 3.可以很好的防止命名冲突
- 4.便于调试
- 5.一次可以定义多个常量,而#define一次只可以定义一个常量
枚举的使用
枚举变量存在类型检查,只能将枚举常量赋值给枚举变量,否则会出现类型不一致的错误
例如:
enum color
{
RED=1,
GREEN=2,
BLUE=4
};
enum color C = RED;
//不能给C赋值其他类型的常量,例如:C=5,这是不行的
联合
什么是联合(共用体)
联合是一种特殊的自定义类型,这种类型也包含一系列成员,特征是这些成员共用一块空间,所以它也叫做共用体
例如:
union Un
{
int i;
char c;
};
union Un un;
联合的特点
例如:下面代码输出结果是什么
#include<stdio.h>
union Un
{
int i;
char c;
};
int main()
{
union Un un;
un.i = 0x11223344;
un.c = 0x55;
printf("%x\n", un.i);
return 0;
}
联合的特点是所有成员共用一块内存空间,所以上边的代码输出结果是0x11223355
联合大小的计算
- 1.联合的大小至少是最大成员的大小
- 2.当最大成员大小不是最大对齐数的整数倍时,就要对齐要最大对齐数的整数倍
例如:
#include<stdio.h>
union Un2
{
char c[5];
int i;
};
union Un1
{
short c[7];
int i;
};
int main()
{
printf("%d\n", sizeof(union Un1));
printf("%d\n", sizeof(union Un2));
return 0;
}
输出结果:16、8
联合和结构体的巧妙使用
将long类型的IP地址装换为点分十进制形式
#include<stdio.h>
union ip_addr
{
unsigned long addr;
struct
{
unsigned char c1;
unsigned char c2;
unsigned char c3;
unsigned char c4;
}ip;
};
int main()
{
union ip_addr my_ip;
my_ip.addr = 16355327675;
printf("%d.%d.%d.%d\n", my_ip.ip.c4, my_ip.ip.c3, my_ip.ip.c2, my_ip.ip.c1);
return 0;
}
判断当前计算机的大小端存储
这道题在我在前面写的一篇博客《大小端详解》里提到了两种解法,一种是利用联合体巧妙的证明了计算机的大小端存储,这里贴出链接https://blog.csdn.net/hansionz/article/details/80871921,题目在博客的最后哦!