打造坚实的基础:C语言的结构体、联合体和枚举

结构体的定义和用途

首先我们需要知道,结构体是什么,为什么要用结构体?

在编程中,特别是在像C和C++这样的语言中,结构体(struct)是一种用于创建复合数据类型的工具。结构体允许你将多个不同类型的数据项组合成一个单一的实体。这对于组织和管理数据非常有用,尤其是当你需要处理复杂的数据集时。

c语言中提供了很多的基本数据类型,当想存储一个复杂的信息,比如一个人的基本信息,那么,结构体就可以对这些信息进行封装

结构体的声明,需要用到struct关键词

struct Stu {
    int age;
    char name[20];
    float height;
    //...
};

Stu即为struct的名称,一般名字首字母取大写,便于与变量区分,使用‘{ }’,并以分号结束,结构体中包含多种属性,上述,构建student结构体,包含年龄(int),名字(char [20]),身高(float)。

创建结构体并初始化

结构体的创建有多种
普通声明:声明一个结构体变量,稍后再填充数据

struct Stu s1;
s1={32"xiaoming",1.84}

注意,这种初始化是按照结构体基本变量的顺序来初始化的,当然,还可以按照下面的方法:

struct Stu s2;
s2={.age=32,.height=1.73,.name="xiaoming"};

结构体成员的直接访问是通过点操作符(.)访问的。
例如我们要打印s2的年龄,操作如下:

printf("%d",s2.age)

使用方式:结构体变量.成员名

特殊声明:在构建结构体时,我们也可以在定义的同时定义变量s3,s4

struct Stu {
    int age;
    char name[20];
    float height;
    //...
}s3,s4;

匿名创建:

struct
{
 int a;
 char b;
 float c;
}x;

由于结构体没有名字,不能直接使用这个结构体类型来声明其他变量。换句话说,x 是这个匿名结构体的唯一实例,除非你重新定义相同的结构体
用途局限:这种方式适用于快速定义一些简单结构,尤其是当这种结构不需要在程序的其他部分重复使用时。
可以通过 x 直接访问结构体的成员,例如 x.a、x.b 和 x.c
适用场景:这种方式在需要定义一次性使用的简单结构时非常有用,比如作为函数参数或在特定的代码块内部定义一些临时数据结构。

结构体的嵌套使用

结构体的嵌套使用是指在一个结构体中定义另一个结构体类型的成员。这是一种常见的方式,用来创建更加复杂的数据结构,能够表示对象的层次关系或组合属性。以下是如何使用嵌套结构体的解释和示例:

// 第一个结构体
struct Date {
    int day;
    int month;
    int year;
};

// 第二个结构体嵌套了第一个结构体
struct Person {
    char name[100];
    struct Date birthday; // 嵌套使用 Date 结构体作为生日
    float height;
};

在上面的代码中,我们有两个结构体Date和Person。Person结构体中有一个Date类型的成员birthday,这就构成了结构体的嵌套。
初始化结构体:

struct Person p1={"xiaoming",{23,6,2005},1.81}

访问嵌套结构体成员:
访问嵌套结构体的成员时,你可以通过外部结构体成员的点(.)操作符来访问内部结构体的成员:

printf("Name: %s\n", p1.name);
printf("Birthday: %d/%d/%d\n", p1.birthday.day, p1.birthday.month, p1.birthday.year);
printf("Height: %.2f\n", p1.height);

嵌套结构体的优点是可以创建出更为复杂、分层次的数据模型,而这对应用程序开发来说是十分常见和重要的。它们使得你可以将相关属性组织在一起,并在更大的上下文中使用它们。这种组织方式提高了代码的可读性和可维护性

结构体指针

结构体指针在编程中扮演着重要角色。它们用于存储结构体类型变量的地址。使用结构体指针,你可以访问和操作结构体变量的成员,而无需复制整个结构体。
定义结构体指针:

struct Stu {
    int age;
    char name[20];
    float height;
    //...
};
struct Stu s1;
struct Stu *ptr=&s1;

访问结构体成员:使用结构体指针访问结构体的成员时,需要使用箭头运算符 (->) 而不是点运算符 (.)

ptr->age=23;
printf("%d",ptr->age);

动态内存分配(后续会更新有关内容):结构体指针常与动态内存分配函数(如 malloc)一起使用,在运行时分配内存。

