C语言---结构体 、联合体、枚举

发布于:2025-05-22 ⋅ 阅读:(20) ⋅ 点赞:(0)

一、初识结构体

1、结构体类型

结构体和数组都是集合,但是结构体有成员,类型可以不同;数组有成员,类型相同。

int main()
{
	struct tag
	{
		member--list //一个或者多个成员,成员变量
	}variable--list;//可以省略,结构体变量列表
}

2、结构体的自引用

int main()
{
	struct Node
	{
		int data;//数据域
		struct Node* next;//指针域,自己类型指针
	};
}

3、结构体的内存对齐

int main()
{
	struct S1
	{
		char c1;
		char c2;
		int n;
	};
	struct S2
	{
		char c1;
		int n;
		char c2;
	};
}

(1)宏

偏移量(相较于结构体开始位置) :offsetof-----宏(需要包含头文件:stddef.h)

可以计算出结构体的成员,相较于结构体起始位置的偏移量。

类型:offsetof (type,member)

举例如下:

int main()
{
	struct S1
	{
		char a;
		char b[10];
		char c;
	};
	printf("%d\n", offsetof(struct S1, a));
	printf("%d\n", offsetof(struct S1, b));
	printf("%d\n", offsetof(struct S1, c));
}

运行结果:

(2)offsetof的返回类型是

无符号整型(只表示0和正数),该返回值代表类型中成员的偏移值。(偏移量是相较于结构体开始位置)

(3)对齐的规则

1、结构体第一个成员对齐到和结构体变量起始位置偏移量为0的地址处

2、其他成员变量要对齐到  某个数字(对齐数)的整数倍的地址处。

(对齐数=编译器默认的一个对齐数与该成员变量大小的较小值)

vs默认对齐数是8。

3、结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍处。

4、如果嵌套的结构体的情况,嵌套结构体成员对齐到自己成员中最大对齐数的整数倍处结构体的· 整体大小就是所有最大对齐数(包含嵌套结构中成员的对齐数的整数倍)。

举例如下:

(4)结构体对齐的原因

1、提高内存访问效率:

cpu并非按照单个字节读取,而是以块为单位读取比如2字节、4字节、8字节等。假如数据在内存中未对齐,CPU可能需要多次读取并拼凑数据会导致耗时增加。

2、满足硬件平台特定要求:

不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

(5)修改默认对齐数

#pragma pack(n)默认对齐数x64是8字节对齐,x86是4字节对齐,结构体的内存对齐其实是拿空间换时间做法。

1、修改默认对齐数的场景

情况1:增大对齐数(用空间换时间):比如将4字节变为16字节,优化CPU访问效率。

原理:CPU访问对齐的内存时效率更高(某些架构上如SSE/AVX指令集要求16/32字节);

对齐数据可以减少缓存行的加载次数,提升缓存利用率。

代价:结构体因填充更多字节导致内存占用增加。

情况2:减小对齐数(用时间换空间)

目的:减少内存占用(如嵌入式系统或网络传输场景)

原理:压缩结构体大小,减少填充字节。

代价:可能导致未对齐内存访问,某些平台上引发性能下降甚至崩溃(需要硬件支持非对方访问)

适用类型:空间换时间适用于内存充足、对游戏高敏感的场景(如:游戏、高频交易)

时间换空间:适合内存受限场景(如嵌入式设备、网络数据包)

(6)结构体实现位段(节省储存空间,方便位操作)

在C语言中,位段是结构体的一种特殊形式允许在结构体中定义具有指定位数的成员,这些成员可以占用结构体变量内部的连续比特位。以下是关于结构体实现位段的详细介绍:
 
位段的声明
位段的声明和结构体类似,但有两个特点:
 
1. 成员类型限制:位段的成员必须是 int 、 unsigned int 或 signed int   ,在C99标准中,位段成员的类型也可以选择其他类型,如 char 等 。

2. 成员定义格式:位段的成员名后边有一个冒号和一个数字这个数字代表该成员变量在结构体内占用的 bit 位数。例如:

int main()
{
    struct BitField {
        unsigned int a : 3;  // 成员a占用3个比特位
        unsigned int b : 4;  // 成员b占用4个比特位
        int c : 6;           // 成员c占用6个比特位
    };
}

位段的内存分配
 
1. 分配单位:位段的空间按照需要以4个字节( int 类型时)或者1个字节( char 类型时)为单位来开辟。

2. 分配方式:当一个字节(或4字节)的空间不足以容纳下一个位段成员时,会根据编译器规则决定是否开辟新的空间。例如:

int main()
{
    struct BitField {
        char a : 3;  // 先开辟1个字节(8比特位)存放a,占用3比特位
        char b : 4;  // 剩余5比特位,可容纳b,继续在该字节存放
        char c : 5;  // 该字节剩余1比特位,不够存放c,此时编译器会决定是开辟新字节
        //,还是舍弃剩余位等(不同编译器处理不同)
    };
}

位段的使用注意事项
 
1. 不能取地址:因为位段的几个成员可能共用同一个字节有些成员的起始位置并非字节起始位置,而内存中是以字节为单位分配地址,位内的比特位没有地址,所以不能对位段成员使用取地址符 &  。

2. 输入方式:不能使用 scanf  直接给位段成员输入值,一般先输入到一个普通变量中,再赋值给位段成员 。举例如下:

#include <stdio.h>

// 定义包含位段的结构体
struct BitField {
    unsigned int a : 3;  // 3位(0~7)
    unsigned int b : 4;  // 4位(0~15)
    unsigned int c : 5;  // 5位(0~31)
};

int main() {
    struct BitField bf;
    unsigned int temp;  // 临时变量用于接收输入

    // 输入a的值(范围0~7)
    printf("输入a(0~7): ");
    scanf("%u", &temp);
    bf.a = temp;  // 赋值给位段成员

    // 输入b的值(范围0~15)
    printf("输入b(0~15): ");
    scanf("%u", &temp);
    bf.b = temp;

    // 输入c的值(范围0~31)
    printf("输入c(0~31): ");
    scanf("%u", &temp);
    bf.c = temp;

    // 输出位段值
    printf("a = %u, b = %u, c = %u\n", bf.a, bf.b, bf.c);

    return 0;
}

位段的取值范围是:位段成员的取值范围是由其占用的位数决定的。对于无符号整数类型的位段,范围的计算方法如下:
 
- 对于占用 n 位的无符号位段成员,其取值范围是 0 到 2^n - 1 。例如,占用3位的无符号位段成员, n = 3 ,则取值范围是 0 到 2^3 - 1 ,即 0 到 7 ;占用4位的无符号位段成员,取值范围是 0 到 2^4 - 1 ,即 0 到 15 ;占用5位的无符号位段成员,取值范围是 0 到 2^5 - 1 ,即 0 到 31 。
 
如果是有符号整数类型的位段,其取值范围的计算会更复杂一些,要考虑符号位。一般来说,对于占用 n 位的有符号位段成员,其取值范围是 -2^(n - 1) 到 2^(n - 1)- 1 。例如,一个8位有符号整数,取值范围是 -2^7 到 2^7 - 1 ,即 -128 到 127 。

二、联合体

联合体关键字:union

编译器只为最大的成员分配足够的内存空间,联合体的特点是所有成员共用同一块内存空间。联合体也叫共用体。

给联合体中一个成员赋值,,其他成员的值也跟着变化。

联合体大小计算:(1)联合体的大小至少是最大成员的大小;(2)当最大成员大小不是最大对齐数整数倍时就要到最大对齐数的整数倍。

联合体对齐规则

1、确定最大成员大小
 
- 首先找出联合体中占用内存空间最大的成员。例如,有一个联合体包含 int (通常占4字节)、 char (占1字节)和 double (通常占8字节),那么最大成员是 double ,大小为8字节。
 
2、考虑内存对齐
 
- 联合体的大小需要满足其所有成员的对齐要求。对齐规则与结构体类似,通常是按照最大成员的对齐方式来对齐。在上述例子中, double 的对齐要求可能是8字节对齐(不同平台可能有所不同),那么联合体的大小就需要是8的倍数。由于最大成员 double 本身大小为8字节,已经满足8字节对齐,所以该联合体的大小就是8字节。

