理解C语言——从小菜到大神的晋级之路(10)——结构体、联合体

2023-11-23 04:38

本文主要是介绍理解C语言——从小菜到大神的晋级之路(10)——结构体、联合体,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

本节视频链接:点击这里


        上篇中讲述的数组是复合数据类型中最简单的一种,一个数组使用一段连续的内存保存了若干个类型相同的数据元素。由于类型和长度相同,数组的每个元素通过数组下标和指针变量访问。如果我们希望一个结构保存多个不同类型的数据元素,那么数组将无能为力。为了实现这样的功能,C语言提供了结构体和联合体。



1、结构体基本概念


(1)结构体的定义


        假设我们需要定义一个图形中的点的概念。在一个使用笛卡尔坐标系表示图像的系统中,点的位置使用两个坐标分量表示,即横坐标x和纵坐标y。那么为了定义点这个变量,我们需要将坐标的两个分量定义于一起。将坐标x和坐标y定义在结构体中的方法为:
struct Point
{int x;int y;
};

        定义一个结构体使用关键字struct实现。关键字struct定义了结构体名称,并在其后用大括号{ }指明了结构体的成员。每一个结构体成员是该结构体实例中所包含的数据。如我们定义的Point结构中就包含了x和y两个int型数据成员。在定义结构体的同时可以定义结构体的实例,如:

struct Point
{int x;int y;
} pt1, pt2, *ppt1;

        另一种方法也可以将定义结构体类型和定义结构体实例分开:

struct Point
{int x;int y;
};
struct Point pt1, pt2, *ppt1;

        在定义结构体时,更常用是使用typedef定义一种新的类型,这样后面再定义新的结构体实例时,便不需要再添加关键字struct,使得我们定义的结构体更像C语言提供的其他基本数据类型一样,使用更加简洁。

typedef struct _Point
{int x;int y;
} Point;
Point pt1, pt2, *ppt1;


(2)结构体成员的访问


        当我们定义了结构体实例后,可以轻松访问包含在结构体中的各种变量。例如我们定义了以下的Point变量:
Point pt1 = {1,3}, pt2 = {4, 8}, *ppt1 = &pt1;

        使用结构体成员运算符“.”,可以按照结构体成员的名称获取其数值:

printf(“Point 1: (%d, %d), Point 2: (%d, %d)\n”, pt1.x, pt1.y, pt2.x, pt2.y);
float distance = sqrt(pow(pt1.x-pt2.x, 2) + pow(pt1.y-pt2.y, 2));
printf(“Distance of two points: %f”, distance);

        对于结构体实例,可以直接访问使用其内部成员,而使用指向结构体实例的指针也可以间接访问结构体的成员,其方法不再是使用”.”而是”->”。例如:

printf(“Point 1: (%d, %d)\n”, ppt1->x, pt1->y);

        对于使用指针访问结构体成员,其效果同使用实例访问结构体成员是一致的。在实际使用时,使用指针的情况还更加频繁。


(3)结构体成员的初始化


        从前面的程序中可以看出,结构体的初始化方法同数组的初始化方法比较类似,使用的是一对大括号所包括的、由逗号分隔的多个初始值。同初始化数组不同的是,由于结构体中的成员可以是不同的数据类型,初始化结构体的数据也可以依据结构体成员的定义类型不同而不同。结构体成员类型相同时的初始化方法已在上一节有所体现,现在我们来定义一个更复杂的结构体:
typedef struct _Student
{long long student_id;char *student_name;char student_gender;int student_age;float student_height;float student_weight;
} Student;

        定义两个结构体实例,并将其初始化:

Student Jack = {201601001, “Jack”, ‘F',17, 180.5, 190.0}, Alice = {201601002, “Alice”, ‘M’, 16, 165.0, 140.8};

        在这两个实例进行初始化时,我们根据定义的数据不同,分别包含了整型、字符串、字符型和浮点型等数据类型,并在初始化时按照不同类型对其成员变量进行了赋初值。如果我们只初始化了前面一部分的成员,那么剩余的结构体成员将被初始化为缺省值。


2、结构体的存储结构



        当我们定义了一个结构体时,有时需要对其存储结构有一定了解。通常情况下,一个结构体的实例的大小等同于结构体内部各个成员大小的总和,因为程序编译时编译器会按照成员列表的顺序一个一个给每一个成员分配内存,但是这只有在满足内存边界对其时才是这种情况。如果定义的结构体成员之间大小关系不满足边界对其要求,那么成员之间可能会出现用于填充的额外内存空间。

        例如我们定义一个结构体:
typedef struct  
{char   ChrVal1;int      IntVal;char   ChrVal2;
} MemAlign;
MemAlign ma1 = {10, ‘A’, ‘b'};

        结构体实例ma1在内存中的实际大小因机器不同而异。如果某个机器的的整型值长度诶4 Byte,且起始存储位置必须能被4整除,则这一结构在内存中的大小为12,而不是1+4+1=6。


        结构体成员的存储必须满足几个原则:
  1. 某一个结构体对象的起始存储位置必须是最大的成员大小的整数倍;
  2. 每个结构体成员相对于本结构体起始位置的偏移量必须是自身大小的整数倍;
  3. 结构体对象所占据的总大小必须是最大数据成员体积的整数倍。如果以上三个条件有任何一个不满足,都将会在相应位置补充填充字节使之满足条件。

        鉴于此,如果不涉及到程序可读性和可维护性的前提下,我们可以重新排列结构体成员的顺序来提高内存的利用效率。如MemAlign结构按照如下方式声明,将只占据8个字节,相对于上文的声明方法存储效率提高了1/3。
typedef struct  
{int      IntVal;char   ChrVal1;char   ChrVal2;
} MemAlign;



3、位段



        位段是结构体中一种特殊的成员变量类型。位段的声明方式同普通的结构体成员类似,但是位段所代表的是一个或多个位(bit)的数据。所有的位段成员必须声明为int/signed/unsigned int类型。合适地使用位段可以根据需要更高效地利用内存,但是可能会降低程序的可移植性。这主要是由于以下情况根据系统的不同而不同:
  • int位段被作为有符号还是无符号;
  • 一个位段中允许的最大bit数;高位系统上允许的程序在低位系统上可能无法运行;
  • 位段成员在内存中分配的顺序(从左到右/从右到左);
  • 体积较大的后续位段的存放位置(直接紧邻前一个/从下一个字开始);
        例如,一个使用位段的结构体声明如下:
typedef struct {unsigned ch: 7;unsigned font: 6;unsigned size: 19;
} CHAR;
CHAR ch1;


4、联合体


(1)联合体的概念和使用


        联合体是C语言中提供的另一种结构。联合体的声明方式同结构体类似,但是它的结构和行为方式同结构体完全不同。结构体中使用不同的内存位置包含了不同的成员,而联合体则是使用相同的内存位置代表不同的联合体成员,而其中内存实际的数据都是相同的。例如,定义一个简单的联合体:
union {float f;int    i;
} fi;

        该联合体实例只占用1个32位的内存空间,其中的f和i成员指代的都是同一段内存。如果我们对其中的一个成员赋值,那么联合体其他成员所指代的含义通常是用其他的格式表示内存中相同的位:

fi.f = 3.14159;
int x = fi.i;

        如果一个联合体中各成员的长度不同,那么联合体的长度是其最长的成员的长度。

(2)联合体实例如何初始化:


        初始化一个联合体实例必须采用联合体第一个成员的类型,而且该值必须位于大括号内部,例如:
union {float f;int    i;
} fi = {3.14};

这篇关于理解C语言——从小菜到大神的晋级之路(10)——结构体、联合体的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/415397

相关文章

C语言中%zu的用法解读

《C语言中%zu的用法解读》size_t是无符号整数类型,用于表示对象大小或内存操作结果,%zu是C99标准中专为size_t设计的printf占位符,避免因类型不匹配导致错误,使用%u或%d可能引发... 目录size_t 类型与 %zu 占位符%zu 的用途替代占位符的风险兼容性说明其他相关占位符验证示

C语言进阶(预处理命令详解)

《C语言进阶(预处理命令详解)》文章讲解了宏定义规范、头文件包含方式及条件编译应用,强调带参宏需加括号避免计算错误,头文件应声明函数原型以便主函数调用,条件编译通过宏定义控制代码编译,适用于测试与模块... 目录1.宏定义1.1不带参宏1.2带参宏2.头文件的包含2.1头文件中的内容2.2工程结构3.条件编

Go语言并发之通知退出机制的实现

《Go语言并发之通知退出机制的实现》本文主要介绍了Go语言并发之通知退出机制的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录1、通知退出机制1.1 进程/main函数退出1.2 通过channel退出1.3 通过cont

Java Spring的依赖注入理解及@Autowired用法示例详解

《JavaSpring的依赖注入理解及@Autowired用法示例详解》文章介绍了Spring依赖注入(DI)的概念、三种实现方式(构造器、Setter、字段注入),区分了@Autowired(注入... 目录一、什么是依赖注入(DI)?1. 定义2. 举个例子二、依赖注入的几种方式1. 构造器注入(Con

Go语言编译环境设置教程

《Go语言编译环境设置教程》Go语言支持高并发(goroutine)、自动垃圾回收,编译为跨平台二进制文件,云原生兼容且社区活跃,开发便捷,内置测试与vet工具辅助检测错误,依赖模块化管理,提升开发效... 目录Go语言优势下载 Go  配置编译环境配置 GOPROXYIDE 设置(VS Code)一些基本

深入理解Go语言中二维切片的使用

《深入理解Go语言中二维切片的使用》本文深入讲解了Go语言中二维切片的概念与应用,用于表示矩阵、表格等二维数据结构,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来一起学习学习吧... 目录引言二维切片的基本概念定义创建二维切片二维切片的操作访问元素修改元素遍历二维切片二维切片的动态调整追加行动态

Go语言中make和new的区别及说明

《Go语言中make和new的区别及说明》:本文主要介绍Go语言中make和new的区别及说明,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1 概述2 new 函数2.1 功能2.2 语法2.3 初始化案例3 make 函数3.1 功能3.2 语法3.3 初始化

从原理到实战深入理解Java 断言assert

《从原理到实战深入理解Java断言assert》本文深入解析Java断言机制,涵盖语法、工作原理、启用方式及与异常的区别,推荐用于开发阶段的条件检查与状态验证,并强调生产环境应使用参数验证工具类替代... 目录深入理解 Java 断言(assert):从原理到实战引言:为什么需要断言?一、断言基础1.1 语

Go语言中nil判断的注意事项(最新推荐)

《Go语言中nil判断的注意事项(最新推荐)》本文给大家介绍Go语言中nil判断的注意事项,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录1.接口变量的特殊行为2.nil的合法类型3.nil值的实用行为4.自定义类型与nil5.反射判断nil6.函数返回的

Go语言数据库编程GORM 的基本使用详解

《Go语言数据库编程GORM的基本使用详解》GORM是Go语言流行的ORM框架,封装database/sql,支持自动迁移、关联、事务等,提供CRUD、条件查询、钩子函数、日志等功能,简化数据库操作... 目录一、安装与初始化1. 安装 GORM 及数据库驱动2. 建立数据库连接二、定义模型结构体三、自动迁