C语言,自定义数据类型

时间:2024-03-22 15:43:50

引言:

C语言当中除了,自己带的基本数据类型,还有一些自定义数据类型,用户自己可以控制数据类型大小,所包含的元素,使用起来更加方便,快捷。

一  数组arr:

对于数组而言,常常记住以下两点: 

1.一定是相同类型元素的数据集合

2.数组只是指数据元素,并不是指只是指是整数;

2.对于数组而言,在内存上是连续分布的

数组的大小等于数据元素类型本本身大小乘以元素个数

二结构体:

2.1结构体介绍:

C 数组允许定义可存储相同类型数据项的变量,结构是 C 编程中另一种用户自定义的可用的数据类型,它允许您存储不同类型的数据项。

结构体中的数据成员可以是基本数据类型(如 int、float、char 等),也可以是其他结构体类型、指针类型等。

2.1.2结构体组成:

结构体定义由关键字 struct 和结构体名组成,结构体名可以根据需要自行定义。

struct 语句定义了一个包含多个成员的新的数据类型,struct 语句的格式如下:

struct tag {
    member-list
    member-list
    member-list//成员列表  
    ...
} variable-list ;//结构体变量
//tag 结构体标签

tag 是结构体标签。

member-list :是标准的变量定义,比如 int i; 或者 float f;,或者其他有效的变量定义。

variable-list :结构变量,定义在结构的末尾,最后一个分号之前,您可以指定一个或多个结构变量。下面是声明 Book 结构的方式:

struct Books
{
   char  title[50];
   char  author[50];
   char  subject[100];
   int   book_id;
} book;  

在一般情况下,tag、member-list、variable-list 这 3 部分至少要出现 2 个。

2.2结构体内存对齐原则:

对于结构体而言,我们需要计算一个结构体内存的时候,需要注意,结构体内存对齐的原则

2.2.1对齐原则:

第一条: 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处;
第二条: 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处;
对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值。
VS 中默认的值为 8
- Linux中 gcc 没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩。
第三条: 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的 整数倍;
第四条:如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构 体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍;

对于以上数据原则,要熟记。

可以结合以下实例逐条分析:

2.2.2示例计算:

示例一:
struct S1
{
 char c1;
 int i;
 char c2;
};
printf("%d\n", sizeof(struct S1));

注意:首先,结构体也是连续存放的空间,但是存在一定的空间浪费,所以要注意结构体成员的列出,来节省内存空间。

存放c1:

首先了解一下:偏移量,就是指相对于第一个元素存放的起始地址而言的偏移多少的数值。

那么根据第一条原则,我们把第一个成员char 地址(偏移量)设置为0 ,

根据第二条,那么1和8的默认对齐数,选择较小的,那么对应,C1的对齐数就是1

那么我们开始存放的地址就必须是1的倍数,而0是1的倍数。所以存放在第一个位置0。

后面两条是针对于整体和嵌套,暂时不考虑;

存放int  i :

首先,我们刚刚存放第一个元素位置为0,那么开始考虑第二条,存放的位置要是最小对齐数的倍数。那么比较int i和默认对齐数比较,取较小值为对齐数。

那么对于4个字节和8个默认对齐数比较。4小于8,所以对应,我们选择 int i的对齐数为4个字节

那么对于开始存放的第二个地址是从1开始:

而1不是最小对齐数的倍数,所以我们要从它的倍数,4的最小对应倍数是4那么从偏移量为4的地址开始存放。

那么对应的而言,就是说,会有1~3之间3个字节的空间浪费;

存放 char c2:

那么对于char c2 依旧是根据原则,一步一步计算。首先针对于我们的字节。char c2求它的对齐数

char 是1个字节和 8默认对齐数比较,选小的,那么对应就是 1;那么对于内存比较而言,我们存放的地址就要是1的倍数,那么开始存放的地址就是8,8是1的倍数,所以我们可以从8的位置开始存放:

整体内存对齐:

根据第三条原则,结构体的内存大小要是,整体对齐数的倍数。那么整体对齐数就是指结构体所有成员对齐数的较大值, char c1,  int  i,char c2分别对齐数是:1,4,1那么整体对齐数就是4,而目前我们存放使用了9个字节,那么9不是4的倍数,所以还要对齐整体偏移,最近4的倍数是12.

所以整体所占内存大小就是12。

实例二:
#include <stdio.h>
struct s2
{
	char c1;
	char c2;
	int i;
};
int main()
{
	struct s2 test;
	printf("S2结构体大小为:%d\n", sizeof(test));
	return 0;
}

