结构体初阶+进阶 系统化的学习

 

结构体

什么是结构体

结构体是一种用户自定义的数据类型,可以组合多个相关值成为一个单一类型。它是由一批数据组合而成的结构型数据,结构体可以包含多个不同类型的字段,如基本数据类型、其他结构体、枚举类型等。在Rust中,结构体有着重要的作用,可以创建更复杂的数据结构,并定义它们的行为。结构体使用struct关键字定义,如`struct Person { name: String, age: i32, }`。结构体实例的创建需要使用构造函数。结构体字段可以是可变的,也可以是不可变的,默认情况下,结构体字段不可变。如需修改结构体字段,需要使用mut关键字。结构体也可以绑定方法,方法允许你以面向对象的方式操作结构体实例。结构体还提供了一种更新语法,使用..运算符(也称为点运算符或展开运算符)。结构体在不同的编程语言中都有类似的实现,如C++和Rust等。 

结构体的解释

结构体(Structure)是C语言中一种组织多个变量的方式,它允许我们将不同的数据类型组合在一起,形成一个单一的实体。
结构体在内存中占用的空间等于其所有成员占用的空间之和。
在C语言中,结构体是通过关键字 `struct` 定义的
下面是一个简单的结构体定义的例子:

struct Person {
    char name[50];
    int age;
    float height;
};

在这个例子中,我们定义了一个名为 `Person` 的结构体,它包含三个成员:`name`(字符数组,用于存储姓名),`age`(整型,用于存储年龄),和 `height`(浮点型,用于存储身高)。
要使用这个结构体,我们可以声明一个 `Person` 类型的变量:

struct Person p1;

然后,我们可以像访问普通变量一样访问 `p1` 的成员:

p1.name = "Alice";
p1.age = 30;
p1.height = 165.5;

我们也可以通过指针来访问结构体的成员,这可以提高代码的灵活性:

struct Person *p2;
p2 = &p1;
printf("%s\n", p2->name); // 输出: Alice

在上述代码中,`p2` 是一个指向 `Person` 结构体的指针。通过 `->` 操作符,我们可以访问它指向的结构体的成员。
结构体在实际编程中的应用非常广泛,例如,它可以用来表示现实世界中的对象或实体,如学生、员工等,每个实体都有其相应的属性。结构体也可以用来组织数据,使得数据管理更加方便和高效。

结构体的基本知识

结构是值的变量

也就是值的集合,这些集合称之为成员变量

数组一组相同元素的集合

结构体是一组不一定相同类型的元素的集合

复杂的对象不能通过简单的内置类型直接描述和表示 此时就有了结构体 来描述复杂类型

结构体的声明

但是需要知道的是,在函数体里面写的名字 ,如果要这个在其他函数进行使用,要么使用函数声明,要么把结构体放到最上面。

因为C语言的运行程序是从上往下进行运行的,但是进行函数的声明之后,就会先进行一次程序走一遍,再继续运行。

struct tag是名字//tag是名字 结构体是本身是不需要进行头文件的

{

}

大括号里面是成员 可以是多个 也可以是0个

最后是变量列表

举例 描述一个学生

一个汉字两个字符串

这个就是结构体类型

————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

结构体的类型名和变量名

在 C 语言中,结构体(struct)中的类型名称和变量名称是有区别的:
1. **类型名称**:这是结构体类型的名称,它是用来定义结构体模板的标识符。当你定义一个结构体时,你需要给它一个名称,例如:

 

   ```c
   struct Person {
       char name[50];
       int age;
   };
   ```


   在这里,`struct Person` 就是一个类型名称,它定义了一个包含两个成员的结构的模板。
2. **变量名称**:这是你给结构体实例化的变量所起的名称。当你声明一个结构体变量时,你需要给它一个名称,例如:

 

   ```c
   struct Person p1;
   ```


   在这里,`p1` 就是结构体变量的名称,它是一个具体的结构体实例,可以存储相应的数据。
类型名称和变量名称之间的区别在于,类型名称用于定义结构体的模板,而变量名称用于创建结构体的实例。类型名称不能被直接使用来存储数据,它只是一个用来创建变量的蓝图。只有通过声明变量名称,你才能在程序中使用结构体类型来存储和操作数据。
例如,你可以使用类型名称来声明一个指向结构体的指针:

 

```c
struct Person *ptr;
```


这里,`struct Person *ptr` 声明了一个指针类型为 `struct Person` 的变量 `ptr`。但是,你不能直接使用 `struct Person` 来声明一个变量,你需要提供一个具体的变量名称,如 `ptr`。 

 ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

结构体的初始化

有了类型 才可以初始化

此时也就完成了结构体的初始化

这个是在main函数里面的初始化

当然这个不仅可以在main函数里面进行初始化 ,也可以在结构体下面的函数连进行初始话

初始化的时候就是 struct + 函数体名字(Stu) 在主函数里面 这个时候就再加上一个名字 arr等任意名字 就按照数组的方式可以进行初始化

也就是 可以是struct + Stu+si;

也可以是struct + Stu+arr;在主函数里面

在C语言中,结构体的初始化可以通过几种方式来完成,包括逐字段初始化、使用结构体数组、使用`malloc`分配内存后初始化,以及使用`memset`或`bzero`对内存块进行初始化。

1.
逐字段初始化
逐字段初始化是最直接的方法,直接为结构体的每个字段赋值例如:
struct Person {
    char name[50];
    int age;
    float height;
};
struct Person p1 = {"张三", 30, 165.5f};//按照顺序 也就是 name=张三,age=年龄,heihgt=身高



2.
 使用结构体数组
如果你有一系列结构体实例,你可以使用结构体数组来初始化它们:

struct Person students[3] = {
    {"Bob", 22, 175.0f},
    {"Charlie", 24, 180.0f},
    {"David", 23, 172.0f}
};


3.
使用`malloc`分配内存后初始化
如果你需要在程序运行时动态分配结构体的内存,你可以使用`malloc`函数,然后手动为每个字段赋值:
struct Person *p2 = (struct Person *)malloc(sizeof(struct Person));
if (p2 != NULL) {
    strcpy(p2->name, "Bob");
    p2->age = 22;
    p2->height = 175.0f;
}


4.
使用`memset`或`bzero`初始化
在某些情况下,你可能需要初始化整个结构体或结构体数组的全部字段,这时可以使用`memset`或`bzero`函数。`memset`将内存中的字节设置为指定的值,而`bzero`只是将内存中的字节设置为0(清零)。
struct Person p3;
memset(&p3, 0, sizeof(struct Person)); // 将p3的字段全部初始化为0
// 或者使用bzero
bzero(&p3, sizeof(struct Person)); // 将p3的字段全部初始化为0
请注意,使用`memset`或`bzero`时,要确保传递的地址是指向结构体的指针,而不是结构体本身。
这些是结构体初始化的常见方法。根据具体的需求和场景,你可以选择最适合你的初始化方式。

结构体的类型和变量

比喻

这个是图纸和房子的关系

结构体就是图示 主函数里面的初始化和框架也就是开始建立房子

类型和变量的关系(局部变量和全局变量)

这里也就是char name[100]所以占用的空间大一点

int age是整形 占据四个字节或者八个字节

char sex[5]又比int大一点

有了类型之后在主函数里面创建s1

这里的s2 s3 s4 就是结构体变量 这三个是函数外面创建的 也就是全局变量 和s1一样 但是s1 是局部变量 但是s2,s3,s4是结构体的全局变量

代码举例
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
struct Stu
{
	char name[100];//这里看理解为名字 一个汉字占两个字符
	int age;//这里可以设置成整形 因为这里是名字的意思 
};
struct Stu s3[4] = { {"张三",12} ,{"lisi",23}, {"王五",2}, {"二狗",1} };
struct Stu s4[4] = { {"张三",12} ,{"lisi",23}, {"王五",2}, {"二狗",1} };

void test()
{
	struct Stu s2[4] = { {"张三",12 } ,{"lisi",23}, {"王五",2}, {"二狗",1} };

}

int main()
{
	//此时如果是在main函数里面 或者在test();函数里面创建的结构体变量,此时是局部变量 也就是cs1 s2
	//如果是在结构体外部创建的变量 此时是全局变量 也就是 s3 s4
	struct Stu s1[4] = { {"张三",12 } ,{"lisi",23}, {"王五",2}, {"二狗",1} };

	
	return 0;
}

———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— 

重命名typedef

 typedef的使用
在编程中,`typedef` 是一个关键字,用于为已存在的数据类型创建一个别名。这样做可以让代码更易于阅读和维护,特别是在处理复杂或者冗长的类型名称时。
例如,在 C 语言中,您可以使用 `typedef` 为标准数据类型如 `int` 创建别名:

typedef int INT32; // 将 int 类型重命名为 INT32

之后,您就可以使用 `INT32` 代替 `int` 来声明变量:

INT32 a, b; // 这里实际上是指 int 类型的变量

在不同的编程语言中,`typedef` 的作用和用法可能会有所不同,但核心概念是类似的,都是为了简化代码中对数据类型的引用。

typedef对变量重命名 但是需要知道 C语言里面 如果没有对结构体类型进行typedef,struct是不能省略的

也就是如果存在typedef 结构体后面的stu是一个类型


例如,如果我们有一个结构体:

struct Student {
    char name[50];
    int age;
    float score;
};

我们可以使用 `typedef` 为这个结构体定义一个新的类型名称:

typedef struct Student stu;

这样,`stu` 就成为了 `struct Student` 的一个别名。之后,你就可以使用 `stu` 来声明这个结构体的变量:

stu s1;

这里,`s1` 是一个 `struct Student` 类型的变量,但使用了 `stu` 作为它的类型名称。
所以,如果你看到代码中有 `typedef struct Student stu;`,那么 `stu` 就是一个代表 `struct Student` 类型的别名。

或者 

———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— 

结构体的成员访问

结构体是可以互相包含的

可以在上一个 结构体里面 定义一个新的 结构体类型

上面我们知道在结构体外面创建的结构变量的全局变量

但是其实在这个外面创建的全局变量也是可以直接进行初始化的 

在全局变量和局部变量里面的代码举例里面进行了举例

举例

在s1里面进行修改

初始化 并且给他一些数值

初始化 逐步初始化,

100 字符 空指针

struct S +名字s2 然后初始化 括号{} 和 数组的初始化有些类似

这里依旧是结构体的初始化的举例

选择初始化

.选择 中间       ,      隔开

.的方式找到成员

这里需要记住

1   .  是  结构成员访问操作符 .

2   ->是结构成员访问操作符 .

重复一下

1   .  是  结构成员访问操作符 .

2   ->是结构成员访问操作符 .

 这里打印的是这三个

如果是需要进行这个多个结构体的访问和打印 需要用上循环

struct的B sb进行初始化和成员访问

怎么放里面怎么拿出来

也就是如何打印出来 这里需要一一对应的方式打印出来,哪怕是进行循环打印,这个结构体里面的数值也要进行一一对应的方式进行打印

下面打印的是结构体B 初始化函数名sb 

打印的时候就是sb.ch//意思就是sb下的struct ch

同理打印结构体第二个数值 也就是sb.s.a//意思就是struct B 结构体创建的sb下的,struct B里面的struct S s。

数值的传递

这里是直接把结构体传参过去了 传到set_stu函数里面 在后期会用得上

但是记得,在传参的时候需要带上struct+名字 +初始化的名字 

进行拷贝

把张三拷贝到name里面去

strcpy是拷贝函数(记着就行)

包含头文件string.h

此时set_stu就成功的把函数拷贝过来了
语法形式记着就可以

strcmp的解释

在C语言中,`strcpy` 函数用于将一个字符串复制到另一个字符串中。它的原型定义在 `string.h` 头文件中。`strcpy` 函数的语法格式如下:

char *strcpy(char *dest, const char *source);

参数说明:
- `dest`:指向目标字符串的指针,即要复制字符串到的位置。
- `source`:指向源字符串的指针,即要复制的字符串。
`strcpy` 函数会复制 `source` 指向的字符串到 `dest` 指向的空间中,包括字符串结束符 `\0`。注意,目标字符串数组必须有足够的空间来容纳源字符串,否则可能会导致缓冲区溢出。
示例用法:

#include <stdio.h>
#include <string.h>
int main() {
    char dest[20];
    const char *source = "Hello, World!";
    // 将源字符串复制到目标字符串中
    strcpy(dest, source);
    printf("复制后的目标字符串: %s\n", dest);
    return 0;
}

在这个例子中,`strcpy` 函数将 "Hello, World!" 复制到 `dest` 数组中,然后程序打印出复制后的字符串。

回归主体 

打印结构体s

t.什么什么 ,这个t其实就是创建的形参可以理解为

但是此时是错误

因为结构体是需要明确指向哪个地址的

需要用指针进行接收,所以传参的时候需要取地址,因为指针指向的是地址

画图解析原因

此时需要解决需要传址

在指针(1)指针篇章-(1)-CSDN博客里面解释了什么是传址 什么是传值

&s也就是需要取地址 然后运算 结构体函数的参数 需要用指针接收

这样就是正确的

这里指向的不是打印是age和name而是struct stu里面的name和age

这里也需要取地址名字s 然后指向这个位置 打印出来 这样打印的才是正确 

升级版本 箭头 结构体成员

打印结构体的代码以及结构体传参

 不包含循环的打印

此时也就是指针指向的是首元素的地址 所以打印的时候也就是打印的首元素的地址

要是想循环打印出内容的情况下 只需要在外部加个for循环 

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
struct Stu
{
	char name[100];//这里看理解为名字 一个汉字占两个字符
	int age;//这里可以设置成整形 因为这里是名字的意思 
};
struct Stu s3[4] = { {"张三",12} ,{"lisi",23}, {"王五",2}, {"二狗",1} };
struct Stu s4[4] = { {"张三",12} ,{"lisi",23}, {"王五",2}, {"二狗",1} };

void test(struct Stu *ps)//这里调用的是main函数里面的结构体进行打印 
{
	printf("%s %d\n", ps->name, ps->age);
}

int main()
{
	//此时如果是在main函数里面 或者在test();函数里面创建的结构体变量,此时是局部变量 也就是cs1 s2
	//如果是在结构体外部创建的变量 此时是全局变量 也就是 s3 s4
	struct Stu s1[4] = { {"张三",12 } ,{"lisi",23}, {"王五",2}, {"二狗",1} };
	test(&s1);
	struct Stu s2[4] = { {"zhangsan",12 } ,{"lisi",23}, {"王五",2}, {"二狗",1} };
	test(&s2);

	return 0;
}

循环打印出结构体

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
struct Stu
{
	char name[100];//这里看理解为名字 一个汉字占两个字符
	int age;//这里可以设置成整形 因为这里是名字的意思 
};
struct Stu s3[4] = { {"张三",12} ,{"lisi",23}, {"王五",2}, {"二狗",1} };
struct Stu s4[4] = { {"张三",12} ,{"lisi",23}, {"王五",2}, {"二狗",1} };

void test(struct Stu *ps)//这里调用的是main函数里面的结构体进行打印 
{
	printf("%s %d\n", ps->name, ps->age);
}

int main()
{
	//此时如果是在main函数里面 或者在test();函数里面创建的结构体变量,此时是局部变量 也就是cs1 s2
	//如果是在结构体外部创建的变量 此时是全局变量 也就是 s3 s4
	struct Stu s1[4] = { {"张三",12 } ,{"lisi",23}, {"王五",2}, {"二狗",1} };
	int sz1 = sizeof(s1) / sizeof(s1[0]);
	for (int i = 0; i < sz1; i++)
	{
		test(&s1[i]);
	}
	printf("\n");
	struct Stu s2[4] = { {"zhangsan",12 } ,{"lisi",23}, {"王五",2}, {"二狗",1} };
	int sz2 = sizeof(s2) / sizeof(s2[0]);
	for (int i = 0; i < sz2; i++)
	{
		test(&s2[i]);

	}

	return 0;
}

——————————————————————————————————————————————————————————————————————————————————————

 结构体指针补充

在C语言中,结构体(struct)和指针(pointer)是两种不同的数据类型,它们用于存储和操作数据。访问操作符主要是指点操作符(->)和解引用操作符(*)。 

1. 结构体(struct):
结构体是C语言中的一种复合数据类型,它允许我们将多个不同类型的数据项组合成一个单一的实体。结构体变量的每个成员都可以通过点操作符(.)来访问。


例如,定义了一个结构体 `Person`:

struct Person {
    char *name;
    int age;
    float height;
};

创建了一个 `Person` 类型的结构体变量 `person1`:

struct Person person1;

访问 `person1` 的 `name` 成员:

person1.name = "Alice";


2. 指针(pointer):
指针是一个变量,它存储了另一个变量的内存地址。指针通过解引用操作符(*)来访问它指向的变量的值。指针变量可以通过箭头操作符(->)来访问它指向的结构体的成员,前提是指针必须指向一个结构体变量。


例如,定义了一个指针 `Person`:
struct Person *ptr;

将 `Person` 类型的变量 `person1` 的地址赋给指针 `ptr`:

ptr = &person1;

通过指针 `ptr` 访问 `person1` 的 `name` 成员:

(*ptr).name = "Alice";

或者,使用箭头操作符:

ptr->name = "Alice";

总结:
- 结构体的成员访问使用点操作符(.)。
- 指针的成员访问先使用解引用操作符(*)得到指针指向的结构体,然后再使用点操作符(.)访问结构体的成员。
- 箭头操作符(->)是C++中的一种语法糖,它实际上是解引用操作符(*)和点操作符(.)的组合,用于指针访问结构体成员。在C语言中,你应该使用解引用和点操作符的组合来达到相同的效果。 

————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

结构体传参 

传值


   当结构体作为函数参数时,如果使用传值方式,那么函数将接收一个结构体变量的副本。这意味着在函数内部对结构体的修改不会影响到原始的结构体变量。因为在函数调用时,会创建一个结构体的副本并传递给函数,函数操作的是这个副本,而不是原始数据。
   示例: 


   
   struct Student {
       char name[50];
       int age;
   };
   void printStudent(struct Student s) {
       printf("Name: %s, Age: %d\n", s.name, s.age);
   }
   int main() {
       struct Student student = {"Alice", 20};
       printStudent(student);  // 调用时传递的是student的副本
       return 0;
   }
 

传址


   当使用传址方式时,函数接收的是结构体变量的地址。这意味着在函数内部对结构体的修改会影响到原始的结构体变量,因为函数操作的是存储在原始地址中的数据。
   示例:
  


   struct Student {
       char name[50];
       int age;
   };
   void modifyStudent(struct Student *s) {
       strcpy(s->name, "Bob");  // 修改的是传入的结构体变量
       s->age = 21;
   }
   int main() {
       struct Student student = {"Alice", 20};
       modifyStudent(&student);  // 传递的是student的地址
       printf("Name: %s, Age: %d\n", student.name, student.age);  // 输出已修改的值
       return 0;
   }

总结

 在实际编程中,通常根据是否需要修改原始数据来选择使用传值还是传址。如果函数需要修改结构体数据,或者结构体较大,为了节省内存和提高效率,通常使用传址方式。如果函数只是读取结构体数据,而不进行修改,使用传值方式即可。

原因函数传参的时候 参数压栈 参数过大的时候 压栈就过大 不仅浪费空间 而且浪费时间

如果直接传递地址过去 无非就是四个或者八个字节 只要有一个指针大小的空间就够了

————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

结构体的自引用

结构体的自引用指的是什么

结构体的自引用是指在一个结构体中直接或间接地包含对其自身的引用。在编程中,这种结构通常是不推荐的,因为它可能导致无限递归或者难以理解和维护的代码。
在C或C++等语言中,如果一个结构体中含有指向自身类型的指针,就可能出现自引用的情况。例如:

typedef struct Node {
    int data;
    struct Node *next;
} Node;

