C语言重点突破(四)自定义类型详解

前言

本文意在介绍C语言里的常规自定义类型,它是C语言里最重要的概念之一,是我们从简单使用C语言到综合运用必不可少的知识之一,在C语言中具有重要的地位和作用,掌握自定义类型的使用方法和技巧对于写出高质量的C程序是非常重要的。

目录

本章重点
结构体
结构体类型的声明
结构的自引用
结构体变量的定义和初始化
结构体内存对齐
结构体传参
结构体实现位段(位段的填充&可移植性)
枚举
枚举类型的定义
枚举的优点
枚举的使用
联合
联合类型的定义
联合的特点
联合大小的计算

一.结构体

1.结构体类型的声明

C语言里已经内含了一些基本的数据类型(整型,字符型等),但在实际编程中,我们会碰到一些复杂的数据类型,例如描述一个学生,或者是一辆汽车等一些实际事物光靠基础的类型是不能简单描述的,这时候结构体就派上了用场。

结构体说白了,就是数据的集合,里面的成员可以有多种类型,例如描述一个学生,得有名字,性别,年龄,学号等一些信息,这时就可以用结构体来进行定义。

结构体定义如下

定义一个学生:

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

上面是常规的声明,缺点是每次定义时都要将struct关键字写入,影响编写效率,下面有一种特殊的声明,此时省略了结构体标签(匿名结构体类型,只能使用一次)

//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;

要注意,这时候定义的结构体变量(x,a【20】,*p)都是全局变量,而在主函数里进行定义则是局部变量(对主函数全局)。

那么问题来了,如果我此时再加上p = &x这一行代码,阁下又该如何应对呢?

可以试着编译一下,运行是没有问题的,但编译器会报警告,尽管两个结构体组成是一样的,但编译器会把它们当作不同的类型进行编译,这种做法不建议。

2.结构体的引用

既然结构体能存放不同的类型,那能不能存放结构体类型呢?

答案是可以的。

//代码1
struct Node
{
int data;
struct Node next;
};
//可行否?
如果可以,那sizeof(struct Node)是多少?

编译一下你会发现,甚至都无法编译,这相当于一个结构体里存放一个自己的结构体,同时这个结构体也可能会存放和自己的结构体,大小根本计算不了,那该如何引用呢?

那就引用地址嘛,通过地址就可以找到该结构体并进行引用,而同时存放地址的指针在编译器里的大小是确定的,这样一来也能计算该结构体的大小。

//代码2
struct Node
{
int data;
struct Node* next;
};

讲到这里我们再看看下面一段代码

//代码3
typedef struct
{
int data;
Node* next;
}Node;
//这样写代码,可行否?

答案显然是不行的,虽然是匿名结构体,但体内已经有了Node类型的指针,后面才生成Node类型,这就导致指针的类型是未定义的,要注意编译的先后顺序。

正确代码

//解决方案:
typedef struct Node
{
int data;
struct Node* next;
}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.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处,对齐数=编译器默认的一个对齐数与该成员大小的较小值。

3.结构体总大小为最大对齐数(每一个成员变量都有一个对齐数)的整数倍。

4.如果嵌套了结构体的情况,嵌套的结构对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

看到这里,你可能还有一点懵,我们来个例子解释一下:

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

 我们来分析一下:(在vs环境里默认对齐数是8)

char类型在内存里占1个字节,由于起始地址是0,与8比较起来1较小,所以对齐数是1,而内存起始地址我们设为0,可以看出,结构体的第一个成员永远放在内存的起始地址。

接下来是int类型,在内存中占4个字节,但0的下一位就是1,不是4的整数倍,根据对齐规则,就得对齐到4的位置进行存放4个字节。

最后是char,和第一个一样,直接存放下一个(8)即可。

现在结构体占的大小是0~8,一共九个字节,而结构体成员的最大对齐数是4,还得对齐到4的整数倍上才能算结构体的大小,就是12.

 很多人会有疑问了,为什么会存在内存对齐这种说法呢?

结构体内存对齐是为了使结构体的访问更加高效。当结构体中的字段内存对齐后,CPU 可以更快地访问字段所对应的内存地址,因为它们与 CPU 的缓存结构更加匹配。如果结构体的字段没有进行内存对齐,则会导致 CPU 访问内存的效率较低,这会影响程序的性能。

