一、结构体
在之前,我们学了很多数据类型int char …等,还学到了同类型元素构成的数组,以及这些数据类型的指针,在一些小应用可以灵活使用,然而,在实际应用中,每一种变量进行一次声明,再结合起来过于麻烦,类如一位学生的信息管理,他可能有,姓名(char),学号(int)成绩(float)等多种数据。如果把这些数据分别单独定义,就会特别松散、复杂,难以规划,因此我们需要把一些相关的变量组合起来,以一个整体形式对对象进行描述,这就是结构体的好处。
1. 结构体的相关概念
- 一般结构体的第一个字母大写;
- 花括号之后要记得加上分号;
- 该程序表示指定了一个新的结构体类型struct Student (struct 是声明结构体类型时所必须使用的关键字,不可省略),它向编译系统声明这是一个“结构体类型”,它包括id,name,score等不同类型的数据项;
- 结构体的指定并不分配内存空间,只有在定义了变量或者进行初始化之后才分配地址;
2. 结构体变量的定义
(1)先定义结构体类型,再声明变量
// 定义结构体类型
struct Student {
int id;
char name[20];
float score;
};
// 声明结构体变量
struct Student s1, s2; // 声明两个Student类型的变量
(2)定义类型的时候同时声明变量
struct Student {
int id;
char name[20];
float score;
} s1, s2; // 定义类型的同时声明变量
// 后续还可以继续声明该类型的变量
struct Student s3;
3. 结构体变量的初始化
(1)按顺序赋值
s.id = 1;
s.name = "zs"; //错误写法,字符数组不可以被整体赋值
strcpy(s.name,"zs"); //正确方式
s.score = 90;
(2) 直接初始化,不挨个赋值
注意:这种方式,次序不能颠倒,所以不利于有时候我们只需要对部分信息进行初始化;
(3)部分初始化
此时,如果是局部变量,其余没有进行初始化的信息都为0;
!!
结构体变量互相之间可以进行赋值
s1 = s; 两个打印出来的值是一样的
4. 计算结构体的字节数
在 C 语言中,计算结构体的字节数(即sizeof
的结果)需要考虑内存对齐规则。内存对齐是编译器为了提高内存访问效率而采用的一种优化策略,它会在结构体成员之间插入填充字节。硬件通常按字长为单位读取内存,例如:32位系统每次从内存读取4字节,64位系统每次读取8字节。
对齐访问:数据地址是其大小的整数倍,此时硬件可一次性读取数据。(如果是非对齐访问,此时硬件需要多次读取数据并进行拼接数据,导致性能下降)。
内存对齐的基本原则:
1. 成员对齐规则
- 基本类型对齐值:每个基本类型(如
char
、int
、double
)都有一个对齐值,通常等于其自身大小(如int
为 4 字节,double
为 8 字节)。- 结构体成员偏移量:每个成员的起始地址必须是其对齐值的整数倍。如果前一个成员结束后剩余空间不足,则会插入填充字节。
2. 结构体整体对齐规则
- 结构体总大小:必须是结构体中最大对齐值的整数倍。如果不足,则在结构体末尾插入填充字节。
计算步骤——简单结构体:
struct S1 { char a; // 1字节 int b; // 4字节 short c; // 2字节 };
a
从偏移量 0 开始,占用 1 字节(地址 0);b
的对齐值为 4,需从偏移量 4 开始,因此a
后填充 3 字节(地址 1~3),b
占用 4 字节(地址 4~7);c
的对齐值为 2,从偏移量 8 开始,占用 2 字节(地址 8~9);- 结构体总大小需是最大对齐值(4)的整数倍,因此总大小为 12 字节(地址 0~11),末尾填充 2 字节(地址 10~11)。
计算步骤——包含数组和指针:
struct S2 { char a; // 1字节 int* p; // 指针:64位系统为8字节 short arr[3]; // 数组:3*2=6字节 };
a
从偏移量 0 开始,占用 1 字节(地址 0);p
的对齐值为 8,需从偏移量 8 开始,因此a
后填充 7 字节(地址 1~7),p
占用 8 字节(地址 8~15);arr
的对齐值为 2(short
类型),从偏移量 16 开始,占用 6 字节(地址 16~21);- 结构体总大小需是最大对齐值(8)的整数倍,因此总大小为 24 字节(地址 0~23),末尾填充 2 字节(地址 22~23)。
对齐几大原则:
默认按照计算机位数对齐:64 / 8 = 8,最终大小必须为8的整数倍
从结构体中的成员中查找最大字节的成员,最终按此成员大小对齐
把每个成员按照声明顺序依次存放入内存,偏移量sizeof(成员)必须能够整除
5. 结构体的值传递和指针传递
(1)值传递:
(2)指针传递:比起值传递效率更高
、
6. 练习
- 结构体遍历
- 逆序
- swap函数实现逆序
- 排序
无法直接使用结构体变量进行比较,但是结构体内部成员单独拎出来可以进行比较
- 采用qsort函数按学生姓名排序
二、共用体
1. 共用体的定义
2. 内存占用特点
- 空间是重合的 共用体的所有成员共用了相同的内存空间,最终的内存大小和它最大的是一样的
- 只有最后被赋值的那个成员是有效的
- 共享总是从起始地址开始共享
3. 应用
- 判断计算机大端序还是小端序
#include <stdio.h>
union Endian {
int i;
char c;
} e;
int main() {
e.i = 1;
if (e.c == 1) {
printf("小端字节序\n");
} else {
printf("大端字节序\n");
}
return 0;
}
三、枚举
- 实际上也是一种数据类型
- 将变量的值全部列举出来,变量的值只限于列出来的值的范围内
eg:创造一个存放星期的
注意:
- 在c语言中,对枚举元素按常量处理,故称为枚举常量,他们不是变量,不能进行赋值;
- 枚举元素作为常量,是有值的,按照顺序依次为0,1,2 ...;
- 枚举值可以用来作判断比较:
四、typedef
- 作用:给已有的数据类型起别名
int INT;
typedef int INT; //相当于给原来的int 起了别名为INT
通过输出字节数得已验证:
//运行结果为4
- 作用:避免我们写很长的关键字
此时,就可以写成Students s的形式了,避免再去写struct了
- 但并不是时时刻刻都可以用,不建议使用其进行简化,是的代码较难读懂
*PStudent 也变成类型名了,为student*型,所以说明t是个指针,此时t不带*了,容易分不清
输出结果为 80 ,是一个装有指针的数组
五、位运算
位运算是指二进制位的运算,常用来处理二进制位的问题,
- 位运算符中除了~以外,均为双目运算符,即要求两侧各有一个运算量
- 运算量只能是整形或者字符型的数据,不能为实型数据
1. 按位与 &
0 & 0 = 0;
0 & 1 = 0;
1 & 0 = 0;
1 & 1 = 1
作用:指定位清零
2. 按位或 |
0 | 0 = 0;
0 | 1 = 1;
1 | 0 = 1;
1 | 1 = 1;
作用:指定位置1
3. 按位异或 ^
参与运算的两个二进制位,同号则为0(假),异号则为1 (真)。
0 ^ 0 = 0
0 ^ 1 = 1;
1 ^ 0 = 1;
1 ^ 1 = 0;
作用:指定位翻转
4. 取反 ~
~ 0 = 1;
~ 1 = 0;
逻辑取反不等于按位取反
5. 左移 <<
- 将二进制位向左移动指定的位数,右侧空出的位用 0 填充,左侧溢出的位被丢弃。
unsigned int a = 5; // 二进制: 0000 0000 0000 0000 0000 0000 0000 0101 unsigned int b = a << 2; // 左移2位 → 0000 0000 0000 0000 0000 0000 0001 0100 → 十进制20
计算步骤:
5 << 2
→5 * (2^2)
→5 * 4
=20
- 对负数的处理:对于有符号数,若符号位未被移出(即左移后符号不变),则结果仍为原符号的数值;若符号位被移出,则结果变为负数(溢出导致)。
int a = -5; // 补码表示: 1111 1111 1111 1111 1111 1111 1111 1011 int b = a << 2; // 左移2位 → 1111 1111 1111 1111 1111 1111 1110 1100 → 十进制-20
6. 右移 >>
- 将二进制位向右移动指定的位数,左侧空出的位根据原数的符号位填充(算术右移)或用 0 填充(逻辑右移)。
算术右移:用于有符号数,左侧补符号位(即正数补 0,负数补 1)。
逻辑右移:用于无符号数,左侧补 0。
unsigned int x = 0x80000000; // 二进制: 1000 0000 ... 0000
unsigned int y = x >> 1; // 逻辑右移 → 0100 0000 ... 0000 → 十进制2147483648
int a = 0x80000000; // 有符号数,二进制: 1000 0000 ... 0000(表示-2147483648)
int b = a >> 1; // 算术右移 → 1100 0000 ... 0000 → 十进制-1073741824
- 应用: