【C语言】结构体还不会?这一篇就够了

news2024/11/24 14:23:50

在这里插入图片描述

👦个人主页:Weraphael
✍🏻作者简介:目前正在回炉重造C语言(2023暑假)
✈️专栏:【C语言航路】
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注😍


目录

  • 一、结构体的声明
      • 1.1 结构的概念
      • 1.2 结构的定义
      • 1.3 结构成员的类型
      • 1.4 结构体变量的初始化
      • 1.5 结构的特殊声明
      • 1.6 结构的自引用
  • 二、结构体成员的访问
      • 2.1 变量 . 成员
      • 2.2 指针变量 -> 成员
      • 2.3 解引用访问
  • 三、结构体嵌套
  • 四、结构体传参
      • 4.1 传值调用
      • 4.2 传址调用
      • 4.3 传值调用和传址调用的区别
  • 五、结构体内存对齐(重点)
      • 5.1 计算结构体大小
      • 5.2 嵌套结构体大小计算
      • 5.3 验证偏移量为0的地址处
      • 5.4 修改默认对齐数
  • 六、实战 - 通讯录
  • 七、位段
      • 7.1 什么是位段
      • 7.2 位段的大小
      • 7.3 位段的内存分配
      • 7.4 位段跨平台问题。
      • 7.5 总结

一、结构体的声明

1.1 结构的概念

  • 结构是一些=值的集合,这些值称为成员变量。结构体的每个成员可以是不同类型的变量。
  • 类比数组。数组也是一些值的集合,但类型是相同的

1.2 结构的定义

结构体是用来描述复杂对象的

// tag - 是标签名,是可以根据需求改变的
struct tag //这一整串是类型
{
	//代码块里是结构体成员
	//可以是不同类型
   	member_list // 成员列表	
}list; 
//list - 全局结构体变量 
//注意:后面有分号

还可以用typedef来定义

//描述一位学生
typedef struct tag
{
	member_list // 成员列表	

}tag; //tag- 将结构体类型struct tag重新命名为tag
//分号不可缺

通过上面的模板来举个例子

假设要描述一名学生(名字、年龄、分数)

struct student //类型
{
    //结构体成员
	char name[20]; 
	int age;      
	double score;
 
}stu,stu2; 
// stu、stu2 - 也是结构体变量(全局变量)
 
int main()
{
	// 创建结构体变量(局部变量)
	// 类型 + 变量
	struct student stu1; 
	
	return 0;
}

1.3 结构成员的类型

结构的成员可以是标量、数组、指针,甚至是其他结构体。

1.4 结构体变量的初始化

struct student //类型
{
	char name[20]; 
	char sex[10];
	int age;
	double score;
 
}; 
 
int main()
{
    //结构体变量初始化
	struct student stu1 = { "小明","男",18,72.5 }; 
		
 	return 0;
}

在这里插入图片描述

1.5 结构的特殊声明

在声明结构的时候,也可以不完全的声明

【匿名结构体类型(缺少标签)】

// 匿名结构体类型(不愿起名)
struct 
{
	int a;
	char b;
	double d;
}s;

注意:

  • 若要使用匿名结构体类型,在创建类型马上在后面创建变量
  • 如果两个匿名结构体中的成员变量完全一样也会被编译器当成两个完全不同的类型
    在这里插入图片描述

1.6 结构的自引用

在结构中包含一个类型为该结构本身的成员是否可以?

【举个例子】

假设内存中有1,2,3。它们不是连续存储的,而是随机分布的。那应该如何存储它们呢?我们可以使用链表(是一种数据结构),一个节点中存储一个数,并且当前的节点能够找到下一个节点。所以我们可以把节点定义成一个结构体

在这里插入图片描述

【错误代码】

struct Node
{
	int data;
	struct Node next;
};

上面的代码是错误的,假如说要计算结构体的大小,data是4个字节,next包含date和下一个节点,下个next又包含date和下一个节点…,这样下来会发现它的大小其实是very very大的。

【正确代码】

我们可以放一个指向下一个节点的指针,也就是结构体指针

struct Node
{
	int data; //4个字节
	struct Node* next; //4/8个字节
};

那么接下来我把代码改成这样是否也是正确的?

typedef struct Node
{
	int data;
	Node* next;
}Node;

这其实是错误的!因为代码编译是从上到下的,当来到Node* next时,struct Node还未被typedef重命名
在这里插入图片描述

【正确代码】

typedef struct Node
{
	int data;
	struct Node* next;
}Node;

二、结构体成员的访问

往期博客链接:点击传送

2.1 变量 . 成员

#include <stdio.h>
 