#include <stdio.h>

    union MyUnion {
        int i;
        char c;
        double d;
    };

    int main() {
        printf("联合体大小: %lu 字节\n", sizeof(union MyUnion));
        return 0;
    }

三、枚举

关键字:enum,自定义数据类型,用于一组具名的整型常量。它通过关键字enum声明,使代码更容易读、易维护,常用于表示有限的取值集合(如状态、颜色、方向等)

1、核心特性


 
.(1) 默认值规则

- 未指定初始值时,第一个枚举常量默认值为 0 ,后续常量依次递增 1 。例如:

enum Color { RED, GREEN, BLUE };  // RED=0, GREEN=1, BLUE=2

(2)- 可显式指定某个常量的值,后续常量以此为基础递增。

enum Week { MON=1, TUE, WED };  // MON=1, TUE=2, WED=3

2. 本质为整数

- 枚举常量本质是整数常量,可直接赋值给 int 类型变量,反之需强制类型转换。

enum Color c = RED;       // 正确:枚举变量赋值为枚举常量
int num = GREEN;          // 正确:枚举常量自动转为int(1)
c = (enum Color)3;        // 正确:int转为枚举类型(需强制转换)

3. 作用域与类型安全

- 枚举常量在所属枚举类型的作用域内唯一,但不同枚举类型的常量可重名。

enum Season { SPRING, SUMMER };
enum Month { SPRING=1, SUMMER=2 };  // 合法,与上一个枚举无关

4、定义与使用示例

(1)匿名枚举并声明变量

#include <stdio.h>

int main() {
    // 匿名枚举:无枚举名,直接声明变量 status
    enum { 
        FAIL = 0, 
        SUCCESS = 1 
    } status;
    
    status = SUCCESS;
    if (status == SUCCESS) {
        printf("操作成功!\n");
    }
    return 0;
}

(2)枚举作为函数参数,类型安全检查

#include <stdio.h>

// 定义枚举类型:方向
enum Direction { UP, DOWN, LEFT, RIGHT };

// 函数参数为枚举类型,增强可读性
void move(enum Direction dir) {
    switch(dir) {
        case UP:    printf("向上移动\n"); break;
        case DOWN:  printf("向下移动\n"); break;
        case LEFT:  printf("向左移动\n"); break;
        case RIGHT: printf("向右移动\n"); break;
    }
}

int main() {
    enum Direction d = LEFT;
    move(d);  // 传递枚举变量
    move(RIGHT);  // 直接传递枚举常量
    return 0;
}

(3)枚举与结构体结合:复杂数据类型

#include <stdio.h>

// 定义枚举类型:性别
enum Gender { MALE, FEMALE, OTHER };

// 结构体中使用枚举
struct Person {
    char name[20];
    enum Gender gender;  // 枚举作为结构体成员
    int age;
};

int main() {
    struct Person p = { "Alice", FEMALE, 25 };
    printf("%s 的性别:%s\n", p.name, 
           p.gender == FEMALE ? "女" : "男");  // 条件判断枚举值
    return 0;
}

5、枚举类型优点

枚举类型具有以下优点:
 (1)增强代码可读性:用有意义的枚举常量名(如 RED   GREEN   BLUE )代替无意义的数字或字符串,使代码更易理解。
(2)提高代码可维护性:当需要修改枚举值对应的实际意义时,只需在枚举定义处修改,而无需在所有使用该值的地方逐一修改。
(3  实现类型安全检查:编译器会检查枚举变量的赋值是否在枚举类型定义的范围内,避免将不相关的值赋给枚举变量,减少错误。
(4)方便代码复用:可以在不同的函数、模块中使用相同的枚举类型,提高代码的复用性。
(5)便于代码组织和管理:将相关的常量组织在一个枚举类型中,使代码结构更清晰,便于管理和维护。

今天的分享就到这里了,谢谢大家,有不足之处请指正!!!感谢