struct Stu *ptr = malloc(sizeof(struct Stu));
if (ptr != NULL) {
    ptr->age = 20;
    ptr->height = 1.78;
}

函数和结构体指针:结构体指针在函数参数中非常有用,特别是当你需要在函数内部修改传递的结构体内容时。

void update(struct Stu *p) {
    p->age = 23;
}

update(ptr);

数组和结构体指针:结构体指针也可以指向结构体数组的元素,并通过指针算术进行操作。

struct Stu st[10];
struct Stu *ptr = st; // 指向数组的第一个元素

// 访问数组的第二个元素
(ptr + 1)->st = 5;

所以,使用结构体指针可以处理复杂的数据结构.

用typedef定义结构体

typedef struct {
    int x;
    int y;
} Point;

在这个例子中,我们定义了一个包含两个整型成员 x 和 y 的结构体,并将这个结构体类型命名为 Point。之后,我们可以直接使用 Point 来声明这种类型的变量:

Point p1, p2;

结构体的自引用
在C语言中,结构体的自引用是指一个结构体内部包含指向同类型结构体的指针。这种技术常用于创建复杂的数据结构,如链表、树、图等。在后续数据结构的学习中我们会遇到
一个简单的自引用结构体的例子是单链表的节点结构。下面是一个这样的结构体的定义:

typedef struct Node {
    int data;           // 数据部分
    struct Node *next;  // 指向下一个节点的指针
} Node;

在这个例子中,Node 结构体包含两个成员:一个 int 类型的 data 和一个指向 Node 类型的指针 next。next 成员用于指向链表中的下一个节点,从而将多个 Node 对象连接成一个链表。在这里,
我们先简单进行了解

结构体内存对齐

我们以一个题作为开始:

struct S1
{
char a;
int b;
char c;
};
struct S2
{
char c1;
char c2;
int i;
};
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));

char,int 分别占一个和四个字节,那么打印结果是什么呢?
如下:

12
8

偏移量

当谈到结构体和偏移量时通常指的是结构体成员相对于结构体开始地址的字节偏移量。编译器在布局结构体成员时会根据内存对齐规则将每个成员放置在合适的位置。
在这里插入图片描述

对齐规则

结构体中每个成员的偏移量取决于前面成员的类型和内存对齐要求:
1. 结构体的第⼀个成员对齐到和结构体变量起始位置偏移量为0的地址处
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的⼀个对齐数与该成员变量大小的较小值。
vs默认对其数为8
3. 结构体总大小为最大对齐数(结构体中每个成员变量都有⼀个对齐数,所有对齐数中最大的)的
整数倍。
3. 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,
4. 结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。

例题

typedef struct {
    char a;   // 偏移量为0
    char b;    // 偏移量为4
    int c;   // 偏移量依内存对齐规则可能为8
} MyStruct;

a占一个字节,对齐到0,偏移量为1
在这里插入图片描述
b占一个字节,对其到对齐数的整数倍及1的倍数上,所以b:
在这里插入图片描述
c占四个字节,则对齐到4的倍数上,则c从四开始:
在这里插入图片描述
那么2 3这两个空间及被浪费掉
结构体总⼤⼩为最⼤对⻬数的整数倍,而刚刚最大对齐数为4, 8为4的倍数,最终结构体大小即为8

struct S1
{
char d;
int e;
char f;
};

d偏移量为零,占一个字节
在这里插入图片描述
e占四个字节,对⻬数 = 编译器默认的⼀个对⻬数(8)与该成员变量大小(4)的较小值。则e对其在4的倍数上,
则从4开始
在这里插入图片描述
f占一个字节,对齐数为1,放在一的倍数:
在这里插入图片描述

结构体大小为最大对齐数(4)整数倍,9不是4的倍数,则浪费9.10.11三块空间,最终大小为12

那么再做下面一道题,判断结构体的大小:

struct S3
{
double d;
char c;
int i;
};
printf("%d\n", sizeof(struct S3));

首先,double占八个字节,偏移量为0:在这里插入图片描述

char对齐数为1,放在一的倍数,对齐8
在这里插入图片描述
i对齐数为4,对齐在4的倍数上,及12
在这里插入图片描述
最大对齐数为8
16为8的倍数,所以 s3 大小为16

结构体嵌套

struct S4
{
char c1;
struct S3 s3;
double d;
};
printf("%d\n", sizeof(struct S4));