Node *createNode(int data) {
    Node *newNode = (Node *)malloc(sizeof(Node));
    if (!newNode)
        return NULL;
    newNode->data = data;
    newNode->next = newNode;  // 这里创建了一个自引用结构体
//所谓的自引用的肯定是需要包含自己的 
    return newNode;
}


在这个例子中,`Node` 结构体包含一个 `int` 类型的 `data` 字段和一个指向自身类型的指针 `next`。在 `createNode` 函数中,`newNode->next` 被设置为 `newNode` 自身,这样就形成了循环引用。

 ——————————————————————————————————————————

自引用结构体可能导致的问题包括:

1. **内存泄漏**:由于循环引用,使用自引用结构体时很难正确管理内存,可能导致内存泄漏。
2. **难以调试**:循环引用可能会导致程序在运行时表现出奇怪的行为,因为它们会干扰内存管理,使调试变得困难。
3. **性能问题**:循环引用可能会导致额外的内存复制和分配,影响程序的性能。
在设计数据结构和算法时,应当尽量避免自引用。如果必须使用,应该采取措施确保不会导致无限递归或其他问题。例如,可以通过设置一个 `NULL` 终止符来打破循环,或者在使用自引用结构体时确保有明确的终止条件。

 ——————————————————————————————————————————

结构体如何自引用

什么是结构体的自引用,简单的说就是 在结构体里面的自我应用

也就是结构体里面包含结构体

但是自引用的时候,你不能直接使用

因为你需要知道,如果结构体里面你直接引用一个结构体的情况下,糟糕的情况产生了,也就是你并不知道这个结构体的大小是多少。

什么意思呢,简单的说就是,结构体子啊没有自己引用自己之前,这个内存的大小是确定的,比如里面放一个char n;这是明确的,但是你要是自己引用的时候,char n;下面放一个自己的结构体,此时会产生一种情况。

char n;的大小是确定的,但是需要计算struct的时候,也就是结构体自己的时候,此时你会发现,结构体产生了二次引用,你又去调用自己,在自己里面,你又去调用自己。导致不知道大小,导致无限循环,最后导致崩盘。

 ——————————————————————————————————————————

那么结构体如何进行自我引用才不会导致死循环导致的崩盘。

很简单,此时我们引用链表的概念

什么是链表,:链表是一种非连续、非顺序的存储结构,其逻辑顺序是通过链表中的指针链接次序实现的。链表由多个独立的节点组成,每个节点都包含一个数据区域和一个指向下一个节点的地址。根据节点的链接方向,链表可以分为单向链表和双向链表。在单向链表中,节点的指针只指向下一个节点;而在双向链表中,节点的指针同时指向下一个节点和上一个节点。链表广泛应用于数据结构中,如线性表、栈、队列等。

这里我们不重点进行讲解,大概知道一下概念。

链表的概念是如何在结构体里面进行使用的。

简单的说就是,这里你自己直接引用自身会导致死循环

但是如果我们只是每次在自引用的时候找到下一个空间的地址,就不会导致这种情况的产生。

之前我们引用的是自身,那么我们可以在自引用的时候,只是指向的是下个空间的首元素的地址,而不是单纯的只是自引用自己

图解

 代码

typedef struct Node {
    int data;           // 数据字段 如果是1 2 3 4 5
    struct Node* next;  // 指向下一个节点的指针 此时如果是1 下一步指向的是2
} Node;

这里我们首先了解一下,后续在链表里面我们会进行详细的说明。 

 ——————————————————————————————————————————

typedef类型的重命名

此时,你的结构体的Node是类型名,他不是变量名字

所以是不能直接写到main函数里面的

此时我们使用typedef重命名,对结构体重新命名, 在重命名之前,如果要引用结构体我们必须

struct Node +n1(变量名)//也就是必须明确为结构体 

但是我们重命名为Node之后,并且放在全局变量,此时也就是可以直接引用所以此时在main函数里面可以是

Node +n2(变量名)//此时不需要明确为结构体 因为已经是全局变量

 —————————————————————————————————————————— 

自引用的使用 图解

这里一定要加struct 因为外部是全局变量

但是里面是局部变量 所以是不行是 还没有进行声明 就已经使用是不行的

————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

结构体的内存对齐(重点)

什么是结构体的内存对齐

结构体的内存对齐是指在计算机编程中,编译器按照一定的规则对结构体中的数据成员在内存中的存放位置进行调整,以确保数据成员的地址满足特定的对齐要求。这种对齐不是指数据类型本身的大小,而是指数据在内存中的位置对齐到某个边界,通常是数据大小的倍数。
内存对齐的目的是为了提高内存访问的效率。许多处理器在访问非对齐的数据时会有性能损失,甚至可能不支持非对齐访问。因此,为了确保数据能够被高效地访问,编译器会按照以下原则对数据进行对齐:

1. **数据成员对齐**:

每个数据成员都会根据其类型的大小和对齐要求进行对齐。例如,如果一个int类型的大小是4字节,那么这个int变量就会放置在4字节的边界上。


2. **结构体整体对齐**:

整个结构体也会根据其最大成员的对齐要求进行对齐。这意味着结构体的尺寸可能是其所有成员大小之和加上一些填充字节(padding),以满足最大成员的对齐要求。


3. **填充字节**:

为了满足对齐要求,编译器可能会在结构体中的数据成员之间或结构体末尾添加不必要的空字节。这些填充字节不会存储任何有用的数据,但它们的存在确保了结构体和其成员都能被正确对齐。


4. **对齐边界**:

对齐边界通常是数据大小的倍数,例如,1字节、2字节、4字节、8字节等。不同的平台和编译器可能有自己的对齐规则。


结构体的内存对齐是一个重要的概念,特别是在涉及到内存效率和性能敏感的应用中,如图形处理、网络编程和对性能要求极高的系统编程等领域。正确地理解和使用结构体内存对齐可以显著提高程序的性能。

———————————————————————————————————————————

结构体对齐的原则

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

2、其他成员变量要与自身类型的整数倍地址处对齐;

3、结构体总大小为要与 “处理器字节数与成员类型所占字节数最大的最小值” 的整数倍对齐;

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

5、#pragma pack(n) 可以用来控制默认对齐数的大小

什么意思呢,下面我们会进行语言的解释

———————————————————————————————————————————

结构体内存对齐的详细图解 

%zd------->打印无符号整形

———————————————————————————————————————————

结构体内存对齐-举例1

首先我们创建一个结构体

我们要计算他的字节大小,如果只是看起来,那就是无非是1字节,4字节,1字节,但是实际放到编译器里面, 计算子字节大小的时候并不是6字节大小,而是12字节大小

这里我们进行图片的解释

六个字节浪费了 1

步骤详解 

———————————————————————————————————————————

结构体内存对齐-举例2

———————————————————————————————————————————

结构体内存对齐-举例3

对齐自己结构体最大成员字节个数


——————————————————————————————————————————— 

为什么存在内存对齐

主要是平台原因和性能原因

虽然浪费空间 但是提升时间

如果是倍数的情况下 只读取一次就解决 但是不对齐两次才能获取


减少浪费空间

占用空间小的放在一起

就会最大限度的节约空间

———————————————————————————————————————————

修改默认对齐数

这里这一段中间的结构体的默认对齐数也就变成1 也就是连续的放置

默认对齐的数一般都是2的次方数

Linux没有对齐数

那也就是变成了自身的大小

———————————————————————————————————————————

结构体对齐的原则总结

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

2、其他成员变量要与自身类型的整数倍地址处对齐;

3、结构体总大小为要与 “处理器字节数与成员类型所占字节数最大的最小值” 的整数倍对齐;

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

5、#pragma pack(n) 可以用来控制默认对齐数的大小

————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

结构体的位段

结构体位段的使用原则 

在C语言中,结构体(Struct)是一种复合数据类型,它允许我们将多个不同类型的数据项组合成一个单一的实体。位段(Bit Field)是结构体中的一个特殊成员,它允许我们只取结构体中的某个位范围的数据,而不是整个字段。位段可以用来高效地访问和操作内存中的位。
位段的定义格式如下:


struct 位段名 {
    unsigned int 位段1:宽度;
    unsigned int 位段2:宽度;
    ...
    unsigned int 位段N:宽度;
} 变量名;

这里,`位段名`是结构体的名称,`位段1`到`位段N`是结构体中的位段名称,`宽度`是指定每个位段占据的位数。`unsigned int`表示位段的数据类型,它保证了位段中的每一位都是非负数。
例如,以下是一个定义了一个包含三个位段的结构体:


struct RGB {
    unsigned int red : 5;
    unsigned int green : 6;
    unsigned int blue : 5;
} pixel;


在这个例子中,`RGB` 结构体有三个位段:`red`、`green` 和 `blue`。每个位段分别占据了 5 位、6 位和 5 位。这样的结构体可以用来表示一个颜色像素的三原色值。
位段的访问可以通过位运算符来进行,例如`&`(按位与),`|`(按位或),`^`(按位异或),`~`(按位取反),`<<`(左移),`>>`(右移)等。
位段的使用可以节省存储空间,并且可以很方便地通过位运算符来操作特定范围的位。这在需要对内存中的位进行精细控制时非常有用。

———————————————————————————————————————————

结构体位段的原理解剖

这里的_a等等这个是变量名称

这里的2是宽度,也就是只要几个字节的意思

结构体内存对齐的时候为了节约时间,导致空间的浪费。

而位段的使用,就是节约空间,但是会导致时间的增长,其实本质时间的差别不是特别大,当然会有。

位段的单位是比特 bit,一个字节8个比特。

解释

存在的意义

程序员不需要那麽多的空间 就产生了位段

———————————————————————————————————————————

位段的内存分配举例1

首先我们知道位段的使用是在内存里面的分配是字节和字节之间是从低地址到高地址,也就是字节之间的存储是从左到右 

