结构体和位段

news2024/11/19 23:28:40

结构体: 

       C语言中,我们之前使用的都是C语言中内置的类型,比如整形(int)、字符型(char)、单精度浮点型(float)等。但是我们知道,我们现实世界中,还有很多其他类型。比如书,水杯,人等各种类型。

结构体的使用: 

       在使用结构体之前,我们先来看看结构体的基本使用语法:

       当内置类型无法满足我们的我们的需求(像定义一本书),此时就会用到结构体了,它就可以自定义一个类型。比如书就是一种类型,组成它的元素就是木头,墨水,胶水等,这些就是组成书的基本元素。而在编程语言中,我们定义一本书,它的基本元素就可以理解为C语言的内置类型,由这些内置类型组合而成。

struct Book
{
	//书由一下属性(元素)组成
	char Book_Name[20];//书的名字

	char Writer_Name[20];//作者姓名

	int edition;//版本号
};//此时变量列表为空

       此时我们就自定义了一种书的类型,这时就可以定义多个书的变量。书是类,那么具体的一本书就是一个书变量(也可以理解为对象),此时我们第一定义一个书的变量,并打印。注:变量列表可以为空

struct Book
{
	//书由一下属性(元素)组成
	char Book_Name[20];//书的名字

	char Writer_Name[20];//作者姓名

	int edition;//版本号
};

int main()
{
	struct Book Book1 = { "大话数据结构", "张三", 20 };
	
	printf("书名是:%s\n", Book1.Book_Name);
	printf("作者是:%s\n", Book1.Writer_Name);
	printf("版本是:%d\n", Book1.edition);


	return 0;
}

       我们用 . 来访问变量中的每一个成员(这不是使用指针的情况)。要按照顺序来定义每一个变量,. 就是一个操作符,意思可以理解为“的”。

       每一种类型都有对应的指针,所以结构体也有指针,就是结构体指针。我们用指针访问结构体变量时就需要用到 -> 来指定访问变量的哪一个具体成员属性。

struct Book* p = &Book1;

//定义结构体指针指向变量Book1
//因为其他类型只需要解引用,但是结构体有多个成员
//用指针找到结构体变量的每一个成员需要用到 ->
printf("书名是:%s\n", p->Book_Name);

         当然也可以对其解引用之后再使用 . 操作符访问具体成员属性。

//通过解引用再使用 . 来访问具体属性
printf("书名是:%s\n", (*p).Book_Name);

       现在我们来举例成员列表的使用。比如此时我们使用成员列表,声明多个成员:

struct Book
{
	//书由一下属性(元素)组成
	char Book_Name[20];//书的名字

	char Writer_Name[20];//作者姓名

	int edition;//版本号
}Book1, Book2, Book3;//这3个相当于全局结构体变量

int main()
{
	struct Book Book4;//局部变量

	return 0;
}

       就相当于全局变量。但是C语言并不支持直接在主函数中直接对全局结构体变量进行赋值。

       此时对赋值属性的字符串赋值也不能使用以下方法赋值:

Book1.Book_Name = "大话数据结构";

       我们只能使用strcpy函数赋值;但对于整数属性的赋值可以直接赋值:

strcpy(Book1.Book_Name, "大话数据结构");
printf("书名是:%s\n", Book1.Book_Name);
Book1.edition = 20;
printf("版本是:%d\n", Book1.edition);

       之后有人就经常和typedef搞混,因为用法相似:

typedef struct Book
{
	//书由一下属性(元素)组成
	char Book_Name[20];//书的名字

	char Writer_Name[20];//作者姓名

	int edition;//版本号
}book;

       此时我们相当于将struct Book重命名了(重命名具体用法可先查看其他文章),之后定义该结构体变量不需要使用struct Book,而直接使用book声明该变量的类型即可:

book Book1 = { "大话数据结构", "张三", 20 };

        这里相当于定义了一本书,由于使用了typedef函数,可以省略struct关键字,写出结构体名称,创建变量即可,这里创建了2个变量,之后打印变量名.成员。

//两种形式都可以使用
book Book1 = { "大话数据结构", "张三", 20 };
	
struct Book Book2 = { "C语言", "我", 1 };
printf("书名是:%s\n", Book1.Book_Name);
printf("书名是:%s\n", Book2.Book_Name);

       结构体成员可以是结构体,要用大括号来说明结构体中另外的结构体。

