《C和指针》笔记35:结构体

本文整理一下结构体的相关知识,记录是为了更好地加深理解。

1. 结构体声明

下面两个声明语句:

struct {int a;char b;float c;
} x;struct {int a;char b;float c;
} y[20], *z;

这两个声明被编译器当作两种截然不同的类型,即使它们的成员列表完全相同。因此,变量y和z的类型和x的类型不同,所以下面这条语句是非法的。

z=&x;

为了能让上面的表达式不报错,我们需要这样书写:

struct SIMPLE {int a;char b;float c;
};

这个声明把标签SIMPLE和这个成员列表联系在一起。该声明并没有提供变量列表,所以它并未创建任何变量。

标签标识了一种模式,用于声明未来的变量,但无论是标签还是模式本身都不是变量。

struct SIMPLE x;
struct SIMPLE y[20], *z;

这些声明使用标签来创建变量。它们创建和最初两个例子一样的变量,但存在一个重要的区别——现在x、y和z都是同一种类型的结构变量。

声明结构时可以使用的另一种良好技巧是用typedef创建一种新的类型,如下面的例子所示。

typedef struct {int a;char b;float c;
} SIMPLE ;

这个技巧和声明一个结构标签的效果几乎相同。区别在于Simple现在是个类型名而不是个结构标签,所以后续的声明可能像下面这个样子:

Simple x;
Simple y[20], *z;

如果你想在多个源文件中使用同一种类型的结构,你应该把标签声明或typedef形式的声明放在一个头文件中。当源文件需要这个声明时可以使用#include指令把那个头文件包含进来。

2. 结构体成员

一个结构体的成员的名字可以和其他结构的成员的名字相同:

struct COMPLEX {float f;int a[20];long *lp;struct SIMPLE s;struct SIMPLE sa[10];struct SIMPLE *sp;
};

一个结构体的成员的名字可以和其他结构体的成员的名字相同,所以这个结构体的成员a并不会与struct SIMPLE s的成员a冲突。

访问结构体成员可以用.(操作对象是结构体)和->(操作对象是结构体指针)。

struct COMPLEX comp;
comp.f
comp.a
comp.sstruct COMPLEX *cp;
cp->f
cp->a
cp->s

3. 结构体的自引用

struct SELF_REF1{int a;struct SELF_REF1 b;int c;
};

这种类型的自引用是非法的,因为成员b是另一个完整的结构,其内部还将包含它自己的成员b。这第2个成员又是另一个完整的结构,它还将包括它自己的成员b。这样重复下去永无止境。这有点像永远不会终止的递归程序。但下面这个声明却是合法的,你能看出其中的区别吗?

struct SELF_REF2{int a;struct SELF_REF2 *b;int c;
};

这个声明和前面那个声明的区别在于b现在是一个指针而不是结构体。编译器在结构的长度确定之前就已经知道指针的长度,所以这种类型的自引用是合法的。更加高级的数据结构体,如链表和树,都是用这种技巧实现的。每个结构体指向链表的下一个元素或树的下一个分枝。

警惕下面这个陷阱:

typedef struct {int a;SELF_REF3 *b;int c;
} SELF_REF3;

这个声明的目的是为这个结构体创建类型名SELF_REF3。但是,它失败了。类型名直到声明的末尾才定义,所以在结构声明的内部它尚未定义。解决方案是定义一个结构标签来声明b,如下所示:

typedef struct SELF_REF3_TAG {int a;struct SELF_REF3_TAG *b;int c;
} SELF_REF3;

4. 结构体不完整声明

有这样一种情况:有多个结构体,其中一个结构包含了另一个结构的一个或多个成员。和自引用结构一样,至少有一个结构必须在另一个结构内部以指针的形式存在。问题在于声明部分:如果每个结构都引用了其他结构的标签,哪个结构应该首先声明呢?

这个问题的解决方案是使用不完整声明,它声明一个作为结构标签的标识符。然后,我们可以把这个标签用在不需要知道这个结构的长度的声明中,如声明指向这个结构的指针。接下来的声明把这个标签与成员列表联系在一起。