此外,一些计算机体系结构需要结构体内存对齐才能正确工作。例如,一些处理器需要对 4 字节或 8 字节的内存地址进行访问,这意味着结构体中的字段必须按照 4 字节或 8 字节的边界进行对齐才能被正确访问。

因此,结构体内存对齐是为了提高程序的性能和可靠性,确保结构体中的字段可以被正确访问。

简单的来说就是:内存对齐是一种舍弃空间换取时间的方法。

不同的编译器默认的对齐数是不一样的,但可以通过 #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->num);
}
int main()
{
print1(s); //传结构体
print2(&s); //传地址
return 0;
}

传结构体和传地址都能实现传参的功能,但那一种会比较好呢?

函数传参传地址和传变量是两种不同的方式。

当使用传地址方式时,函数的参数将是指向变量内存地址的指针。这意味着函数将直接访问变量的内存地址,对变量的操作将在原始地址上进行。这种方式通常用于需要在函数内部修改变量的情况。这种方式可以避免在函数内部对变量进行拷贝,从而提高性能和效率。

当使用传变量方式时,参数是变量本身。这意味着函数将使用变量的副本进行操作,并不会直接改变原始变量。这种方式通常用于不需要修改变量的情况,或者对变量进行操作时不需要改变原始值的情况。

总的来说,传地址方式更加灵活,可以实现更复杂的操作,但需要注意避免因为指针操作不当而导致的错误。传变量方式相对简单,使用起来更为直观,但不能直接在函数内部修改变量的值。

6.结构体实现位段(位段的填充&可移植性)

位段是一种数据结构,它允许程序员在内存中为字段指定特定数量的位数,而不是以字节为单位。这样做有时可以节省内存空间。

在C语言中,可以使用位段来定义一个包含多个字段的结构体。例如,假设我们要定义一个结构体来存储一个16位的数据包,其中包含4个不同的字段,每个字段分别占用4位,可以使用位段来定义这个结构体。

需要注意的是,使用位段可能会导致一些不便之处。例如,不能使用 sizeof 运算符来计算结构体的大小,因为它计算的是按字节对齐的大小。而且不同编译器可能会对位段的实现有所不同,导致可移植性问题。因此,使用位段时需要仔细考虑其适用性和安全性。

位段的声明

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

位段的内存分配

首先要知道的就是位段是比特为单位进行分配空间的

举个例子

如下图,a是char类型,占1个字节(8比特)在主函数里,给a赋值10,但位段要求,只能保留3为比特位,所以要进行截取保留3位,以此类推,当存放的位数已满足一个字节或剩余的比特位空间不够,此时就得再开辟一个字节进行存储。

 

位段在不同编译器和不同平台上的实现是有所不同的,这可能会导致跨平台问题。

最常见的问题之一是,如何对位段进行按位运算。在一些平台上,位段是定义为无符号整数,可以直接进行按位运算;但在另一些平台上,则需要将位段转换为整数类型,才能进行按位运算。

此外,位段的顺序和字节对齐方式也可能会发生变化。例如,在某些平台上,位段的顺序是从左向右,而在其他平台上,顺序是从右向左。同时,一些平台可能会对位段进行字节对齐,而其他平台则不会。

为了避免位段的跨平台问题,可以采取以下措施:

1. 避免在位段中使用多个类型。
2. 明确指定位段的顺序和字节对齐方式。
3. 避免使用位段进行按位运算,或者使用平台无关的按位运算规则。
4. 在不同平台上进行测试和调试,确保代码的可移植性和正确性。

总之,位段虽然能够节省内存空间,但也需要考虑其在不同平台上的实现和兼容性,以保证代码的正确性和可移植性。

二.枚举

枚举顾名思义就是一一枚举

1.枚举类型的定义

enum Day//星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};
enum Color//颜色
{
RED,
GREEN,
BLUE
};

上面的Day、Sex、Color、都是枚举类型,括号里的叫做枚举常量,这些常量都是有值的,默认从0开始,一次递增1,也可以在定义的时候进行赋值。

enum Color//颜色
{
RED=1,
GREEN=2,
BLUE=4
};

2.枚举的优点

  1. 程序可读性强。枚举类型可以使用具有意义的符号名称来表示常量,使得程序的可读性更高,增加代码的可维护性。

  2. 减少代码中的魔数。枚举类型可以减少代码中出现的“魔数”(没有明确含义的数字),从而提高代码的可读性和可维护性。

  3. 编译器提供类型检查。枚举类型被视为一种类型,因此编译器可以进行类型检查,从而避免一些常见的错误,例如将一个枚举类型的值赋给另一个类型的变量。

  4. 枚举类型可以实现类型安全的类型别名。实际上,枚举类型可以用来实现一些类型安全的类型别名,例如使用枚举类型来定义一个有限的整数集合。