struct s
{
	int a;
	char c;
	char arr[20];
	double d;
};
struct t
{
	char ch[10];
	struct s s;
	//结构体成员可以是结构体
	char* pc;
};
int main()
{
	char arr[] = "holle bit";

	struct t t1 = { "hehe",{3,'u',"holle world",3.14},arr };
	printf("%s\n", t1.ch);//hehe
	printf("%s\n", t1.s.arr);//holle world
	printf("%d\n", t1.s.a);//3
	printf("%lf\n", t1.s.d);//3.140000
	printf("%s\n", t1.pc);//holle bit
	return 0;
}

       这里结构体成员中有指针,我们创建一个数组,把数组名放进去。 

结构体的传参: 

       我们知道,形参是实参的一份临时拷贝,我们对结构体进行传参时,如果是传值,就是函数中把这个结构体变量临时复制一份,这样无疑会浪费很多空间。

       所以我们一般进行传址调用,就是传入结构体的指针:

typedef struct stu
{
	char name[20];
	short age;
	char tele[12];
	char sex[5];
}stu;
void print1(stu s)
{
	printf("%s\n", s.name);//张三
	printf("%d\n", s.age);//40
	printf("%s\n", s.tele);//15568886688
	printf("%s\n", s.sex);//男
	//不是指针就用"."
}
void print2(stu* p)
{
	printf("%s\n", p->name);
	printf("%d\n", p->age);
	printf("%s\n", p->tele);
	printf("%s\n", p->sex);
	//指针就用箭头
}
int main()
{
	stu s = { "张三",40,"15568886688","男" };
	print1(s);//用这个不太好
	print2(&s);//用这个函数比较好
	return 0;
}

       函数传参时,参数是需要压栈的。如果传递一个结构体对象的时候,结构体过大,参数压栈系统开销比较大,所以会导致性能的下降。结论:结构体传参的时候,要传结构体的地址。 

匿名结构体: 

       匿名结构体,顾名思义,就是没有名字的结构体,意味着没有标签,但有一个成员变量。

struct 
{//匿名结构体类型
    int a;
    char c;
}sa;
struct
{//匿名结构体类型
    int a;
    char c;
}*psa;//匿名结构体指针类型
int main()
{
    psa = &sa;//编译器会认为这是两种不同的类型
    return 0;
}

        一般最好不要使用,使用一次以后就最好不要使用了。

结构体的自引用:

       结构体可以自信用,并不是递归。结构体类型中可以有一个同类型的结构体指针,结构体自引用牵扯到数据结构,先大致了解。

//结构体的自引用
//数据结构:链表
//在内存中,每个数据是随机分布的,为了让他们有规律的连接起来,就要用到链表
//
struct Node
{
    int data;
    struct Node *next;
};
int main()
{

    return 0;
}

       因为typedef可以定义数据类型的名字,所以可以:

typedef struct Node
{
    int data;
    struct Node* next;
}Node;
int main()
{
    struct Node n1;
    Node n2;
    return 0;
}

结构体的大小: 

       这是结构体最重要的部分,因为我们一定要知道每个类型在内存中的占据规则。结构体在内存中的占据规则是很复杂的。

结构体内存占据规则:

       结构体占据内存遵循地址对齐。第一个成员在与结构体变量偏移量为0的地址处对齐。所有成员都会遵循字节对齐,且第一个成员总是在与结构体变量偏移量为0的地址处对齐。其实结构体是先在内存中找到能被第一个类型整除的地址。

       结构体每个成员都遵循地址对齐,对齐数是根据系统对齐数和当前成员大小对齐的。

       对齐数 = 编译器默认的对齐数 与 改成员大小的较小值

       vs编译器默认对齐数为8。

struct S3
{
    double d;
    char c;
    int i;
};

       先看第一个成员,占据8个字节,所以先在内存中找到能被8整除的地址,偏移量为0(我们一会再解释),所以先占据8个字节,之后又找能被下一个成员内存(较小的对齐数是1)整除的地址,最后又找能被4整除的地址,最后整体结构体大小必须是当前最大成员属性大小的整数倍。

         即使VS默认对齐数是8,但是结构体大小是根据自己本身成员属性最大整数倍对齐的。

结构体嵌套:

       结构体可是可以嵌套的。

struct S3
{
    double d;
    char c;
    int i;
};
struct s4
{
    char c1;
    struct S3 s3;
    double d;
};

使用pragma来指定对齐数: 

       我们可以自己设置默认对齐数,提高空间利用效率,因为对齐数总是等于较小值。先设置默认对齐数为2几次方。要加入预处理指令#pragma pack(设置的默认对齐数)