struct student //类型
{
	char name[20]; 
	int age;
	char sex[10];
	double score;
 
}; 
 
int main()
{
	//结构体变量初始化
	struct student stu1 = { "小明",18,"男",95.5 };
 
	//结构体访问
	printf("名字:%s\n年龄:%d\n性别:%s\n分数:%.1lf\n", stu1.name, stu1.age, stu1.sex, stu1.score);
 
	return 0;
}

【程序结果】

在这里插入图片描述

2.2 指针变量 -> 成员

 
#include <stdio.h>
 
struct student //类型
{
	char name[20]; 
	int age;
	char sex[10];
	double score;
 
}; 
 
int main()
{
	//结构体变量初始化
	struct student stu1 = { "小明",18,"男",95.5 };
 
	struct student* pa = &stu1;
	//结构体访问
	printf("名字:%s\n年龄:%d\n性别:%s\n分数:%.1lf\n", pa->name,pa->age,pa->sex,pa->score);
 
	return 0;
}

【程序结果】

在这里插入图片描述

2.3 解引用访问

#include <stdio.h>
 
struct student //类型
{
	char name[20]; 
	int age;
	char sex[10];
	double score;
 
}; 
 
int main()
{
	//结构体变量初始化
	struct student stu1 = { "小明",18,"男",95.5 };
 
	struct student* pa = &stu1;
	//结构体访问
 
	//*pa就是stu1
	printf("名字:%s\n年龄:%d\n性别:%s\n分数:%.1lf\n",(*pa).name,(*pa).age,(*pa).sex,(*pa).score);
 
	return 0;
}

【程序结果】

在这里插入图片描述

三、结构体嵌套

#include <stdio.h>
 
struct S
{
	int a;
	char c;
};
struct P
{
	double d;
	struct S s;
	double f;
};
 
int main()
{
	//100和's'是结构体s的
	struct P p = { 5.5,{100,'s'},33.3 };
	
	//只打印嵌套的结构体
	printf("%d %c\n", p.s.a, p.s.c);
	return 0;
}

【程序结果】

在这里插入图片描述

只要一步一步去访问,嵌套结构体同样也能访问。当然也可以用指针来访问,这里就不为大家演示了。

四、结构体传参

4.1 传值调用

注意:如果使用传值调用,形参的改变不影响实参。

#include <stdio.h>
struct student
{
	char name[20];
	int age;
	char sex[6];
	double score;
};
//不需要返回参数用void
void Print(struct student stu) //传结构体变量同样也能用结构体变量来接收
{
	printf("名字:%s\n年龄:%d\n性别:%s\n分数:%.1lf\n", stu.name, stu.age, stu.sex, stu.score);
}
int main()
{
	struct student stu1 = { "小明",18,"男",95.5 };
 
	//封装一个Print函数负责打印
	Print(stu1);
 
	return 0;
}

【程序结果】

在这里插入图片描述

4.2 传址调用

#include <stdio.h>
struct student
{
	char name[20];
	int age;
	char sex[6];
	double score;
};

//不需要返回参数用void
void Print(struct student* stu) //传地址需要用指针来接收
{
	printf("名字:%s\n年龄:%d\n性别:%s\n分数:%.1lf\n", (*stu).name, (*stu).age, (*stu).sex, (*stu).score);
	
	// 或者还可以使用->操作符来访问
	//printf("名字:%s\n年龄:%d\n性别:%s\n分数:%.1lf\n", stu->name, stu->age, stu->sex, stu->score);
}

int main()
{
	struct student stu1 = { "小明",18,"男",95.5 };
 
	//封装一个Print函数负责打印
	Print(&stu1);
 
	return 0;
}

【程序结果】

在这里插入图片描述

4.3 传值调用和传址调用的区别

那么大家认为传值调用好还是传址调用好呢?

答案是:传址调用

  • 因为传址调用时,形参也有自己独立的空间,把实参传递(拷贝)给形参就要消耗时间,这时传参的压力就比较大。如果是传址调用,因为一个地址的大小无非就是4/8个字节,形参用指针变量接收,压力也相对较小些。
  • 官方说法:函数传参的时候,参数是需要压栈的。 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。

因此,建议大家在结构体传参时,都使用传址调用

五、结构体内存对齐(重点)

5.1 计算结构体大小

在了解结构体内存对齐之前,我们先想想 如何计算结构体的大小

#include <stdio.h>

struct Stu1
{
	char c;
	int a;
	double x;
};

struct Stu2
{
	char c;
	double x;
	int a;
};

int main()
{
	printf("Stu1:%d\n", sizeof(struct Stu1));
	printf("Stu2:%d\n", sizeof(struct Stu2));

	return 0;
}

