初学者小白复盘22之——结构体

1.结构体类型的声明

1.1结构的说明

现在假如我们来描述一个学生的各种信息,那么我么就可以创建这个结构体:

c 复制代码
int main()
{
	struct student
	{
		char name[20];
		int age;
		char sex[5];
		char id[20];
	};

	return 0;
}

1.2结构体变量的创建和初始化

c 复制代码
int main()
{
	struct student
	{
		char name[20];
		int age;
		char sex[5];
		char id[20];
	};
	struct student s = { "zhangsan",18,"男","25001022" };
	printf("%s\n", s.name);
	printf("%d\n", s.age);
	printf("%s\n", s.sex);
	printf("%s\n", s.id);

当然,我们也可以按照指定的顺序初始化:

c 复制代码
int main()
{
	struct student
	{
		char name[20];
		int age;
		char sex[5];
		char id[20];
	};
	struct student s = {.age = 20,.sex = "女",.name = "lili",.id = "25001100"};
	printf("%s\n", s.name);
	printf("%d\n", s.age);
	printf("%s\n", s.sex);
	printf("%s\n", s.id);

	return 0;
}

1.3结构的特殊声明

c 复制代码
// 匿名结构体类型
struct
{
    int a;
    char b;
    float c;
} x;

struct
{
    int a;
    char b;
    float c;
} a[20], *p;

// 问题:下面的代码合法吗?
p = &x;
c 复制代码
// 有标签的结构体(可以重复使用)
struct Point {
    int x;
    int y;
};
struct Point p1, p2;  // 可以创建多个变量

// 匿名结构体(只能使用一次)
struct {
    int x;
    int y;
} p3;  // p3是这个匿名结构体类型的唯一变量

答案是不合法。

为什么 p = &x 不合法?

类型系统视角:

  • 虽然两个结构体的成员完全相同(都有int a; char b; float c;)

  • 但编译器将它们视为两个完全不同的类型

  • 就像 int 和 float 虽然都是数字类型,但不能直接赋值一样

    编译器视角:

c 复制代码
// 编译器看到的实际上是:
struct AnonymousType1 {
    int a;
    char b;
    float c;
} x;

struct AnonymousType2 {
    int a;
    char b;
    float c;
} a[20], *p;

p = &x;  // 错误:AnonymousType2* 不能指向 AnonymousType1 类型

匿名结构体的限制

主要限制:

  • 只能使用一次(在声明的地方)

  • 不能在其他地方创建该类型的变量

  • 不能作为函数参数或返回值类型

  • 不能与其他结构体(即使是相同成员)互换使用

c 复制代码
struct {int a; char b;} s1;
struct {int a; char b;} s2;

s1 = s2;  // 错误:类型不匹配

1.4结构的自引用

在结构中包含⼀个类型为该结构本⾝的成员是否可以呢?

1.4.1错误的自我引用方式:
c 复制代码
struct Node
{
    int data;
    struct Node next;  // 错误!
};

为什么这是错误的?

问题分析:

  • 如果结构体包含自身类型的成员,会导致无限递归

  • sizeof(struct Node) 会变得无限大

  • 编译器无法确定结构体的大小

c 复制代码
struct Node {
    int data;           // 4字节
    struct Node next {  // 又包含:
        int data;       // 4字节  
        struct Node next { // 再次包含:
            int data;   // 4字节
            struct Node next { // 无限循环...
                ...
            }
        }
    }
}
1.4.2正确的自我引用方式:
c 复制代码
struct Node
{
    int data;
    struct Node* next;  // 正确!
};

为什么这是正确的?

优势分析:

  • 指针的大小是固定的(32位系统4字节,64位系统8字节)

  • sizeof(struct Node) 是确定的值:sizeof(int) + sizeof(指针)

  • 可以实现链表、树等数据结构

c 复制代码
struct Node {
    int data;        // 4字节
    struct Node* next; // 4或8字节(指针)
}
// 总大小:8-12字节,是固定的
1.4.3 typedef 与匿名结构体的陷阱

错误代码:

c 复制代码
typedef struct
{
    int data;
    Node* next;  // 错误!此时Node还未定义
} Node;

问题分析:

  • 这是匿名结构体(没有结构体标签)

  • 在结构体内部试图使用 Node*,但此时 Node 还未定义完成

  • 编译器不知道 Node 是什么类型

    编译过程:

c 复制代码
// 第1步:开始定义匿名结构体
typedef struct {     // 匿名结构体,没有名字
    int data;
    Node* next;      // 错误!Node在这里还未定义
} Node;              // 第2步:到这里才定义Node为这个结构体类型

正确的解决方案:

方案1:使用结构体标签

c 复制代码
// 第1步:定义结构体标签Node
typedef struct Node {  // 编译器知道struct Node是一个类型
    int data;
    struct Node* next; // 正确!编译器认识struct Node
} Node;                // 第2步:创建别名Node

方案2:分开声明

c 复制代码
struct Node;           // 前向声明
typedef struct Node Node; // 创建别名

struct Node {          // 完整定义
    int data;
    Node* next;        // 正确!Node已经声明
};

总结关键要点:

  • 结构体不能直接包含自身,会导致无限大小

  • 结构体可以包含指向自身的指针,指针大小固定

  • 避免在匿名结构体中进行自引用

  • 使用结构体标签或前向声明来解决typedef的自引用问题

记忆口诀:
"结构体包含自己,用指针不要用实体;
typedef要自引用,先给结构体起个名。"

2. 结构体内存对齐

我们已经掌握了结构体的基本使用了。

现在我们深入讨论一个问题:计算结构体的大小。

2.1对齐规则

结构体的对齐规则

第一个成员:结构体的第一个成员放在偏移量为0的地址处。其他成员:每个成员都要对齐到对齐数的整数倍的地址处。对齐数 = min(编译器默认对齐数, 该成员的大小)

在VS中默认对齐数为8,在Linux的gcc中没有默认对齐数,对齐数就是成员自身的大小。

结构体总大小:必须是所有成员中最大对齐数的整数倍。

嵌套结构体:嵌套的结构体要对齐到其自身成员中最大对齐数的整数倍处,整个结构体的大小必须是所有最大对齐数(包括嵌套结构体的成员)的整数倍。

c 复制代码
struct S1
{
   char c1; // 大小1,对齐数min(8,1)=1
   int i;   // 大小4,对齐数min(8,4)=4
   char c2; // 大小1,对齐数min(8,1)=1
};

假设从0开始:

c1放在0,占用1字节。

i的对齐数是4,所以从4开始,占用4-7字节。

c2的对齐数是1,接着放在8,占用8字节。

现在总大小是9字节,但是整个结构体的最大对齐数是4,所以总大小必须是4的倍数,因此需要填充到12字节。

c 复制代码
struct S2
{
   char c1; // 1,对齐数1
   char c2; // 1,对齐数1
   int i;   // 4,对齐数4
};

从0开始:

c1在0

c2对齐数1,接着在1

i对齐数4,所以从4开始,占用4-7

总大小8字节,已经是最大对齐数4的倍数。

c 复制代码
struct S3
{
   double d; // 8,对齐数min(8,8)=8
   char c;   // 1,对齐数1
   int i;    // 4,对齐数4
};

从0开始:

d放在0-7

c对齐数1,接着放在8

i对齐数4,所以从12开始(因为9、10、11不是4的倍数),占用12-15

总大小16,是最大对齐数8的倍数。

c 复制代码
struct S4
{
   char c1;      // 1,对齐数1
   struct S3 s3; // S3的最大对齐数是8,所以s3要对齐到8的倍数
   double d;     // 8,对齐数8
};

从0开始:

c1放在0

s3的对齐数是8,所以从8开始(1-7填充),s3占16字节(8到23)

d对齐数8,接着放在24(24是8的倍数),占用24-31

总大小32,是最大对齐数8的倍数。

2.2为什么存在内存对齐

1.平台原因:不是所有硬件都能访问任意地址,某些硬件只能访问特定对齐的地址。

2.性能原因:对齐的内存只需要一次访问,未对齐的可能需要两次访问。

如何节省空间?

将占用空间小的成员集中在一起,减少填充字节。

例如,S1和S2的成员相同,但S2将两个char放在一起,减少了填充,所以S2的大小小于S1。

修改默认对齐数

使用#pragma pack(n)可以修改默认对齐数,使用#pragma pack()恢复默认。
总体来说:结构体的内存对⻬是拿空间来换取时间的做法。

那在设计结构体的时候,我们既要满⾜对⻬,⼜要节省空间,如何做到:

让占用空间小的成员尽量集中在一起就可以啦,比如说上面的s1与s2,虽然内容相同,但就因为顺序不同而导致大小不同

2.3修改默认对其数

c 复制代码
#include <stdio.h>

#pragma pack(1) // 设置默认对齐数为1
struct S
{
    char c1;    // 大小1,对齐数1
    int i;      // 大小4,对齐数1
    char c2;    // 大小1,对齐数1
};
#pragma pack() // 取消设置,还原为默认

int main()
{
    printf("%d\n", sizeof(struct S)); // 输出:6
    return 0;
}

分析:

  • 设置对齐数为1后,所有成员都按1字节对齐

  • 内存布局:[c1][i][i][i][i][c2]

  • 没有填充字节,总大小就是各成员大小之和:1+4+1=6

  1. 实际编程建议
    空间优化:将小成员变量放在一起

1.性能考虑:对于频繁访问的结构体,保持自然对齐

2.跨平台:注意不同编译器的默认对齐数差异

3.特定需求:使用#pragma pack调整对齐,但要注意可能影响性能

4.通过理解内存对齐规则,可以更好地优化程序的内存使用和性能表现。

3.结构体传参

c 复制代码
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;
}
  1. 两种传参方式的区别
    值传递 (print1):
