C语言——自定义类型结构体_学习笔记

2023-09-17 22:12:03

结构体的基本概念

结构体是一种用户自定义的数据类型,可以包含多个不同类型的变量。通过使用结构体,我们可以将相关联的数据组织在一起,便于管理和使用。

结构体的声明

正常的结构体声明

在C语言中,结构体(struct)指的是一种数据结构,是C语言中聚合数据类型的一类。
结构体可以包含多个不同类型的数据成员,例如:int、float、char等。

结构体的声明方式如下:

struct 结构体名 {
    类型1 数据成员1;
    类型2 数据成员2;
    ...
    类型n 数据成员n;
};

例如:

struct student {
    int id;
    char name[50];
    int age;
};                  //注意,结构体定义后面的分号不要忘了

以上属于是正常的结构体声明↑↑↑↑↑↑↑↑↑↑↑↑

匿名结构体声明

既然有正常的,那就有特殊的结构体声明了↓↓↓↓↓↓↓↓↓↓↓↓↓↓
在声明结构体的时候,可以不完全的声明。(匿名结构体类型)
如下:

//匿名结构体类型
struct
{
	int a;
	char b;
	float c;
}x;
struct 
{
	int a;
	char b;
	float c;
}*p;
  • p是结构体指针,这里可以看到,上下两个结构体的成员内容都是一模一样的,
  • 能不能实现 p = &x 呢?
  • 答案是不能,编译器会把上面的两个声明当成完全不同的结构体类型,所以p不能放x的地址!!!

上面两个结构体在声明的时候省略掉了结构体标签,这样声明也是可以的,但是!匿名的结构体类型,只能在创建的时候定义结构体变量,如果没有对结构体类型重命名的话,基本上只能使用一次。

typedef 结构体起别名

可以使用typedef关键字为结构体起别名,这样一些名字很长的结构体名字就可以简化,这样当我们需要频繁创建结构体变量的时候就比较方便。
例如:

#include <stdio.h>

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

typedef struct Chinese_Student Student; // 为结构体起别名Student

int main() {
    Student stu = {"Tom", 20, 88.5}; // 创建并初始化结构体变量,其实就等价于Chinese_Student stu = {"Tom", 20, 88.5};
    printf("Name: %s\n", stu.name); // 访问结构体变量的成员
    printf("Age: %d\n", stu.age);
    printf("Score: %.1f\n", stu.score);
    return 0;
}

结构体的自引用

在结构体中包含一个类型为结构体本身的成员可以吗?
比如下面这段代码:

struct Node
{
	int data;
	struct Node next;
};                      //error报错

这段代码是会报错的,因为结构体中包含同一个类型的结构体作为成员,这样结构体变量的大小就会无穷的大,是不合理的。

正确的结构体自引用方式,使用结构体指针

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

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

对于结构体变量的创建和初始化,用示例代码来进行说明:

#include <stdio.h>

//创建结构体变量前,要先对结构体进行声明↓↓↓↓↓↓
struct student {
    char name[50];  //这里声明一个student类型的结构体,有三个成员
    int age;        //分别是字符类型姓名,整型类型年龄,浮点型类型分数
    float score;
};

int main() {
   
    // 可以在创建结构体变量的时候同时按声明中的结构体成员顺序对结构体变量进行初始化操作↓↓
    struct student stu1 = {"Tom", 20, 88.5}; 
    
    //也可以用成员访问操作符,不按照成员顺序对结构体成员进行初始化操作↓↓↓↓↓
    struct student stu2 = {.age=18, .name="weil", .score=99.9}; 
    
	// 访问结构体变量的成员↓↓↓↓↓↓↓
    printf("Name: %s\n", stu2.name); 
    printf("Age: %d\n", stu2.age);
    printf("Score: %.1f\n", stu2.score);
    return 0;
}

结构体内存对齐

对于结构体的大小,我们可以用sizeof 运算符进行计算
如下:

struct S1
{
	char c1;       //1字节
	int i;         //4字节
	char c2;       //1字节
};
int main()
{
	printf("%zd\n", sizeof(struct S1));
	return 0;
}

运行结果为
在这里插入图片描述
上面代码的结构体占的字节数大小为什么会是12个字节呢?

这就涉及到结构体内存对齐的知识点了↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

结构体内存对齐规定

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

为什么存在内存对齐

大部分的参考资料都是这样说的:

1.平台原因(移植原因):

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

2.性能原因:

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

假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。

总体来说:结构体的内存对齐是拿空间来换取时间的做法。

举例解释

struct S1
{
    char c1;
    char c2;
    int i;
};
struct S2
{
	char c1;
	int i;
	char c2;
};
int main()
{
	printf("%zd\n", sizeof(struct S1));
	printf("%zd\n", sizeof(struct S2));
	return 0;
}

以上代码运行结果(VS 2022):
在这里插入图片描述
结构体中的成员类型相同,不同的排列顺序也会影响结构体变量的大小,这就是结构体内存对齐的奥妙,接下来画内存布局图来进行解释:
在这里插入图片描述

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

结构体数组

↓↓↓↓↓↓↓↓↓↓↓↓↓创建结构体数组 & 遍历访问结构体数组↓↓↓↓↓↓↓↓↓↓↓↓↓
代码示例如下:

#include <stdio.h>

// 定义一个结构体类型
struct Student {
    char name[20];
    int age;
    float score;
};

int main() {
    // 创建结构体数组并初始化
    struct Student students[] = {
        {"Tom", 20, 88.5},
        {"Jack", 21, 92.0},
        {"Alice", 19, 95.5}
    };

    // 遍历访问打印结构体数组中的所有内容
    int len = sizeof(students) / sizeof(struct Student);  // 计算数组长度
    for (int i = 0; i < len; i++) {
        printf("Name: %s\n", students[i].name);
        printf("Age: %d\n", students[i].age);
        printf("Score: %.1f\n", students[i].score);
    }

    return 0;
}

在这个示例中,我们首先定义了一个名为Student的结构体类型,包含三个成员变量:name、age和score。然后,我们在main函数中创建了一个Student类型的结构体数组students,并初始化其中的元素。接着,我们使用一个for循环遍历访问数组中的每个元素,并使用printf函数打印出每个学生的姓名、年龄和分数。

在访问结构体数组元素时,我们使用了结构体变量名加上索引的方式来访问,例如students[i].name表示访问第i个元素的name成员变量。

结构体指针

↓↓↓↓↓↓↓↓↓↓↓↓↓结构体指针的创建 & 初始化 & 成员访问↓↓↓↓↓↓↓↓↓↓↓↓↓

#include <stdio.h>

// 定义一个结构体类型
struct Student {
    char name[20];
    int age;
    float score;
};

int main() {
    // 创建结构体变量并初始化
    struct Student stu1 = {"Tom", 20, 88.5};
    struct Student stu2 = {"Jack", 21, 92.0};
    struct Student stu3 = {"Alice", 19, 95.5};

    // 创建结构体指针变量
    struct Student *stuPtr;

    // 将结构体变量的地址赋给结构体指针变量
    stuPtr = &stu1;

    // 遍历打印结构体指针中的内容
    printf("Name: %s\n", stuPtr->name);
    printf("Age: %d\n", stuPtr->age);
    printf("Score: %.1f\n", stuPtr->score);

    // 将结构体指针变量指向下一个结构体变量
    stuPtr = &stu2;

    // 遍历打印结构体指针中的内容
    printf("Name: %s\n", stuPtr->name);
    printf("Age: %d\n", stuPtr->age);
    printf("Score: %.1f\n", stuPtr->score);

    // 将结构体指针变量指向下一个结构体变量
    stuPtr = &stu3;

    // 遍历打印结构体指针中的内容
    printf("Name: %s\n", stuPtr->name);
    printf("Age: %d\n", stuPtr->age);
    printf("Score: %.1f\n", stuPtr->score);

    return 0;
}

在这个示例中,我们首先定义了一个Student结构体类型,包含了学生的姓名、年龄和分数。在main函数中,我们创建了三个Student类型的结构体变量,并分别初始化它们。

然后,我们创建了一个Student类型的指针变量stuPtr,用于指向这些结构体变量。我们将第一个结构体变量的地址赋给了stuPtr,并使用->运算符通过指针访问结构体中的数据,并打印出学生的信息。接着,我们将指针变量指向下一个结构体变量,重复上述操作,直到遍历完所有的结构体变量。

更多推荐

亚运之城:杭州的搞钱之王 首富都得靠边站

作者:积溪简评:这届亚运会,杭州成了显眼包,也卷成了蚊香,这背后杭州的搞钱实力,究竟有多牛?#杭州亚运会#马云#阿里手机可以“打”公交10分钟就能到还只要1块钱?这届亚运会杭州的宝子们真成了“显眼包”杭州也卷成了蚊香钱塘江边的灯光秀玉皇大帝睡觉都得戴眼罩路边的椅子能充电垃圾桶穿上新外套就连“美男计”都不放过地铁口人均1