#pragma pack(1)//设置默认对齐数为4
struct S
{
    char c1;//1
    double d;//8
};
#pragma pack()//取消设置的默认对齐数
int main()
{
    struct S s;
    printf("%d\n", sizeof(s));
    return 0;
}

       此时最小默认对齐数为1,所以所有属性都找到能被1整除的地址即可。结构在对齐方式不合适的时候,我蛮可以自己更改默认对齐数。一般是2几次方。 

相对偏移函数offsetof:

       我们可以求出它相对于结构体偏移了几个字节。要引入头文件stddef.h。

#include<stddef.h>//offsetof的头文件
struct S1
{
	char c1;
	char c2;
	int i;
};

int main()
{
	printf("%zd\n", offsetof(struct S1, c1));
	printf("%zd\n", offsetof(struct S1, c2));
	printf("%zd\n", offsetof(struct S1, i));


	return  0;
}

       相对起始位置的偏移量。

内存对齐的意义: 

  1. 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的数据的,某些硬件只能在某某些地址处去某些特定类型的数据,否则抛出异常。
  2. 性能原因:对于未对齐的内存,处理器需要两次内存访问;而对齐的的内存访问仅需要一次。假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数,如果我们能保证将所有的doubl类型数据的地址都对齐成8的倍数,就可以用一个内存操作来读取或者写值了,否则,我们可能需要执行两次内存访问,因为对象可能分放在两个8字节内存中。

       总体来说,结构体的内存对齐是拿空间来换时间的做法。我们在设计结构体是,既要满足对齐,又要节省空间,所以我们让占用空间小的成员尽量集中在一起。 

位段: 

位段是什么?

       位段的出现就是为了节省空间,因为结构体遵循内存对齐,有时候会造成空间浪费,于是衍生出来了位段。位段的声明和结构体是类似的,有两个不同:

  1. 位段成员必须是int、 unsigned int 、signed int或者char等类型。

  2. 位段的成员名后面有一个冒号和一个数字。

位段的使用和大小: 

       位段的使用是类似于结构体的。


//1.位段的成员必须是int、unsigned int、signed int
//2.位段的成员名后有一个冒号和数字
//位段 - 二进制位
struct A
{
    int a : 2;//2
    //冒号后面的数字表示a只需要两个比特位就够了
    int b : 5;//5
    int c : 10;
    int d : 30;
};
//47bit - 6个字节*8 = 48bit
//因为位段有自己的对齐方式
int main()
{
    struct A s;
    printf("%d\n", sizeof(s));//8个字节
    return 0;
}

       上图中A就是一个位段类型。A的大小是8个字节。 

       位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

struct S
{
    char a : 3;
    char b : 4;
    char c : 5;
    char d : 4;
};
int main()
{
    struct S s={0};

    s.a = 10;
    s.b = 20;
    s.c = 3;
    s.d = 4;
    return 0;
}

       上面的代码就是相当于先创建一个位段类型,之后声明每个成员占多少个bit,之后有给成员赋值,但很明显,给a赋值10所占据的比特位已经超过了3个bit,于是只将10的二进制前后3个为给成员a。如果不够,高位补0。之后以此类推。 

位段成员的赋值: 

       位段的几个成员共有一个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。

       内存中每个字节分配一个地址,一个字节内部的bit位是没有地址的,所以不能对位段成员使用&操作,这样就不能使用scanf直接给位段的成员输入值,只能是先输入放在一个变量中,然后赋值给位段的成员。

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

int main()
{
	struct A sa = { 0 };
	//scanf("%d", &sa._b);//这里报错

	//只能这样赋值
	int b = 0;
	scanf("%d", &b);
	sa._b = b;
	printf("%d\n", sa._b);
	return 0;
}

位段存在的意义: 

       学过网络的都知道,我们的数据都是封装成帧发送的,我们一般采用IP数据报的形式发送,我们观察IP数据报的格式:

       因为地址最小的的地址编号是字节,1个字节8个bit位,若使用结构体,必然会造成空间的浪费,位段的出现使我们将每一个bit位都合理的使用,但有人会问?既然现在硬件内存都那么大了,还有必要限制内存吗?

       我们可以将网络通道想象成一条高速公路,如果都是大型文件,就像是都是大卡车,这样势必会造成交通拥挤;但是如果都是小文件,就是小客车,即使会用交通拥挤也会比都是大卡车的路况好。 