如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处.
那么这串代码结果是什么呢?
我们来分析
c1为零偏移量,s3占十六个字节,但对齐位置则在8的倍数上即可,
在这里插入图片描述
d对齐在8的倍数,从24开始
总大小及为最大对齐数(8)的整数倍,最终大小为32

为什么会有内存对齐?

大多数计算机硬件访问内存时都是以一定的“字”大小来操作的。这个“字”通常是几个字节,例如,许多系统上的“字”大小是4字节或8字节。如果数据结构(如结构体成员)没有对齐到这些边界,处理器可能需要进行额外的内存访问操作才能读取或写入非对齐的数据。这会降低访问速度。

在某些处理器架构上,对数据进行不对齐的访问会导致硬件异常。这意味着处理器不能处理这样的内存访问请求,可能导致程序崩溃。例如,一些老的RISC(精简指令集计算机)架构就有这样的限制。

那在设计结构体的时候,我们既要满足对齐,又要节省空间,可以让占用空间小的成员放在一起:

struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};

修改默认对齐数

#include <stdio.h>
#pragma pack(1)//设置默认对⻬数为1
struct S {
char c1;
int i;
char c2;
};

此时s大小即为6.

#pragma pack()//取消设置的对⻬数,还原为默认

结构体传参

通过值传递

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);
}

这种方法 ,减少了内存使用和传递成本,特别是对于大型结构体,且允许函数修改原始结构体

如果你想通过指针传递结构体,但又不希望函数修改结构体的内容,可以使用常量指针

void print2(const struct S* ps)
{
printf("%d\n", ps->num);
}

结构体实现位段

在 C 语言中,位段(Bitfields)是一种数据结构,允许你为结构体中的成员分配一个特定数量的位(bit),而不是分配完整的字节。这可以在需要精确控制内存布局或减少内存占用的情况下非常有用,比如硬件访问和协议设计
位段的声明和结构是类似的,但也有不同:
位段是通过在结构体定义中为成员后添加一个冒号和位数来创建的,例如:

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

位段的内存分配

位段的成员可以是 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;

我们来讨论这块空间是如何开辟的
位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义
假设从右向左,我们来进行分析,首先a占3个字节,一个bit占八个字节,且a的值为10,二进制为1010,取二进制后三位010 :在这里插入图片描述
b占四个字节,值为12,二进制为1100,取四位为1100

在这里插入图片描述
下一个c占五个字节,第一个位段不满五直接跳转到下一个位段
c的值为3,二进制为00011,放入:
在这里插入图片描述
d占四个字节,第二个位段不满直接跳到下一个位段
值为4,二进制为00100,取四位放入:
在这里插入图片描述
则二进制序列则为:

01110010 00000011 00000100

对应十六进制为:

0x62     0x03     0x04

由低地址到高地址存储
位段非常适合节省空间以及打包和解包紧凑的数据

位段的跨平台问题

位段的内存布局:不同的编译器可能会以不同的顺序排列位段。有些编译器可能会按照声明的顺序排列位段,而其他编译器可能会重新排序以优化空间或访问效率
位段的存储单元:位段通常存储在整型存储单元中,但不同的编译器可能会选择不同的类型作为存储单元,例如 int、unsigned int 或 long。
位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义
端序影响大端和小端架构中,位段的物理存储顺序可能不同。

位段使用的注意事项

一个bit位是没有地址的,位段的几个成员共有同⼀个字节这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的内存中每个字节分配⼀个地址
则不能用&符来为其赋值:

struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
}
int main()
{
struct A sa = {0};
scanf("%d", &sa._b);//这是错误的
//正确的⽰范
int b = 0;
scanf("%d", &b);
 sa._b = b;
return 0; }

联合体

联合体类型的声明

像结构体⼀样,联合体也是由⼀个或者多个成员构成,这些成员可以不同的类型。
联合体的大小至少为其最大成员的大小,确保可以存储其中任何一个成员。联合体通常用于节省空间和处理不同类型的数据。
联合体通过关键字 union 定义。例如:

union Data {
    int i;
    float f;
    char str[20];
};

在这个例子中,union Data 可以存储一个 int、一个 float 或一个 char 数组,但只能存储其中之一

联合体的特点

共享内存:联合体的所有成员共享同一块内存区域。修改一个成员会影响其他成员的值。

