目录
2021.8.26
结构体
结构体类型的声明
结构的基础知识
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
结构的声明
代码如下(示例):
struct tag
{
member-list;// 成员列表
}variable-list;// 变量列表
// 声明一个结构体类型
struct Stu
{
char name[20];// 姓名
char tele[12];// 电话
char sex[10];// 性别
int age;// 年龄
}s3,s4;// 必须加分号
struct Stu s2;// 全局变量
int main()
{
// 创建的结构体变量
struct Stu s1;
return 0;
}
匿名结构体
代码如下(示例):
// 如果这里没有tag,则说明我们创建的是一个匿名结构体,也就只能使用我们创建好的s了
struct
{
char name[20];// 姓名
char tele[12];// 电话
char sex[10];// 性别
int age;// 年龄
}s;
struct
{
char name[20];// 姓名
char tele[12];// 电话
char sex[10];// 性别
int age;// 年龄
}* ps;
int main()
{
ps = &s;// err,编译器会把上面两个声明当成完全不同的两个类型
return 0;
}
结构体的自引用
结构体自引用的错误方式:
代码如下(示例):
struct Node
{
int data;
struct Node next;
};
结构体自引用的正确方式:
代码如下(示例):
struct Node
{
int data;
struct Node* next;// 想用这种方式,则结构体不能匿名
};
// 结构体重命名
typedef struct Node
{
int data;
struct Node* next;// 想用这种方式,则结构体不能匿名
}Node;
结构体变量的定义和初始化
代码如下(示例):
struct T
{
double weight;
int age;// 年龄
};
struct Stu// 结构体定义
{
char name[10];// 姓名
struct T st;
double b;
char c;
};
int main()
{
struct Stu s = { "张三",{71.2 , 21}, 20.00,'t' };// 结构体初始化
printf("%s, %lf, %d, %lf, %c", s.name, s.st.weight, s.st.age, s.b, s.c);
return 0;
}
结构体内存对齐
结构体的对齐规则:
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值,vs中默认值为8
-
第一个成员在与结构体变量偏移量为0的地址处。
-
其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
-
结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
-
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
代码如下(示例):
struct S1
{
char c1;
int a;
char c2;
};
struct S2
{
char c1;
char c2;
int a;
};
int main()
{
struct S1 s1 = { 0 };
struct S2 s2 = { 0 };
printf("%d\n", sizeof(s1));// 12
printf("%d\n", sizeof(s2));// 8
return 0;
}
为什么存在内存对齐?
-
平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
-
性能原因:数据结构(尤其是栈)应该尽可能的在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说:
结构体的内存对齐是拿空间来换取时间的做法;
为了避免过多内存的浪费,我们在设计结构体的时候,应该尽量让内存小的成员集中在一起
修改默认对齐数
代码如下(示例):
#pragma pack(4)// 设置默认对齐数为4
struct S1
{
char c1;
int a;
char c2;
};
#pragma pack()// 取消设置的默认对齐数,还原为默认
#pragma pack(1)// 设置默认对齐数为1
struct S2
{
char c1;
char c2;
int a;
};
offsetof
计算结构体中某变量相对于首地址的偏移
代码如下(示例):
offsetof (type,member);// 宏
struct S1
{
char c1;
int a;
char c2;
};
int main()
{
printf("%d\n", offsetof(struct S1, c1));
printf("%d\n", offsetof(struct S1, a));
printf("%d\n", offsetof(struct S1, c2));
return 0;
}
结构体传参
参数传值的时候,参数需要压栈(先进后出),会有时间和空间上的开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比价大,所以会导致性能的下降
结论:结构体传参的时候,要传结构体的地址。
代码如下(示例):
struct Stu
{
char a;// 姓名
double b;
int c;
};
void Init(struct Stu* ps)
{
ps->a = 'f';
ps->b = 3.14;
ps->c = 20;
}
int main()
{
struct Stu s = {0};
Init(&s);
// Init(s);
printf("%c, %lf, %d", s.a, s.b, s.c);
return 0;
}
结构体实现位段(位段的填充&可移植性)
结构体讲完就得讲讲结构体实现位段的能力。
什么是位段
位段的声明和结构是类似的,有两个不同:
位段的成员必须是 int,unsigned int,signed int。
位段的成员名后边有一个冒号和一个数字
位段的声明
代码如下(示例):
struct S
{
int a : 2;// a占2个bit位
int b : 5;// b占5个bit位
int c : 10;// c占10个bit位
int d : 30;// d占30个bit位
};
// 2+5+10+30=47 --> 6个字节*8 = 48bit
int main()
{
struct S s = {0};
s.a = 3;// 3 的二进制是 011,内存的存储是从右向左存的,所以对于a来说,系统内只存了 11
s.b = 100;// 100 = 0110 0100,而b只有5个字节,所以对于b来说,系统内只存了 0 0100
printf("%d", sizeof(s));// 8
return 0;
}
位段的内存分配
-
位段的成员可以是 int unsigned int signed int 或者是 char(属于整形家族)类型
-
位段的空间上是按照需求以4个字节(int)或者一个字节(char)的方式来开辟的。
-
位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
解释一下为什么我们只需要6个字节就够了,却还是占用了8个字节
因为是int类型,所以我们先拿出4个字节(32个比特位)来存放a,b,c,d4个变量。如果是char类型则一次开辟一个字节
当前面a,b,c存放完成以后,第一个32比特位还剩下15个字节,又由于我们的 d需要占用30个bit位,所以仅剩的15个字节是不够用的,所以系统会直接浪费掉剩下的15个字节,再另外开辟4个字节来存放这30个bit,最后这剩下的两个bit也会被浪费掉。
另外,对于位段来说,因为只能定义int类型,所以我们赋值给的bit位不能大于32位
位段的跨平台问题
int位段被当成有符号数还是无符号数是不确定的。
位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写出27,在16位机器会出问题)
位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的bit位时,是舍弃剩余的bit还是利用,这也是不确定的
总结:
跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
枚举
枚举类型的定义
代码如下(示例):
// 枚举类型
enum Sex
{
// 枚举的可能取值 - 常量
MALE,// 0
FEMALE,// 1
SECRET = 5 // 2,我们也可以赋予一个初始值
};
int main()
{
enum Sex s = MALE;// 只能是枚举可能取值中的一种
printf("%d %d %d",MALE,FEMALE,SECRET);// 0 1 2
return 0;
}
枚举的优点
增加代码的可读性和可维护性
和#define定义的标识符比较,枚举有类型检查,更加严谨
防止了命名污染(封装)
便于调试
使用方便,一次可以定义多个常量
枚举的使用
代码如下(示例):
enum Color
{
RED = 1,
GREEN = 2,
BLUE = 5
};
enum Color c = GREEN;// 只能拿枚举常量才能给枚举变量赋值
c = 5;
联合(共用体)
联合类型的定义
联合也是一种特殊的自定义类型,这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。比如:
代码如下(示例):
// 联合-联合体-共用体
union Un
{
char c;
int i;
};
联合的特点
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大得那个成员)
代码如下(示例):
// 联合-联合体-共用体
union Un
{
char c;
int i;
};
int main()
{
union Un u;
printf("%d\n", sizeof(u));// 这个大小至少是最大成员的大小
printf("%p\n", &u);// 所有成员共用同一个地址
printf("%p\n", &(u.c));
printf("%p\n", &(u.i));
return 0;
}
联合体判断大小端内存存储
代码如下(示例):
int check_sys() {
union Un
{
char c;
int i;
}u;
u.i = 1;
// i占4个字节,c占1个字节,因为他俩共用一个地址,
// 所以我只要返回c(i的第一个字节),也就知道当前的存储方式是大端还是小端了
return u.c;
}
联合大小的计算
-
联合的大小至少是最大成员的大小
-
当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
代码如下(示例):
union Un
{
char c[5];
int i;
};
int main()
{
union Un u;
printf("%d\n", sizeof(u));// 虽然最大字节数为5,但最大对齐数是4,所以对齐到最大对齐数的倍数就是8
return 0;
}
完