3.枚举的使用

enum Color//颜色
{
RED=1,
GREEN=2,
BLUE=4
};
enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
clr = 5; //ok??

三.联合体(共用体) 

1.联合类型的定义

联合体是一种特殊的自定义类型,这种类型定义的变量也包含一系列成员,特征是这些成员公用一块空间。

//联合类型的声明
union Un
{
char c;
int i;
};
//联合变量的定义
union Un un;
//计算连个变量的大小
printf("%d\n", sizeof(un));

2.联合的特点

联合体的成员是共用一块内存空间的,所以一个联合变量的大小,至少是最大成员的大小。

3.联合体的大小计算

要注意的问题:

1.联合的大小至少是最大成员的大小

2.当最大成员大小不是最大对齐数的整数倍时,就要对齐到最大对齐数的整数倍。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/119795.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

prometheus监控kafka

一、前言 关于对kafka的监控&#xff0c;要求高的话可以使用kafka-exorter和jmx-exporter一起收集监控数据&#xff0c;要求不高的情况下可以使用kafka-exporter收集监控数据即可 二、部署 kafka-exporter 部署kafka-exporter&#xff0c;我是在k8s集群中部署的 编辑yaml文件…

0基础学习PyFlink——用户自定义函数之UDAF

大纲 UDAF入参并非表中一行&#xff08;Row&#xff09;的集合计算每个人考了几门课计算每门课有几个人考试计算每个人的平均分计算每课的平均分计算每个人的最高分和最低分 入参是表中一行&#xff08;Row&#xff09;的集合计算每个人的最高分、最低分以及所属的课程计算每课…

UI自动化测试工具推荐

UI自动化测试已经成为现代软件开发过程中不可或缺的一部分。它能够提供诸多优势&#xff0c;包括提高测试效率、减少人力成本、提升软件质量等。同时&#xff0c;可视化工具为UI自动化测试带来了更多便利和灵活性。然而&#xff0c;可视化工具也存在一些潜在的劣势。本文将探讨…

【iOS安全】提取app对应的URLScheme

获取app的URLScheme 在已越狱的iPhone上&#xff0c;使用Filza进入app列表目录&#xff1a; /private/var/containers/Bundle/Application/ 比如我要分析Microsoft Authenticator&#xff0c;明显对应的是这里面的“Authenticator”&#xff0c;那就在Filza中点击进入“Authen…

MySQL多表关联on和where速度对比实测谁更快

MySQL多表关联on和where速度对比实测谁更快 背景 今天发现有人在讨论&#xff1a;两张MySQL的数据表按照某一个字段进行关联的时候查询&#xff0c;我们使用on和where哪种查询方式更快。百闻不如一见&#xff0c;我们来亲自测试下。 先说结论 Where、对等查询的join速度基本…

Android WMS——概述(一)

Android 中的 WMS 指的是 Window Manager Service(窗口管理服务)。WMS 是 Android 系统中的核心服务,主要分为四大部分,分别是窗口管理,窗口动画,输入系统中转站和 Surface 管理 。负责管理应用程序窗口的创建、移动、调整大小和显示等操作。 一、功能简介 WMS 的职责可…

在Eclipse中使用Junit