c 复制代码
void print1(struct S s)  // 创建结构体的完整副本
{
    printf("%d\n", s.num);
    // 函数结束时,副本s被销毁
}

指针传递 (print2):

c 复制代码
void print2(struct S* ps)  // 只传递地址(指针)
{
    printf("%d\n", ps->num);
    // 操作的是原始结构体,不创建副本
}
  1. 性能对比分析
    内存开销:
c 复制代码
struct S s;  // 假设sizeof(struct S) = 1000*4 + 4 = 4004字节

// print1调用:在栈上分配4004字节的副本
print1(s);   // 栈空间消耗:4004字节

// print2调用:在栈上分配一个指针(4或8字节)  
print2(&s);  // 栈空间消耗:4字节(32位)或8字节(64位)

时间开销:

  • print1:需要将4004字节的数据从调用者栈帧复制到被调用函数栈帧

  • print2:只需要复制一个指针地址(4-8字节)

    3.其他考虑因素

c 复制代码
// print1:安全,不会修改原始数据(操作的是副本)
void print1(struct S s) {
    s.num = 0;  // 只修改副本,不影响原始数据
}

// print2:可能意外修改原始数据
void print2(struct S* ps) {
    ps->num = 0;  // 修改了原始数据!
}

// 安全的指针传递版本:
void print2_safe(const struct S* ps) {  // 使用const保护
    printf("%d\n", ps->num);
    // ps->num = 0;  // 编译错误!
}
  1. 总结
    为什么首选 print2(指针传递):

