C语言——自定义类型

简介: C语言——自定义类型

前言

       在之前的学习中,我们学习过数组,数组用来存放一些相同类型的变量;那如果我们需要存储不同类型的数据,数组就无法满足我们的需求,所以,现在就来学习一下能够存放不同类型的变量--自定义类型

结构体

引言:

我们现在需要存放一名学生的信息,姓名、年龄、学号等,在之前,我们就需要创建多个变量来存储

#include<stdio.h>
int main(){
    char name[] = "zhangsan";
    int age = 18;
    char id[]="2325813007";
    return 0;
}

如果学生信息有很多,我们就要创建更多的变量,现在我们使用结构体来存储学生的信息

struct Student 
{
    char name[20];//姓名
    int age;//年龄
    char id[10];//学号
};
int main(){
    struct Student s1={"zhangsan",18,"2325813007"};
    return 0;
}

这里可以看到,确实把学生信息存储起来了。接下来,来详细学习一下结构体:

       结构体类型

结构体声明:

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

       struct是关键字,tag是结构体类型名,member-list是结构体类型变量(就是结构体中的所有变量),variable是结构体变量名(可以不写,写了就相当于在声明结构体类型时创建的结构体变量)。

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

       声明完结构体类型,接下来创建并初始化结构体变量

struct Student  //声明结构体变量
{
    char name[20];//姓名
    int age;//年龄
    char id[10];//学号
};
int main() {
    struct Student s1 = { "zhangsan",18,"202309107" };  //创建s1并初始化
    printf("%s\n", s1.name);
    printf("%d\n", s1.age);
    printf("%s\n", s1.id);
    struct Student s2 = { "lisi",20,"202309106" };      //创建s2并初始化
    printf("%s\n", s2.name);
    printf("%d\n", s2.age);
    printf("%s\n", s2.id);
    return 0;
}

这里补充一下结构体访问成员操作符    . 和 ->

       在上述代码中我们看到了 s1.age 这个访问的就是s1这个结构体变量中的age。

       我们也可以使用->来访问结构体变量,但是需要注意 ->前面应该是结构体变量的地址

int main() {
    struct Student s1 = { "zhangsan",18,"202309107" };
    struct Student* ps1 = &s1;
    printf("%s\n", ps1->name);
    printf("%d\n", ps1->age);
    printf("%s\n", ps1->id);
    struct Student s2 = { "lisi",20,"202309106" };
    printf("%s\n", (&s2)->name);
    printf("%d\n", (&s2)->age);
    printf("%s\n", (&s2)->id);
    return 0;
}

       在这里要注意:使用 ->时,前面应该是结构体变量的地址,而不是结构体变量名

结构的自引用:

       接下看这样一段代码:

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

这是应该错误的代码,因为一个结构体中再包含一个同类型的结构体变量,那这样的结构体变量大小会特别特别大,显然是不可以的。

       那我们想要用结构体来实现链式访问又该如何去写呢?

       可以这样,在结构体类型中存放下一个要访问的结构体变量的地址。

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

这里涉及到一点链表的知识,数据结构中会学到。

       到这里,我们会发现一个问题,每一次这样创建结构体变量是都要写 struct Node   感觉很麻烦,我们使用typedf 对匿名结构体类型重命名

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

这样,在创建结构体变量时就只需要写 Node

typedef struct Node
{
    int data;
    struct Node* next;
}Node;
 
int main(){
    Node a2 = { 0 };
    Node a1 = { 1, &a2 };
    return 0;
}

       结构体内存对齐

       到这里,应该已经掌握了结构体的基本使用

       现在思考应该问题,结构体大小怎么计算呢?

       现在也是比较热门的考点:结构体内存对齐

对齐规则:

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

看使用规则那肯定是看着很懵,接下来,自己计算一下结构体的大小:

 

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

这里结构体大小是12,如何计算的呢?这里分析一下

接下来再看下面这个:

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

这个结构体与上面那一个都是两个char类型一个int型,但是它们大小一样吗?

我们可以看到,这两个结构体大小不一样,我们来分析一下S2结构体的大小:

接下来看,结构体嵌套该如何去求大小?简单来说就是把结构体看成一个变量整体

struct S4
{
    char c1;
    struct S2 s;
    double d;
};

来分析一下,

为什么要有内存对齐?

1.平台原因:

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

2.性能原因

       数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要做两次访问,而对齐的内存访问仅需要一次访问。假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分开放在两个8字节内存块中。

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

但是这样空间浪费有点多,我们既要满足对齐,又要节省空间,我们就需要让占用空间小的尽可能集中在一起,就如上面的S1和S2 ,S2的两个char类型的集中在一起,占用空间要比S1 小。

       结构体传参

 
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);
}
int main()
{
    print1(s); //传结构体
    print2(&s); //传地址
    return 0;
}

这里print2函数要比较好  原因如下:

       函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销

       如果传递的一个结构体的时候,结构体特别大,参数压栈的系统开销就比较大,所以会导致性能的下降。

总而言之:结构体传参的时候,要传结构体的地址

       结构体实现位段

       位段的声明和结构是类似的,有两处不同

  1. 位段的成员必须是int、unsigned int或signed int(C99当中位段成员类型也可以选择其他类型)。
  2. 位段的成员名后面有一个冒号和一个数字。(数字就是成员所占bit位数)

例如这样:

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

位段的内存分配:

  • 位段的成员可以是int 、unsinged int 、signed int 或者char 类型
  • 位段的空间上是按照需要以4个字节或者1个字节的方式来开辟的。
  • 位段涉及很多不确定因素,位段是不跨平台的,注重可一职的程序应该避免使用位段。

接下来,来看一下位段到底是如何分配内存空间的?

//⼀个例⼦
struct S
{
 char a:3;
 char b:4;
 char c:5;
 char d:4;
};
int main(){
    struct S s = {0};
    s.a = 10;    
    s.b = 12;
    s.c = 3;
    s.d = 4;
//空间是如何开辟的?
    return 0;
}

位段的跨平台问题:

  1. int位端对当成有符号数还是无符号数是不确定的
  2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32;写成27,在16位机器就会出现问题
  3. 位段的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。
  4. 当结构体包含两个位段,第二个位段成员比较大,无法容纳在第一个位段的剩余位时,舍弃剩余位还是继续使用,这也是不确定的。

位段使用时注意:不能对位段成员使用&操作符,这样就不能使用scanf直接给位段成员输入值,只能先输入在一个变量中,然后赋值给位段成员。

联合体(共用体)

在数据在内存中存储中,用到了联合体,去验证大小端字节序,在这里详细讲解联合体

联合体同结构体一样,联合体也是由多个不同类型的变量构成(也可以是一个变量),

但是,与 结构体不同的是,编译器只会为最大的成员分配足够的空间,(这也是联合体的特点,所有成员共用同一块空间,也称为共用体),联合体关键字 union

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

这里来看一下联合体的大小:

union Un
{
 char c;
 int i;
};
int main()
{
//联合变量的定义
     union Un un = {0};
//计算连个变量的⼤⼩
     printf("%d\n", sizeof(un));
    return 0;
}

联合体的特点:

       联合体成员共用一块内存

#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);
     return 0;
}

可以看到,对un.i   un.c和un取地址的结果都是一样的。

      对一个成员赋值,其他成员的值跟着变化

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

可以看到,对un.c赋值,un.i的值也会变化。

联合体的大小:

接下来,来探究一下联合体所占内存的大小

  • 联合体的大小至少是最大成员的大小
  • 当最大成员的大小不是最大对齐数的整数倍时,就要对齐到最大对齐数的整数倍

来看一段代码:

union Un1
{
     char c[5];
     int i;
};
union Un2
{
     short c[7];
     int i;
};
int main()
{
     printf("%d\n", sizeof(union Un1));
     printf("%d\n", sizeof(union Un2));
     return 0;
}

       看到这里,可能感觉到懵了,不是只给最大的成员开辟空间吗,为什么结果是这样的?我第一次看到这样代码结果时,也是很懵,但是理解了是怎样计算的它的大小就懂了,

