深入了解C语言中的结构体类型与内存对齐

news2024/11/26 17:37:31

引言:

在C语言中,结构体是一种自定义的数据类型它允许我们将不同类型的数据组合在一起,形成一个新的数据类型。结构体的使用为我们解决了一些复杂数据的表示和处理问题,不仅限于单单的整型或者字符。本文将深入探讨结构体类型、结构体变量的创建和初始化,并详细介绍结构体中的内存对齐规则。

1.结构体类型

结构体类型是由不同类型的数据成员组成的集合,其中每个数据成员可以是任意类型的数据,包括基本数据类型、数组、指针、其他结构体等。结构体类型的定义使用关键字"struct"。

1.1 结构体的声明

形式如下:

struct tag
{
	member-list; // 成员列表
};

举例:描述一个学生

struct student
{
	char name[20]; // 名字 
 	int age; // 年龄 
 	char sex[5]; // 性别
 	char id[20]; // 学号
}; // 注意分号不要遗忘

1.2 结构体的创建和初始化

  • 结构体可以在声明的同时创建
struct student
{
	char name[20]; // 名字 
 	int age; // 年龄 
 	char sex[5]; // 性别
 	char id[20]; // 学号
}stu; // 这样就在声明的同时创建了一个struct student类型的变量stu
  • 结构体也可以声明完后再创建
struct student
{
	char name[20]; // 名字 
 	int age; // 年龄 
 	char sex[5]; // 性别
 	char id[20]; // 学号
};

int main()
{
	struct student stu; // 这样也是创建了一个struct student类型的变量stu
	return 0;
}
  • 结构体的初始化
struct Stu
{
	char name[20];//名字 
	int age;//年龄 
	char sex[5];//性别 
	char id[20];//学号 
};

int main()
{
	//按照结构体成员的顺序初始化 
	struct Stu s = { "张三", 20, "男", "20230818001" };
	printf("name: %s\n", s.name);
	printf("age : %d\n", s.age);
	printf("sex : %s\n", s.sex);
	printf("id : %s\n", s.id);
	
	//按照指定的顺序初始化 
	struct Stu s2 = { .age = 18, .name = "lisi", .id = "20230818002", .sex = "女" };
	printf("name: %s\n", s2.name);
	printf("age : %d\n", s2.age);
	printf("sex : %s\n", s2.sex);
	printf("id : %s\n", s2.id);
	return 0;
}
  • 输出结果:
    在这里插入图片描述

1.3 访问结构体成员

  • 结构体指针变量成员的访问:
struct student
{
	char name[20]; // 名字 
	int age; // 年龄 
	char sex[5]; // 性别
	char id[20]; // 学号
};

int main()
{
	struct student* stu = (struct student*)malloc(sizeof(struct student));
	stu->age = 18;
	printf("age : %d\n", stu->age); // 只有指针能用箭头访问成员
	(*stu).age = 20;
	printf("age : %d\n", (*stu).age); // 必须要加(),因为.的优先级高于*
	return 0;
}
  • 输出结果:
    在这里插入图片描述
    只有指针能用箭头->访问成员

  • 非指针的结构体成员的访问
struct student
{
	char name[20]; // 名字 
	int age; // 年龄 
	char sex[5]; // 性别
	char id[20]; // 学号
};

int main()
{
	struct student stu;
	stu.age = 18;
	printf("age : %d\n", stu.age);
	return 0;
}
  • 输出结果:
    在这里插入图片描述
    非指针的结构体只能通过.访问成员

1.4 小技巧

当我们需要多次使用结构体时,我们可以通过重命名结构体变量来减少代码量

  • 可以在声明结构体类型的时候重命名:
typedef struct student
{
	char name[20]; // 名字 
 	int age; // 年龄 
 	char sex[5]; // 性别
 	char id[20]; // 学号
}student;

int main()
{
	student stu; // 这样创建的变量是和struct student一个类型的,但是可以一定程度减少代码量
	return 0;
}
  • 也可以声明后重命名
struct student
{
	char name[20]; // 名字 
 	int age; // 年龄 
 	char sex[5]; // 性别
 	char id[20]; // 学号
};

typedef struct student student; // 将struct student重命名为student

