【C语言三种自定义类型】

2023-11-06

前言

大家好,我是熊猫,今天我们来认识一下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;//价格
};

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

例如:

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

struct book b3,b4;

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是不同的,虽然它们的类型看起来“完全一样”
//但是编译器还是会判定为不同的类型

3.结构体的自引用

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

例如:

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

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);//->为箭头运算符,可以令结构体指针直接指向结构体成员

5.结构体内存对齐

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

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

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

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

这里如果我们不知道结构体的内存对齐规则,那么肯定有很多朋友会认为这两个结构体的大小都是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. 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说:
结构体的内存对齐是拿空间来换取时间的做法。
我们上面的两个例子中成员是完全相同的,但是一个大小为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;
}

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

还原默认对齐数:

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

在这里插入图片描述


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

如上面这种情况,
结构体非常大,如果我们进行传址传参的话形参是实参的一份临时拷贝,
编译器就会在内存中开辟一块和实参一样大的区域存放形参,这样做会浪费很大的空间,
而使用传址传参就只是传出一个指针,而一个指针大小无非是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;
}

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

2.位段的使用

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

在这里插入图片描述


二、枚举(enum)

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

1.枚举类型的定义

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

这里枚举类型默认从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;
}

三、联合(union)

1.联合类型的定义

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

union un
{
	int num;
	float fa;
	char str[10];
};

2.联合的特点

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


3.联合的使用

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

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

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

如下:

union un
{
	int a;
	char ch;
};

int main()
{
	union un d = { 0 };
	scanf("%d", &d.a);
	printf("%d\n", d.ch);
	return 0;
}

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

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

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

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


总结

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

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

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【C语言三种自定义类型】 的相关文章

