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;
}
🔖解释说明:
- struct是结构体的关键字
- Stu是结构体标签名
- struct Stu是结构体的类型
- 大括号内包围的是结构体成员变量的列表
- 变量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、结构体内存对齐
掌握了结构体的基本使用,还应当重点了解结构体内存对齐问题从而计算结构体的大小,这是一个关于结构体的重点考点
结构体的对齐规则:
- 第一个成员在与结构体变量偏移量为0的地址处。
- 其他成员变量需要对齐到对齐数的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。
VS中默认的值为8,Linux没有默认对齐数- 结构体总大小为最大对齐数的整数倍。
- 当嵌套结构体时,嵌套的结构体对齐需要到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数的整数倍(包含嵌套结构体的对齐数)。
✒️代码展示:
//练习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 S1和struct S2两者的成员组成是一样的,但是定义顺序有所差别,后者与前者相比将占用空间小的变量集中在了一起,导致两者在遵循结构体对齐条件下,所占内存大小不一样。做个对比吧!
结构体类型struct S3和struct 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语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位域” 。利用位段能够用较少的位数存储数据。
位段的声明和结构是类似的,有两个不同:
- 位段的成员必须是 int、unsigned int 、signed int、char 。
- 位段的成员名后边有一个冒号和一个数字(指该成员占的比特位)。
✒️代码展示:
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
2、位段的内存分配
位段的内存分配规则:
- 位段的成员可以是 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;
🔖解释说明:
在VS编译器中开辟了空间以后,先使用低地址再使用高地址。并且剩余的比特位不够下一个变量存储时,那这一片空间将会被浪费。
简而言之,跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
3、位段的应用
🔖解释说明:
上图是网络上IP数据包的格式,当你想要在网络上发一条消息给你的好友,信息是需要进行分装的,消息作为数据只是传输的一部分,还有一部分传输的是分装中的其他信息。比如4位版本号,4位首部长度,这些信息只需要4个bit,如若不使用位段,直接每个部分一个整形的给空间,就会造成空间的大量浪费。
三、枚举
1、枚举类型的定义
在数学和计算机科学理论中,一个集的枚举是列出某些有穷序列集的所有成员的程序,或者是一种特定类型对象的计数。这两种类型经常(但不总是)重叠。枚举在日常生活中很常见,例如表示星期的SUNDAY、MONDAY、TUESDAY、WEDNESDAY、THURSDAY、FRIDAY、SATURDAY就是一个枚举。
2、枚举的优点
枚举的优点:
- 代码的可读性变高和可维护性变强
- 和#define定义的标识符相比较枚举更加严谨,因为有类型检查。
- 防止命名污染的现象
- 方便调试,且使用方便,可以一下子定义很多常量
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。
枚举变量只能取枚举说明结构中的某个标识符常量。
枚举值是常量,不是变量,不能在程序中用赋值语句再对它赋值(比如上面的代码出现y3 = 3; ❎)。
只能把枚举值赋予枚举变量,不能把元素的数值直接赋予枚举变量,除非进行了强制类型转换(比如上面的代码出现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、联合体的大小计算
联合体大小计算规则:
- 联合的大小至少是最大成员的大小。
- 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
✒️代码展示:
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语言之自定义类型的主要内容,如果未能解决你的问题,请参考以下文章
09.AutoMapper 之自定义类型转换器(Custom Type Converters)