当前位置:   article > 正文

【C语言三种自定义类型】_c语言当中用户自定义的类型

c语言当中用户自定义的类型

前言

大家好,我是熊猫,今天我们来认识一下C语言中的自定义数据类型,
C语言中的char,short,int,long,float,double这些类型我们大家肯定已经非常熟悉了,
这些都属于C语言自身所带的类型,但是在我们的日常生活中只具有单一属性的事物少之又少,
更多的是同时具有各种各样的不同属性,
比如作为在校大学生的我:“姓名”,“年龄”,“班级”,“学号”,“身高”等等等,
再比如一本书:“书名”,“作者”,“编号”,“价格”等等等
这些都不是用一个简单的数据类型就可以表示的,那么,我们就需要用到这些自定义类型
来定义我们需要的数据类型。下面就让我们从结构体开始认识吧!


一、结构体(struct)

结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。

(一)结构体的声明

1.结构的声明

struct tag{
member—list;
};
member—list是成员列表

例如描述一本书籍:

struct book{
char title[20];//书名
char writer[20];//作者
float price;//价格
};
  • 1
  • 2
  • 3
  • 4
  • 5

这里的struct是结构体关键字,book是结构体标签,struct book才是一个完整的结构体名。
在创建结构体变量是必须写完全。
创建结构体有两种方法:
一种是直接在声明结构时直接创建,这种创建出来的是全局变量,
另一种是通过结构体类型创建。

例如:

struct book{
char title[20];//书名
char writer[20];//作者
float price;//价格
}b1,b2;

struct book b3,b4;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

2.特殊的声明(不完全声明)

我们在声明一个结构体时,可以不给它“起名字”,这个称为不完全声明

例如:

struct {
char ch;
char str[20];
int num;
}d1,d2;//只能在声明结构体的同时创建变量
//struct d3,d4; 这种是错误的,因为我们只有结构体关键字,而并不知道这个结构体的名字

struct {
char ch;
char str[20];
int num;
}d3,d4;
//这里我们需要注意:这里的d3和d4是相同的,
//但是d3和d1、d2是不同的,虽然它们的类型看起来“完全一样”
//但是编译器还是会判定为不同的类型
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

3.结构体的自引用

在结构中包含一个为结构体本身的类型的变量

例如:

struct str{
int data;
struct str* ps;
};
//在这里我们可以设置一个指向自身类型的指针,这属于数据结构中的链表的用法.
//struct str{
//int data;
//struct str s;
//};
//这里的用法是不行的,这里s变量里还有一个结构体变量s,s变量里面还有一个s,属于无限套娃,无法计算内存大小.
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

4.结构体的初始化与赋值

看代码:

//结构体的初始化
struct str{
char ch;
int data;
}s1={'a',10};  //  	1
struct str s2={'b',20};	   //  2
struct str s3={.data=30,.ch='c'};	//  3

//结构体的赋值
struct str s4;
scanf("%c %d",&s4.ch,&s4.data);
//如果是结构体指针,那就有两种赋值方式
struct *p=s4;
scanf("%c %d",&(*p).ch,&(*p).data);//  *p等同于s4
scanf("%c %d",&p->ch,&p->data);//->为箭头运算符,可以令结构体指针直接指向结构体成员
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

5.结构体内存对齐

结构体的内存对齐是结构体的一个很重要的知识,这个与结构体在内存中的存储方式有关

下面我们先来计算一下下面这两个结构体的大小:

struct str1
{
char ch;
short sh;
int num;
};

