结构体类型的声明
结构的声明
struct tag
{
member-list;
}variable-list;
例子
struct book
{
char title[MAXTITL];//字符串类型的titile
char author[MAXAUTL];//字符串类型的author
float value;//浮点型的value
};
关于其声明的位置,也就是这段代码要放到哪里。同样这也是具有作用域的。
这种声明如果放在任何函数的外面,那么则可选标记可以在本文件中,该声明的后面的所有函数都可以使用。如果这种声明在某个函数的内部,则它的标记只能咋内部使用,并且在其声明之后
结构体变量的创建和初始化
注意这只适用于c99
在 Visual Studio 中,使用带有指定初始化器的结构体变量的语法可能会导致编译错误,因为 Visual Studio 对 C99 标准的支持有限,并不支持这种初始化方式。
要在 Visual Studio 中正确初始化结构体变量,可以按照结构体成员的顺序进行初始化,或者使用普通的赋值语句来初始化。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
struct Stu
{
char name[20]; // 名字
int age; // 年龄
char sex[5]; // 性别
char id[20]; // 学号
};
int main()
{
// 按照结构体成员的顺序初始化
struct Stu s = { "张三", 20, "男", "20230818001" };
printf("name: %s\n", s.name);
printf("age : %d\n", s.age);
printf("sex : %s\n", s.sex);
printf("id : %s\n", s.id);
// 按照指定的顺序初始化
struct Stu s2 = { .age = 18, .name = "lisi", .id = "20230818002", .sex = "女" };
printf("name: %s\n", s2.name);
printf("age : %d\n", s2.age);
printf("sex : %s\n", s2.sex);
printf("id : %s\n", s2.id);
return 0;
}
结构的特殊声明
可以不完全的声明,也就是匿名,在声明的时候省略掉了结构体标签。
struct
{
int x;
int y;
} point;
在这个例子中,定义了一个匿名的结构体,它包含了两个 int
类型的成员 x
和 y
。同时,也声明了一个结构体变量 point
,这个结构体变量的类型就是刚刚定义的匿名结构体类型。
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
p = &x;
编译器会把上⾯的两个声明当成完全不同的两个类型,所以是非法的。
使用匿名结构体类型的优点是可以节省代码行数,特别是在定义简单的结构体时。不过,匿名结构体类型不能被重复使用,只能在定义的同时声明结构体变量。
结构的自引用
结构的自引用是指在结构体的定义中包含对自身类型的引用。 这种引用可以是直接的,也可以是通过指针间接实现的。
看看下面这个例子可以吗
struct Node
{
int data;
struct Node next;
};
它在结构中包含⼀个类型为该结构本⾝的成员是不行的,因为结构体Node
中的next
成员直接引用了结构体Node
类型。这种直接引用会导致编译错误,因为结构体的大小无法确定,会导致无限循环嵌套。
正确的做法是通过指针来实现结构体的自引用
struct Node {
int data;
struct Node* next; // 使用指针引用自身类型
};
next
成员是一个指向结构体Node
类型的指针,而不是直接引用结构体类型。这样就避免了结构体大小不确定的问题,也可以实现链表等数据结构。
那这种typedef对匿名结构体类型重命名可行吗
typedef struct
{
int data;
Node* next;
}Node;
Node
结构体内部使用了一个指向Node
类型的指针next
,但是在声明该指针时并没有给出完整的类型信息,因为在 Node* next;
中Node
并不是一个已知的类型。编译器会报错,因为在Node* next;
语句中并没有定义Node
这个结构体类型,Node是对前⾯的匿名结构体类型的重命名产生的,但是在匿名结构体内部提前使用Node类型来创建成员变量,这是不行的。
使用定义结构体不要使用匿名结构体了
typedef struct Node {
int data;
struct Node* next; // 使用完整的类型信息
} Node;
结构体内存对齐
对齐规则
结构体的第⼀个成员对齐到和结构体变量起始位置偏移量为0的地址处
结构体的每个成员都按照其自身的大小进行对齐。例如,一个char
类型的成员通常在任何位置都能被访问,而一个int
类型的成员可能需要在地址上对齐到4字节或8字节的边界。
#include <stdio.h>
// 定义一个包含不同数据类型成员的结构体
struct MyStruct {
char c; // 字符型,占用1字节
int i; // 整型,占用4字节
double d; // 双精度浮点型,占用8字节
};
int main() {
struct MyStruct s;
// 打印结构体的大小
printf("Size of struct MyStruct: %zu bytes\n", sizeof(s));
// 打印各个成员的偏移量
printf("Offset of char c: %zu bytes\n", offsetof(struct MyStruct, c));
printf("Offset of int i: %zu bytes\n", offsetof(struct MyStruct, i));
printf("Offset of double d: %zu bytes\n", offsetof(struct MyStruct, d));
return 0;
}
会有如下输出
Size of struct MyStruct: 16 bytes
Offset of char c: 0 bytes
Offset of int i: 4 bytes
Offset of double d: 8 bytes
由于int
和double
的对齐要求,编译器在结构体中插入了填充字节,使得结构体的大小为16字节。char
类型的成员c
位于起始地址,int
类型的成员 i
的偏移量为 4 字节,double
类型的成员d
的偏移量为8字节。
为什么存在内存对齐?
内存对齐是为了兼顾硬件要求和性能优化而存在的,它可以提高计算机系统的整体性能和稳定性
平台原因
硬件要求
性能优化
内存访问的原子性
//例如:
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
修改默认对齐数
#pragma 这个预处理指令,可以改变编译器的默认对齐数
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#pragma pack(1)//设置默认对⻬数为1
struct S
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的对⻬数,还原为默认
int main()
{
//输出的结果是什么?
printf("%d\n", sizeof(struct S));
return 0;
}
可以看到非常有效的减少了空间
结构体传参
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
struct S
{
int data[1000];
int num;
};
struct S s = { {1,2,3,4}, 1000 };
//结构体传参
void print1(struct S s)
{
printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{
printf("%d\n", ps->num);
}
int main()
{
print1(s); //传结构体
print2(&s); //传地址
return 0;
}
上面两种方式那种比较好呢
首选print2
print2
函数通过传递结构体的指针来访问结构体的成员,而不是直接传递整个结构体。这种方式通常更加高效,原因如下:
-
减少内存拷贝: 当结构体比较大时,直接传递结构体会导致整个结构体的拷贝,这可能会产生额外的内存开销和性能损耗。而通过传递指针,只需传递一个地址,无需进行整个结构体的拷贝,节省了内存和时间。
-
避免数据不一致性: 如果通过传递整个结构体,函数内部对结构体的成员进行修改,那么修改只会影响到函数内部的副本,而不会影响到调用函数的原始结构体。这可能导致数据不一致性的问题。而通过传递指针,可以直接修改原始结构体的内容,避免了这种问题。
-
更灵活的内存管理: 通过传递指针,可以更灵活地管理内存。如果需要在函数内部动态修改结构体的内容,传递指针是更好的选择。此外,如果结构体的大小非常大,传递指针可以避免额外的内存开销。
结构体实现位段
什么是位段
语法形式如下
struct {
type member1 : width1;
type member2 : width2;
// ...
} structure_name;
其中,type
表示成员的数据类型,member1
、member2
等为成员名,width1
、width2
等为成员占用的位数。
例如
struct a{
unsigned int flags : 4;
unsigned int status : 2;
} packet;
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};


