结构体与位段

结构体类型的声明

结构是⼀些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量

 

 结构的声明

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 类型的成员 xy。同时,也声明了一个结构体变量 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

由于intdouble的对齐要求,编译器在结构体中插入了填充字节,使得结构体的大小为16字节。char类型的成员c位于起始地址,int类型的成员 i 的偏移量为 4 字节,double类型的成员d的偏移量为8字节。

为什么存在内存对齐?

内存对齐是为了兼顾硬件要求和性能优化而存在的,它可以提高计算机系统的整体性能和稳定性

平台原因
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
硬件要求
许多计算机体系结构要求数据按特定的边界对齐存储,这有助于提高内存访问速度。例如,某些处理器可能要求整数按照 4 字节或 8 字节的边界对齐存储,而浮点数可能要求按照 4 字节或 8 字节的边界对齐存储。
性能优化
内存对齐可以提高数据的访问速度。当数据按照硬件要求的边界对齐存储时,处理器可以更有效地访问这些数据,而无需进行额外的操作。相比于非对齐访问,对齐访问通常更快。
内存访问的原子性
一些硬件平台要求对于某些类型的数据访问是原子性的,即要么整个数据被读取,要么整个数据被写入。如果数据不是按照要求对齐存储,那么访问可能会导致两次内存访问,这可能导致不一致的结果或性能下降。
结构体的内存对齐是拿空间来换取时间的做法
那在设计结构体的时候,我们既要满足对齐,⼜要节省空间,就要让占用空间小的成员尽量集中在⼀起
//例如:
struct S1
{
    char c1;
    int i;
    char c2;
};

struct S2
{
    char c1;
    char c2;
    int i;
};
S1 和 S2 类型的成员⼀模⼀样,但是 S1 和 S2 所占空间的大小有了⼀些区别

修改默认对齐数

#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函数通过传递结构体的指针来访问结构体的成员,而不是直接传递整个结构体。这种方式通常更加高效,原因如下:

  1. 减少内存拷贝: 当结构体比较大时,直接传递结构体会导致整个结构体的拷贝,这可能会产生额外的内存开销和性能损耗。而通过传递指针,只需传递一个地址,无需进行整个结构体的拷贝,节省了内存和时间。

  2. 避免数据不一致性: 如果通过传递整个结构体,函数内部对结构体的成员进行修改,那么修改只会影响到函数内部的副本,而不会影响到调用函数的原始结构体。这可能导致数据不一致性的问题。而通过传递指针,可以直接修改原始结构体的内容,避免了这种问题。

  3. 更灵活的内存管理: 通过传递指针,可以更灵活地管理内存。如果需要在函数内部动态修改结构体的内容,传递指针是更好的选择。此外,如果结构体的大小非常大,传递指针可以避免额外的内存开销。

结构体实现位段

什么是位段

语法形式如下

struct {
    type member1 : width1;
    type member2 : width2;
    // ...
} structure_name;

其中,type表示成员的数据类型,member1member2等为成员名,width1width2等为成员占用的位数。

例如

struct a{
    unsigned int flags : 4;
    unsigned int status : 2;
} packet;
位段的声明和结构是类似的,有两个不同:
位段的成员必须是int、unsigned int 或signed int ,在C99中位段成员的类型也可以
选择其他类型。
位段的成员名后边有一个冒号和一个数字。
struct A
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};
那上面这个位段的大小是多少
why

在大多数情况下,位段的大小会被调整为满足所在数据类型大小的最小值,以确保对齐要求的满足。

需要注意的是,对于位段,编译器可能会进行优化或调整,以满足对齐和最小化内存消耗的需求。因此,对于特定的位段定义,其大小可能因编译器而异。

位段的内存分配

位段的成员可以是 int unsigned int signed int 或者是 char 等类型
位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
//⼀个例⼦
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;

空间是如何开辟的

 位段的跨平台问题

不同编译器对于位段的实现方式可能存在差异,从而导致代码在不同平台上的行为不一致

  1. 对齐方式差异: 不同编译器可能对位段的对齐方式有不同的实现方式。例如,一些编译器可能会按照位段的宽度进行对齐,而另一些编译器可能会按照整个数据类型进行对齐。这可能导致在不同平台上,同样的位段定义在内存中的布局不同,从而影响程序的行为。

  2. 符号位处理不一致: 在一些编译器中,位段的符号位可能会被扩展到整个数据类型,而在另一些编译器中,符号位可能会被保留在位段中。这可能导致在不同平台上,对于有符号位段的处理不一致,从而引发错误或未定义的行为。

  3. 位段宽度限制: 标准规定了位段的宽度不能超过其所在数据类型的大小,但一些编译器可能会对此做出特殊的限制或扩展,导致代码在不同平台上的行为不一致。

为了解决位段的跨平台问题,可以考虑以下几点:

  1. 尽可能避免使用位段,特别是在需要跨平台的情况下。如果确实需要使用位段,应该仔细测试并针对不同平台进行充分验证。
  2. 了解不同编译器对于位段的实现方式和限制,避免依赖于特定编译器的行为。
  3. 使用标准的 C 语言特性和数据类型,尽量避免依赖于编译器特定的行为。

 跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在

  • 46
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值