【程序结果】

在这里插入图片描述

为什么Stu1Stu2的成员变量一样,就仅仅交换了位置,结构体大小却不一样?这就要涉及到 结构体内存对齐
🎈结构体内存对齐的规则

  1. 第一个成员在与结构体变量偏移量为0的地址处
  2. 其他成员变量要对齐到某个数字 (对齐数)的整数倍的地址处
    注:对齐数是 编译器默认对齐数与该成员类型大小的较小值。
    VS中默认对齐数为8
    Linux环境下无对齐数,对齐数就是结构体成员的自身大小
  3. 结构体总大小为所有结构体成员最大对齐数的整数倍。

现在解释开头的代码

以下解析默认是在visual studio环境下

【Stu1】

在这里插入图片描述

【Stu2】

在这里插入图片描述

5.2 嵌套结构体大小计算

#include <stdio.h>

struct Stu1
{
	char c;
	int a;
	double x;
};

struct Stu2
{
	char c;
	struct Stu1 s1;
	int a;
};

int main()
{
	printf("Stu2:%d\n", sizeof(struct Stu2));

	return 0;
}

如果结构体中嵌套了结构体成员,要将嵌套的结构体成员对齐到自己成员中最大对齐数的整数倍处,最后的最大对齐数还要包括嵌套成员的对齐数。

在这里插入图片描述

5.3 验证偏移量为0的地址处

C语言有一个宏叫offsetof,它的功能是计算一个结构体成员相较起始位置的偏移量(头文件:#include <stddef.h>

在这里插入图片描述

5.4 修改默认对齐数

在计算结果体大小中,我们提到了默认对齐数,在VS中默认对齐数为8,那能否修改默认对齐数呢?

可以用#pragma这个预处理指令

#pragma pack(需要修改的对齐数)

【例如】

在这里插入图片描述

那么接下来问题来了,怎么恢复呢?

// 恢复默认对齐数
#pragma pack()

六、实战 - 通讯录

点击跳转

七、位段

7.1 什么是位段

位段的声明和结构其实是类似的,但有两个不同:

  1. 位段的成员必须是intunsigned intsigned int,但也可以是char类型,因为char是属于整型家族的
  2. 位段的成员名后边有一个冒号和一个数字
  3. 位段里的成员一般都是同类型
  4. 位段的 其实表示二进制位

【举个例子】

#include <stdio.h>
struct segment
{
	int a : 2; //表示a只占内存的2个二进制位
	int b : 5;//表示b只占内存的5个二进制位
	int c : 10;//表示c只占内存的10个二进制位
	int d : 30;//表示d只占内存的30个二进制位
};

struct segment就是一个位段类型。

7.2 位段的大小

结构体的大小计算和位段的计算是一样的吗?答案其实是不一样的

在这里插入图片描述

位段的计算过程其实是这样的

  • int是4个字节,有32个比特位,a占内存2个二进制位,还剩下30个比特位
  • b占内存5个二进制位,还剩下25个比特位
  • c占内存10个二进制位,还剩下15个比特位
  • d占内存30个比特位,上一步还剩下15个比特位,假设把这15个比特位舍弃,继续向内存申请32个比特位给d用,所以还剩余2个比特位
    在这里插入图片描述
    从上过程中,类型在创建的时候向内存一共申请了32+32个比特位,也就8字节。也能看出位段其实比结构体更加节省空间,有多少用多少。

7.3 位段的内存分配

  1. 位段的成员可以是intunsigned intsigned int或者是char类型
  2. 位段的空间上是按照需要以4和字节(int)或者1个字节(char)的方式来开辟的
  3. 位段涉及很多不确定因素,位段是不跨平台的注重可移植的程序应该避免使用位段

【举个例子】

我们一起看看下面这一串代码在内存中是如何分配的

#include <stdio.h>
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 = 12;
	s.c = 3;
	s.d = 4;
}

首先先定制一些规则:

  1. 假设位段中的成员在内存中是从低位向高位分配
  2. 当第二个位段成员占内存二进制位比较大时,无法容纳前一个剩余位段时,直接把前一个剩余的舍弃
  3. 在visual studio测试
    分配过程如下:结构体初始化为000000000
  • a被赋值成10,转化为二进制:00001010,而a只占内存3个二进制位,也就是010(最高位的1被截断),分配:00000010
  • b被赋值成12,转化为二进制:00001100,b占内存4个二进制位,恰好能容纳上一个剩余位段。分配01100010
  • c被赋值成3,转化为二进制:00000011c只占内存5个二进制位,也就是:00011,然后这次的位段成员无法容纳上一个剩余位段,我们就舍弃,继续向内存申请:00000000,分配:00000011
  • d被赋值成4,转化为二进制:00000100d只占内存4个二进制位,然而上一个剩余位段还是无法容纳这次的成员位段,继续像内存申请:00000000,分配:00000100
  • 所以现在内存里为:0110010 00000011 00000100
    转化为十六进制也就是 62 03 04,我们F10调试,在内存中看看
    在这里插入图片描述
    这结果恰好就是我们分析出来的结果!

