当前位置: 首页 > news >正文

进阶-自定义类型(结构体、位段、枚举、联合)

自定义类型:结构体,枚举,联合

结构体
结构体类型的声明
结构的自引用
结构体变量的定义和初始化
结构体内存对齐
结构体传参
结构体实现位段(位段的填充&可移植性)

枚举
枚举类型的定义
枚举的优点
枚举的使用

联合
联合类型的定义
联合的特点
联合大小的计算

结构体

1. 结构体的声明

1.1 结构的基础知识
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。

1.2 结构的声明

struct tag
{
    member - list;
}variable - list;

例如描述一个学生:

struct stu
{
    //学生的相关属性
    char name[20];//名字
    int age;//年龄
    char sex[5];//性别
    char id[20];//学号
};//分号不能丢

这只是一个类型,类似与int char

struct stu
{
    //学生的相关属性
    char name[20];//名字
    int age;//年龄
    char sex[5];//性别
    char id[20];//学号
}s1, s2;//全局变量

利用这个类型,创建变量s1,s2,创建类型的时候捎带创建变量

两种写法都行

int main()
{
    struct stu s3;//内部创建也行,只不过变成了局部变量
    return 0;
}

1.3 特殊的声明

在声明结构的时候,可以不完全的声明。
比如 :

//匿名结构体类型
//只能用一次
struct
{
    int a;
    char b;
    float c;
}x;

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

P = &x;

警告:

编译器会把上面的两个声明当成完全不同的两个类型。
所以是非法的。

1.4 结构的自引用

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

前提补充:

数据结构
数据在内存中的存储结构

线形:数组,存储地址连续的 1 2 3 4 5 6
链表:在内存中各个位置,但是可以通过1找到2,通过2找到3...(把1,2,3..称为节点)

树形
二叉树

struct Node//节点
{
    int data;//存放一个数值
    struct Node next;//存放下一个节点 
};
//可行否?

如果可以,那sizeof(struct Node)是多少 ?

是未知的,所以这种写法是错误的

struct Node//节点
{
    int data; //存放一个数值(数据域)
    struct Node* next;//存放下一个节点的指针(指针域)
};

这样sizeof(struct Node)就是固定的了

结构体内可以包含一个同类型的结构体指针

1.5 结构体变量的定义和初始化

有了结构体类型,那如何定义变量

struct Point
{
    int x;
    int y;
}p1;//声明类型的同时定义变量p1

struct Point p2;//创建类型后,再重新定义结构体变量p2

struct Point p3 = { x,y }//初始化:定义变量的同时赋初值。;

struct stu//类型声明
{
    char name[15];//名字
    int age;//年龄
};

struct stu s = { "zhangsan",20 };//初始化


初始化


struct Point
{
    int x;
    int y;
}p1 = { 2,3 };

struct score
{
    int n;
    char ch;
};

struct Stu
{
    char name[20];
    int age;
    struct _score s;
};

int main()
{
    struct Point p2 = { 3,4 };
    struct Stu sl = { "zhangsan",20 ,{100,'q'} };
    printf("%s %d %d %c\n", sl.name, sl.age, sl.s.n, sl.s.ch);
    return 0;
}

1.6 结构体内存对齐

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

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

这也是一个特别热门的考点 : 结构体内存对齐

struct s1
{
    char c1;//1
    int i;//4
    char c2;//1
};

struct s2
{
    char c1;//1
    char c2;//1
    int i;//4
};

int main()
{
    printf("%d\n", sizeof(struct s1));//12.

    printf("%d\n", sizeof(struct s2));//8
    return 0;
}

成员相同,只是顺序发生变化后,大小也发生了变化,那这到底是为什么呢?

如何计算 ?

首先得掌握结构体的对齐规则 :

1.第一个成员在与结构体变量偏移量为0的地址处。

2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小 的较小值。
vs中默认的值为8
    
3.结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
    
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

struct s1
{
    char c1;//1
    int i;//4
    char c2;//1
};

偏移量
0     c1
1
2
3
4    i   对齐数为8和4的较小值,选4,对齐到4的倍数,四个字节
5    i   
6    i
7    i
8    c2  对齐数为1和8的较小值,1个字节
9    结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
10   
11
12   三个对齐数1,4,1 最大为4,4的整数倍,12,结构体的最终大小
13
14


补充
offsetof,是一个宏,头文件为#include<stddef.h>
可以返回一个成员在这个类型中的偏移量

printf("%d\n", offsetof(struct s1, c1));//0
printf("%d\n",offsetof(struct s1, i));//4
printf("%d\n", offsetof(struct s1, c2));//8