int main()
{
	student stu; // 这样创建的变量是和struct student一个类型的,但是可以一定程度减少代码量
	return 0;
}

1.5 结构的特殊声明

  • 在声明结构的时候,可以不完全的声明。
//匿名结构体类型 
struct
{
	int a;
	char b;
	float c;
}x;

struct
{
	int a;
	char b;
	float c;
}a[20], *p; 

上⾯的两个结构在声明的时候省略掉了结构体标签(tag)。

那么此时有一个问题,如下的代码合法吗?

p = &x;
  • 警告:
    • 编译器会把上⾯的两个声明当成完全不同的两个类型,所以是⾮法的。
    • 匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使⽤⼀次。

1.6 结构体的自引用

在链表和二叉树中常常用到这样一种结构:

typedef struct Node
{
	struct Node* next; // 结构体指针可以指向下一个节点
	int data;
}Node;

当我们对结构体指针next赋值时,我们就可以通过next找到它指向的下一个结构体变量节点

  • 注意:
    • 以下形式是非法的:
typedef struct Node
{
	Node* next; // 此时struct Node 还没有重命名为Node ,所以不能使用Node自引用
	int data;
}Node;

2.结构体内存对齐

我们已经掌握了结构体的基本使⽤了。
现在我们深⼊讨论⼀个问题:计算结构体的⼤⼩
这也是⼀个特别热⻔的考点: 结构体内存对⻬

2.1 对齐规则

⾸先得掌握结构体的对⻬规则:

1.结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为 0 的地址处

2.其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。

  • 对⻬数 = 编译器默认的⼀个对⻬数与该成员变量⼤⼩的较⼩值。
  • VS中对齐数默认的值为 8
  • Linux中gcc没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩

3.结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的整数倍。

4.如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。

2.1.1 内存对齐例题

下面例题均在VS环境下

题目1

struct S1 // 计算该结构体大小
{
	char c1;
	int i;
	char c2;
};

解析:


在这里插入图片描述


题目2

struct S2 // 计算该结构体大小
{
	char c1;
	char c2;
	int i;
};

解析:


在这里插入图片描述


题目3

struct S3 // 计算该结构体大小
{
	double d;
	char c;
	int i;
};

解析:


在这里插入图片描述


题目4

struct S4 // 计算该结构体大小
{
	char c1;
	struct S3 s3; // 题目3中的结构体
	double d;
};

解析:


在这里插入图片描述


2.2 为什么存在内存对⻬?

大部分的参考资料都是这样说的:

  1. 平台原因(移植原因):
    不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

  2. 性能原因:
    数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取 8 个字节,则地址必须是 8 的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成 8 的倍数,那么就可以⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两个 8 字节内存块中。

  • 总体来说:结构体的内存对⻬是拿空间来换取时间的做法

  • 所以我们在设计结构体的时候,尽量让占⽤空间⼩的成员尽量集中在⼀起,这样既可以满⾜对⻬,⼜可以节省空间。

例如:

struct S1
{
	char c1;
	int i;
	char c2;
}; // 12 byte

struct S2
{
	char c1;
	char c2;
	int i;
}; // 8 byte

S1 和 S2 类型的成员⼀模⼀样,但是 S2 占的空间比 S1 要小。


2.3 修改默认对⻬数

结构体在对⻬⽅式不合适的时候,我们可以⾃⼰更改默认对⻬数。

#pragma 这个预处理指令,可以改变编译器的默认对⻬数。

#include <stdio.h>
#pragma pack(1)//设置默认对⻬数为1 

struct S
{
	char c1;
	int i;
	char c2;
};

#pragma pack()//取消设置的对⻬数,还原为默认 

int main()
{
	//输出的结果是什么? 
	printf("%d\n", sizeof(struct S));
	return 0;
}
  • 输出结果:
    在这里插入图片描述
    如果不修改默认对齐数,则结果为 12 。

3.结构体传参

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

上⾯的 print1 和 print2 函数哪个好些?
——答案是:⾸选 print2 函数。

原因:

  • 函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
  • 如果传递⼀个结构体对象的时候,结构体过⼤,参数压栈的的系统开销⽐较⼤,所以会导致性能的下降。