7.4 位段跨平台问题。

  1. int位段被当成有符号数还是无符号数是不确定的
  2. 位段中最大位的数目不能确定。16位机器最大16,32位机器最大32,如果int位段成员占内存27个二进制位,那么在16位机器会出现问题
  3. 位段中的成员咋子内存中从左向右分配,还是从右向左分配标准还未确定。
  4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余位时,是舍弃剩余的位还是利用,这也是未确定的。

7.5 总结

跟结构体相比,位段可以达到同样的效果,但是位段可以很好的节省空间,但是有跨平台的问题存在

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

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

相关文章

pytorch深度学习 线性回归

import torch import matplotlib.pyplot as pltx_data torch.tensor([[1.0], [2.0], [3.0]]) # x_data是一个张量 y_data torch.tensor([[2.0], [4.0], [6.0]]) # y_data是一个张量# 定义一个线性回归模型 class LinearModel(torch.nn.Module): # 继承torch.nn.Moduledef …

SpringBoot项目中MVC使用--【JSB系列之010】

SpringBoot系列文章目录 SpringBoot知识范围-学习步骤【JSB系列之000】 文章目录 SpringBoot系列文章目录Http协议是马冬梅Cookie机制Session机制Token MVC模型本章的专注内容UserController代码 ThymeleafLets GO!总结作业配套资源题外话 Http协议是马冬梅 HTTP简介 1. HTTP…

软件测试如何实现月薪2万?

其实我一直在强调&#xff0c;钱的多少和技能是息息相关的&#xff0c;所以你要赚更多钱&#xff0c;就得付出更多努力&#xff0c;去不断提升自己的技能和见识。 因为最近在群里有一些同学&#xff0c;之前没做过自动化测试&#xff0c;但是限于领导要求&#xff0c;或者自己…

剑指offer21.调整数组顺序使得奇数位于偶数前面 57.和为s的两个数字 58.反转单词顺序

暴力二次遍历&#xff08;时间复杂度空间复杂度都是n&#xff09; class Solution { public:vector<int> exchange(vector<int>& nums) {vector<int> result(nums.size());int left0;for(int i0;i<nums.size();i){if(nums[i]%21) result[left]nums[i…

深拷贝浅拷贝有什么区别?怎么实现深拷贝?

目录 一、浅拷贝二、深拷贝三、两者区别&#xff1f; 一、浅拷贝 浅拷贝&#xff0c;指的是创建新的数据&#xff0c;这个数据有着原始数据属性值的一份精确拷贝。 如果属性是基本类型&#xff0c;拷贝的就是基本类型的值。如果属性是引用类型&#xff0c;拷贝的就是内存地址 …

理解Vuex

Vuex是什么 专门在Vue中实现集中式状态&#xff08;数据&#xff09;管理的一个Vue插件&#xff0c;对Vue应用中多个组件的共享状态进行集中式的管理&#xff08;读/写&#xff09;&#xff0c;也是一种组件间通信的方式&#xff0c;且适用于任意组件间通信 Vuex Github地址 …

走向具身智能丨美格高算力AI模组 以端侧智慧连接人和家庭

“贾维斯&#xff0c;我需要你的帮助。”这是钢铁侠Tony Stark在电影中向他的人工智能助手Jarvis寻求支持的场景。《钢铁侠》中的贾维斯不仅令观众着迷&#xff0c;也点燃了人们对于智能助手的想象力。正如电影《她》中所描绘的那样&#xff0c;智能助手还可以与人类建立真实的…

dolphinscheduler伪分布式安装

1、上传安装包 2、安装 #解压 重命名 [rootdatacollection conf]# cd /opt/modules/ [rootdatacollection modules]# tar -zxf apache-dolphinscheduler-2.0.6-bin.tar.gz -C /opt/installs/ [rootdatacollection modules]# cd ../installs/ [rootdatacollection installs]# m…

这8种算法——程序员必会

一个程序员一生中可能会邂逅各种各样的算法&#xff0c;但总有那么几种&#xff0c;是作为一个程序员一定会遇见且大概率需要掌握的算法。今天就来聊聊这些十分重要的“必抓&#xff01;”算法吧~ 算法一&#xff1a;快速排序法 快速排序法是对冒泡排序的一种改进&#xff0c…