首先我们知道位段的单位是比特bit,此时需要知道,位段之间的存储的在字节内,从右到左的方式进行存储,如果我们需要存储的第一个字节是7个bit,第二个存储的位段占据的空间是6个bit,这个时候第二个位段的存储不会直接从右到左的第八个字节直接进行存储。而是会从第二个字节从新开始进行存储,因为计算机在存储的时候,是可以直接计算出来的,当计算到剩下的空间不足以存储剩余的bit多少时候,他会去下一个字节空间进行存储。

这样的目的是防止数据的丢失。连续的存储的时候,计算机拿出来的时候是需要进行重新计算的,而且很容易导致丢失数据。所以当剩余的存储空间不能满足完全存下这个位段的时候,会去下一个空间进行存储。

图解

从右向左

接下来我们继续进行计算

当然更一下,在vs的编译器里面,位段的存储是字节之间采取从右向左

这里是长度不够的时候申请一个字节的长度

内部是从右向左使用

举例1原理解释

简单的说也就是再进行存储的时候,也就发生了截断

也就是10 12 3 4 实际上是发生了截断

所以也就是

0x 62 03 04

—————————————————————————————————————————— 

位段的内存分配举例2

同理这里也是一样的

这里是2 5 10 30个bit

int是4个字节 也就是32bit

—————————————————————————————————————————— 

位段的应用

需要节省内存的时候用位段

需要节约时间的时候 用结构体


—————————————————————————————————————————— 

位段的应用和使用时候的注意事项

ab可能共用一个字节

不能对位段的成员使用&操作符(看清楚 这里是错误的)

————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

枚举

枚举的讲解

在C语言中,没有内置的枚举(enum)数据类型,但我们可以使用整数类型来模拟枚举的行为。C99标准之前,C语言使用`#define`指令来定义枚举,但这种方式并不安全,因为如果枚举值发生变化,可能需要在代码中到处修改。C99标准引入了枚举类型,提供了更安全和方便的方式来定义和使用枚举。
下面是C99标准中枚举的基本使用示例:

#include <stdio.h>
// 定义一个枚举类型
enum color {
    RED,
    GREEN,
    BLUE
};
int main() {
    // 声明一个枚举变量
    enum color c;
    // 初始化枚举变量
    c = RED;
    // 打印枚举变量的值
    printf("The value of c is %d\n", c);
    // 枚举值的比较
    if (c == GREEN) {
        printf("c is GREEN\n");
    }
    // 枚举值的遍历
    for (int i = 0; i < RED; i++) {
        printf("enum value %d is %d\n", i, enum_val(i));
    }
    return 0;
}
// 定义一个函数返回枚举值
enum color enum_val(int i) {
    return i;
}


在这个例子中,我们定义了一个名为`color`的枚举类型,它有三个成员:`RED`、`GREEN`和`BLUE`。这些成员被自动赋予整数值,`RED`通常是0,`GREEN`是1,`BLUE`是2,但这个顺序并不是固定的,除非你在定义时明确指定每个成员的值。
我们声明了一个`enum color`类型的变量`c`,并将其初始化为`RED`。然后,我们打印了`c`的值,并进行了比较操作。我们还演示了如何通过一个函数`enum_val`来获取枚举值。
请注意,枚举成员的值是整数,所以你可以使用整数值来表示它们,也可以在for循环中使用枚举成员的值来遍历枚举类型。
C99标准之前的枚举定义通常是这样的:

#include <stdio.h>
// 使用 #define 定义枚举
#define RED 0
#define GREEN 1
#define BLUE 2
int main() {
    // 声明一个整型变量来模拟枚举
    int color;
    // 初始化模拟的枚举变量
    color = RED;
    // 打印模拟的枚举变量的值
    printf("The value of color is %d\n", color);
    // 模拟枚举值的比较
    if (color == GREEN) {
        printf("color is GREEN\n");
    }
    return 0;
}


在这个例子中,我们使用`#define`指令来定义枚举值,而不是使用`enum`关键字。

这种方式在C99标准之前是常见的,但不推荐使用,因为它不够安全,容易出错。

———————————————————————————————————————————

枚举类型


枚举是默认0开始的

枚举常量 是不能更改的

但是创建的时候 给一个初始值是可以的

但是这里是0 5 6

 ——————————————————————————————————————————

 计算器(转移表的实现)

这里不进行计算器多余的解释,这里有之前写的博客进行解释,可以看一下。

转移表回调函数实现-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/Jason_from_China/article/details/136590531 ——————————————————————————————————————————

枚举实现计算器

 —————————————————————————————————————————— 

枚举的检查类型

枚举是有类型检查的

枚举可以调试

但是#define是不能调试的

枚举常量是遵循作用域的 只能再函数内使用


枚举的使用


————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

联合体

联合体是什么

联合体(Union)在C语言中是一种特殊的数据类型,它允许在相同的内存位置存储不同的数据类型。这意味着联合体中的成员变量共享同一块内存区域,因此任何时候只能有一个成员变量被赋予值。联合体的大小通常是其最大成员的大小,以确保所有成员都能存储在同一块内存中。
下面是对联合体的详细讲解:

———————————————————————————————————————————

1. 联合体的定义

联合体在C语言中是通过`union`关键字来定义的。它的基本语法如下:

union 联合体名称 {
    类型1 成员1;
    类型2 成员2;
    类型3 成员3;
    // ...
};

联合体中的每个成员都可以是不同类型的数据。它们都占用相同的内存空间,但是只有在使用其中一个成员时,其他成员的数据才会被覆盖。

———————————————————————————————————————————

2. 联合体的使用

使用联合体时,首先要声明一个联合体类型,然后可以声明该类型的变量。例如:


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

这里定义了一个名为`Data`的联合体,它有三个成员:一个整数`i`、一个浮点数`f`和一个字符数组`str`。

———————————————————————————————————————————

3. 联合体的内存模型

由于联合体中的成员共享内存,因此在任何时刻,只能有一个成员被赋予有效的值。当一个成员被赋值时,它会覆盖其他成员的值。这意味着联合体中的数据类型必须是可以互相转换的,否则就会导致数据丢失。
例如,如果我们有一个整数成员和一个浮点数成员,我们不能同时保存一个整数和一个浮点数,因为它们的大小不同。在这种情况下,如果我们保存了一个浮点数,原来的整数就会被覆盖。

———————————————————————————————————————————

4. 访问联合体成员

访问联合体成员时,需要使用圆点运算符(`.`)。例如:
 


data.i = 10; // 设置整数成员
printf("整数成员的值: %d\n", data.i); // 打印整数成员
data.f = 3.14f; // 设置浮点数成员
printf("浮点数成员的值: %f\n", data.f); // 打印浮点数成员
strcpy(data.str, "Hello, World!"); // 设置字符串成员
printf("字符串成员的值: %s\n", data.str); // 打印字符串成员

——————————————————————————————————————————— 

5. 联合体的优点和缺点

**优点**:
- 节省内存空间,因为不同类型的成员可以共享同一块内存。
- 灵活,可以根据需要存储不同类型的数据。
**缺点**:
- 数据访问复杂,因为只能有一个成员有效。
- 需要谨慎处理数据,以避免数据丢失。

———————————————————————————————————————————

6. 联合体的大小

联合体的大小通常是其最大成员的大小。这是因为联合体需要足够大以存储最大成员,以确保所有成员都能在同一块内存中。例如,如果一个联合体有一个整数成员和一个浮点数成员,联合体的大小将是浮点数的大小,因为浮点数通常比整数大。
总之,联合体是一种非常有用的数据类型,可以在节省内存空间和灵活存储不同类型数据方面发挥作用。然而,使用联合体时需要特别注意数据的访问和转换,以避免数据丢失和不必要的复杂性。

 ——————————————————————————————————————————

联合体的使用、

下面是联合体在C语言中的基本使用示例:

#include <stdio.h>
// 定义一个联合体
union Data {
    int i;        // 整数成员
    float f;      // 浮点数成员
    char str[20]; // 字符串成员
};
int main() {
    // 声明一个联合体变量
    union Data data;
    // 初始化联合体变量,设置整数成员
    data.i = 10;
    // 打印整数成员的值
    printf("整数成员的值: %d\n", data.i);
    // 重新初始化联合体变量,设置浮点数成员
    data.f = 3.14f;
    // 打印浮点数成员的值
    printf("浮点数成员的值: %f\n", data.f);
    // 重新初始化联合体变量,设置字符串成员
    strcpy(data.str, "Hello, World!");
    // 打印字符串成员的值
    printf("字符串成员的值: %s\n", data.str);
    return 0;
}


在这个例子中,我们定义了一个名为`Data`的联合体,它有三个成员:一个整数`i`、一个浮点数`f`和一个字符串`str`。整数`i`和浮点数`f`被存储在同一个内存位置,但它们的数据类型不同。当我们设置其中一个成员的值时,另一个成员的值会丢失。
我们声明了一个`union Data`类型的变量`data`,并分别初始化了它的整数、浮点数和字符串成员。每次初始化后,我们打印了相应成员的值。请注意,由于联合体成员共享内存,因此在每次初始化后,之前的数据可能会丢失。
联合体在需要节省内存空间并且不同时间需要存储不同类型数据时非常有用。例如,在处理二进制数据时,可以使用联合体来根据不同的位模式存储不同的数据类型。

———————————————————————————————————————————

联合体的讲解图解


成员和初始化


占据多大空间

占据四个字节


共用同一块空间

所以联合体就是你要是 用c就尽量别用i 不然空间会重复

也就是如果共用 更改i c也会变化

反过来也是同理

如下

联合体的特点 以及结构体之间去区别


联合体的大小不是最大成员的大小

这样理解是错的

应该理解为

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

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

联合体的总大小 也得是最大成员的倍数

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

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

举例

short是两个字节 7个也就是十四个字节

———————————————————————————————————————————

联合体的使用

描述礼品清单的时候 虽然可以完成任务 但是每一次都有很多的空间是浪费的

联合体

三种属性先放在一起 其他的再放在其他地方

大小端判断


————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

结构体和枚举的专用题型

结构体内存对齐+枚举+文件操作的专用题型-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/Jason_from_China/article/details/137007489

  • 40
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值