结论:结构体传参的时候,要传结构体的地址

结语:

通过本文的介绍,我们了解到了C语言中结构体类型的定义和使用方法,以及结构体变量的创建和初始化。同时,我们还详细讲解了结构体中存在的内存对齐规则,帮助我们更好地理解结构体在内存中的存储方式。结构体在C语言中的应用非常广泛,掌握了结构体的基本用法及内存对齐规则,有助于我们写出更加高效、灵活的程序代码。

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

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

相关文章

C++ vector 动态 向量/数组

文章目录 【 1. vector 的声明与初始化 】1.1 vector 的声明1.2 vector 的初始化1.2.1 构造一个空的 vector1.2.2 指定数量初值的方式初始化 vector1.2.3 迭代器的方式初始化1.2.4 构造一个相同的 vector 【 2. vector 的相关操作 】2.1 插入元素2.1.1 在vector的末尾插入新元素…

【ESP32 ECO V3】使用 Flash 下载工具完成 Secure Boot V2 功能

ESP32 ECO3 及以上版本 的芯片支持 Secure Boot V1 和 Secure Boot V2。使用 ESP32 ECO3 版本的芯片推荐使用 Secure Boot V2, 此篇文档记录基于 ESP32 ECO3 芯片通过 Flash 下载工具开启 Secure Boot V2 的实现。 支持 Secure Boot V2 的产品如下&#xff1a; 芯片型号Secur…

号称史上最全的PostgreSQL备份恢复,送给有缘人...

&#x1f4e3;&#x1f4e3;&#x1f4e3; 哈喽&#xff01;大家好&#xff0c;我是【IT邦德】&#xff0c;江湖人称jeames007&#xff0c;10余年DBA及大数据工作经验 一位上进心十足的【大数据领域博主】&#xff01;&#x1f61c;&#x1f61c;&#x1f61c; 中国DBA联盟(ACD…

【Linux】ubuntu安装google gtest框架

本文首发于 ❄️慕雪的寒舍 ubuntu 22.04.03 LTS 安装 google gtest 框架 1.依赖项 首先在ubuntu中安装如下包 sudo apt install -y unzip g gcc cmake make automake2.下载软件包 进入google gtest的github页面&#xff0c;下载源码包 Releases google/googletest https…

使用 PDManer 对数据库表建模(建表语句生成,代码生成)

目录 前言 基本使用教程 新建项目 创建表 关系图 建表语句 生成代码 导入 前言 在软件开发中过程中&#xff0c;一般分为几个过程&#xff1a;需求分析、概要设计、详细设计、编码实现、软件测试和软件交付。 在概要设计和详细设计过程中&#xff0c;则需要对业务进…

Java学习11

目录 一.类变量和类方法&#xff1a; 1.类变量定义访问&#xff1a; 2.类变量的使用细节&#xff1a; 3.类方法&#xff1a; 4.静态main方法&#xff08;public static void main(String [] args)&#xff09;&#xff1a; 二.代码块&#xff1a; 1.基本语法&#xff1a;…

Chatgpt掘金之旅—有爱AI商业实战篇|文案写作|(三)

演示站点&#xff1a; https://ai.uaai.cn 对话模块 官方论坛&#xff1a; www.jingyuai.com 京娱AI 一、前言 人工智能&#xff08;AI&#xff09;技术作为当今科技创新的前沿领域&#xff0c;为创业者提供了广阔的机会和挑战。随着AI技术的快速发展和应用领域的不断拓展&…

搜索与图论——Prim算法求最小生成树

在最小生成树问题里&#xff0c;正边和负边都没问题 朴素版prim算法 时间复杂度O(n^2) 生成树&#xff1a;每一次选中的t点&#xff0c;它和集合的距离对应的那条边&#xff0c;就是生成树的一条边 算法流程和dijkstra算法非常相似 #include<iostream> #include<cs…

漏洞扫描神器:Netsparker 保姆级教程(附链接)

一、介绍 Netsparker是一款专业的网络安全扫描工具&#xff0c;用于自动化地发现和修复网站和Web应用程序中的安全漏洞。它提供了全面的安全测试&#xff0c;包括SQL注入、跨站脚本攻击、远程文件包含、命令注入等常见的安全漏洞。 Netsparker具有以下特点&#xff1a; 自动化…