随机推荐

  • angularJs 一些API

    angular bind 返回一个调用self的函数fn self代表fn里的this 可以给fn提供参数args 这个功能也被称为局部操作 以区别功能 格式 angular bind self fn args self object 对象
  • Swiper参数说明(swiper参数配置)

    1 自由模式与滑块容器 Free Mode and Scroll Container Parameter 参数 Type 类型 Default Value 默认值 Example 例子 Description 说明 speed number
  • 计算机教师招聘笔试总结

    大专数字媒体专业教师中级岗 笔试内容 数字媒体技术 艺术史 计算机图形学 虚拟现实 数字媒体艺术 数据库 教育基础 笔试成绩 第一且只有我一人进面 准备时间 一周 准备过程 这所学校3月份就发了招聘简章 但因为我白天要上课 晚上要打游戏 所
  • Eclipse中断点调试的基本使用

    Eclipse中断点调试的基本使用 A Debug的作用 调试程序 查看程序执行流程 B 如何查看程序执行流程 什么是断点 就是一个标记 从哪里开始 如何设置断点 你想看哪里的程序 你就在那个有效程序的左边双击即可 在哪里设置断点 哪里不会
  • 2021年第十二届蓝桥杯javaA组国赛

    文章目录 试题 A 纯质数 试题 B 完全日期 试题 C 最小权值 试题 D 覆盖 试题 E 123 试题 F 二进制问题 试题 G 冰山 试题 H 和与乘积 试题 I 异或三角 试题 J 积木 以下均为个人想法和解题思路 如有错误或不足
  • openGL之API学习(五十二)透视分割 透视除法的执行位置

    根据文章https blog csdn net hankern article details 89220736 的分析 透视分割 又叫透视除法 执行的位置在栅格化阶段
  • Integer类型和int类型比较是否相等 == equals - Java

    非常经典的一个面试题 先说清楚一个 再来说另一个 用来判断两个变量之间的的值是否相等 变量就可以分为 基本数据类型变量 引用类型 1 基本数据类型的变量直接比较值 2 引用类型比较对应的引用指向的内存的首地址 equals只有引用数据类型有
  • docker的运行原理

    Docker 是一个开源的容器化技术 它能够让开发者将应用及其依赖打包到一个轻量级的 可移植的容器中 这个容器可以在几乎任何机器上一致地运行 要了解 Docker 的运行原理 我们首先要理解以下几个核心概念 容器 Container 容器是
  • android自定义控件onclick,android – 如何在ImageButton中设置onClick事件?

    我在 android中创建了一个图像按钮 但是当我点击该按钮时没有发生任何事情 我已经设置了所有属性 但仍然没有发生任何事情 所以 在我错的地方 你能帮助我吗 xml文件 android id id widget39 android lay
  • 产品数据管理系统框架与信息安全

    2 1引言 产品数据管理 Product Data Management PDM 以产品为中心 通过计算机网络和数据库技术 把企业生产过程中所有与产品相关的信息和过程集成起来 统一管理 使产品数据在其生命周期内保持一致 最新和安全 为工程技
  • Vue中如何进行数据缓存

    Vue中如何进行数据缓存 Vue是一款流行的前端框架 它提供了许多方便的功能来处理数据 其中一个非常有用的功能是数据缓存 数据缓存可以提高应用程序的性能 减少网络请求 提高用户体验 在本文中 我们将介绍Vue中如何进行数据缓存 并提供一些示
  • 用html实现一个静态登陆页面-可根据需求更改样式

    一 创建html文件 vscode下载相关插件 我们先选择一个文件夹创建login html 之前的文件命无所谓 之后就必须为html或者htm 在编辑改文件输入 且出现提示后按回车或按tab快捷生成基础代码 然后我们下载一个可以方便我们开
  • 解决数据库连接池连接mysql时,每隔8小时mysql自动断开连接的问题

    解决数据库连接池连接mysql时 每隔8小时mysql自动断开所有连接的问题 最近有个问题非常讨厌 我们的工程中使用自己的连接池连接mysql数据库 可mysql数据库每隔8小时就会自动断开所有链接 连接池就失效 需要重新启动tomcat才
  • 组合类、派生类 拷贝构造函数

    在派生类中如何写拷贝构造函数 一种形式 派生类拷贝构造函数名 对象p的引用 基类构造函数名 参数列表 如 student student student p stud p num p name p sex 注意 参数形式 是对象的引用 我们
  • java基础题实现猴子吃桃的问题

    一只猴子一天摘了许多桃子 第一天吃了一半 然后忍不住又吃了一个 第二天又吃了一半 再加上一个 后面每天都是这样吃 到第10天的时候 小猴子发现只有一个桃子了 问小猴子第一天共摘了多少个桃子 问题分析 第十天时只剩下一个桃子 又知道猴子每次吃
  • 【VB技巧】VB静态调用与动态调用dll详解

    摘 自 http lcx cc i 489 请注意 在以下语法格式中 请注意 函数名 的 大小写 静态与动态比较 静态调用简单 动态调用麻烦 静态调用占用资源多 动态调用占用资源少 正所谓鱼和熊掌不可兼得 静态调用定义 就是常用的申明API
  • Smart-tools 免费的开发工具箱

    Smart tools 免费的开发工具箱 背景介绍 作为一名开发人员 我希望有一款轻便且简洁的工具能够帮助自己快速解决一些日常开发的麻烦 因此 我创建了此网站 Smart tools 其目标是帮助开发人员提高工作效率 目前 本网站已支持20
  • FPGA--synplify+vivado综合 时钟路径优化Error

    项目场景 目前在做FPGA验证 使用到了synplify vivado的流程 rtl使用了synopsys的ip vivado似乎不能直接使用 而且vivado读取rtl文件时间也很长 需要3 4个小时才能读取完 主要是工程有点大 源文件比
  • 地址栏从输入url到页面展示经历了什么过程

    URL解析 浏览器会解析URL 确定其协议 例如HTTP或HTTPS 主机名和路径 如果主机名是一个域名 经过DNS服务器转换成对应的IP地址 建立连接 浏览器使用TCP协议通过IP地址建立到Web服务器的连接 在此过程中经历三次握手四次挥
  • 【C语言三种自定义类型】

    目录 前言 一 结构体 struct 一 结构体的声明 1 结构的声明 2 特殊的声明 不完全声明 3 结构体的自引用 4 结构体的初始化与赋值 5 结构体内存对齐 内存对齐规则 6 修改默认对齐数 7 结构体传参 二 位段 1 位段的声明