struct str2
{
char ch;
int num;
short sh;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

这里如果我们不知道结构体的内存对齐规则,那么肯定有很多朋友会认为这两个结构体的大小都是7,
sizeof(str1)=1+2+4=7,
sizeof(str2)=1+4+2=7;
那么既然我们专门讲了这个例子的话那就说明这是错误的结果了,
那么到底是第一个错了还是第二个错了还是两个大小都不对呢,

下面看实际运行结果:
在这里插入图片描述

这里为什么会出现这样的结果呢?
这里我们先来了解一下结构体内存对齐的规则。

内存对齐规则:
  1. 第一个成员在与结构体变量偏移量为0的地址处。
    2.从第二个成员开始,偏移量必须是 对齐数(默认对齐数与它自身大小中的较小者) 的整数倍。
    3.结构体总大小为最大对齐数的整数倍。
    4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
    体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

下面我们通过画图进行详细了解:
在这里插入图片描述

我们也可以通过offsetof()函数来得到哥哥成员的偏移量来进行验证:

代码如下:

#include<stdio.h>
#include<stdlib.h>

struct str1
{
	char ch;
	short sh;
	int num;
};

struct str2
{
	char ch;
	int num;
	short sh;
};

int main()
{
	printf("offsetof(struct str1, ch) = \t%d\n", offsetof(struct str1, ch));
	printf("offsetof(struct str1, sh) = \t%d\n", offsetof(struct str1, sh));
	printf("offsetof(struct str1, num) = \t%d\n", offsetof(struct str1, num));
	printf("offsetof(struct str2, ch) = \t%d\n", offsetof(struct str2, ch));
	printf("offsetof(struct str2, num) = \t%d\n", offsetof(struct str2, num));
	printf("offsetof(struct str2, sh) = \t%d\n", offsetof(struct str2, sh));
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

在这里插入图片描述

为什么存在内存对齐?
大部分的参考资料都是如是说的:
1.平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;
某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说:
结构体的内存对齐是拿空间来换取时间的做法。
我们上面的两个例子中成员是完全相同的,但是一个大小为8,一个却为12;
那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起。


6.修改默认对齐数

修改对齐数需要用到预处理指令#pragma

代码实现:

#include<stdio.h>

#pragma pack(1)//设置默认对齐数为1
struct str1
{
	char ch;
	short sh;
	int num;
};

struct str2
{
	char ch;
	int num;
	short sh;
};
int main()
{

printf("%d\n", sizeof(struct str2));
return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

在这里插入图片描述在这里插入图片描述

还原默认对齐数:

#include<stdio.h>

#pragma pack(1)//设置默认对齐数为1
#pragma pack()//还原默认对齐数
struct str1
{
	char ch;
	short sh;
	int num;
};

struct str2
{
	char ch;
	int num;
	short sh;
};
int main()
{

printf("%d\n", sizeof(struct str2));
return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

在这里插入图片描述


7.结构体传参

我们在进行函数传参时既可以进行传值传参也可以进行传址传参
结构体也同样可以使用以上两种方法

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; }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

如上面这种情况,
结构体非常大,如果我们进行传址传参的话形参是实参的一份临时拷贝,
编译器就会在内存中开辟一块和实参一样大的区域存放形参,这样做会浪费很大的空间,
而使用传址传参就只是传出一个指针,而一个指针大小无非是4/8个字节,
因此,我们在进行结构体传参时更建议使用传址传参。

(二)位段

结构体讲完就得讲讲结构体实现 位段 的能力。
我想,大多数同学都没有听说过位段这个概念吧,所以接下来我们就不卖关子,
直接通过下面的实例来了解它。

1.位段的声明

位段的声明和结构体是类似的,有两个不同:
1.位段的成员必须是 int、unsigned int 、signed int 或 char 的整形家族。
2.位段的成员名后边有一个冒号和一个数字。

代码实例:

#include<stdio.h>
struct str
{
int a:4;
int b:10;
int c:20;
int d:8;
};

int main()
{
printf("%d\n",sizeof(struct str));
return 0;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

运行结果:
在这里插入图片描述
在这里插入图片描述

2.位段的使用

这里关于位段的知识不进行过多赘述,我们知道有这个知识点就好,
当然他也有自己的使用场景:计算机网络里对数据的分段传输时需要加上描述信息,这时就可以使用位段,
可以对空间进行合理地使用。

在这里插入图片描述


二、枚举(enum)

枚举顾名思义就是–一一列举,把可能的情况全部都列举出来
一周有七天,可以一一列举,
一天有二十四个小时,可以一一列举,
英文字母有二十六个,也可以一一列举。

1.枚举类型的定义

enum Day//星期
{
	 MON,
	 TUES,
	 WED,
	 THUR,
	 FRI,
	 SAT,
	 SUN
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

这里枚举类型默认从0开始,既:
MON == 0 , TUES == 1 , WED == 2 ……
在初始化时可以更改他们的值,eg:
MON = 3,
那么TUES就会变为4,往后依次增大1


2.枚举的优点

为什么使用枚举?
我们可以使用 #define 定义常量,为什么非要使用枚举?
枚举的优点:
1.增加代码的可读性和可维护性
2.和#define定义的标识符比较枚举有类型检查,更加严谨。
3.防止了命名污染(封装)
4.便于调试
5.使用方便,一次可以定义多个常量


3.枚举的使用

enum Day
{
 MON,
 TUES,
 WED
 THUR,
 FRI,
 SAT,
 SUN
};

int main()
{
	 enum Day d;
	 scanf("%d",&d);
	 switch(d)
	 {
	 case MON:
		 printf("星期一\n");
	 	break;
	 case TUES:
	 	 printf("星期二\n");
	 	break;
	 case WED:
	 	printf("星期三\n");
	 	break;
	 case THUR:
	 	printf("星期四\n");
	 	break;
	 case FRI:
	 	printf("星期五\n");
	 	break;
	 case SAT:
	 	printf("星期六\n");
	 	break;
	 case SUN:
	 	printf("星期日\n");
	 	break;
	 	}
return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

三、联合(union)

1.联合类型的定义

联合体我们也顾名思义一下那就是–站在一起,共同使用。
联合体也是一个特殊的自定义类型,可以包含不同的成员,而这些成员共同使用同一块内存空间。(所以也叫公用体)

union un
{
	int num;
	float fa;
	char str[10];
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

2.联合的特点

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


3.联合的使用

经典例题:判断该计算机是大端存储还是小端存储

int main()
{
	int a=0x1;
	//p存放的是变量a的首地址(也就是低地址),
	//因为小端存储时低位放在低地址处,所以当*p为1是则为小端存储,*p为0则是大端存储
	char*p=(char*)&a;
	printf("%d\n",*p);
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

上面我们是使用了强制类型转换的方法取得了a的地址,但是根据今天我们讲的共用体,我们就可以设计一种更巧妙的方法进行判断

如下:

union un
{
	int a;
	char ch;
};

int main()
{
	union un d = { 0 };
	scanf("%d", &d.a);
	printf("%d\n", d.ch);
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

4.联合大小的计算

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

union un1
{
char ch1;
char ch2;
int data;
};

union un2
{
char ch1;
char str[10];
int data;
};

int main()
{
	printf("%zu\n",sizeof(union un1));
	printf("%zu\n",sizeof(union un2));
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

运行结果:
在这里插入图片描述

我是在VS下测试的,VS的默认对齐数为8

下面看图解:
在这里插入图片描述


总结

以上就是关于结构体、枚举、以及联合的知识,这里我再写几点熊猫自己的总结:

  1. 结构体和联合体都需要内存对齐,设计时尽量将小变量放在一起,内存对齐有时会造成内存的浪费,但是却可以提高成员访问速度,
    也就是我们常说的用内存换时间。
  2. 结构体位段的存在就是为了节省空间,所以位段不需要内存对齐,使用位段时要注意成员后面的 “:” 以及分配的比特位。
  3. 枚举类型各个成员之间是通过 “,” 连接的,也就是说枚举类型实际上只有一个变量,因此:sizeof(enum day)== 4。
  4. 在定义自定义类型时要注意大括号后面的 “;” ,这是一条语句结束的标志,如果有的编译器没有自动给出我们也不能忘记。

那么今天的内容就写到这里,感谢大家的支持,欢迎大家来评论区一起探讨,大家的鼓励是在这里插入图片描述继续更新的巨大动力。

本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号