结构体
在C语言中有多种类型,比如有修饰整型变量的 int ,修饰字符的 char 等,这些常见的数据类型修饰的只是一些简单的变量,而对于复杂的数据就难以修饰了,本次就来介绍一个新的数据类型——结构体。
1. 结构体
1.1 结构体的介绍
结构体是由一批数据组合而成的结构型数据。结构型数据是由多个数据组合而成的,每一个数据被称为结构型数据的"成员"。
数组也是构造类型的一种,也是由多个 “成员” 组成。
结构体和数组的最大不同是:
数组是由多个相同的数据类型组成的。
结构体可以由不同的数据类型组成。
1.2 结构体类型的声明
struct tag {
member-list
} variable-list ;
tag、member-list、variable-list这3部分至少要出现2个。下面将会详细介绍······
1.3 结构体变量的定义
例如我们要定义一个 student 的结构体
struct student
{
int age; //年龄
char name[10]; //姓名
char sex[5]; //性别
char class[10]; //班级
float score; //成绩
};
1.3.1 先定义结构体类型,再定义结构体变量
struct student
{
int age;
char name[10];
char sex[5];
char class[10];
float score;
};
struct student stu1; //stu1是一个结构体类型的变量
1.3.2 结构体类型和结构体变量同时定义
struct student
{
int age;
char name[10];
char sex[5];
char class[10];
float score;
}stu2; //stu2是一个结构体类型的变量
1.3.3 直接定义结构体变量
struct //这里的student省略
{
int age;
char name[10];
char sex[5];
char class[10];
float score;
}stu3; //stu3是一个结构体类型的变量
这种情况属于匿名结构体类型,只能够用一次,为了避免出错,这种情况尽量不要使用。
1.3.4 typedef 的加入
以上三种对结构体变量定义的情况很常见,选择任意一种熟悉的即可。但是还有一种情况例外,就是在 struct 前加上 typedef ,就不是定义变量了,而是数据类型。(typedef 的作用是为复杂的声明定义简单的别名)
具体情况参考以下代码:
typedef struct student //这里student同样可以省略
{
int age;
char name[10];
char sex[5];
char class[10];
float score;
}stu; //相当于将 struct student 的变成 stu ,但功能都是相同的
stu stu1; //等同于struct student stu1;
如果还不理解,就再举个例子:
别人给李明同学取了一个外号叫做小明,虽然名字不一样,但是说的都是一个人。
1.4 结构体的变量的初始化
变量创建好之后就可以开始初始化了。
#include<stdio.h>
struct student
{
int age;
char name[10];
char sex[5];
char class[10];
float score;
}stu1 = { 18,"zhangsan","nan","一年级1班",99 }; //方法1
int main()
{
struct student stu2 = { 19,"lisi","nv","一年级2班",99 }; //方法2
struct student stu3 = { .age = 20,.score = 98,.class = "一年级3班",.sex = "nv",.name =
"wangwu" }; //方法3
printf("%d %s %s %s %f", stu3.age, stu3.class, stu3.name, stu3.sex, stu3.score);
return 0;
}
以上三种方法都能够输出,注意:方法3 成员变量并不是按照结构体中成员的顺序排列的,也能够正常输出,所以顺序是不影响的。
方法1和方法2 、3 又有些不同,方法1 的初始化是全局变量,另外两种方法就属于局部变量,这里就不做过多说明。
2. 结构体内存对齐
我们先来看一段代码:
#include<stdio.h>
struct
{
char ch1;
char ch2;
int x;
}st1 = { 'a','b',10 };
struct
{
char ch1;
int x;
char ch2;
}st2 = { 'a',10 ,'b' };
int main()
{
printf("%d\n", sizeof(st1)); //计算字节数
printf("%d\n", sizeof(st2));
return 0;
}
运行结果:
8
12
经过观察,发现 sizeof 计算两结构体变量字节数有些出入,两结构体的成员只是位置不同,字节数却也会不同,这就要对结构体大小的计算进行探讨了。
2.1 内存对齐规则
- 结构体的第⼀个成员对齐到相对结构体变量起始位置偏移量为0的地址处。(简单来说就是从头开始)
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的⼀个对齐数 与 该成员变量大小的较小值。
- 比如 int 是 4 个字节 ,VS默认对齐数为 8 ,那么 4 和 8 相比 ,4<8,所以 int 的对齐数是 4 (较小的一个)。
- int 类型变量就只能对齐到 4 的整数倍(4,8,12······)的位置,具体位置以实际情况为准
- 每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”。
- 结构体总大小为最大对齐数(结构体中每个成员变量都有⼀个对齐数,所有对齐数中最大的)的整数倍。
- 上文代码为例,在 char (1),int(4) ,char(1)三个数据类型中,最大对齐数为 4,所以结构体总大小为 4 的整数倍(4,8,12,16,20······)
- 结构体也可以嵌套,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
- 如果发生嵌套的情况,可以先计算最里面一层结构体的总大小,再一步步计算。最重要的是把多个结构体内最大对齐数记住,再来根据最大对齐数来存放
图解:
st1:
st2:
2.2 为什么要内存对齐
根据资料:
1、平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2、性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
简单来说:内存对齐提升了效率。
所以尽量把相同类型的变量放在一起
2.3 修改默认对齐数
前面提到了对对齐数的修改,现在就来实践一下:
(如果有需求,要更改默认对齐数,修改的值最好为 2 的次方数)
#include<stdio.h>
#pragma pack(1) //将默认对齐数修改为 1
struct
{
char ch1; // 1个字节
int x; // 4个字节
char ch2; // 1个字节
}st2 = { 'a',10 ,'b' };
#pragma pack() //取消修改默认对齐数
int main()
{
printf("%d\n", sizeof(st2));
return 0;
}
运行结果:
6
3.结构体的传参
通常情况下,传参有两种方法,1是传值调用,2是传址调用。结构体的传参两种方法都可以使用,只是优劣的问题。
对于结构体来说,传值调用的情况下形参需是实参的一份临时拷贝,结构体中的成员可能会有极大的数组或者其他占用空间大的值,这样会很占用空间;而传址调用则会直接根据地址指向相对应的位置,就没有拷贝这一说法。
4.结构体成员访问操作符
在访问结构体成员用到的操作符有两种:
#include<stdio.h>
struct student
{
char name[10];
int age;
};
void print1(struct student* t1) //指针接收
{
printf("%s\n", t1->name);
}
void print2(struct student t2)
{
printf("%d", t2.age);
}
int main()
{
struct student s1 = { "zhangsan",20 };
print1(&s1); //传址
print2(s1); //传值
return 0;
}
运行结果:
zhangsan
20
总结为以下:
结构体变量 . 成员;
结构体指针 -> 成员;
变量对应 .
指针对应 ->
5. 位段
位段,C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位域”( bit field) 。利用位段能够用较少的位数存储数据。(1. 位段基于结构体;2. 为了节省空间;)
5.1 位段的声明和结构
声明:
- 位段的成员必须是 int、unsigned int 或signed int (在C99中位段成员的类型也可以选择其他类型。)
结构:
- 位段的成员名后边是⼀个冒号和一个数字
struct X
{
int a : 2; //数字表示 bit 位
int a : 5;
int a : 8;
int a : 12;
}; //位段 X 所占内存大小为 4
5.2 位段内存的分配
- 位段的成员是 int、unsigned int 或signed int 。
- 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
- 位段不跨平台。
开辟了 1 个字节后,bit 位的使用的方向是不确定的。
当一个空间使用后,剩余空间不足以给下一个成员使用时,剩下的空间是否补齐是 不确定的。
由此可见,在VS中不会使用不足的空间,而是直接开辟新的空间。
5.3 位段的跨平台问题
- int 位段被当成有符号数还是无符号数是不确定的。
- 位段中最大位的数⽬不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
- 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
- 当⼀个结构包含两个位段,第⼆个位段成员比较⼤,无法容纳于第⼀个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
本章在理解上有些难度,注意多加思考!