文章目录
- 一、结构体
- 1. 结构体类型的概念和声明
- 2. 结构体变量的创建和初始化
- 3. 结构体成员的访问
- 3.1 直接访问
- 3.2 间接访问
- 4. 结构体的内存对齐
- 4.1 内存对齐的规则
- 4.2 内存对齐的原因
- 4.3 修改默认对齐数
- 5. 结构体传参
- 6. 结构体实现位段
在C语言中,已经提供了一些基本的内置类型,如int、char、short等等。但有时,我们的一些数据不能只从是简单地归结于这些单一的数据类型。所以,我们就需要 自定义类型。常见的自定义类型有三种: 结构体、联合体、枚举。今天我们首先来介绍一下最重要的:结构体。
一、结构体
1. 结构体类型的概念和声明
结构是一些值的集合,这些值被称为成员变量。结构的每个成员可以是不同类型的变量,如指针、字符、数组,甚至是又一个结构体。
结构体的声明为:
struct name
{
member;
member;
......
};
其中,struct是C语言中的一个关键字,name是结构体变量的名字,里面的member是成员。
举个栗子,我想从名字、年龄、身高描述一个人,写成结构体类型就是:
struct human
{
char name[20];//名字
int age;//年龄
float length;//身高
};
//一定记得最后的分号不能丢
//一个话题外的小知识点:一个汉字相当于两个字符的大小
除此之外,还有一种结构体的匿名声明,在声明结构体类型时不写出名字,这个类型的变量只能在声明后紧接着创建,结构体变量创建好后它的声明直接销毁,也就是只能用一次。在实际应用中,这种特殊的声明方式其实完全没必要使用,大家仅做了解就好~
2. 结构体变量的创建和初始化
创建好一个结构体类型后,我们就可以创建这种类型的结构体变量了,创建的方式有两种:
//方式一:在结构体类型的声明后直接创建,如a1
struct human
{
char name[20];
int age;
float length;
}a1;
//方式二:结构体声明后在下面的语句创建,如a2
struct human a2;
创建后,当然也要存入数据了,结构体变量的初始化方法为:
struct human
{
char name[20];
int age;
float length;
}a1;
//默认情况下,数据是按照声明的结构体成员顺序初始化的:
a1 = {"zhangsan", 18, 180.0};
struct human a2 = {"lisi", 20, 178.0};
//也可以指定顺序初始化,要在成员名前加. :
struct human a3 = {.length=175.5, .name="wangwu", .age=30};
3. 结构体成员的访问
3.1 直接访问
结构体成员的直接访问是通过点操作符.
完成的,如下:
struct human
{
char name[20];
int age;
float length;
}a1 = {"zhangsan", 18, 180.0};
printf("年龄是:%d\n", a1.age);
printf("身高是:%f\n", a1.length);
3.2 间接访问
结构体类型也是一种类型,创建了一种结构体类型,自然也有指向这种结构体变量的指针类型了:
struct human
{
char name[20];
int age;
float length;
}a1;
struct human* p1 = &a1;
有时候我们得到的不是一个结构体变量,而是一个指向结构体的指针,我们需要用这个指针找到结构体的成员,这就是结构体成员的间接访问,要用到->
操作符:
struct human
{
char name[20];
int age;
float length;
}a1 = {"zhangsan", 18, 180.0};
struct human* p1 = &a1;
printf("年龄是:%d\n", p1->age);
printf("身高是:%f\n", p1->length);
结果也是对的
4. 结构体的内存对齐
4.1 内存对齐的规则
知道了结构体的基本使用方法,我们更深入一点,学习一下计算结构体的大小,这涉及到结构体内存对齐。
基本规则为:
- 结构体的第一个成员,对齐到和结构体变量起始位置相同的地址(偏移量为0)
- 其他成员变量,对齐到偏移量为对齐数的整数倍的地址处
- 每个成员的对齐数是,编译器的默认对齐数和该成员变量的大小两者中的较小值。VS的默认对齐数是8;Linux中gcc没有默认对齐数,对齐数即是成员自身大小。
- 结构体的总大小是:所有成员的对齐数中的最大值的整数倍
我们来分析两个结构体就理解了(以下代码都在VS下运行):
例子1:
struct s1
{
char c;
int i;
double d;
};
- 第一个成员c的大小是1,占据内存开始偏移量为0~1的地址。1和默认对齐数8相比,较小值是1,所以name的对齐数是1。
- 第二个成员i的大小是4,和默认对齐数8相比,较小值是4,所以i的对齐数是4,要存在偏移量是4的整数倍的地址处。从1往后数,是4,所以i存在偏移量4~8的地址处。
- 第三个成员d的大小是8,和默认对齐数8相比,较小值是8,所以d的对齐数是8,要存在偏移量是8的整数倍的地址处。8正好是,所以id存在偏移量8~16的地址处。
- 而所有成员的对齐数中最大对齐数是8,结构体的总大小是8的倍数。16正好是,
所以这个结构体的大小是16。
例子2:
struct s2
{
char c1;
int i;
char c2;
}
- 第一个成员c1的大小是1,占据内存开始偏移量为0~1的地址。1和默认对齐数8相比,较小值是1,所以c1的对齐数是1。
- 第二个成员i的大小是4,和默认对齐数8相比,较小值是4,所以i的对齐数是4,要存在偏移量是4的整数倍的地址处。从1往后数,是4,所以i存在偏移量4~8的地址处。
- 第三个成员c2的大小是1,和默认对齐数8相比,较小值是1,所以c2的对齐数是1,要存在偏移量是1的整数倍的地址处。8正好是,所以id存在偏移量8~9的地址处。
- 而所有成员的对齐数中最大对齐数是4,结构体的总大小是4的倍数。从9往后数,是12,
所以这个结构体的大小是12。
4.2 内存对齐的原因
原因了解一下就好:
- 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
- 性能原因:数据机构应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需一次访问。假设一个处理器总是从内存中取八个字节,则地址必须是八的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成八的倍数,那么就可以用一个内存操作来读或写值了。否则,我们需要执行两次内存访问,因为对象可能被分别放在两个八字节内存块中。
换句话说,结构体的内存对齐,是用空间节省时间的做法。
所以,我们在设计结构体时,为了尽量节省空间,应该做到:让占用空间小的成员尽量集中在一起。比如:
struct s1
{
char c1;
int i;
char c2;
};
struct s2
{
char c1;
char c2;
int i;
}
s2所占空间就比s1要小。
4.3 修改默认对齐数
#pragma
这个预处理指令,可以改变编译器的默认对齐数
用法是:#pragma pack(n)
,n是我们希望改成的默认对齐数;使用完这个我们修改的对齐数,写下#pragma pack()
,就能取消设置的对齐数,还原为编译器默认的。
这样,在结构体内存对齐方式不合适的时候,我们可以自己设计对齐方式。
5. 结构体传参
结构体类型也是可以作为函数参数传递的,但传的时候,我们有传值和传址两种选择。大多数情况下,都建议选择传递结构体的地址(指针)。
原因是:函数传参的时候,参数需要压栈,会有时间和空间上的系统开销。如果传递一个结构体时这个结构体过大,参数压栈的系统开销比较大,可能会导致性能的下降。
6. 结构体实现位段
位段也是一种数据结构,今天做一个初步的了解就好,它的声明和使用和结构体极为相似。只有两点不同:
- 位段的成员必须是int、unsigned int、signed int,在C99中位段成员的类型也可以选择其他类型。
- 位段的成员名后边要写一个冒号和一个不超过32的数字。
每个成员名后的数字代表着给这个成员变量申请的空间大小(单位是比特)。
比如:
struct s1
{
int i:10;
char c1:2;
char c2:4;
};
但要知道的是:
- 位段上的空间是按照需要以4个字节或1个字节的方式来开辟的
- 位段涉及很多不确定因素:int位段被当做有符号数还是无符号数是不确定的、位段中最大的数目不能确定(16位机器上不能超过16,32位机器上不能超过32)、位段中成员在内存中是从左向右分配还是从右向左分配的是没有标准定义的、一个位段成员用剩下的比特位是给下一个位段成员用还是浪费是不确定的。综上所述,位段涉及很多不确定因素,它是不跨平台的,注重可移植性的程序应当避免使用位段。而在企业中,使用位段,一般会根据不同的环境,写出不同的代码。
- 访问位段成员时,不能对位段成员使用&操作符,也不能用scanf函数直接给位段成员输入值,只能是先输入放在一个变量中,再赋值给位段成员。
本篇完,感谢阅读