位段的跨平台问题:

  1. int位段被当做有符号数还是无符号数是不确定的。
  2. 位段中最大位的数目不确定。(16位机器int是2个字节,写成27会出问题)。

  3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。

  4. 当一个结构包括两个位段,第二位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这个是不确定的。

       最后我们来看一道关于位段的练习题:

int main()
{
    unsigned char puc[4];
    struct tagPIM
    {
        unsigned char ucPim1;
        unsigned char u0 : 1;
        unsigned char u1 : 2;
        unsigned char u2 : 3;
    }*p;
    p = (struct tagPIM*)puc;
    memset(puc, 0, 4);//设置4个字节,每个内容为0
    p->ucPim1 = 2;
    p->u0 = 3;
    p->u1 = 4;
    p->u2 = 5;
    printf("%02x %02x %02x %02x\n", puc[0], puc[1], puc[2], puc[3]);//%02x打印出两个16进制的数
    return 0;
}

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

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

相关文章

用Rust刷LeetCode之27 移除元素

27. 移除元素 难度: 简单 原描述: 新描述: func removeElement(nums []int, val int) int { for i : 0; i < len(nums); i { if nums[i] val { nums append(nums[:i], nums[i1:]...) i-- } } return len(nums)} Rust 版本 下面这种写法编译无法通过: pub fn remove_…

b样条原理与测试

为了保留贝塞尔曲线的优点&#xff0c;同时克服贝塞尔曲线的缺点&#xff0c;b样条在贝塞尔曲线上发展而来&#xff0c;首先来看贝塞尔曲线的定义&#xff1a; 对于贝塞尔中的基函数而言&#xff0c;是确定的&#xff0c;全局唯一的&#xff0c;这导致了如果控制点发生变换将会…

Linux基本指令(超详版)

Linux基本指令&#xff08;超详版&#xff09; 1. ls指令2.pwd指令3. cd 指令4.touch指令5mkdir指令6.rmdir指令&&rm指令7.man指令7.cp指令8.mv指令9.echo指令10.cat指令11.more指令12.less指令13.head指令14.tail指令15.date指令16.find指令17.grep指令zip(打包压缩) …

使用cmake构建Qt6.6的qt quick项目,添加应用程序图标的方法

最近&#xff0c;在学习qt的过程中&#xff0c;遇到了一个难题&#xff0c;不知道如何给应用程序添加图标&#xff0c;按照网上的方法也没有成功&#xff0c;后来终于自己摸索出了一个方法。 1、准备一张图片作为图标&#xff0c;保存到工程目录下面&#xff0c;如logo.ico。 …

二维码智慧门牌管理系统:引领未来的城市管理

文章目录 前言一、主要特点二、升级带来的优势与意义 前言 随着科技的快速发展&#xff0c;智能化管理已经成为我们生活和工作的重要方面。门牌管理系统是城市管理的基础设施之一&#xff0c;其智能化程度直接影响着城市管理的效率和质量。为了适应这一需求&#xff0c;二维码…

Helio 升级为 LISTA DAO,开启多链时代新篇章并宣布积分空投计划

Helio Protocol 是 BNB Chain 上排名第一的去中心化稳定币协议&#xff0c;其推出的超额抵押和清算机制支持的去中心化稳定币 HAY&#xff0c;在 BNB Chain 有非常广泛的应用&#xff0c;包括流动性挖掘、质押、交易、储值等&#xff01; 2023 年 7 月&#xff0c;Helio Protoc…

【小沐学Python】Python实现语音识别(SpeechRecognition)

文章目录 1、简介2、安装和测试2.1 安装python2.2 安装SpeechRecognition2.3 安装pyaudio2.4 安装pocketsphinx&#xff08;offline&#xff09;2.5 安装Vosk &#xff08;offline&#xff09;2.6 安装Whisper&#xff08;offline&#xff09; 3 测试3.1 命令3.2 fastapi3.3 go…

C#注册表技术及操作

目录 一、注册表基础 1.Registry和RegistryKey类 &#xff08;1&#xff09;Registry类 &#xff08;2&#xff09;RegistryKey类 二、在C#中操作注册表 1.读取注册表中的信息 &#xff08;1&#xff09;OpenSubKey()方法 &#xff08;2&#xff09;GetSubKeyNames()…

SpringSecurity6 | 自定义认证规则

✅作者简介&#xff1a;大家好&#xff0c;我是Leo&#xff0c;热爱Java后端开发者&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;Leo的博客 &#x1f49e;当前专栏&#xff1a; Java从入门到精通 ✨特色专栏&#xf…