1.性能优势:

  • 避免大数据复制

  • 减少栈空间使用

  • 提高函数调用速度

2.内存效率:

  • 传递指针(4-8字节) vs 传递整个结构体(可能数千字节)

3.实际影响:

  • 对于频繁调用的函数,性能差异会累积

  • 在内存受限的嵌入式系统中尤为重要

4.例外情况:

  • 结构体很小(通常<16字节)时,值传递也可接受

  • 需要确保不修改原数据且结构体不大时

因此,在大多数情况下,结构体传参时应传递指针而不是整个结构体。

4.结构体实现位段

4.1什么是位段

位段的定义:

位段(bit-field)是C语言中一种特殊的数据结构,它允许我们按位来指定结构体成员所占用的内存大小。

位段的特点

1.位段的成员必须是 int、unsigned int 或 signed int,C99中也可以使用其他类型

2.位段的成员名后边有一个冒号和一个数字,表示该成员占用的位数

示例:

c 复制代码
struct A
{
    int _a:2;   // 占用2位
    int _b:5;   // 占用5位  
    int _c:10;  // 占用10位
    int _d:30;  // 占用30位
};
c 复制代码
printf("%d\n", sizeof(struct A));  // 输出是多少?

4.2 位段的内存分配

内存分配规则

1.位段的成员可以是 int、unsigned int、signed int 或 char 等类型

2.位段的空间按照需要以4个字节(int)或1个字节(char)的方式开辟

3.位段涉及很多不确定因素,不跨平台,注重可移植的程序应该避免使用位段

内存分配示例分析:

c 复制代码
struct S
{
    char a:3;  // 占用3位
    char b:4;  // 占用4位
    char c:5;  // 占用5位
    char d:4;  // 占用4位
};

struct S s = {0};
s.a = 10;  // 二进制:1010 -> 截断为010 (2)
s.b = 12;  // 二进制:1100 -> 1100 (12)
s.c = 3;   // 二进制:11 -> 00011 (3)
s.d = 4;   // 二进制:100 -> 0100 (4)
c 复制代码
内存地址:    低地址  ------------> 高地址
字节0:     01100010  (0x62)
字节1:     00000011  (0x03)  
字节2:     00000100  (0x04)

详细分解:
字节0: [b的4位][a的3位][空闲1位] = [1100][010][0] = 01100010 = 0x62
字节1: [c的低3位][空闲5位] = [011][00000] = 00000011 = 0x03
       实际上c需要5位,但第一个字节只剩下1位不够,所以c从第二个字节开始
字节2: [d的4位][空闲4位] = [0100][0000] = 00000100 = 0x04
       实际上d需要4位,但第二个字节剩下的5位中,由于对齐或其他原因,可能重新开一个字节

验证:

c 复制代码
#include <stdio.h>
struct S
{
    char a:3;
    char b:4; 
    char c:5;
    char d:4;
};