首先 C1作为第一个成员,设置为地址为0的偏移处,c1为一个字节,默认对齐8个字节选择较小的,1作为c1的对齐数。c2起始地址为偏移量1处,c2为1个字节,默认对齐数为8个字节选择较小值为1作为c2的对齐数。1是偏移量1地址的倍数,所以可以存放1个字节。int i为4个字节,默认对齐数为8个字节,选择较小值那么对应为4。开始存放偏移量为3不是4的倍数。所以得从最小公倍数处,即偏移量为4处开始存放 int i。那么整体存放0~7对应整体字节为8个字节,那么开始计算整体对齐数,根据c1,c2,i的对齐数取最大值,即为4。8为4的倍数,所以满足内存对齐原则。

所以整体结构体内存大小为4个字节。

由此S1,S2大小对比可以发现,相同较小元素放一起可以有效节省结构体所占内存大小空间

示例三(结构体嵌套):
struct S3
{
 double d;
 char c;
 int i;
};
printf("%d\n", sizeof(struct S3));
结构体嵌套问题
struct S4
{
 char c1;
 struct S3 s3;
 double d;
};
printf("%d\n", sizeof(struct S4));

计算S3结构体计算方法和上面基本相同,大概看一下我的计算过程:

 

可以计算出大小为16个字节。

计算S4结构体大小:成员c1第一个元素大小为1个字节默认对齐数为1个字节,放置在位置为0处。

计算c1的对齐数,1小于默认对齐数8,所以c1的对齐数为1。开始计算成员2,也就是S3那么S3对齐数,取决于该结构体内最大对齐数,也就是对应8作为该结构体的对齐数。那么对于8,所以要求起始地址就是要求是对齐数倍数处。而成员2的起始偏移量为1,那么对应应该从地址8处开始存放16个字节。也就是存放到8~23。对应成员3 double d,大小为8个字节,默认对齐数8,选择较小值8

那么d开始存放位置要是8的倍数。那么对应成员3开始地址刚好是24为8的倍数开始存放,成员d存放到24~31处。对于整体而言就是0~31整体大小为32。开始计算整体最大对齐数,1,8,8选择最大对齐数8作为对齐数。那么32刚好是8的倍数。所以对应整体大小就是32字节。

2.3结构体“.”和->的区别:

结构体中 “->” 与 “.” 的区别以及使用
两者在同一个代码块内使用的时候其实没有什么太大不同,无非就是声明结构体的时候一个是声明指针,一个是声明结构体。声明结构体的时候分配了内存空间,所以可以用".“直接访问,而声明指针之后并没有分配内存空间,所以用”->“来指向开辟的空间。也可以用”(*buffer).foo" ,等价于"buffer->foo"。

"->"是在声明结构体指针时,访问结构体成员变量时使用。

"."是声明结构体时,访问结构体成员变量时使用。

三 联合体(共用体):

3.1联合体(共用体)介绍:

像结构体⼀样,联合体也是由⼀个或者多个成员构成,这些成员可以不同的类型。
但是编译器只会为最⼤的成员分配⾜够的内存空间。联合体的特点是所有成员共⽤同⼀块内存空间,也就是共用一块空间。所以联合体也叫:共⽤体。 给联合体其中⼀个成员赋值,其他成员的值也跟着变化。

3.2 联合体组成:

为了使用联合体,需要使用 uion关键词来定义联合体。那么对于整体架构其实和结构体差不多:

union [union tag]
{
   member definition;
   member definition;
   ...
   member definition;
} [one or more union variables];

union tag 是可选的,也就是我们声明这个联合体类型叫做声明名字,每个 member definition 是标准的变量定义,比如 int i; 或者 float f; 或者其他有效的变量定义。在共用体定义的末尾,最后一个分号之前,您可以指定一个或多个共用体变量,这是可选的。下面定义一个名为 Data 的共用体类型,有三个成员 i、f 和 str:

3.3联合体(共用体)大小计算:

和结构体一样,我们对于共用体计算同样需要根据相应的对齐原则,去计算大小;

3.3.1共用体对齐原则:

联合的⼤⼩⾄少是最⼤成员的⼤⼩。
当最⼤成员⼤⼩不是最⼤对⻬数的整数倍的时候,就要对⻬到最⼤对⻬数的整数倍。

3.4示例计算:

示例一:
union Un
{
 char c;
 int i;
};

计算:第一根据两条原则,计算即可;

 char c 为1个字节  , int i为4个字节计算最大成员大小,最大成员为int 4个字节

那么联合体大小为4个字节,两者对齐数为1 ,4取最大对齐数4。4为4的倍数,所以

整体结构体大小为4个字节。

示例二:
union Un2
{
	short c[7];
	int i;
};

那么对于共用体2而言,它的整体大小,首先成员1为应该数组,那么对于其大小为2*7,大小为14个字节。成员2,int i大小为4个字节。那么取决于较大一位14字节

那么计算共用体对齐,计算,对齐数是取决于其成员的基本数据类型元素大小,而不是整体大小