联合体计算大小,当最大成员不是最大对齐数的整数倍就要对齐到最大对齐数的整数倍

       这里就是怎样,Un1中c[5]大小为5,但对齐数char是1,int的对齐数是4,所以就要对齐到4 的整数倍,就是8。

联合体与结构体比较:

       通过一个例子来对比一下联合体和结构体

现在,要搞一个活动,需要上线一个礼品兑换单,这个兑换单中有三个商品:图书、杯子、衬衫。而每一个商品都有:库存量、价格、商品类型等信息

图书:书名、作者、页数

杯子:设计

衬衫:设计、可选颜色、可选尺寸

用结构体来表示:

struct gift_list1
{
    //公共属性
    int stock_number;//库存量
    double price; //定价
    int item_type;//商品类型
 
    //特殊属性
    char title[20];//书名
    char author[20];//作者
    int num_pages;//⻚数
 
    char design[30];//设计
    int colors;//颜⾊
    int sizes;//尺⼨
};

这样看用结构体来表示,这样会占用很多空间,我们再来看一下联合体:

struct gift_list2
{
    int stock_number;//库存量
    double price; //定价
    int item_type;//商品类型
 
    union {
        struct
        {
            char title[20];//书名
            char author[20];//作者
            int num_pages;//⻚数
        }book;
        struct
        {
            char design[30];//设计
        }mug;
        struct
        {
            char design[30];//设计
            int colors;//颜⾊
            int sizes;//尺⼨
        }shirt;
    }item;
};

我们来计算一下这两个的大小

可以看出,使用联合体确实要比结构体节省空间。

枚举类型

枚举顾名思义就是一一列举

就是把可能的取值一一列举出来

举个例子,生活中,星期一到星期日可以一一列举、性别也可以一一列举、月份也可以一一列举。

这些都可以使用枚举

enum Day {//星期几
    Mon,
    Tues,
    Wed,
    Thur,
    Fri,
    Sat,
    Sun
};
enum Sex//性别
{
    MALE,
    FEMALE,
    SECRET
};
enum Color//颜⾊
{
    RED,
    GREEN,
    BLUE
};

上述这些都是枚举类型{}内的内容就是枚举类型可能的取值,也叫做枚举常量

       这些可能取值都是有值的,默认从0开始,一次递增1,当然也可以声明时对其进行赋初值

enum Color//颜⾊
{
     RED=2,
     GREEN=4,
     BLUE=8
};

学习到这里,我们会感觉到枚举常量很多余,我们可以使用#define定义常量,为什么非要使用枚举?

枚举常量优点:

  1. 可以增加代码的可读性和可维护性
  2. 相对于#idefine定义的标识符,枚举类型有类型检查,更加谨慎
  3. 便于调试,预处理阶段会删除#define定义的符号
  4. 使用方便,一次可以定义多个变量
  5. 枚举常量是遵循作用域规则的,枚举声明在函数内,就只能在函数内使用
enum Day {//星期几
    Mon,
    Tues=10,
    Wed,
    Thur=20,
    Fri,
    Sat=30,
    Sun
};
int main() {
    enum Day today = Mon;
    printf("%d\n", today++);
    printf("%d\n", Tues);
    printf("%d\n", Wed);
    printf("%d\n", Thur);
    printf("%d\n", Fri);
    printf("%d\n", Sat);
    printf("%d\n", Sun);
    return 0;
}

感谢观看,希望一下内容对你有所帮助,如果内容对你有作用,可以一键三连加关注,作者也正在学习中,有错误的地方还请指出,感谢!!!