考虑下面这个例子,两个不同类型的结构内部都有一个指向另一个结构的指针。

struct B;
struct A {struct B *partner;
};
struct B {struct A *partner;
};

在A的成员列表中需要标签B的不完整的声明。一旦A被声明之后,B的成员列表也可以被声明。

5. 结构体的初始化

结构的初始化方式和数组的初始化很相似。一个位于一对花括号内部、由逗号分隔的初始值列表可用于结构各个成员的初始化。这些值根据结构成员列表的顺序写出。如果初始列表的值不够,剩余的结构成员将使用缺省值进行初始化

结构中如果包含数组或结构成员,其初始化方式类似于多维数组的初始化。一个完整的聚合类型成员的初始值列表可以嵌套于结构的初始值列表内部。这里有一个例子:

struct INIT_EX {int a;short b[10];Simple c;
} x = {10,{1,2,3,4,5},{25,'x',1.9}
}

6. 结构体和指针

这里举了一个例子,我们先定义一个复杂的结构体

typedef struct {int a;short b[2];
} Ex2;
typedef struct EX {int a;char b[3];Ex2 c;struct Ex *d;
} Ex;

Ex的结构可以这样表示:

在这里插入图片描述
初始化:

Ex x = { 10, "Hi", { 5, { -1, 25 } }, 0 };
Ex *px = &x;

在这里插入图片描述

  • int *pi = &px->a;

->的优先级要高于&,所以下面对结构体内部的f取地址不用加括号:

int *pi = &px->a;

在这里插入图片描述

  • px->b

下面的表达式的值是一个指针常量,因为b是一个数组。这个表达式不是一个合法的左值。它的右值是有意义的:

px->b

在这里插入图片描述

  • px->b[1]

如果我们对这个表达式执行间接访问操作,它将访问数组的第1个元素。使用下标引用或指针运算,我们还可以访问数组的其他元素。下面的表达式访问数组的第2个元素:

px->b[1]

在这里插入图片描述

  • px->c

为了访问本身也是结构的成员c,我们可以使用下面的表达式。它的左值是整个结构:

px->c

在这里插入图片描述

  • px->c.a

这个表达式可以使用点操作符访问c的特定成员。例如下面表达式的右值是有意义的,这个表达式既包含了点操作符,也包含了箭头操作符。之所以使用箭头操作符,是因为px并不是一个结构体,而是一个指向结构体的指针。接下来之所以要使用点操作符是因为px->s的结果并不是一个指针,而是一个结构体。

px->c.a

在这里插入图片描述

  • *px->c.b

这里有一个更为复杂的表达式:

*px->c.b

它有三个操作符,首先执行的是箭头操作符。px->c的结果是结构c。在表达式中增加.b访问结构c的成员b。b是一个数组,所以px->b.c的结果是一个(常量)指针,它指向数组的第1个元素。最后对这个指针执行间接访问,所以表达式的最终结果是数组的第1个元素。(*的优先级是最后的)这个表达式可以图解如下:

在这里插入图片描述

  • px->d

来看这样一个表达式:

px->d

表达式的结果正如你所料——它的右值是0,它的左值是它本身的内存位置

  • *px->d

如果对上面的式子进行解引用呢?

`*px->d`

这里间接访问操作符作用于成员d所存储的指针值。但d包含了一个NULL指针,所以它不指向任何东西。对一个NULL指针进行解引用操作是个错误,但正如我们以前讨论的那样,有些环境不会在运行时捕捉到这个错误。在这些机器上,程序将访问内存位置零的内容,把它也当作是结构体成员之一,如果系统未发现错误,它还将高高兴兴地继续下去。这个例子说明了对指针进行解引用操作之前检查一下它是否有效是非常重要的。

我们创建另一个结构,并把x.d设置为指向它。

Ex y;
x.d = &y;

现在我们可以对表达式*px->d求值。成员d指向一个结构,所以对它执行间接访问操作的结果是整个结构。这个新的结构并没有显式地初始化,所以在图中并没有显示它的成员的值。