在大多数情况下,位段的大小会被调整为满足所在数据类型大小的最小值,以确保对齐要求的满足。
需要注意的是,对于位段,编译器可能会进行优化或调整,以满足对齐和最小化内存消耗的需求。因此,对于特定的位段定义,其大小可能因编译器而异。
位段的内存分配
//⼀个例⼦
struct S
{
char a:3;
char b:4;
char c:5;
char d:4;
};
struct S s = {0};
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
空间是如何开辟的
位段的跨平台问题
不同编译器对于位段的实现方式可能存在差异,从而导致代码在不同平台上的行为不一致
-
对齐方式差异: 不同编译器可能对位段的对齐方式有不同的实现方式。例如,一些编译器可能会按照位段的宽度进行对齐,而另一些编译器可能会按照整个数据类型进行对齐。这可能导致在不同平台上,同样的位段定义在内存中的布局不同,从而影响程序的行为。
-
符号位处理不一致: 在一些编译器中,位段的符号位可能会被扩展到整个数据类型,而在另一些编译器中,符号位可能会被保留在位段中。这可能导致在不同平台上,对于有符号位段的处理不一致,从而引发错误或未定义的行为。
-
位段宽度限制: 标准规定了位段的宽度不能超过其所在数据类型的大小,但一些编译器可能会对此做出特殊的限制或扩展,导致代码在不同平台上的行为不一致。
为了解决位段的跨平台问题,可以考虑以下几点:
- 尽可能避免使用位段,特别是在需要跨平台的情况下。如果确实需要使用位段,应该仔细测试并针对不同平台进行充分验证。
- 了解不同编译器对于位段的实现方式和限制,避免依赖于特定编译器的行为。
- 使用标准的 C 语言特性和数据类型,尽量避免依赖于编译器特定的行为。
跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在