short是2个字节。那么对于int 4个字节,其对齐数采取4。

那么对于整体共用体大小14不是4的对齐数,所以应该对齐到实际最小公倍数到16个字节。

那么对于两条原则,如果共用体成员只有基本数据类型,不存在自定义数据类型,计算大小往往不需要考虑对齐数,因为对齐数就是基本数据类型元素的字节大小,即使考虑本是上内存大小不受影响

3.5联合体(共用体)简单应用:

可以用来计算所使用内存存储空间到底是大端存储还是小端存储。

知识补充:大小端存储(Endianess)是指多字节数据在计算机内存中的字节排列顺序。具体来说,它决定了数据的高位字节(Most Significant Byte, MSB)和低位字节(Least Significant Byte, LSB)在内存地址中的存放顺序。根据字节的存储顺序,主要可以分为两种类型:

  1. 大端(Big Endian)存储
    • 在大端存储中,高位字节存储在内存的低地址端,低位字节存储在内存的高地址端。
    • 举例来说,一个16位的整数0x1234在大端存储中,其内存表示会是12 34(十六进制),即高位字节12在前,低位字节34在后。
  2. 小端(Little Endian)存储
    • 在小端存储中,低位字节存储在内存的低地址端,高位字节存储在内存的高地址端。
    • 同样以16位整数0x1234为例,在小端存储中,其内存表示会是34 12(十六进制),即低位字节34在前,高位字节12在后。

这两种存储方式对于单个字节的数据没有区别,但对于多字节的数据(如整型、浮点型等)就有明显的不同。在编写跨平台的软件时,需要特别注意数据的字节序,因为不同的硬件平台可能采用不同的字节序。例如,Intel和AMD的x86和x86_64架构通常使用小端存储,而一些网络协议和文件格式(如网络字节序)则采用大端存储。

在实际编程中,有时需要进行字节序的转换,以确保数据在不同平台之间的正确传输和解析。例如,可以使用函数如htonl(host to network long)和ntohl(network to host long)在网络编程中进行32位整数的字节序转换。对于更通用的字节序处理,可以使用联合(union)或者位操作等技巧来进行编程。

int check_sys()
{
 union
 {
 int i;
 char c;
 }un;
 un.i = 1;
 return un.c;//返回1是⼩端,返回0是⼤端
}解读程序

解读:

这里的关键在于理解整数1在内存中的表示。在计算机中,整数1通常表示为0000 0001(对于32位整数)。但是,这个表示如何映射到char类型的un.c取决于系统的字节序。

  • 大端系统上,高位字节(即0000)会存储在内存的低地址端,而un.c(一个char类型)只会读取这个地址的值,所以它会返回0。
  • 小端系统上,低位字节(即0001)会存储在内存的低地址端,所以un.c会返回1。

四 枚举 :

4.1枚举介绍:

枚举顾名思义就是⼀⼀列举,把可能的取值⼀⼀列举。
⽐如我们现实⽣活中:
⼀周的星期⼀到星期⽇是有限的7天,可以⼀⼀列举
性别有:男、⼥、保密,也可以⼀⼀列举
⽉份有12个⽉,也可以⼀⼀列举
三原⾊,也是可以意义列举
这些数据的表⽰就可以使⽤枚举了。
enum Day//星期
{
 Mon,
 Tues,
 Wed,
 Thur,
 Fri,
 Sat,
 Sun
};
enum Sex//性别
{
 MALE,

 FEMALE,
 SECRET
};
enum Color//颜⾊
{
 RED,
 GREEN,
 BLUE
};

简单来说就是一一列举,实际应用非常方便。

4.2枚举赋值计算:

对于枚举的值,实际上枚举的成员都是有值的,对于枚举的值往往声明定义的时候就已经定了。

不能在改变。所以对于我们而言。往往就是一句话枚举常量。

默认枚举成员数值是从0开始依次递增,其次每次递增量为1

示例一:

enum Color//颜⾊
{
 RED,
 GREEN,
 BLUE
};

默认就是从0开始,对于递增量为1。

那么就是 0,1,2;

示例二:

enum Color//颜⾊
{
 RED=1,
 GREEN,
 BLUE
};

RED为1,开始递增,增量为1,那么对应1,2,3 

示例三:

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

RED没有赋值从开始,默认从0,开始递增,GREEN赋值4,开始递增,增量为1 ,那么BLUE开始从4开始递增,增量为1,那么对应5.

所以结果1,4,5。

4.3枚举好处:

1. 增加代码的可读性和可维护性
2. 和#define定义的标识符⽐较枚举有类型检查,更加严谨。
3. 便于调试,预处理阶段会删除 #define 定义的符号
4. 使⽤⽅便,⼀次可以定义多个常量
5. 枚举常量是遵循作⽤域规则的,枚举声明在函数内,只能在函数内使⽤