正如你可能预料的那样,这个新结构的成员可以通过在表达式中增加更多的操作符进行访问。我们使用箭头操作符,因为d是一个指向结构的指针。下面这些表达式是执行什么任务的呢?

在这里插入图片描述

  • px->d->c.b[1]
px->d->c.b[1]

在这里插入图片描述

7. 结构体的存储

编译器按照成员列表的顺序一个接一个地给每个成员分配内存。只有当存储成员时需要满足正确的边界对齐要求时,成员之间才可能出现用于填充的额外内存空间。为了说明这一点,考虑下面这个结构:

struct ALIGN {char a;int b;char c;
};

如果某个机器的整型值长度为4个字节,并且它的起始存储位置必须能够被4整除,那么这一个结构体在内存中的存储将如下所示:

在这里插入图片描述

系统禁止编译器在一个结构体的起始位置跳过几个字节来满足边界对齐要求,因此所有结构体的起始存储位置必须是结构体中边界要求最严格的数据类型所要求的位置。因此,成员a(最左边的那个方框)必须存储于一个能够被4整除的地址。结构体的下一个成员是一个整型值,所以它必须跳过3个字节(用灰色显示)到达合适的边界才能存储。在整型值之后是最后一个字符。

如果声明了相同类型的第2个变量,它的起始存储位置也必须满足4这个边界,所以第1个结构的后面还要再跳过3个字节才能存储第2个结构。因此,每个结构体将占据12个字节的内存空间但实际只使用其中的6个,这个利用率可不是很出色。

可以在声明中对结构的成员列表重新排列,让那些对边界要求最严格的成员首先出现,对边界要求最弱的成员最后出现。这种做法可以最大限度地减少因边界对齐而带来的空间损失。例如,下面这个结构体

struct ALIGN2 {int b;char a;char c;
};

所包含的成员和前面那个结构一样,但它只占用8个字节的空间,节省了33%。两个字符可以紧挨着存储,所以只有结构最后面需要跳过的两个字节才被浪费。

当程序将创建几百个甚至几千个结构时,减少内存浪费的要求就比程序的可读性更为急迫。在这种情况下,在声明中增加注释可能避免可读性方面的损失。

sizeof操作符能够得出一个结构的整体长度,包括因边界对齐而跳过的那些字节。如果你必须确定结构某个成员的实际位置,应该考虑边界对齐因素,可以使用offsetof宏(定义于stddef.h)。它的使用格式是:

offsetof( type, member )

type就是结构体的类型,member就是你需要的那个成员名。表达式的结果是一个size_t值,表示这个指定成员开始存储的位置距离结构开始存储的位置偏移几个字节。例如,对前面那个声明而言,

offsetof( struct ALIGN, b )

它的值是4。

8. 作为函数参数的结构体

我们可以给函数传递结构体:

typedef struct {char product [PRODUCT_SIZE];int quantity;float unit_price;float total_amount;
} Transaction;

我们可以将整个结构体作为参数传递:

void print_receipt(Transaction trans)
{printf("%s\n", trans.product);printf("%d @ %.2f total %.2f\n", trans.quantity, trans.unit_price, trans.total_amount);
}

如果current_trans是一个Transaction结构,我们可以像下面这样调用函数:

print_receipt( current_trans );

这个方法能够产生正确的结果,但它的效率很低,因为C语言的参数传值调用方式要求把参数的一份拷贝传递给函数。如果PRODUCT_SIZE为20,而且在我们使用的机器上整型和浮点型都占4个字节,那么这个结构将占据32个字节的空间。要想把它作为参数进行传递,我们必须把32个字节复制到堆栈中,以后再丢弃。

把前面那个函数和下面这个进行比较:

void print_receipt(Transaction *trans)
{printf("%s\n", trans->product);printf("%d @ %.2f total %.2f\n", trans->quantity, trans->unit_price, trans->total_amount);
}

这个函数可以像下面这样进行调用:

print_receipt( &current_trans );