struct s2
{
    char c1;//1
    char c2;//1
    int i;//4
};

偏移量
0     c1
1     c2,编译器默认对齐数为8,自身为1,选最小值1,偏移量为1就行了
2
3
4    i   对齐数为8和4的较小值4,偏移量为4  
5    i   四个字节
6    i
7    i
8    结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
9    三个对齐数1,4,1 最大为4,4的整数倍,8就行了,结构体的最终大小为8
10
11
12   
13
14


printf("%d\n", offsetof(struct s1, c1));//0
printf("%d\n",offsetof(struct s1, c2));//1
printf("%d\n", offsetof(struct s1, i));//4


gcc等其他一些编译器没有默认对齐数这个概念,
他们的对齐数就是自身的大小

练习:

struct s3
{
    double d;
    char c;
    int i;
};

求s3类型的大小

0      d 偏移量为0,
1      .....
2      double占8个字节
3
4
5
6
7      d
8      c 的对齐数为1和8的较小值,为1,接下来最近的一个1的整数倍为8,c占一个字节
9      
10
11
12    i 的对齐数为4和8的较小值,为4,接下来最近的一个4的整数倍为12,
13    
14    
15    i 占4个字节
16    结构体最终大小为每个成员对齐数最大值的整数倍 1 8 4 ,8的足够内存的整数倍为16
17
18

#include<stddef.h>
int main()
{
    struct S3
    {
        double d;
        char c;
        int i;
    };
    printf("%d\n", sizeof(struct S3));//16
    printf("%d\n", offsetof(struct S3, d));//0
    printf("%d\n", offsetof(struct S3, c));//8
    printf("%d\n", offsetof(struct S3, i));//12
    return 0;
}


struct S4
{
    char c1;
    struct S3 s3;
    double d;
};

c1在偏移量为0的地址处,c1为char类型,占1个字节,

s3为嵌套结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,

即s3的类型为struct S3这个结构体中最大的对齐数,struct S3的对齐数为8 1 4,最大为8
s3要对齐到8的整数倍,即从偏移量8开始

s3的类型为struct S3,本身大小为16,从偏移量8处占16个字节,到偏移量为23处结束

d的对齐数为8,要对齐到最近的8的整数倍处,即24,偏移量为24
d为double类型,从偏移量24往下占8个字节,到偏移量31处,即32字节


结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。(8 1 4    S3中的 1 8 1) 即8的整数倍

所以struct S4的大小为32


那为什么存在内存对齐呢 ?
大部分的参考资料都是如是说的 :
    
1.平台原因(移植原因) :
不是所有的硬件平台都能访问任意地址上的任意数据的; 某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

2.性能原因 :
数据结构(尤其是栈)应该尽可能地在自然边界上对齐,
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问; 而对齐的内存访问仅需要一次访问。

未对齐:
c i(c紧挨着i,c占一个字节,i占四个字节)

对齐:
c   i(c和i之间空着三个字节)

32位机器,一次性可以访问32bit即四个字节,
如果未对齐,从c开始访问i,一次访问对i未访问完全,还剩一个字节,需要再访问一次
如果对齐了,可以直接从i的首字节开始访问四个字节,直接把i访问完全了
    
总体来说 :
结构体的内存对齐是拿空间来换取时间的做法。

“对齐是为了让CPU更高效地访问内存。如果数据未按对齐要求存放,CPU可能需要多次读取或触发错误,导致性能下降甚至崩溃。
同时,不同硬件的对齐要求不同,对齐规则能保证代码在多种平台上正常运行。”

那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到 : 
让占用空间小的成员尽量集中在一起。

1.7 修改默认对齐数

之前我们见过了 #pragma 这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数。

#pragma pack(4)

struct s
    int i;//4 4 4 0~3
   //4
    double d;//8 4 4 4~11
};
#pragma pack()
int main()
{
    printf("%d\n", sizeof(struct s));//12 
    return 0;
}

如果不想对齐的话就改为1
#pragma pack(1)
struct s
    int i;

double d;
};
#pragma pack()

1.8 结构体传参

变量和指针都行

struct S
{
    int data[1000];
    int num;
};

void print1(struct S ss)
{
    int i = 0;
    for (i = 0; i < 3; i++)
    {
        printf("%d", ss.data[i]);
    }
    printf("%d\n", ss.num);
}

void print2( const struct S* ps)//conts 修饰防止通过指针修改了结构体
{
    int i = 0;
    for (i = 0; i < 3; i++)
    {
        printf("%d", p->data[i]);
    }
    printf("%d\n", p->num);
}

