C语言之自定义类型

Posted 富春山居_ZYY

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言之自定义类型相关的知识,希望对你有一定的参考价值。

前言

一、结构体

1、结构体类型的声明

当我们想要描述一个复杂变量——学生,可以这样声明。

✒️代码展示:

struct Stu
{
    char name[20];//名字
    int age;//年龄
    char sex[5];//性别
    char id[20];//学号
}s1;//分号不能丢
int main()
{
    struct Stu s2; 
    return 0}

🔖解释说明:

  1. struct是结构体的关键字
  2. Stu是结构体标签名
  3. struct Stu是结构体的类型
  4. 大括号内包围的是结构体成员变量的列表
  5. 变量s1是类型为struct Stu的全局变量,变量s2是该类型的局部变量

在声明结构时,也有特殊的声明,比如不完全声明——匿名结构体类型,省略掉了结构体标签。

✒️代码展示:

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

那么,此时,问题来了!

在上面的代码基础上,p = &x,这样的代码合理吗?

而且,像这样的匿名结构体类型只能使用一次,因为没有标签名。

2、结构体的自引用

众所周知,函数可以自己调用自己,叫做函数的递归,那么结构体是否也有自己引用自己呢?如果有又是如何实现的呢?

✒️代码展示:

//代码一:
struct N
{
    int data;
    struct N next;
};
//代码二:
struct Node
{
    int data;
    struct Node* next;
};
//代码三:
typedef struct
{
    int data;
    Node* next;
}Node;
//代码四:
typedef struct Node
{
    int data;
    struct Node* next;
}Node;

🔖解释说明:

代码一:

这样自引用是不正确的。当想要计算struct N类型所占空间大小时,就会出现疯狂套娃现象,无法计算结果,因此是不可取的

代码二:

这才是自引用的正确打开方式。data中存放的数据,next中存放着下一个struct Node类型数据的地址

代码三:

该代码想要实现匿名结构体的自引用,但这样做是不可取的。因为需要完整的定义了该结构体才可以重新命名为Node。然而定义的成员列表中又有Node*,先后问题产生了。

代码四:

可以通过这种重定义方式实现自引用。

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

既然已经有了结构体类型,那么对其定义和初始化就变得非常的简单