这次传递给函数的是一个指向结构的指针。指针比整个结构要小得多,所以把它压到堆栈上效率能提高很多。传递指针另外需要付出的代价是我们必须在函数中使用间接访问来访问结构的成员。结构越大,把指向它的指针传递给函数的效率就越高。

可以把参数声明为寄存器变量,从而进一步提高指针传递方案的效率。在有些机器上,这种声明在函数的起始部分还需要一条额外的指令,用于把堆栈中的参数(参数先传递给堆栈)复制到寄存器,供函数使用。但是,如果函数对这个指针的间接访问次数超过两三次,那么使用这种方法所节省的时间将远远高于一条额外指令所花费的时
间。

向函数传递指针的缺陷在于函数现在可以对调用程序的结构变量进行修改。如果我们不希望如此,可以在函数中使用const关键字来防止这类修改。经过这两个修改之后,函数的原型写作:

void print_receipt( register Transaction const *trans );

参考:

  1. 《C和指针》

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

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

相关文章

批量拍摄剪辑短视频,这几个实用又简单的拍摄技巧可以收藏学习

现在越来越多的朋友都当起了视频博主,自己来拍短视频,但说来简单,到了拍摄的时候,却有些无从下手,直接拿起手机就拍吗?是不是太平淡了?可自己又不会专业的拍摄技巧啊~ 今天来分享4个简单的拍摄…

Guava-RateLimiter详解

简介: 常用的限流算法有漏桶算法和令牌桶算法,guava的RateLimiter使用的是令牌桶算法,也就是以固定的频率向桶中放入令牌,例如一秒钟10枚令牌,实际业务在每次响应请求之前都从桶中获取令牌,只有取到令牌的请…

Apache ActiveMQ RCE漏洞复现(CNVD-2023-69477)

0x01 产品简介 ActiveMQ是一个开源的消息代理和集成模式服务器,它支持Java消息服务(JMS) API。它是Apache Software Foundation下的一个项目,用于实现消息中间件,帮助不同的应用程序或系统之间进行通信。 0x02 漏洞概述 Apache ActiveMQ 中存…

计算机视觉 计算机视觉识别是什么?

计算机视觉识别(Computer Vision Recognition)是计算机科学和人工智能领域中的一个重要分支,它致力于使计算机系统能够模拟和理解人类视觉的过程,从而能够自动识别、分析和理解图像或视频中的内容。这一领域的发展旨在让计算机具备…

系列三十五、Spring AOP失效原因以及解决方式

一、Spring AOP失效原因 (1)内部调用不会触发AOP; (2)方法是private修饰的,AOP会失效; 解决方法:改成public (3)目标类没有配置为bean&#xf…

数据结构——线性表①(顺序表)

一、线性表定义 线性表是一种数据结构,它是由n个具有相同数据类型的数据元素a1,a2,…,an组成的有限序列。 其中,除第一个元素a1外,每一个元素有且只有一个直接前驱元素,除了最后一个元素an外,每一个元素有且只有一个…

云原生-AWS EC2使用、安全性及国内厂商对比

目录 什么是EC2启动一个EC2实例连接一个实例控制台ssh Security groups规则默认安全组与自定义安全组 安全性操作系统安全密钥泄漏部署应用安全元数据造成SSRF漏洞出现时敏感信息泄漏网络设置错误 厂商对比参考 本文通过实操,介绍了EC2的基本使用,并在功…

关于ABB 机器人多任务的建立

关于ABB 机器人多任务的建立.需要实时监控某一区域,或者某一信号,或者计件到达某一数量机器人自动停止报警,显示到示教器上,多任务可以实现,类似发那科机器人后台逻辑指令 当软件选项漏选或者少选可以选择修改选项&…

近期面试小结

作者:究极逮虾户 最近面试了不少的公司,行情整体来说还是非常差的,如果没有必要不建议大家裸辞,另外就不总结面试的题目了。这次打算着重从项目经验上来给大家讨论下,我觉得这部分可能才是面试中得分比重比较大的部分&…