int main()
{
    struct S s = { {1,2,3},100 };
    print1(s);//传值调用
    print2(&s);//传指调用
    return 0;
}

首选print2函数。

函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。

结论:结构体传参尽量传结构体的地址

2. 位段

2.1 什么是位段

位段的声明和结构是类似的,有两个不同:
1. 位段的成员必须是 int、 unsigned int 或者 signed int(char也可以,只要是整型家族的都可以)
2. 位段的成员名后边有一个冒号和一个数字。

比如 :

struct A
{
    int _a : 2;
    int _b : 5;
    int _c : 10;
    int _d : 30;
};

A就是一个位段类型。

位段的位就是比特位,后面的数字代表所占的比特位

比如,
2代表_a只需要2个比特位,只用给它分配两个比特位就行了,不用那么多
5代表_b只需要5个比特位

比如定义一个变量flag,
int flag;
只需要它表示真假,真为1,假为0。
这时候就只需要1个比特位,不需要4个字节32个比特位那么多。

如果一个值的取值范围是0 1 2 3
那只需要两个比特位就够了
因为
00就是0
01就是1
10就是2
11就是3

位段是可以节省空间的

那位段A的大小是多少?

struct A
{
    int _a : 2;
    int _b : 5;
    int _c : 10;
    int _d : 30;
};

int main()
{
    printf("%d\n", sizeof(struct A));//8
    return 0;
}
47个比特位
6byte就够了,48bit
但是结果是8byte - 64bit

2.2 位段的内存分配
1.位段的成员可以是int unsigned int signed int 或者是char(属于整形家族)类型
2.位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的。
3.位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段

struct A
{
    int _a : 2;
    int _b : 5;
    int _c : 10;
    int _d : 30;
};

位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的。
A里面都是int上来先开辟4个字节,32bit
到_c的时候,还剩15个bit,但是不够_d用,就再开辟4个字节,32bit,一共8个字节
一般位段的成员是同一类型的

看一个例子:

struct S
{
    char a : 3;
    char b : 4;
    char c : 5;
    char d : 4 //冒号后面的数字,不能超过前面类型的大小<8
};

int main()
{
    struct S s = { 0 };
    s.a = 10;
    s.b = 12;
    s.c = 3;
    s.d = 4;
    return 0;
}

首先开辟一个字节,8bit,到c的时候只剩1个bit了,不够,再开辟一个字节
c有5个bit,那么它是把前面剩余的1个bit消耗完再使用新开辟的8个bit呢,还是直接使用新的

如果c用了前面剩余的1个bit,那么还会使用新开辟的4个bit,新开辟的一个byte还是4个字节,够存放d4个bit,这个时候位段一共开辟2个字节

如果c不使用前面剩余的bit直接使用新开辟的,使用完后,新开辟的还剩8 - 5 = 3个bit,那么存放d的时候是不够的,还需要再开辟一个字节,那这个时候一共开辟3个字节

我们计算一下就好了
printf("%d\n", sizeof(struct S));
结果是3byte

说明,不够的时候会跳过前面剩余的bit直接开始使用新开辟的字节。

详细分析一下:

前面提到位段struct S一共开辟3个字节,定义变量s,初始化为0

00000000 00000000 00000000
a占3bit, b占4bit, c占5bit, d占4bit

s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;

a占3bit,将10存进a里面,10的二进制为1010,假设内部,字节从低位向高位访问,a只占3bit,存放1010存不下
就会类似于截断,把010存放进去,现在内存中就变成了:
00000 010   00000000   00000000

b占4bit,将12存进b里面,12的二进制为1100,四个bit刚好放进b里现在内存中就变成了
0 1100 010   00000000   00000000

c占5bit现在还剩一个bit但是不够放c,跳过直接使用下一个字节,现在把3放进c里,3的二进制是011,可以放进,不够5位,补0,00011,现在内存中就变成了
0 1100 010   000 00011   00000000

d占4bit,现在还剩3个bit不够,浪费掉,直接再开辟一个字节,把4放进d,4的二进制为0100,现在内存中就变成了
0 1100 010   000 00011   0000 0100
整理一下:
01100010   00000011   00000100
十六进制就是
6 2 0 3 0 4
如果内存中看到的是 62 03 04,那么前面的假设存放方式就是对的
调试,查看内存,&s,看s的内存
s占3个字节
果真是 62 03 04
说明在vs2019环境下就是这样存储进去的
该浪费的浪费,字节内从左向右存储进去