大小:联合体的大小等于其最大成员的大小。在上面的例子中,如果 int 为4字节,float 为4字节,char 数组为20字节,那么 union Data 的大小将为20字节

使用:联合体适用于那些可能需要在不同时间以不同类型访问同一块内存的场景
举例如下:

#include <stdio.h>

union Data {
    int i;
    float f;
    char str[20];
};

int main() {
    union Data data;        

    data.i = 10;
    printf("data.i : %d\n", data.i);

    data.f = 220.5;
    printf("data.f : %f\n", data.f);

    strcpy(data.str, "C Programming");
    printf("data.str : %s\n", data.str);

    // 此时,打印 data.i 或 data.f 可能会得到无意义的值
    return 0;
}

在这个例子中,当 data 被赋予新的值时,先前存储的数据会被覆盖。因此,即使 data.i 和 data.f 在赋值后仍然存在,但它们的值可能已经被破坏,不再有意义
我们再看一个例子:

#include<stdio.h>
union Un{
char c;
int i;
};
int main()
{
//联合变量的定义
union Un un = {0};
printf("%p\n", &(un.i));
printf("%p\n", &(un.c));
printf("%p\n", &un);
 un.i = 0x11223344;
 un.c = 0x55;
printf("%x\n", un.i);
return 0; }

打印结果为

001AF85C
001AF85C
001AF85C
11223355

我们可以试着画出,un的内存布局:

在这里插入图片描述

相同成员的结构体和联合体对比

struct S {
char c;
int i;
};
struct S s = {0};
union Un{
char c;
int i;
};
union Un un = {0};

在这里插入图片描述

联合体大小的计算

联合体的大小是由其最大成员决定的,但是还要考虑内存对齐要求。联合体的大小至少是其最大成员所需的字节量,且可能因为对齐要求而增加。
对于如何计算联合体的大小,这里是一些关键的步骤和考虑因素:

确定最大成员: 查找联合体中所有成员的类型,确定占用空间最大的成员。

内存对齐:在有些情况下,为了满足内存对齐要求,编译器可能会将联合体的总大小调整为特定的边界。通常,这个对齐边界是所有成员对齐要求的最大值。
以下是几个例子,假设在一个32位系统上(整数和浮点数是4字节,字符是1字节):

union Example1 {
    char c; // 1字节
    int i;  // 4字节
};

Example1 的大小是4字节,因为 int 是最大的成员,并且不需要额外的对齐(int 的对齐通常是4字节)。

union Example2 {
    char c[5]; // 5字节
    int i;     // 4字节
};

Example2 的大小将是5字节,虽然 int 对齐要求是4字节,但是因为 char 数组使用了5个字节,所以联合体的大小至少是5字节。

union Example3 {
    char c[3]; // 3字节
    int i[2];  // 8字节 (每个int 4字节)
};

Example3 的大小将是8字节,因为 int i[2] 这部分占用了8字节,这是联合体中最大的占用空间。

对齐要求可以根据不同的编译器、编译选项和平台有所不同。当计算联合体的大小时,如果需要考虑最严格的对齐要求,就必须要考虑各成员类型在目标平台上的具体对齐规则。在不同的系统或不同的编译器设置中,同一个联合体的大小可能有所不同。

联合体的一个练习!

写一个程序判断是大端还是小端?

int check_sys()
{
union
{
   int i;
   char c;
}un;
   un.i = 1;
   return un.c;//返回1是⼩端,返回0是⼤端 
}

绝了

枚举

定义枚举
枚举通过关键字 enum 定义。例如:

enum Color {
    RED,
    GREEN,
    BLUE
};

在这个例子中,enum Color 定义了一个枚举类型,包含三个枚举成员:RED、GREEN 和 BLUE。
枚举成员的值
默认情况下,枚举的第一个成员的值是 0,后续成员的值依次递增。但是,你也可以手动为枚举成员指定值:

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

使用
枚举可用于变量声明、参数类型或作为函数的返回类型:

enum Color favoriteColor;

favoriteColor = RED;

if (favoriteColor == RED) {
    printf("Red is your favorite color!");
}

优点
代码可读性:使用枚举可以使代码更清晰,更易于理解。
类型安全:枚举提供了一个类型安全的方法来表示一组整数值。
调试易用性:在调试时,枚举类型的值通常比整数更易于理解

感谢阅读!我会不断更新新的内容

  • 76
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 19
    评论
评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

QuiteCoder

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值