kubeEdge安装初探

1.准备k8s集群 1. 准备一台机器用来部署单节点kubernetes,机器地址192.168.1.10 2. 使用kubekey安装1.27.0版本的kubernetes&#xff1a; apt install conntrack socat export KKZONEcn kk create cluster --with-kubernetesv1.27.0 --container-managercontainerd 2. 安装…

Maplesoft Maple 2024(数学科学计算)mac/win

Maplesoft Maple是一款强大的数学计算软件&#xff0c;提供了丰富的功能和工具&#xff0c;用于数学建模、符号计算、数据可视化等领域的数学分析和解决方案。 Mac版软件下载&#xff1a;Maplesoft Maple 2024 for mac激活版 WIn版软件下载&#xff1a;Maplesoft Maple 2024特别…

C语言第三十八弹---编译和链接

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】 编译和链接 1、翻译环境和运行环境 2、翻译环境 2.1、预处理&#xff08;预编译&#xff09; 2.2、编译 2.2.1、词法分析 2.2.2、语法分析 2.2.3、语义分…

回溯算法|78.子集

力扣题目链接 class Solution { private:vector<vector<int>> result;vector<int> path;void backtracking(vector<int>& nums, int startIndex) {result.push_back(path); // 收集子集&#xff0c;要放在终止添加的上面&#xff0c;否则会漏掉自…

Mysql 常用SQL语句

1、查看mysql中所有的数据库&#xff0c; show databases; 2、创建库 create database 库名;&#xff08;也可以用 create database if not exists 库名; 表示如果库不存在再创建&#xff09; 例&#xff1a;create database if not exists ecology; 3、删除库 …

Visual Studio安装遇到的问题

因为在安装pytorch3d0.3.0时遇到问题&#xff0c;提示没有cl.exe&#xff0c;VS的C编译组件&#xff0c;查了下2019版比2022问题少&#xff0c;下载安装时遇到的问题记录&#xff1a; 查看搜素栏搜时间&#xff0c;查看系统日志&#xff0c;报错为&#xff1a; 创建 TLS 客户端…

农业信息管理(源码+文档)

农业信息管理系统&#xff08;小程序、ios、安卓都可部署&#xff09; 文件包含内容程序简要说明功能项目截图客户端首页我的今日动态动态详情登录修改资料今日价格今日报价注册页 后端管理文章管理用户管理分类管理 文件包含内容 1、搭建视频 2、流程图 3、开题报告 4、数据库…

从智能驾驶全技术供应链看智能驾驶“灵魂”

从智能驾驶全技术供应链看智能驾驶“灵魂” 附赠自动驾驶学习资料和量产经验&#xff1a;链接 智能驾驶主机厂与技术供应链的灵魂之争伴随着智能驾驶技术兴起而来&#xff0c;也伴随着技术供应链的繁荣虽然嘴上很少提起但是行动却很雷厉&#xff0c;首先是扛起民族大旗的华为…

全网营销:提升知名度与市场份额的有效策略

一、什么是全网整合营销&#xff1f; 是指整合产品规划、产品开发、网站建设、网上商店运营、品牌推广、产品分销等一系列电子商务内容的新营销模式&#xff0c;是传统网络、移动互联网、个人电脑互联网的整合。根据目前的观点&#xff0c;是个人电脑移动集成、在线和离线集成…

vue2 列表一般不使用索引删除的原因

在 Vue 中使用索引来删除列表项可能会导致一系列问题&#xff0c;尤其是在处理动态列表时。以下是一些可能的问题和相应的例子&#xff1a; 1. 数据不一致问题 当你使用索引来删除列表中的某个项时&#xff0c;如果列表中的其他项发生了变化&#xff08;比如新增或重新排序&a…

SSM框架学习——Spring事务管理

Spring事务管理 概念 事务&#xff08;Transaction&#xff09;是访问并可能操作各种数据项的一个数据库操作序列&#xff0c;这些操作要么全部执行&#xff0c;要么都不执行&#xff0c;是一个不可分割的工作单元。 事务有如下特性&#xff1a; 原子性隔离性一致性持久性 …