✒️代码展示:

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 Node
{
    int data;
    struct Point p;
    struct Node* next;
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化

4、结构体内存对齐

掌握了结构体的基本使用,还应当重点了解结构体内存对齐问题从而计算结构体的大小,这是一个关于结构体的重点考点

结构体的对齐规则:

  1. 第一个成员在与结构体变量偏移量为0的地址处。
  2. 其他成员变量需要对齐到对齐数的整数倍的地址处
    对齐数 = 编译器默认的一个对齐数与该成员大小的较小值
    VS中默认的值为8,Linux没有默认对齐数
  3. 结构体总大小为最大对齐数的整数倍
  4. 当嵌套结构体时,嵌套的结构体对齐需要到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数的整数倍(包含嵌套结构体的对齐数)。

✒️代码展示:

//练习1
struct S1
{
    char c1;
    int i;
    char c2;
};
printf("%d\\n", sizeof(struct S1));
//练习2
struct S2
{
    char c1;
    char c2;
    int i;
};
printf("%d\\n", sizeof(struct S2));
//练习3
struct S3
{
    double d;
    char c;
    int i;
};
printf("%d\\n", sizeof(struct S3));
//练习4-结构体嵌套问题
struct S4
{
    char c1;
    struct S3 s3;
    double d;
};
printf("%d\\n", sizeof(struct S4));

👁效果展示:

🔖解释说明:

结构体类型struct S1struct S2两者的成员组成是一样的,但是定义顺序有所差别,后者与前者相比将占用空间小的变量集中在了一起,导致两者在遵循结构体对齐条件下,所占内存大小不一样。做个对比吧!

结构体类型struct S3struct S4是另外两个典型例子,后者嵌套前者。

简而言之,该做法就是为了拿空间换取时间

如果。。。

另外。。。

结构在对齐方式不合适的时候,我么可以自己更改默认对齐数。

这里我们将使用预处理指令#pragma来改变默认对齐数

✒️代码展示:

#include <stdio.h>
#pragma pack(8)//设置默认对齐数为8
struct S1
{
    char c1;
    int i;
    char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)//设置默认对齐数为1
struct S2
{
    char c1;
    int i;
    char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
    printf("%d\\n", sizeof(struct S1));
    printf("%d\\n", sizeof(struct S2));
    return 0; 
}

👁效果展示:

🔖解释说明:

5、结构体传参

✒️代码展示:

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

👁效果展示:

🔖解释说明:

函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,导致性能的下降。比如在这里,如果直接传值s的话,由于结构体中创建了一个很大的数组data,导致结构体过大,传参时浪费的内存空间很大,效率低下。但是如果传址&s的话,作为一个指针,占四个字节,极大提高了运行效率。

简而言之,结构体传参时,传结构体的地址更好

二、位段

1、位段的定义

位段,C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位域” 。利用位段能够用较少的位数存储数据。

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

  1. 位段的成员必须是 int、unsigned int 、signed int、char 。
  2. 位段的成员名后边有一个冒号和一个数字(指该成员占的比特位)。

✒️代码展示:

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

2、位段的内存分配

位段的内存分配规则

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

✒️代码展示:

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;

🔖解释说明:

在VS编译器中开辟了空间以后,先使用低地址再使用高地址。并且剩余的比特位不够下一个变量存储时,那这一片空间将会被浪费。

简而言之,跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。

3、位段的应用

🔖解释说明:

上图是网络上IP数据包的格式,当你想要在网络上发一条消息给你的好友,信息是需要进行分装的,消息作为数据只是传输的一部分,还有一部分传输的是分装中的其他信息。比如4位版本号,4位首部长度,这些信息只需要4个bit,如若不使用位段,直接每个部分一个整形的给空间,就会造成空间的大量浪费。

三、枚举

1、枚举类型的定义

在数学和计算机科学理论中,一个集的枚举是列出某些有穷序列集的所有成员的程序,或者是一种特定类型对象的计数。这两种类型经常(但不总是)重叠。枚举在日常生活中很常见,例如表示星期的SUNDAY、MONDAY、TUESDAY、WEDNESDAY、THURSDAY、FRIDAY、SATURDAY就是一个枚举。

2、枚举的优点

枚举的优点

  1. 代码的可读性变高和可维护性变强
  2. 和#define定义的标识符相比较枚举更加严谨,因为有类型检查。
  3. 防止命名污染的现象
  4. 方便调试,且使用方便,可以一下子定义很多常量

3、枚举的使用

枚举的说明与结构和联合相似, 其形式为:

enum 枚举名
{
    标识符[=整型常数],
    标识符[=整型常数]...
    标识符[=整型常数]
} 枚举变量;

如果枚举没有初始化,即省掉"=整型常数"时, 则从第一个标识符开始,顺次赋给标识符0, 1, 2, …但当枚举中的某个成员赋值后,其后的成员按依次加1的规则确定其值。

✒️代码展示:

//代码1
enum Num1
{
    x1,
    x2,
    x3,
    x4
}x;
//代码2
enum Num2
{
    y1,
    y2 = 0,
    y3 = 50,
    y4
};
int main()
{
    printf("%d %d %d %d\\n", x1, x2, x3, x4);
    printf("%d %d %d %d\\n", y1, y2, y3, y4);
    return 0;
}

👁效果展示:

注意

  1. 枚举中每个成员(标识符)结束符是==","== 不是";", 最后一个成员可省略","。

  2. 初始化时可以赋负数, 以后的标识符仍依次加1。

  3. 枚举变量只能取枚举说明结构中的某个标识符常量。

  4. 枚举值是常量,不是变量,不能在程序中用赋值语句再对它赋值(比如上面的代码出现y3 = 3; ❎)。

  5. 只能把枚举值赋予枚举变量,不能把元素的数值直接赋予枚举变量,除非进行了强制类型转换(比如上面的代码出现x = x2✔️ x = 1❎x = (enum Num1)1✔️)

四、联合体(共用体)

1、联合体的定义

需要使几种不同类型的变量存放到同一段内存单元中。也就是使用覆盖技术,几个变量互相覆盖。这种几个不同的变量共同占用一段内存的结构,在C语言中,被称作“共用体”类型结构,简称共用体,也叫联合体。

2、联合体的特点

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

✒️代码展示:

//联合类型的声明
union Un
{
    char c;
    int i;
};
//联合变量的定义
union Un un;
int main()
{
    //例①
    printf("%p\\n", &(un.i));
    printf("%p\\n", &(un.c));
    //例②
    un.i = 0x11223344;
    un.c = 0x55;
    printf("%x\\n", un.i);
    return 0;
}

👁效果展示:

🔖解释说明:

通过例①的结果,我们可以直观发现成员变量c和成员变量i共用地址

例②更加证实这一点,由于大小端存储,变量i是以44 33 22 11这样的顺序存储的,因为变量c与其公用地址,因此55将44覆盖,在内存中变量i为55 33 22 11,打印出来为11 22 33 55

联合体的相关应用

在之前我们已经学会了判断计算机大小端的方法,这里可以通过共用体的特点来实现

#include <stdio.h>
union Un
{
    char c;
    int i;
}num;
int main()
{
    num.i = 1;
    if(num.c == 1)
    {
        printf("小端存储")
    }
    else
    {
        printf("大端存储")
    }
    return 0;
}

向成员变量i中存放一个1,查看成员变量c的值,由于该变量是char类型,因此只访问了第一个字节。

3、联合体的大小计算

联合体大小计算规则

  1. 联合的大小至少是最大成员的大小
  2. 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。

✒️代码展示:

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

👁效果展示:

完!

以上是关于C语言之自定义类型的主要内容,如果未能解决你的问题,请参考以下文章

C语言初阶笔记解题篇之自定义函数的运用

09.AutoMapper 之自定义类型转换器(Custom Type Converters)

java之自定义数据类型

Android之自定义属性

Python学习(三十七)—— 模板语言之自定义filter和中间件

[死磕 Spring 35/43] --- IOC 之自定义类型转换器