结构体
1.如何定义和使用结构体指针?
1.结构体指针的定义
首先需要定义结构体类型,例如表示学生信息的结构体:
struct Student {char name[50];int age;float score;
};
接着,使用struct 关键字和指针符号* 声明结构体指针:
struct Student *pStudent; // 声明指向Student结构体的指针)
2.结构体指针的初始化
1.可以指向现有结构体变量:
struct Student stu1 = {"Alice", 20, 90.5};
struct Student *pStu = &stu1; // 指针指向stu1的地址)
2.使用malloc 在堆上分配内存:
struct Student *pStu = (struct Student*)malloc(sizeof(struct Student));
if (pStu != NULL)
{pStu->age = 22; // 通过指针初始化成员strcpy(pStu->name, "Bob");
}
// 使用后需手动释放内存
free(pStu); // 避免内存泄漏)
3.通过指针访问结构体成员
1.箭头运算符->
printf("姓名:%s\n", pStu->name); // 输出:Bob)
2.解引用与点运算符结合
printf("年龄:%d\n", (*pStu).age); // 输出:22)
4.结构体指针的应用场景
1.函数参数传递
传递指针可以避免复制大型结构体,提升效率:
void updateScore(struct Student *s, float newScore)
{s->score = newScore; // 直接修改原结构体成员)
}
2.动态数据结构
用于构建链表、树等动态结构:
struct Node {int data;struct Node *next; // 指向下一个节点的指针)
};
3.遍历结构体数组
通过指针操作数组元素:
struct Student stus[3] = {{"Tom", 18, 85.5}, ...};
struct Student *p = stus;
for (int i = 0; i < 3; i++)
{printf("%s的分数:%.1f\n", p->name, p->score);p++; // 指针移动到下一个元素)
}
2. 如何将结构体作为函数参数?
主要有三种方式:值传递、指针传递和数组传递。
1.结构体值传递
将结构体变量作为参数直接传递给函数。函数会复制整个结构体的副本。
优点:函数内部对结构体的修改不会影响外部变量。
缺点:如果结构体较大,会比较耗时间。
// 定义结构体
typedef struct {int id;char name[20];
} Student;// 值传递:函数接收结构体副本
void printStudent(Student stu)
{printf("ID: %d, Name: %s\n", stu.id, stu.name);
}int main()
{Student tom = {1, "Tom"};printStudent(tom); // 传递结构体变量return 0;
}
2.结构体指针传递
将结构体变量的地址作为参数传递给函数,函数通过指针操作结构体。
优点:无需复制整个结构体,因此效率高,可在函数中修改原始结构体的值。
缺点:需要使用指针语法->,增加代码复杂度。
typedef struct {int id;char name[20];
} Student;// 指针传递:函数接收结构体指针
void updateStudent(Student *stu, int newId, char *newName)
{stu->id = newId; // 通过指针访问成员(-> 操作符)strcpy(stu->name, newName);
}int main()
{Student tom = {1, "Tom"};updateStudent(&tom, 2, "Jerry"); // 传递结构体地址printf("ID: %d, Name: %s\n", tom.id, tom.name); // 输出修改后的值return 0;
}
3.结构体数组传递
当函数参数是结构体数组时,可传递数组名。
typedef struct {int id;char name[20];
} Student;// 传递结构体数组(数组名作为指针)
void printStudents(Student students[], int count)
{for (int i = 0; i < count; i++) {printf("ID: %d, Name: %s\n", students[i].id, students[i].name);}
}int main()
{Student class[] = {{1, "Alice"},{2, "Bob"}};printStudents(class, 2); // 传递结构体数组return 0;
}
实际开发中,指针传递时最常用的方式,既能避免复制开销,又能灵活操作结构体内容。
3. 结构体的内存对齐是什么?如何按指定字节对齐?
结构体的内存对齐是指编译器在为结构体分配内存时,按照一定规则(如成员自身大小、指定对齐字数等)来调整成员的存储位置,使得每个成员的地址满足特定对齐要求的过程。
内存对齐的目的是提高内存访问效率。
1.内存对齐的基本规则
1.成员对齐规则
结构体中的每个成员的偏移量(相对于结构体起始地址)必须是该成员类型大小的整数倍。
例如:
struct Example {char a; // 大小 1 字节,偏移量 0(0 是 1 的倍数)int b; // 大小 4 字节,偏移量需是 4 的倍数。由于前一个成员占 1 字节,下一个可用偏移量为 1,不是 4 的倍数,因此编译器会在 char 后填充 3 字节,使 int 的偏移量为 4
}; // 结构体总大小为 8 字节(4 + 4,最后一个成员大小的整数倍)
2.结构体整体对齐规则
结构体的总大小必须是其成员中最大对齐数的整数倍。
2.按指定字节对齐的方法
1.使用#pragma pack()---------通用方法
// 设置对齐字节数为 n(n 通常为 1、2、4、8、16 等 2 的幂次)
#pragma pack(n) struct AlignedStruct {char a; // 偏移量 0(1 的倍数)double b; // 若 n=8,double 大小 8 字节,偏移量需是 8 的倍数。char 后需填充 7 字节,使 double 偏移量为 8
}; // 总大小为 16 字节(8 的倍数)#pragma pack() // 恢复默认对齐(或用 #pragma pack(pop))
2.GCC 编译器 __attribute__((aligned(n)))
// 指定结构体按 n 字节对齐(n 需是 2 的幂次)
struct AlignedStruct __attribute__((aligned(8))) {char a;double b; // 偏移量 8(8 的倍数)
};
共用体
1.共用体和结构体有什么区别?
1.内存分配方式
结构体的每个成员有独立的内存空间,结构体的总大小是所有成员大小之和。
struct Data {int a; // 占 4 字节char b; // 占 1 字节(对齐后可能补 3 字节)double c; // 占 8 字节
}; // 总大小至少为 4 + 4(对齐) + 8 = 16 字节
共用体所有成员共享一块内存空间,同一时刻只能有一个成员有效。共用体的总大小为其最大成员的大小。
union Data {int a; char b; double c;
}; // 总大小为 8 字节(取最大成员 `double` 的大小)
2.内存访问特性
结构体可以同时访问所有成员,每个成员的值独立存储。
struct Data var;
var.a = 10; // 合法
var.b = 'x'; // 合法
var.c = 3.14; // 合法
共用体同一时间只能访问最后一次赋值的成员,访问其他成员会导致数据错误(除非成员类型兼容)
union Data var;
var.a = 10; // 此时内存存储 `int` 类型数据
printf("%d\n", var.a); // 合法,输出 10
printf("%f\n", var.c); // 未定义行为(强行将 `int` 当作 `double` 解析)
3.初始化方式
结构体在定义时可以初始化所有成员:
struct Data var = {10, 'x', 3.14}; // 合法
共用体只能初始化第一个成员:
union Data var = {10}; // 合法,初始化 `a`
union Data var = {.c=3.14}; // C99 及以上支持指定成员初始化
4.典型用途
结构体用于存储同时需要存在的多个相关数据,例如学生信息(姓名、年龄、成绩),坐标点(x、y、z)等。
共用体用于节省内存,当数据在不同场景下以不同类型存在时(即互斥使用),例如表示一个变量可能是整数、字符、浮点数等,常见场景包括协议解析、内存共享等。
5.语法关键字
结构体用struct 关键字来定义:
struct 结构体名 { 成员列表; };
共用体用union 关键字来定义:
union 共用体名 { 成员列表; };
2. 共用体在内存中的存储特点是什么?
1.内存共享空间
共用体的所有成员共享同一块内存区域,该区域的起始地址相同。
例如:如果要定义一个包含int 和 char 成员的共用体,这两个成员会从同一地址开始存储。
union Data {int i;char c;
};
union Data d; // d.i 和 d.c 共享同一块内存
2.内存大小由最大成员决定
共用体的内存占用空间等于其最大成员的大小(需考虑内存对齐)。例如:
union Example {char c; // 1 字节int i; // 4 字节(假设 int 为 4 字节)double dbl; // 8 字节
}; // 共用体大小为 8 字节(由 double 决定)
3.同一时间仅存储一个成员的值
每次只能向共用体的一个成员赋值,后续赋值会覆盖之前成员的数据。例如:
d.i = 100; // 此时内存中存储 int 类型的 100
d.c = 'A'; // 此时内存中存储 char 类型的 'A'(覆盖之前的 int 数据)
4.内存对齐规则
共用体的内存对齐方式遵循其成员中对齐要求最严格的成员。例如:
若成员包含double ,则共用体的起始地址会按 8 字节对齐。
5.访问成员的类型安全问题
由于共用体成员共享内存,访问时必须明确当前存储的是哪个成员的类型,否则会导致未定义行为,产生段错误。
3. 如何用共用体判断大小端?
利用共用体所有成员共享内存的特性,
1.定义一个包含 int 类型和 char 数组的共用体
2.给 int 成员赋值为1
3.检查char 数组的第一个字节:
小端模式下最低位字节存放在低地址,所以第一个字节是1
大端模式下最高位字节存放在低地址,所以第一个字节是0
程序运行后会直接输出当前系统的字节序类型。
union EndianCheck {int num;char bytes[sizeof(int)];
};int main()
{union EndianCheck ec;ec.num = 1;if (ec.bytes[0] == 1) {printf("小端模式(Little-Endian)\n");} else {printf("大端模式(Big-Endian)\n");}return 0;
}
位域
1.什么是结构体位域?如何定义和使用位域?
结构体位域是一种允许在结构体中直接定义以位为单位的成员的机制,主要用于优化内存,比如处理寄存器配置。
定义:
位域通过在结构体成员声明中使用 类型 成员名 : 位数
struct 结构体名 {类型 成员名1: 位数1; // 位域成员类型 成员名2: 位数2;// 普通成员(非位域)类型 普通成员名;
};
类型:必须是int 、 unsigned int 、 signed int
位数:表示该成员占用的二级制位数,取值为正整数。
例如:定义一个表示颜色分量(RGB)的结构体,每个分量占5位(共15位,用unsigned int 存储)
struct Color {unsigned int red: 5; // 红色分量,占 5 位(0-31)unsigned int green: 5; // 绿色分量,占 5 位unsigned int blue: 5; // 蓝色分量,占 5 位unsigned int alpha: 7; // 透明度,占 7 位(0-127),剩余 1 位未使用
};
使用:
1.声明变量并赋值:
struct Color c;// 像普通结构体成员一样赋值
c.red = 20; // 20 是合法值(20 < 32,未超过 5 位范围)
c.green = 31; // 最大值 31(2^5 - 1)
c.blue = 0;
c.alpha = 127; // 最大值 127(2^7 - 1)
2.访问位域成员:
通过结构体变量名 + 成员运算符 . 直接访问:
printf("Red: %u\n", c.red); // 输出:20
printf("Alpha: %u\n", c.alpha); // 输出:127
2. 位域的应用场景有哪些?
位域是一种允许在结构体中定义以位为单位的字段的特性,其核心优势是节省内存空间并直接操作二进制位。
1.硬件寄存器映射
嵌入式设备的寄存器通常由多个独立位段组成,通过位域可直接读写寄存器的特定位段,无需手动进行位运算(如& 、 |):
// 寄存器地址:0x40000000
// 位31-24:保留
// 位23-16:时钟频率选择(8位)
// 位15-8:使能标志(1位有效,其余保留)
// 位7-0:数据长度(8位)
struct peripheral_reg {unsigned int reserved1:8; // 位31-24(保留)unsigned int clk_freq:8; // 位23-16(时钟频率)unsigned int enable:1; // 位15(使能标志,其余7位保留)unsigned int data_len:8; // 位7-0(数据长度)
};
volatile struct peripheral_reg *reg = (volatile struct peripheral_reg*)0x40000000;// 使用位域操作:
reg->enable = 1; // 使能设备
reg->clk_freq = 0b1010; // 设置时钟频率
不过,如果需要频繁的对字段进行位运算(异或、移位),位域不如直接操作整数来得高效。