Java基础-java.util.Scanner接收用户输入

目录 1. 导入所需要的jar包2. 编写代码运行3. 输出运行结果 1. 导入所需要的jar包 import java.util.Scanner;2. 编写代码运行 public class ScannerDemo {public static void main(String[] args) {/** 使用Scanner接收用户键盘输入的数据* 1. 导包&#xff1a;告诉程序去JD…

角谷定理 C语言xdoj32

角谷定理定义如下&#xff1a; 对于一个大于1的整数n&#xff0c;如果n是偶数&#xff0c;则n n / 2。如果n是奇数&#xff0c;则n 3 * n 1&#xff0c;反复操作后&#xff0c;n一定为1。 例如输入22的变化过程&#xff1a; 22 ->11 -> 34 -> 17 -> 52 -> 26 …

探索 Python 中链表的实现:从基础到高级

# 更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com 链表是一种基础的数据结构&#xff0c;它由一系列节点组成&#xff0c;每个节点都包含数据和指向下一个节点的引用。在Python中&#xff0c;可以使用类来实现链表&#xff0c;本文将介绍如何实现链表&#xff…

人工智能原理复习--搜索策略(二)

文章目录 上一篇启发式搜索与或图搜索博弈下一篇 上一篇 人工智能原理复习–搜索策略&#xff08;一&#xff09; 启发式搜索 提高一般图搜索效率的关键是优化OPEN表中节点的排序方式 最理想的情况是每次排序OPEN表表首n总在解答路径上 全局排序–对OPEN表中的所有节点进行…

论文阅读:PointCLIP: Point Cloud Understanding by CLIP

CVPR2022 链接&#xff1a;https://arxiv.org/pdf/2112.02413.pdf 0、Abstract 最近&#xff0c;通过对比视觉语言预训练(CLIP)的零镜头学习和少镜头学习在2D视觉识别方面表现出了鼓舞人心的表现&#xff0c;即学习在开放词汇设置下将图像与相应的文本匹配。然而&#xff0c;…

内外联动——记建行江门鹤山支行营业部堵截一起新型骗局

建设银行广东省江门市分行&#xff08;以下简称“江门建行”&#xff09;认真贯彻落实党中央、国务院决策部署&#xff0c;紧紧围绕当地市委工作部署和上级行要求&#xff0c;扛牢国有大行责任&#xff0c;坚守金融工作的政治性、人民性&#xff0c;以深化新金融行动助力江门全…

skynet 中 mongo 模块运作的底层原理解析

文章目录 前言总览全流程图涉及模块关系连接数据库函数调用流程图数据库操作函数调用流程图涉及到的代码文件 建立连接SCRAMSASL 操作数据库结语参考链接 前言 这篇文章总结 skynet 中 mongo 的接入流程&#xff0c;代码解析&#xff0c;读完它相信你对 skynet 中的 mongo 调用…

蓝桥杯日期问题

蓝桥杯其他真题点这里&#x1f448; 注意日期合法的判断 import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader;public class Main{static int[] days {0,31,28,31,30,31,30,31,31,30,31,30,31};static BufferedReader in new Buf…

Python数据科学视频讲解:数据清洗、特征工程和数据可视化的注意事项

1.6 数据清洗、特征工程和数据可视化的注意事项 视频为《Python数据科学应用从入门到精通》张甜 杨维忠 清华大学出版社一书的随书赠送视频讲解1.6节内容。本书已正式出版上市&#xff0c;当当、京东、淘宝等平台热销中&#xff0c;搜索书名即可。内容涵盖数据科学应用的全流程…

【数电笔记】58-同步D触发器

目录 说明&#xff1a; 1. 电路组成 2. 逻辑功能 3. 特性表、特性方程 4. 状态转移图 例题 5. 同步D触发器的特点 6. 集成同步D触发器&#xff1a;74LS375 74LS375内部原理 说明&#xff1a; 笔记配套视频来源&#xff1a;B站本系列笔记并未记录所有章节&#xff0c;…

2024最新软件测试面试最全八股文

请你说一说测试用例的边界 参考回答&#xff1a; 边界值分析法就是对输入或输出的边界值进行测试的一种黑盒测试方法。通常边界值分析法是作为对等价类划分法的补充&#xff0c;这种情况下&#xff0c;其测试用例来自等价类的边界。 常见的边界值 1)对16-bit 的整数而言 32…