Jmeter测试脚本编写详解(配详图)

一、简介 Apache JMeter是Apache组织开发的基于Java的压力测试工具。用于对软件做压力测试&#xff0c;它最初被设计用于Web应用测试&#xff0c;但后来扩展到其他测试领域。 它可以用于测试静态和动态资源&#xff0c;例如静态文件、Java 小服务程序、CGI 脚本、Java 对象、数…

ORACLE数据库scott没有相关权限

1. 首先登陆具有DBA权限的用户 1.1 打开cmd 1.2 输入以下命令 sqlplus / as sysdab上述命令中的 / as sysdba 表示使用操作系统认证登录&#xff0c;同时指定DBA权限 1.3 回车执行命令&#xff0c;系统或将提示输入密码&#xff08;没有则直接跳过&#xff09; 1.4 密码正…

AndroidStudio-实现登录界面(数据存储在SQLite)

要求&#xff1a;每种错误信息采用Toast进行提示 &#xff08;1&#xff09;未注册的用户不能进行登录&#xff1b; &#xff08;2&#xff09;用户名和密码不能为空&#xff1b; &#xff08;3&#xff09;用户名不能重复&#xff1b; 一、创建新工程 点击next 修改名字 &…

JQuery 实现点击按钮添加及删除 input 框

前言 用于记录开发中常用到的&#xff0c;快捷开发 需求新增功能 比如说&#xff0c;我台设备可以设置一个或多个秘钥&#xff0c;有时候我配置一个秘钥时&#xff0c;就不需要多个输入框&#xff0c;当我想配置多个秘钥时&#xff0c;就需要添加多个输入框。 实现 HTML …

Adobe打印机另存pdf出错生成log文件,打印失败

目录预览 一、问题描述二、原因分析三、解决方案四、参考链接 一、问题描述 用adobe打印机转pdf出错生成log文件,打印失败&#xff0c;log文件内容如下&#xff1a; %%[ ProductName: Distiller ]%% FZXBSJW--GB1-0 not found, using Courier. %%[ Error: typecheck; Offendi…

Mac安装MySQL详细教程

1、MySQL安装包下载 还没下载的话请前往官网下载 我们可以看到这里有两个不同架构的dmg的安装包&#xff0c;如果不知道自己电脑是ARM还是X86的话可以打开终端输入&#xff1a;uname -a 或者 uname -a | awk -F " " {print $(NF-1)} 来查看如下图&#xff1a; 这里显…

v-cloak和v-once和v-pre指令

v-cloak指令&#xff08;没有值&#xff09;&#xff1a; 1.本质是一个特殊属性&#xff0c;Vue实例创建完毕并接管容器后&#xff0c;会删掉v-cloak属性。 2.使用css配合v-cloak可以解决网速慢时页面展示出{{xxx}}的问题 v-once: v-once指令&#xff1a; 1.v-once所在节点在初…

基于linux下的高并发服务器开发(第一章)- Linux开发环境搭建

​​​​​​基于linux下的高并发服务器开发&#xff08;第一章&#xff09;-Linux环境开发搭建1.1_呵呵哒(&#xffe3;▽&#xffe3;)"的博客-CSDN博客https://blog.csdn.net/weixin_41987016/article/details/131681333?spm1001.2014.3001.5501 解决Ubuntu 虚拟机没…

高数笔记01:函数、极限、连续

图源&#xff1a;文心一言 本文是我学习高等数学第一章的一些笔记和心得&#xff0c;主要介绍了函数、极限、连续这三个基本概念&#xff0c;以及它们的性质和很基础的计算技巧。希望可以与考研路上的小伙伴一起努力上岸~~&#x1f95d;&#x1f95d; 第1版&#xff1a;查资料…

Python自动化测试之cookie绕过登录(保持登录状态)

前言 在编写接口自动化测试用例或其他脚本的过程中&#xff0c;经常会遇到需要绕过用户名/密码或验证码登录&#xff0c;去请求接口的情况&#xff0c;一是因为有时验证码会比较复杂&#xff0c;比如有些图形验证码&#xff0c;难以通过接口的方式去处理&#xff1b;再者&…

系统学习Halcon视觉软件指南

要系统学习Halcon视觉软件&#xff0c;您可以按照以下步骤进行&#xff1a; 我这里刚好有嵌入式、单片机、plc的资料需要可以私我或在评论区扣个6 学习基本概念&#xff1a;了解Halcon的基本概念和术语&#xff0c;例如图像处理、特征提取、图像匹配等。可以查阅Halcon的官方…