1、准备测试类 public class Calculator {private static int result;public void add(int n) {result result n;} public void substract(int n) {result result -1; //Bug:正确的应该是resultresult-n}public void multiply(int n) {// result result*n;方法未写好}publi…

2.9每日一题(定积分的奇偶性以及比较大小)

1、用定积分奇函数和偶函数的性质 2、用常用的基本不等式推出大小 3、用区间相同的情况下被积函数大的定积分大的定理

嵌入式系统设计师考试笔记之操作系统基础复习笔记二

目录 3、任务管理 &#xff08;1&#xff09;嵌入式操作系统的任务管理可以分为 &#xff08;2&#xff09;进程 &#xff08;3&#xff09;线程 &#xff08;4&#xff09;任务 &#xff08;5&#xff09;任务的创建与中止 &#xff08;6&#xff09;任务的状态任务有三…

Spring Boot 使用 Disruptor 做内部高性能消息队列

这里写自定义目录标题 一 、背景二 、Disruptor介绍三 、Disruptor 的核心概念3.1 Ring Buffer3.2 Sequence Disruptor3.3 Sequencer3.4 Sequence Barrier3.5 Wait Strategy3.6 Event3.7 EventProcessor3.8 EventHandler3.9 Producer 四、案例-demo五、总结 一 、背景 工作中遇…

代码随想录算法训练营第2天| 977有序数组的平方、209长度最小的子数组。

JAVA代码编写 977. 有序数组的平方 给你一个按 非递减顺序 排序的整数数组 nums&#xff0c;返回 每个数字的平方 组成的新数组&#xff0c;要求也按 非递减顺序 排序。 示例 1&#xff1a; 输入&#xff1a;nums [-4,-1,0,3,10] 输出&#xff1a;[0,1,9,16,100] 解释&…

postgis ST_CoverageInvalidEdges使用说明

官方文档 函数说明 概要 geometry ST_CoverageInvalidEdges(geometry winset geom, float8 tolerance 0); 描述 一个窗口函数&#xff0c;用于检查窗口分区中的多边形是否形成有效的多边形覆盖范围。 它返回线性指示器&#xff0c;显示每个多边形中无效边&#xff08;如果…

【微信小程序】实现投票功能(附源码)

一、Vant Weapp介绍 Vant Weapp 是一个基于微信小程序的组件库&#xff0c;它提供了丰富的 UI 组件和交互功能&#xff0c;能够帮助开发者快速构建出现代化的小程序应用。Vant Weapp 的设计理念注重简洁、易用和高效&#xff0c;同时提供灵活的定制化选项&#xff0c;以满足开发…

知识付费系统的移动应用开发:跨平台和原生应用的比较

移动应用在知识付费系统中发挥着重要作用&#xff0c;为用户提供了便捷的访问方式。在开发知识付费系统的移动应用时&#xff0c;开发团队通常需要考虑使用跨平台开发工具或原生开发。本文将比较这两种方法&#xff0c;讨论它们的优点和缺点&#xff0c;并提供示例代码来说明它…

面向对象设计原则之接口隔离原则

目录 定义接口隔离原则与单一职责原则示例 定义 接口隔离原则&#xff0c;全称为 Interface Segregation Principle&#xff0c;缩写ISP。 原始定义&#xff1a;Clients should not be forced to depend upon interfaces that they don’t use。 翻译&#xff1a; 不应该强行…

k8s 部署nginx前端

1.构建docker镜像&#xff0c;k8s拉取镜像运行 ​​​​​​​docker自己安装 [rootmaster1 ~]# docker pull nginx:1.24.0 [rootmaster1 ~]# mkdir k8s-nginx [rootmaster1 ~]# cd k8s-nginx [rootmaster1 k8s-nginx]# vim nginx.conf server_tokens off;server {listen …

MVC架构_Qt自己的MV架构

文章目录 前言模型/视图编程1.先写模型2. 视图3. 委托 例子&#xff08;Qt代码&#xff09;例1 查询本机文件系统例2 标准模型项操作例3 自定义模型示例:军事武器模型例4 只读模型操作示例例5 选择模型操作例6 自 定 义委 托(在testSelectionModel上修改) 前言 在Qt中&#xf…

ROS自学笔记十五:URDF工具

要使用工具之前&#xff0c;首先需要安装&#xff0c;安装命令: sudo apt install liburdfdom-tools 1.check_urdf 语法检查 在ROS中&#xff0c;你可以使用.check_urdf命令行工具来对URDF&#xff08;Unified Robot Description Format&#xff09;文件进行语法检查和验证。…

Pytorch实现深度学习常见问题

RuntimeError: stack expects each tensor to be equal size, but got [3, 300, 300] at entry 0 and [3, 301, 301] at entry 24 这里的问题出现的原因肯定是在数据预处理处&#xff0c;如下图&#xff0c;当数据使用不同的transforms处理方式时&#xff0c;会导致数据的尺寸大…

使用docker部署flask接口服务 一

文章目录 一&#xff1a;说明二&#xff1a;dockerfile 参数说明1. 一般常用的 参数&#xff0c;以及它的含义2. 我自己的 dockerfile 三&#xff1a;示例操作1. Gunicorn Gevent启动服务的好处2. 用Gunicorn Gevent的好处&#xff1a;3. Gunicorn Gevent的 使用示例4. 创建…