结构体是一些值的集合,这些值称为成员。结构体的每个成员·可以是不同的类型的变量
我们来看结构体声明的基本格式:
struct tag
{
member - list;
}varlible-list;
例如我们要描述一个学生:
struct Student
{
char name[20]; //姓名
int age; //年龄
long long id; //学号
};
上面的数组name,变量age,id都叫做这个学生结构体的成员变量
而有了上面的结构体的声明,我们就可以创建出关于这个结构体的结构体变量:
struct Student
{
char name[20];
int age;
long long id;
}Stu1,Stu2,Stu3;
struct Student Stu4, Stu5, Stu6;
注:此类特殊的声明实际用处不大,大家仅作了解即可
在声明结构体的时候,可以是不完全的声明:
struct
{
int a;
float b;
char c;
};
例如上面的结构体类型省略了标签名,我们就称之为匿名结构体类型
那么匿名结构体类型怎么创建变量呢?
我们需要清楚,如果想要用匿名结构体创建变量,只能在其结构体类型声明好后立刻创建,如:
struct
{
int a;
float b;
char c;
}x,*p,member[20];
我们继续来看下面的代码:
#include
struct
{
int a;
float b;
char c;
}x;
struct
{
int a;
float b;
char c;
}* p;
int main()
{
p = &x; //ok?
return 0;
}
我定义了两个成员列表一模一样的匿名结构体变量x和*p,那么大家认为这串代码p = &x;
是否正确?也就是说,编译器是否会认为这两个匿名结构体类型会一模一样呢?
编译后我们发现,会报警告:
p = &x;
这个操作在本质上也是错误的,**这点大家需要注意结构体是可以自引用的
那么具体的,结构体该如何自引用呢?
可能有小伙伴会这样写:
struct Node
{
int data;
struct Node next;
};
sizeof(struct Node)
sizeof()
的值便无法计算,显然这种方法是错误的struct Node
{
int data;
struct Node* next;
};
需要注意,下面这种自引用的写法也是错误的
//error example
typedef struct Node
{
int data;
Node* next;
}Node;
typedef
之前,就使用已经typedef
后重命名的名称一个结构体中可以嵌套另一个结构体
例如:
struct Grade
{
float math;
float English;
float Chinese;
};
struct Stu
{
struct Grade grade;
char name[20];
long long id;
};
//error example
struct Stu
{
struct Grade grade;
char name[20];
long long id;
};
struct Grade
{
float math;
float English;
float Chinese;
};
.
来进行结构体成员引用->
来进行结构体成员引用#include
typedef struct Stu
{
struct Grade grade;
char name[20];
long long id;
}Stu;
int main()
{
Stu s1;
Stu* s2 = &s1;
printf("math:%f English:%f Chinese:%f\nname:%s\nid:%lld\n",
s1.grade.math, s1.grade.English, s1.grade.Chinese, s1.name, s1.id);
printf("math:%f English:%f Chinese:%f\nname:%s\nid:%lld\n",
s2->grade.math, s2->grade.English, s2->grade.Chinese, s2->name, s2->id);
return 0;
}
对结构体变量成员的初始化有很多方式
//方法一:
//按默认顺序依次初始化
struct Grade
{
float math;
float English;
float Chinese;
}g1 = {12,13,14};
struct Stu
{
struct Grade grade;
char name[20];
long long id;
}Stu1 = { {12,13,14},"abcde",123456 }; //结构体的嵌套初始化
//方法二:
//用操作符 . 实现自定义顺序初始化
struct Grade
{
float math;
float English;
float Chinese;
}g1 = {.Chinese = 76, .English = 42, .math = 55};
#include
struct Grade
{
float math;
float English;
float Chinese;
}g1;
int main()
{
g1.Chinese = 12;
g1.English = 13;
g1.math = 15;
return 0;
}
我们先来看一串代码:
struct Text1
{
char a;
int b;
char c;
};
struct Text2
{
int b;
char a;
char c;
};
int main()
{
printf("%d\n", sizeof(struct Text1));
printf("%d\n", sizeof(struct Text2));
}
1+1+4 = 6
,让我们看看结果:要搞清楚如何计算结构体的大小,就需要搞清楚结构体在内存中是如何存储的,而要弄清楚结构体在内存中是怎么存储的,我们就需要借助一个宏offsetof来帮助我们查看
offsetof (type,member)
offsetof()
可以计算结构体成员相较于结构体起始位置的偏移量需要头文件
现在,我们就以上面的结构体struct Text1
为例,看看他在内存中到底是怎么存放的
#include `
#include
struct Text1
{
char a;
int b;
char c;
};
int main()
{
printf("%d\n", offsetof(struct Text1, a));
printf("%d\n", offsetof(struct Text1, b));
printf("%d\n", offsetof(struct Text1, c));
return 0;
}
可以得到这样的结果:
通过画图分析,可以得到结构体成员a,b,c大概是这样存储的:
通过上面现象的分析,我们发现结构体成员不是按照顺序在内存中连续存放的,而是有一定的对齐规则的
结构体内存对齐的规则:
结构体的第一个成员永远放在相较于结构体变量起始位置偏移量为0的位置
从第二个成员开始,往后的每个成员都要对齐到某个对齐数的整数倍处(对齐数:结构体成员自身大小和默认对齐数的较小值)
VS上默认对齐数是8
gcc没有默认对齐数,对齐数就是结构体成员的自身大小
结构体的总大小,必须是最大对齐数的整数倍(最大对齐数:所有成员的对齐数中的最大值)
如果是嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
这样,我们就可以对上面的两个例子做出合理的解释了:
struct Text1
,char a
的对齐数为1,故存放在偏移量为0的第一个字节,int a
的对齐数为4,而1.2.3
都不是4的整数倍,因此,这三个字节都被浪费,a从偏移量为4的字节开始存放,共占4个字节,char c
的对齐数为1,8是1的整数倍,故存放在偏移量为8的字节处,而整个结构体成员的最大对齐数为4,且此时结构体已经占了9个字节,为了达到所占字节数为4的整数倍,故还要浪费3个字节,因此该结构体所占的字节数就为12struct Text2
,仍是同理,这里就不再用文字赘述,我通过画图来分析:我们再来看最后一道题
#include
struct Text1
{
char a;
double b;
int c;
};
struct Text2
{
struct Text1 text;
char d;
double e;
};
int main()
{
printf("%d\n", sizeof(struct Text2));
return 0;
}
为什么要内存对齐呢?
主要有两点原因:
- 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则会出现硬件异常
- 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要两次内存访问;而对于对齐的内存仅需一次访问
举个例子:
对于32位机器(字长是32位,每次读写数据是32bit)
总的来说:结构体的内存对齐是那空间换时间的做法
因此,在设计结构体的时候,为了做到既节省空间,又要满足对其,要如何做到呢?
让占用空间小的成员尽量集中在一起
例如我们最开始讲的两个成员相同但所占空间不同的结构体:
struct Text1
{
char a;
int b;
char c;
};
struct Text2
{
int b;
char a;
char c;
};
我们可以用#pragma
预处理指令,来实现对默认对齐数的修改
例如:
#pragma pack(1) //将默认对齐数修改为1
struct Text1
{
int b;
char a;
char c;
};
#pragma pack() //取消设置的对齐数,还原为默认值
struct Text2
{
int b;
char a;
char c;
};
int main()
{
printf("%d\n", sizeof(struct Text1));
printf("%d\n", sizeof(struct Text2));
return 0;
}
可以得到:
直接上实例代码:
#include
struct Stu
{
char name[20];
long long id;
int age;
};
//结构体传参
void Print1(struct Stu s)
{
printf("%s\n%lld\n%d\n", s.name, s.id, s.age);
}
//结构体地址传参
void Print2(struct Stu* s)
{
printf("%s\n%lld\n%d\n", s->name, s->id, s->age);
}
int main()
{
struct Stu s = { "abcde",123456,18 }; //初始化
Print1(s); //传结构体
Print2(&s); //传结构体地址
return 0;
}
需要注意:
函数传参的时候,参数是需要压栈的,会有时间和空间上的系统开销
如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降
故:结构体传参的时候,要传地址
我们可以用结构体来实现位段
位段的声明和结构体是类似的,但是有两个不同:
- 位段的成员必须是
int, unsigned int, signed int, char
型- 位段的成员名后边有一个冒号和一个数字
- 位段的成员名后面的数字的大小不能大于成员类型所占比特位的大小
例如:
struct Text
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
Text就是一个位段类型
那么位段Text的大小又是多少呢?
printf("%d\n", sizeof(struct Text));
下面以VS2019编译器为例,分析一个位段在内存中如何存储的例子:
#include
struct Text
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct Text s;
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
printf("%d\n", sizeof(struct Text));
return 0;
}
我们先来进行一波假设:
可以得到我们的分析似乎是正确的,那我们继续分析,可以得到位段成员应该在内存中是这么存储的:
通过观察内存,可以得到我们的分析是正确的
再来看一个例子:
struct Text
{
char a : 3;
int b : 4;
char c : 5;
char d : 4;
};
printf("%d\n", sizeof(struct Text)); //?
可以得到:
为什么?
可以得出结论:
在VS2019编译器上:
- 联合体成员是从低位向高位存储的
- 当一次性开辟的空间不足以存储下一个数据时,应该浪费剩余的空间,重新开辟一块新的内存进行存储
- 位段由结构体实现,也存在内存对齐的现象,位段的大小也应该是最大对齐数的整数倍
- int位段被当成有符号数还是无符号数是不确定的
- 位段中最大位的数目不能确定。
- 位段中的成员在内存中从左向右分配,还是从右向左分配尚未定义
- 当一个结构包含两个位段成员,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是否舍弃剩余的位还是利用,这是不确定的
总结:
跟结构体相比,位段可以达到同样的效果,而且可以很好的节省空间,但是有跨平台的问题。
例如:网络上IP数据包
的格式
枚举顾名思义就是一一列举。
把有可能的取值一一列举。
例如现实生活中:
一周的星期一到星期日我们可以一一列举
一年有十二个月我们也能一一列举
…………
C语言中,同样存在枚举类型:
需要特别注意:枚举是对可能情况的一一列举,因此使用,
号分割,这与结构体成员用;
分隔是不同的
enum Day
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
上面定义的enum Day
就是枚举类型,{}
中的内容就是枚举类型的可能取值,叫做枚举常量
这些可能取值都是有值的,默认从0开始,依次递增;也可以在声明枚举类型的时候进行赋值
例如:
enum Color
{
RED,
YELLOW,
BLUE
};
int main()
{
printf("%d %d %d", RED, YELLOW, BLUE);
}
结果为:
enum Color
{
RED = 3,
YELLOW = 1,
BLUE = 5
};
int main()
{
printf("%d %d %d", RED, YELLOW, BLUE);
}
结果为:
enum Color
{
RED,
YELLOW,
BLUE = 5
};
int main()
{
printf("%d %d %d", RED, YELLOW, BLUE);
}
结果为:
我们可以用# define
定义常量,为什么非要使用枚举呢?其实,枚举有以下几个优点
增加代码的可读性和可维护性
和
# define
定义的标识符比较,枚举类型具有类型检查例如,下面这串代码在cpp文件中是不能通过的:
//error example enum Color { RED, YELLOW, BLUE = 5 }; int main() { enum Color cc = 3; //3是int类型,而cc是枚举类型 }
便于调试
使用方便,一次可以定义多个常量
#include
enum Color
{
RED = 3,
YELLOW = 4,
BLUE =6
};
int main()
{
int a = RED; //将一种可能情况赋值给整形a
printf("%d\n", a);
enum Color clr = RED; //只能拿枚举常量给枚举变量赋值
printf("%d\n", clr);
return 0;
}
联合也是一种特殊的自定义类型
这种类型定义的变量也是包含一系列的成员,特征是这些成员公用一块空间(所以联合体也叫共用体)
例如:
//联合类型的声明
union UN
{
char c;
int i;
};
//联合变量的定义
union UN un;
联合的成员是公用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)
我们来看一个例子:
union UN
{
char c;
int i;
};
union UN un;
int main()
{
//输出结果一样吗?
printf("%p\n", &(un.c));
printf("%p\n", &(un.i));
//un.i会是多少呢?
un.i = 0x11223344;
un.c = 0x55;
printf("%x\n", un.i);
return 0;
}
我们一起来分析(以VS2019编译器为例):
sizeof(int)
4个字节un.i
的数据在内存中就是这样存储的:un.c
赋值,那么存储的数据就变成了这样:可以得出结论:
联合体所有成员公用一块空间,并且,每个成员的起始地址应该是一样的
联合的大小至少是最大成员的大小
当最大成员的大小不是最大对齐数的整数倍时,就要对齐到最大对齐数的整数倍
例如:
union UN
{
char c[5];
int i;
};
union UN un;
printf("%d\n", sizeof(un));
sizeof(c) = 5
,但是最大对齐数为sizeof(int) = 4
,因此为了达到最大对齐数的整数倍,要浪费3个字节,故这个联合的大小为8个字节本次我们学习了C语言的自定义类型——结构体(struct)、位段、枚举(enum)、联合(union)
应该重点掌握以下类容
自定义类型的基本使用
熟悉结构体内存对齐的规则
熟悉各自定义类型的特点,并知道计算各自定义类型所占空间的大小
如果觉得本篇文章对你有所帮助,还请点个赞支持一下博主,同时你也可以关注博主,博主会继续发布更多博文与大家分享、学习
共勉!!!( ̄y▽ ̄)╭ Ohohoho…