区别于大小端,大小端是字节顺寻,这里每次只有一个字节

2.3 位段的跨平台问题

1.int 位段被当成有符号数还是无符号数是不确定的。

2.位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,当写成27比如int a : 27 ,在16位机器会出问题。

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

4.当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
    
总结 : 跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。

2.4 位段的应用

位段允许你在结构体中以位为单位指定成员的宽度,常用于需要高效利用内存或按位操作的场景

1. 硬件寄存器映射:

如在嵌入式系统中,硬件寄存器通常按位定义功能。使用位段可以直接映射寄存器结构,方便读写特定位:
struct GPIO_Register 
{
    unsigned int pin0 : 1;  // 1位:引脚0状态
    unsigned int pin1 : 1;  // 1位:引脚1状态
    unsigned int pin2 : 1;  // 1位:引脚2状态
    unsigned int pin3 : 1;  // 1位:引脚3状态
    unsigned int : 4;     // 4位填充,未使用
    unsigned int mode : 2; // 2位:工作模式
};

// 使用示例
volatile struct GPIO_Register* gpio = (struct GPIO_Register*)0x40000000;
gpio->pin0 = 1;    // 设置引脚0为高电平
gpio->mode = 0b10; // 设置为模式2

2. 标志位集合

当程序需要管理多个布尔标志时,使用位段可以将它们压缩到一个字节或字中:

struct ProcessFlags 
{
    unsigned int running : 1;   // 进程是否运行
    unsigned int paused : 1;    // 进程是否暂停
    unsigned int has_error : 1; // 进程是否有错误
    unsigned int priority : 2;  // 进程优先级(0-3)
};

// 使用示例
struct ProcessFlags flags = { 1, 0, 0, 2 }; // 运行中,优先级2
if (flags.running) {
    printf("进程正在运行\n");
}

3. 节省内存

当需要存储大量小范围数据时,位段可以显著减少内存占用:

// 存储颜色信息(8位:3位红、3位绿、2位蓝)
struct Color 
{
    unsigned int red : 3;   // 范围0-7
    unsigned int green : 3; // 范围0-7
    unsigned int blue : 2;  // 范围0-3
};

// 1000个颜色只需250字节(而非3000字节)
struct Color palette[1000];

注意事项

1. 位段的布局依赖编译器:不同编译器可能以不同方式排列位段(如从左到右或从右到左),建议配合 #pragma pack 或特定编译选项使用。
2. 不可取地址:位段成员不能使用& 取地址,因为它们可能不占用完整字节。
3. 性能权衡:位段访问可能比直接位操作稍慢,因为需要编译器生成额外代码。

3. 枚举

枚举顾名思义就是--列举。

把可能的取值--列举。

比如我们现实生活中 :
一周的星期一到星期日是有限的7天,可以一一列举。
性别有 : 男、女、保密,也可以一一列举
月份有12个月,也可以一一列举
这里就可以使用枚举了。

3.1 枚举类型的定义

enum Day//星期
{
    Mon,//0
    Tues,//1
    wed,//2
    Thur,//3
    Fri,//4
    Sat,//5
    Sun//6
};
enum sex//性别
{
    MALE,
    FEMALE,
    SECRET
};

enum color//颜色
{
    RED,
    GREEN,
    BLUE
};

int main()
{
    enum Day d = Thur;
    printf("%d\n", Mon);//0
    printf("%d\n", Tues);//1
    printf("%d\n", Wed);//2
    return 0;
}


以上定义的 enum Day,enum sex,enum co1or 都是枚举类型。
{ }中的内容是枚举类型的可能取值,也叫 枚举常量。
这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。
如:

enum Day//星期
{
    Mon,//0
    Tues,//1
    wed,//2
    Thur,//3
    Fri,//4
    Sat,//5
    Sun//6
};

int main()
{
    enum Day d = Thur;
    printf("%d\n", Mon);//0
    printf("%d\n", Tues);//1
    printf("%d\n", Wed);//2
    return 0;
}

enum Day//星期
{
    Mon = 1,
    Tues,
    wed,
    Thur,
    Fri,
    Sat,
    Sun
};

int main()
{
    printf("%d\n", Mon);//1
    printf("%d\n", Tues);//2
    printf("%d\n", Wed);//3
    return 0;
}

enum Color//颜色
{
    //枚举常量,不能修改,赋值只是初始化
    RED = 1,
    GREEN = 2,
    BLUE = 4
};
3.2 枚举的优点
为什么使用枚举 ?
我们可以使用 #define 定义常量,为什么非要使用枚举 ? 