相关文章
|
24天前
|
弹性计算 人工智能 架构师
阿里云携手Altair共拓云上工业仿真新机遇
2024年9月12日,「2024 Altair 技术大会杭州站」成功召开,阿里云弹性计算产品运营与生态负责人何川,与Altair中国技术总监赵阳在会上联合发布了最新的“云上CAE一体机”。
阿里云携手Altair共拓云上工业仿真新机遇
|
16天前
|
存储 关系型数据库 分布式数据库
GraphRAG:基于PolarDB+通义千问+LangChain的知识图谱+大模型最佳实践
本文介绍了如何使用PolarDB、通义千问和LangChain搭建GraphRAG系统,结合知识图谱和向量检索提升问答质量。通过实例展示了单独使用向量检索和图检索的局限性,并通过图+向量联合搜索增强了问答准确性。PolarDB支持AGE图引擎和pgvector插件,实现图数据和向量数据的统一存储与检索,提升了RAG系统的性能和效果。
|
20天前
|
机器学习/深度学习 算法 大数据
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
2024“华为杯”数学建模竞赛,对ABCDEF每个题进行详细的分析,涵盖风电场功率优化、WLAN网络吞吐量、磁性元件损耗建模、地理环境问题、高速公路应急车道启用和X射线脉冲星建模等多领域问题,解析了问题类型、专业和技能的需要。
2577 22
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
|
18天前
|
人工智能 IDE 程序员
期盼已久!通义灵码 AI 程序员开启邀测,全流程开发仅用几分钟
在云栖大会上,阿里云云原生应用平台负责人丁宇宣布,「通义灵码」完成全面升级,并正式发布 AI 程序员。
|
3天前
|
JSON 自然语言处理 数据管理
阿里云百炼产品月刊【2024年9月】
阿里云百炼产品月刊【2024年9月】,涵盖本月产品和功能发布、活动,应用实践等内容,帮助您快速了解阿里云百炼产品的最新动态。
阿里云百炼产品月刊【2024年9月】
|
2天前
|
存储 人工智能 搜索推荐
数据治理,是时候打破刻板印象了
瓴羊智能数据建设与治理产品Datapin全面升级,可演进扩展的数据架构体系为企业数据治理预留发展空间,推出敏捷版用以解决企业数据量不大但需构建数据的场景问题,基于大模型打造的DataAgent更是为企业用好数据资产提供了便利。
163 2
|
20天前
|
机器学习/深度学习 算法 数据可视化
【BetterBench博士】2024年中国研究生数学建模竞赛 C题:数据驱动下磁性元件的磁芯损耗建模 问题分析、数学模型、python 代码
2024年中国研究生数学建模竞赛C题聚焦磁性元件磁芯损耗建模。题目背景介绍了电能变换技术的发展与应用,强调磁性元件在功率变换器中的重要性。磁芯损耗受多种因素影响,现有模型难以精确预测。题目要求通过数据分析建立高精度磁芯损耗模型。具体任务包括励磁波形分类、修正斯坦麦茨方程、分析影响因素、构建预测模型及优化设计条件。涉及数据预处理、特征提取、机器学习及优化算法等技术。适合电气、材料、计算机等多个专业学生参与。
1576 16
【BetterBench博士】2024年中国研究生数学建模竞赛 C题:数据驱动下磁性元件的磁芯损耗建模 问题分析、数学模型、python 代码
|
22天前
|
编解码 JSON 自然语言处理
通义千问重磅开源Qwen2.5,性能超越Llama
击败Meta,阿里Qwen2.5再登全球开源大模型王座
977 14
|
4天前
|
Linux 虚拟化 开发者
一键将CentOs的yum源更换为国内阿里yum源
一键将CentOs的yum源更换为国内阿里yum源
221 2
|
17天前
|
人工智能 开发框架 Java
重磅发布!AI 驱动的 Java 开发框架:Spring AI Alibaba
随着生成式 AI 的快速发展,基于 AI 开发框架构建 AI 应用的诉求迅速增长,涌现出了包括 LangChain、LlamaIndex 等开发框架,但大部分框架只提供了 Python 语言的实现。但这些开发框架对于国内习惯了 Spring 开发范式的 Java 开发者而言,并非十分友好和丝滑。因此,我们基于 Spring AI 发布并快速演进 Spring AI Alibaba,通过提供一种方便的 API 抽象,帮助 Java 开发者简化 AI 应用的开发。同时,提供了完整的开源配套,包括可观测、网关、消息队列、配置中心等。
734 9