int main()
{
    struct S s = {0};
    s.a = 10;  // 10的二进制1010,但只有3位,所以是010(2)
    s.b = 12;  // 12的二进制1100,4位正好
    s.c = 3;   // 3的二进制11,5位就是00011(3)
    s.d = 4;   // 4的二进制100,4位就是0100(4)
    
    // 通过调试器查看内存,可以看到三个字节:0x62, 0x03, 0x04
    return 0;
}

4.3位段的跨平台问题

主要跨平台问题

1.符号问题:int 位段被当成有符号数还是无符号数不确定

2.最大位数:位段中最大位的数目不能确定

16位机器最大16位,32位机器最大32位,写成27位在16位机器会出问题

3.分配方向:位段成员在内存中从左向右还是从右向左分配,标准未定义

4.空间利用:当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余位还是利用,不确定

总结:

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

4.4位段的应用

网络协议中的应用:

位段在网络协议中广泛应用,如IP数据报格式:

c 复制代码
IP数据报格式:
0                   15 16                   31
┌───────────────────┬───────────────────┬───────────────────┬───────────────────┐
│   4位版本号       │   4位首部长度     │   8位服务类型     │   16位总长度      │
├───────────────────┼───────────────────┼───────────────────┼───────────────────┤
│   16位标识符      │   3位标志         │   13位片偏移      │                   │
├───────────────────┼───────────────────┼───────────────────┼───────────────────┤
│   8位生存时间     │   8位协议         │   16位首部校验和  │                   │
├───────────────────┴───────────────────┴───────────────────┴───────────────────┤
│                               32位源IP地址                                   │
├─────────────────────────────────────────────────────────────────────────────┤
│                               32位目的IP地址                                 │
├─────────────────────────────────────────────────────────────────────────────┤
│                               32位选项(若有)                               │
└─────────────────────────────────────────────────────────────────────────────┘

优势

  • 很多属性只需要几个bit位就能描述

  • 使用位段能够实现想要的效果,同时节省空间

  • 网络传输的数据报大小较小,有助于网络畅通

4.5位段使用的注意事项

重要限制:不能取地址

位段的几个成员共有同一个字节,有些成员的起始位置并不是某个字节的起始位置,这些位置处是没有地址的。

原因:

  • 内存中每个字节分配一个地址

  • 一个字节内部的bit位是没有地址的

  • 因此不能对位段的成员使用&操作符

    所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段的成员输⼊值,只能是先输入放在一个变量中,然后赋值给位段的成员。

    错误示例:

c 复制代码
struct A
{
    int _a:2;
    int _b:5;
    int _c:10;
    int _d:30;
};

int main()
{
    struct A sa = {0};
    scanf("%d", &sa._b);  // 错误!不能对位段成员取地址
    
    return 0;
}

正确使用方法:

c 复制代码
int main()
{
    struct A sa = {0};
    
    // 正确的示范
    int b = 0;
    scanf("%d", &b);    // 先输入到普通变量
    sa._b = b;          // 再赋值给位段成员
    
    return 0;
}

位段使用总结

优点

1.节省空间:可以精确控制每个成员占用的位数

2.内存效率:对于标志位、状态位等小数据特别有用

3.网络应用:在网络协议中广泛使用,减少数据传输量

缺点

1.不可移植:不同编译器、不同平台的行为可能不同

2.不能取地址:无法直接对位段成员使用&操作符

3.调试困难:内存布局不直观,调试时难以理解

使用建议:

  • 在需要极致节省内存的嵌入式系统中可以考虑使用

  • 在网络编程中处理协议头时可以使用

  • 在注重可移植性的应用中尽量避免使用

  • 使用时要充分测试目标平台的行为

相关推荐
xlq223224 小时前
15.list(上)
数据结构·c++·list
我不会插花弄玉4 小时前
排序【由浅入深-数据结构】
c语言·数据结构
XH华5 小时前
数据结构第三章:单链表的学习
数据结构
No0d1es5 小时前
电子学会青少年软件编程(C/C++)六级等级考试真题试卷(2025年9月)
c语言·c++·算法·青少年编程·图形化编程·六级
Knox_Lai6 小时前
数据结构与算法学习(0)-常见数据结构和算法
c语言·数据结构·学习·算法
逐步前行6 小时前
C项目--羊了个羊(两关全)--含源码
c语言·开发语言
blammmp7 小时前
算法专题二十:贪心算法
数据结构·算法·贪心算法
小白程序员成长日记7 小时前
2025.11.17 力扣每日一题
数据结构·算法·leetcode
赖small强8 小时前
【Linux C/C++开发】第10周:STL容器 - 理论与实战
linux·c语言·c++·stl容器
q***58198 小时前
在21世纪的我用C语言探寻世界本质——字符函数和字符串函数(2)
c语言·开发语言