软考 -- 计算机学习(2)

文章目录一、安全性知识1.1信息安全和信息系统安全1.2信息安全技术1.3网络安全技术二、多媒体技术三、软件工程基础知识3.1信息系统生命周期3.2软件过程模型3.3信息系统开发方法3.4系统分析和设计概述3.5结构化开发方法3.6系统运行与维护四、项目管理4.1进度管理4.2质量管理4.3风险管理一、安全性知识1.1

OpenText EnCase 客户案例——诺贝丽斯(Novelis)

OpenTextEnCase客户案例——诺贝丽斯(Novelis)诺贝丽斯(世界领先的铝材压延和回收企业)通过OpenText保存数据和节省资金诺贝丽斯在内部引入电子发现,通过OpenTextEnCaseInformationAssurance(以前称为EnCaseeDiscovery)解决方案交付复杂的案例。挑战电子

MySQL常见面试题(四)

😀前言在进行数据库设计和优化的过程中,我们不得不面对多样化的技术和方法来确保我们的系统可以高效、可靠地运行。为了深入了解和掌握这个领域,我们将讨论InnoDB存储引擎的多种索引类型,以及索引的不同方面和分类。我们还将深入探讨为什么通常推荐使用自增列作为主键,以及主键和唯一键之间的区别。最后,我们将提供一系列可用于SQ

activiti流程变量

activiti流程变量定义流程变量在Activiti中是一个十分重要的角色,流程运转时,需要靠流程变量,业务系统和activiti结合时少不了流程变量,流程变量就是activiti在管理工作流时根据管理需要而设置的变量。比如:在出差申请流程流转是如果出差天数大于三天则需要总经理审批,否者只需要认识审批,出差天数就可以

AI&DAO,将会引领我们走向何方?

人工智能(AI)和分布式自治组织(DAO)都是区块链赛道的热门项目之一,他们看似在不同的领域独立发展,然而,它们之间也存在着巨大的协同潜力。未来,AI有望成为推动DAO发展的重要动力,同时,DAO也可成为AI的最佳实验场所。DAO的下一波浪潮可能是AIDAO。释放生产力的未来首先,让我们来思考一下,AI如何在DAO中释

创建一个简单的外卖订餐系统

在今天的快节奏生活中,外卖订餐系统已经成为了人们日常生活中不可或缺的一部分。这些系统通过在线点餐和配送服务,为用户提供了便捷的用餐体验。在本文中,我们将创建一个简单的外卖订餐系统,使用Python和Flask框架构建后端,以及HTML、CSS和JavaScript构建前端。技术栈我们将使用以下技术栈来构建这个外卖订餐系

【基本数据结构 三】线性数据结构:栈

学习了数组和链表后,再来看看第三种线性表结构,也就是栈,栈和后边讲的队列一样是一种受限的线性表结构,正是因为其使用有限制,所以对于一些特定的需要操作可控的场合,受限的结构就非常有用。栈的定义我们平时放盘子的时候,都是从下往上一个一个放;取的时候,我们也是从上往下一个一个地依次取,不能从中间任意抽出。栈的结构后进者先出,

Rust认识所有权(4)

认识所有权1.认识所有权2.什么是所有权?2.1程序运行管理运行的方式2.2栈(Stack)和堆(Heap)1.栈(Stack)2.堆(Heap)2.3所有权规则2.4变量作用域2.4String类型2.5内存与分配1.以String类型为参考2.变量与数据交互的方式(一):移动2.1String版本3.变量与数据交互

Linux内核源码分析 (B.4) 深度剖析 Linux 伙伴系统的设计与实现

Linux内核源码分析(B.4)深度剖析Linux伙伴系统的设计与实现文章目录1\.伙伴系统的核心数据结构2\.到底什么是伙伴3\.伙伴系统的内存分配原理4\.伙伴系统的内存回收原理5\.进入伙伴系统的前奏5.1获取内存区域zone里指定的内存水位线5.2检查zone中剩余内存容量是否满足水位线要求5.3内存分配成功之

Vision Transformer(ViT)论文解读与代码实践(Pytorch)

VisionTransformerVisionTransformer(ViT)是一种基于Transformer架构的神经网络模型,用于处理计算机视觉任务。传统的计算机视觉模型如卷积神经网络(CNN)在处理图像任务时取得了很大的成功,但CNN存在一些局限,例如对于长距离依赖的建模能力较弱。ViT通过引入Transform

热文推荐