结构体
1、结构体的声明
基础知识:结构是一些值的集合,这些值称为成员变量,结构的每个成员可以是不同的变量。
结构体也是一种类型。
结构的声明:
struct tag //struct 关键字 tag 名称(标签)(可以但不建议省略) { member_list; //成员不能为空(C语言中) }variable_list; //可以省略
举个例子:
struct Stu //描述一个学生 { char name[20]; int age; char sex[5]; char id[20]; }; //注意:分号不能丢
特殊的声明:
在声明结构的时候,可以不完全的声明
比如:
struct { int a; char b; float c; }x; struct { int a; char b; float c; }a[20],*p; //这两个都为匿名结构体类型,省略了结构体标签(tag)但若在上面的基础上,
p=&x;
合法吗?
答案是非法的,,,因为编译器会把上面的两个声明当成是完全不同的两个类型。
2、结构的成员
结构的成员可以是标量、数组、指针、甚至是其他结构体。
结构体成员的访问:
结构体变量访问成员是通过点操作符(.)访问的。
如上述的结构体:
struct S s; strcpy(s.name,"zhangsan"); s.age = 20;
但有时候我们得到的不是一个结构体变量,而是指向一个结构体的指针,那么则应该这样访问:
struct S { char name[20]; int age; }s; void print(struct S* ps) { printf("name=%s age=%d\n", (*ps).name, (*ps).age); printf("name=%s age=%d\n", ps->name, ps->age); }
3.结构的自引用
结构中包含一个类型为该结构本身的成员。
例如:
typedef struct Node { int date; struct Node* next; }Node;
结构的不完整声明:
若两个结构体互相包含,且要正常使用,则需要这样:
struct B; struct A { int _a; struct B* pb; }; struct B { int _b; struct A* pa; };
4.结构体变量的定义和初始化
定义的时候有两种方式:
①声明类型的同时定义变量; ②直接定义结构体变量;
struct Point //① { int x; int y; }p1; struct Point p2; //②
初始化:定义变量的同时赋初值
struct Point p3 = { x, y }; struct Stu //类型声明 { char name[15]; int age; }; struct Stu s = { "zhangsan", 20 }; //初始化
结构体还可以嵌套初始化:
struct Node { int date; struct Point p; struct Node* next; }n1 = { 10, { 4, 5 }, NULL }; struct Node n2 = { 20, { 5, 6 }, NULL };
5.结构体内存对齐
首先我们要知道为什么要存在内存对齐?
①平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
②性能原因:
数据结构(尤其是栈)应该尽可能的在自然边界上对齐。
原因是要访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问只需要一次。
结构体的对齐规则(重要):
1.第一个成员在与结构体变量偏移量为0的地址处(所以结构体成员的第一个元素你需要内存对齐,默认对齐);
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 对齐数:编译器默认的一个对齐数与该成员大小得到较小值; VS中默认为8 Linux中默认为4
3.结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4.如果嵌套了结构体,嵌套的结构体对齐到自己最大对齐数的整数倍处,结构体的整体大小为所有最大对齐数的整数倍。
当我们了解了结构体的内存对齐后,就要来计算结构体的大小了:
来看几个例子:
//练习1 struct S1 { char c1; //偏移量为1 int i; //大小为4,所以需要对齐,所以总大小为1+3+4 char c2; //大小为1,不需要对齐 1+3+4+1 }; //所以总大小为最大对齐数的倍数: 12 printf("%d\n", sizeof(struct S1));
//练习2 struct S2 { char c1; char c2; int i; }; //这次的结果是8 只是变换了一下成员的位置,结果就不一样,正好说明了结构体的对齐规则 printf("%d\n", sizeof(struct S2));
//练习3 struct S3 { double d; char c; int i; }; //按照规则,结果为16 printf("%d\n", sizeof(struct S3));
下面来一个复杂的,结构体嵌套:
//练习4 struct S4 { char c1; //1+3 struct S3 s3; //由上可知 16 double d; //8 }; //∴ 1+3+16+8=28 但要满足总大小为最大对齐数的倍数 ∴结构体总大小为32 printf("%d\n", sizeof(struct S4));
总的来说:结构体的内存对齐就是用空间来换取时间。
扩展:
#pragma pack()
这个可以用来修改编译器默认的对齐数;
括号中只能是1、2、4、8; 若括号中什么都不写,表示恢复默认。
结构体在传参的时候不发生降维;且尽量不要直接传结构体变量,而应该传结构体指针。
位段
位段的声明和结构是类似的,但有两个不同:
①位段的成员必须是int,unsigned int,signed int。
②位段的成员名后边有一个冒号和数字。
如:
struct A { int _a : 2; int _b : 5; int _c : 10; int _d : 30; }; //因为它的单位是比特位,所以它的大小是8
位段的内存分配:
①位段的成员可以是int,unsigned int,signed int或者是char(属于整形家族)类型;
②位段的空间上是按照需要以4个字节(int)或者一个字节(char)的方式来开辟的;
③位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序尽量避免使用位段。
位段的跨平台问题:
①int位段被当成是有符号数还是无符号数是不确定的;
②位段中最大位的数目不能确定;
③位段中的成员在内存中从左行右分配,还是从右向左标准尚未定义;
④当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,也不确定。
总结:位段跟结构相比,可以达到同样的效果,还可以节省空间,但是有跨平台的问题存在。
枚举
枚举其实就是把可能的取值一一列举;
这里就不介绍枚举了,我们来看看它的优点吧:
①增加代码的可读性和可维护性;
②和#define定义的标识符比较枚举有类型检查,更加严谨;
③防止了命名污染(封装)
④便于调试;
⑤使用方便,一次可以定义多个常量。
联合(共用体)
联合其实也是一种特殊的自定义类型,它定义的变量也包含一系列的成员,
最大的特点就是这些成员共用同一块空间;
那么一个联合变量的大小,就至少是最大成员的大小;
联合大小的计算:
①联合的大小至少是最大成员的大小;
②当最大成员大小不是最大对齐数的整数倍时,就要对齐到最大对齐数的整数倍。
这里我们看一个联合和结构体的巧妙使用:
union ip_addr { unsigned long addr; struct { unsigned char c1; unsigned char c2; unsigned char c3; unsigned char c4; }ip; }; union ip_addr my_ip; my_ip.addr = 176238749; printf("%d.%d.%d.%d\n", my_ip.ip.c4, my_ip.ip.c3, my_ip.ip.c2, my_ip.ip.c1);可以将long类型的IP地址转化为点分十进制的表示形式。