在C语言中,有许多数据类型,如整型,浮点型,字符型等,结构体,枚举也是一种类型,他们包含在自定义类型中,结构体可以被声明为变量、指针或数组等,用以实现较复杂的数据结构。结构体同时也是一些元素的集合,这些元素称为结构体的成员(member),且这些成员可以为不同的类型,成员一般用名字访问。关于位段,C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为位段,利用位段能够用较少的位数存储数据。枚举则可以列举未来的可能取值,下面具体介绍一下。
结构体
结构体的基本构成
结构体声明
struct tag
{
member-list;
}variable-list;
举例
定义学生类型
struct Stu
{
//成员变量
char name[20];
int age;
float weight;
} s4, s5, s6;//这是全局变量
struct Stu s7;//全局变量
int main()
{
//局部变量
struct Stu s1;
struct Stu s2;
struct Stu s3;
return 0;
}
在上面代码中,我们声明了一个结构体类型,名字为Stu,(定义末尾要注意分号)在这个结构体中,有三个成员变量,同时,我们还定义了一个结构体变量,在结构体定义中,可以允许使用者在结构体末尾定义属于这个结构体的变量,当然,也可以在结构体外,例如main函数中,或main函数外,要注意的是,在不同的位置定义变量,他的适用范围也是不同的
除了这种结构体定义外,还有一种特殊的结构体类型声明
匿名结构体
struct
{
char c;
int a;
double d;
}s1;
对于匿名结构体来说,编译器会把他当成一种不同的类型,举例来说
struct
{
char c;
int a;
double d;
}s1;
struct
{
char c;
int a;
double d;
}*ps;
int main()
{
ps = &s1;//err
return 0;
}
在上面代码中,我们把一个结构体变量定义为了指针变量,让他去指向另一个结构体的地址,显然这是不行的,因为两个匿名结构体的类型在编译器看来完全不同,因此不能完成该代码,同时,匿名结构体不允许使用者在结构体外创建该结构体变量,只能在该结构体末尾创建。
结构的自引用
在结构中包含一个类型为该结构本身的成员是否可以呢?
struct Node
{
int data;
struct Node n;
};//err
//不能在自己的类型中包含一个自己类型的变量
//只能包含一个自己类型的指针
int main()
{
printf("%d\n",sizeof(struct Node));
return 0;
}
显然,使用这种方式进行自引用是不行的,想要达到目的,只能包含一个自己类型的指针
struct Node
{
int data;
struct Node n;
};//err
struct Node
{
int data;
struct Node* next;
};
int main()
{
struct Node n1;
struct Node n2;
n1.next = &n2;
return 0;
}
同时,下面的代码也说明了结构体在引用时的一些问题
typedef struct
{
int data;
char c;
} S;//用了typedef后S是类型名而不是变量
typedef struct
{
int data;
Node* next;
}Node;//err
//先后顺序有问题
正确用法
typedef struct Node
{
int data;
struct Node* next;
}Node;
结构体变量的定义和初始化
有了结构体类型,我们就可以开始定义变量,定义变量的以及初始化方法很简单
struct S
{
int a;
char c;
}s1;
struct B
{
float f;
struct S s;
};
int main()
{
int arr[10] = { 1,2,3 };
int a = 0;
struct S s2 = { 100,'q' };
struct S s3 = { .c = 'r',.a = 200 };// .+变量名可以指定初始化
struct B sb = { 3.14f,{200,'w'} };
return 0;
}
在初始化中,我们可以根据定义变量的先后进行初始化,也可以用.+变量名对指定的变量进行初始化,同时,我们也可以进行结构体嵌套初始化
结构体内存对齐
对于内存对齐,具有一定的规则:
结构体的第一个成员永远都放在0偏移处
从第二个成员开始,以后的每个成员都要对齐到某个对齐数的整数倍数,这个对齐数是:成员自身大小和默认对齐数的较小值
vs环境下默认对齐数是8
gcc环境下没有默认对齐数,没有的情况下,对齐数就是成员自身大小
当成员全部存放进去后,结构体的总大小必须是,所有成员的对齐数中,最大对齐数的整数倍
如果不够,则浪费空间对齐
如果有嵌套结构体的情况,即结构体里面有一个结构体,那么嵌套的结构体对齐到自身成员的最大对齐数的整数倍数处
结构体有多少字节就放多少,大小必须是最大对齐数的整数倍,最大对齐数包含嵌套的结构体成员中的对齐数
了解了这些规则后,我们再来通过代码计算内存对齐
struct S1
{
int a;
char c;
};
struct S2
{
char c1;
int a;
char c2;
};
struct S3
{
char c1;
int a;
char c2;
char c3;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
printf("%d\n", sizeof(struct S1));//8
printf("%d\n", sizeof(struct S2));//12
printf("%d\n", sizeof(struct S3));//12
printf("%d\n", sizeof(struct S4));
return 0;
}
比如整型有4个字节,从0开始,默认对齐数8,4较小所以对齐数为4,对齐到4的倍数
那么为什么存在内存对齐?
原因有以下
1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的,某些硬件平台只能在某些地址处取某些特
定类型的数据,否则抛出硬件异常
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问,而对齐的内存访问仅需要一次访
问
也就是说,结构体的内存对齐是拿空间来换取时间的做法
因此,我们应该让占用空间小的成员尽量集中在一起,这样所浪费的空间也会减少
例如
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
通过计算可以发现,两个结构体所定义的变量相同,但是S2所占的空间却要比S1少一些
修改默认对齐数
修改默认对齐数可以通过#pragma pack(1)指令修改,通过修改括号内的数字就可以修改默认对齐数,同理,当括号内没有数字时,就会取消修改,还原默认,所以,当使用者觉得对齐方式不合适时,就可以使用这行代码修改默认对齐数
#pragma pack(1)
struct S
{
char c1;//1字节 默认1 较小取1
int i;//4 1 1
char c2;//1 1 1
};
#pragma pack()
int main()
{
printf("%d\n", sizeof(struct S));
return 0;
}
结构体传参
struct S
{
int data[1000];
int num;
};
struct S s = { {1,2,3,4},1000 };
void print(const struct S* ps)//加const增加安全性
{
printf("%d\n", ps->num);
}
int main()
{
print(&s);
return 0;
}
结构体传参尽量传结构体地址,为什么不传结构体本身进行传值呢?结构体在传参的时候,参数会被系统压缩,例如在main函数中调用时,会在main函数外创建一个空间进行存放,会进行压栈,也就是说当结构体越大所进行的压栈的浪费的资源越大,会损耗时间空间,因此尽量传结构体地址
位段
位段介绍
位段的声明和结构是类似的,位段的成员是整型家族,即int、unsigned int ,signed int,char
位段 - 二进制位
节省空间
struct a
{
int _a : 2;//位段成员
int _b : 5;
int _c : 10;
};
int main()
{
struct a sa = { 0 };
printf("%d\n", sizeof(sa));
return 0;
}
成员后面的数字指的是比特位,为什么成员的大小设置要变成这样呢?我们知道,位段的位是二进制位,将数据转化为二进制后,表示为00 01 10 11等数字,a是整型,有四个字节32个比特位,但是他只用来表示00 01 10 11的数字,也就是说他只需要两个比特位就能表示存储数据,如此一来就会浪费30个比特位,因此将他设置成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 = 12;
s.c = 3;
s.d = 4;
return 0;
}
枚举
枚举类型的定义
枚举未来的可能取值,默认从0开始,递增1,如果修改后面的值,第一个仍然是0,后面的从改后数字递增1
enum Sex
{
//枚举未来的可能取值
//枚举常量,可改
MALE=5,
FEMALE,
SECRET
};
int main()
{
enum Sex s = MALE;
return 0;
}
枚举的优点
我们可以使用 #define 定义常量,为什么非要使用枚举?
枚举的优点:
1. 增加代码的可读性和可维护性
2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
3. 防止了命名污染(封装)
4. 便于调试
5. 使用方便,一次可以定义多个常量