枚举的优点 :
    
1.增加代码的可读性和可维护性
2.和#define定义的标识符比较枚举有类型检查,更加严谨。
3.防止了命名污染(封装)
4. 便于调试
5.使用方便,一次可以定义多个常量

3.3 枚举的使用

3.3 枚举的使用

enum Color//颜色
{
    RED = 1,
    GREEN = 2,
    BLUE = 4
};
enum color cIr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
clr = 5;//ok??

枚举在 C 语言中 本质是整数类型,枚举变量的底层存储是 int(默认)。例如:

printf("%d\n", clr);  // 输出:2(GREEN的值)
clr = 100;           // 合法,但无对应枚举常量
printf("%d\n", clr);  // 输出:100

4.联合(共用体)(共享单车,你用的时候我不用,不会同时用)

4.1 联合类型的定义
联合也是一种特殊的自定义类型
这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。比如:

union Un//类似于结构体
{
    int a;//4
    char c;//1
};

struct St
{
    int a;
    char c;
};

int main()
{
    union Un u;
    printf("%d\n", sizeof(u));//4
    printf("%p\n", &u);//004FF8A8
    printf("%p\n", &(u.a));//004FF8A8
    printf("%p\n", &u(u.c));//004FF8A8
    eturn 0;
}

4.2 联合的特点
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)


union Un//类似于结构体
{
    int a;//4
    char c;//1
};

u.a = 0x11223344;//44 33 22 11(小端存储,倒着的)
u.c = 0x00;
a变成了 // 00 33 22 11
改c也改变了a

题目:

判断当前计算机的大小端存储

int a = 1;
小端:
低  01 00 00 00  高
大端:
低  00 00 00 01  低
只要能拿出a四个字节中的第一个字节就能判断
如果是1就是小端,0就是大端

int main()
{
    int a = 1;
    char* p = (char*)&a;
    if (*p == 1)
        printf("小端\n");
    else
        printf("大端\n");
    return 0;
}

现在可以用联合

int check_sys()
{
    union un
    {
        char c;
        int i;
    }u;
    u.i = 1;
    return u.c;
}

int main()
{

    int ret = check_sys();
    if (ret == 1)
    {
        printf("小端");
    }
    else
    {
        printf("大端");
    }
    return 0;
}

u:
01 00 00 00

int i:
01 00 00 00

char c:
01 00 00 00

c一定和i共用第一个字节

4.3 联合大小的计算

联合的大小至少是最大成员的大小。

当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。

union Un {

    char arr[5];//5,对齐数是char类型的是1相当于5个char
    int i;//4
}
int main()
{
    printf("%d\n", sizeof(union Un));//8
    return 0;
}

答案是8,需要对其至最大成员对齐数的整数被,最大对齐数是4,4不够,只能是8

char char char char char null null null
int  int  int  int

int 和arr共用前4个字节,后面3个字节浪费掉

结构体、位段、枚举、联合结束!

相关文章:

  • Lua基础语法
  • 在Windows平台基于VSCode准备GO的编译环境
  • Mustache 模板引擎详解_轻量、跨语言、逻辑无关的设计哲学
  • 一文讲透golang channel 的特点、原理及使用场景
  • 正则表达式:字符串模式匹配的利器
  • 历年华南理工大学保研上机真题
  • 什么是前端工程化?它有什么意义
  • 并发编程(6)
  • linux学习第15天(dup和dup2)
  • GO 语言进阶之 进程 OS与 编码,数据格式转换
  • Docker(零):本文为 “Docker系列” 有关博文的简介和目录
  • 二叉树--oj1
  • 计算机基础核心课程
  • 详解Mysql redo log与binlog的两阶段提交(2PC)
  • 2025年AI代理演进全景:从技术成熟度曲线到产业重构
  • 加密货币投资亏损后,能否以“欺诈”或“不当销售”索赔?
  • 【JAVA】线程创建方式:继承Thread vs 实现Runnable(32)
  • LeetCode-图论-岛屿数量+腐烂的橘子
  • 【linux】mount命令中,data=writeback参数详细介绍
  • 分布式缓存:CAP 理论在实践中的误区与思考
  • 从事网站建设/自媒体发布平台
  • 长春网长春网站建设站建设/上海关键词优化报价
  • 室内设计网站模板/广东百度推广的代理商
  • 哈尔滨政府网站建设/app营销策略有哪些
  • 北京的网站建设/elo机制
  • 科技网站设计案例/上海网站优化公司