C的缺陷和陷阱读书笔记

词法陷阱 1、if语句的特殊用法 1、if(x>max) maxx;2、if(x>max?x;max) //条件表达式,是执行第二个,否执行第三个3、if(x>max); //条件成立后执行——空语句4、if((fopen(arg v[i],0))>0) //open函数执行,成功返回后面的0&a…

处理大数据的基础架构,OLTP和OLAP的区别,数据库与Hadoop、Spark、Hive和Flink大数据技术

处理大数据的基础架构,OLTP和OLAP的区别,数据库与Hadoop、Spark、Hive和Flink大数据技术 2022找工作是学历、能力和运气的超强结合体,遇到寒冬,大厂不招人,可能很多算法学生都得去找开发,测开 测开的话&am…

前端的简单介绍

前端核心的分析 CSS语法不够强大,比如无法嵌套书写,倒是模块化开发中需要书写很多重复的选择器 没有变量和合理的样式复用机制,使逻辑上相关的属性值必须字面量的心事重复的输出,导致难以维护 CSS预处理器,减少代码的笨重&#…

DNS 域名解析系统

文章目录 前言什么是 DNS 域名解析系统为什么需要 DNS 域名解析DNS 是如何发展的hosts 文件维护域名和IP的映射关系DNS 系统(服务器)DNS 镜像系统 前言 前面为大家分享了关于计算机网络中应用层——自定义协议、传输层——UDP、TCP 协议、网络层——IP协…

pgsql 分组查询,每组取10条

需求: 按照表的字段分组,然后每组取10条结果,返回即可 sql 如下: SELECT* FROM (SELECT chk_id,feature_id,task_id, ROW_NUMBER () OVER (PARTITION BY chk_id ORDER BY chk_id) AS row_num FROM ics_check_report WHERE task…

肺癌不再是老年病:33岁作家的离世引发关注,有这些情况的请注意

近期,90后网络小说家七月新番和26岁男艺人蒋某某因肺癌去世,引发关注。他们都没有吸烟习惯,那么他们为什么会得肺癌呢?浙大二院呼吸内科副主任医师兰芬说,现在年轻人熬夜、加班导致身体过劳,在劳累情况下身…

Vue Router使用VueUse更改标签页名称的工具函数

进入正题 安装 npm i vueuse/core or pnpm i vueuse/core or yarn add vueuse/corerouter/helper.js import { useTitle } from vueuse/coreexport const usePageTitle (to) > {const projectTitle import.meta.env.VITE_APP_TITLE // 将可变名抽出到 .env 内配置cons…

element-plus走马灯不显示

问题描述 依赖正确&#xff0c;代码用法正确&#xff0c;但是element-plu走马灯就是不显示&#xff01;&#xff01; <div class"content"><el-carousel height"150px" width"200px"><el-carousel-item v-for"item in 4&qu…

Windows详细安装和彻底删除RabbitMQ图文流程

RabbiitMQ简介 RabbitMQ是实现了高级消息队列协议&#xff08;AMQP&#xff1a;Advanced Message Queue Protocol&#xff09;的开源消息代理软件&#xff08;亦称面向消息的中间件&#xff09;。RabbitMQ服务器是用Erlang语言编写的&#xff0c;而聚类和故障转移是构建在开放…

基于springboot框架的房屋租赁租房系统

功能如图所示 摘要 房屋租赁租房系统是一个基于Spring Boot框架开发的全新房地产管理平台&#xff0c;它旨在简化租房流程&#xff0c;提供房东和租户之间的高效互动&#xff0c;以满足现代社会对房屋租赁的不断增长的需求。该系统充分利用了Spring Boot的强大特性&#xff0c;…

arcpy.describe

描述 根据输入的数据&#xff0c;返回输入数据的属性 arcpy.da.Describe与arcpy.Describe返回的数据是一样的但是返回的的类型不一样&#xff0c;arcpy.da.Describe返回的是字典&#xff0c;arcpy.Describe返回的是string 如果要访问数据对象不存在的属性&#xff0c;将返回…