首页 > 结构和联合--结构体内存和位段内存开辟规则

结构和联合--结构体内存和位段内存开辟规则

一.  结构的基本知识

聚合数据类型能够存储多个数据,C语言提供了两种类型的聚合数据类型,数组和结构。数组是相同的数据,结构是不同类型的数据聚合。结构也是一些值得集合,这些值成为它的成员,每个结构都有它的名字,他们是通过名字来访问的。

1.      结构声明

在结构声明时,必须列出它包含的所有成员,这个列表包括每个成员的类型和名字。

struct tag {member-list}variable-list;

结构体的相关语法:

strict {

int a;

char b;

float c;

} x;

这个声明创建了一个名字叫做x的结构体变量。它包含了三个元素。

struct {

int a;

char b;

float c;

}  y[20], *z;

这个声明中y创建了一个结构体数组,它包含了20个结构体,z是一个指向结构体的指针变量。

警告:虽然上面的两种结构的成员的数据类型一样,但是编译器会把它们当成是完全不同的数据类型,所以下面的这条语句是非法的:z=&x;

如果想定义一个指针变量使它可以指向x应该怎么定义呢,下面提供了两种方式:

我们可以在定义的时候给这个结构体加上一个标签,比如:

首先定义一个结构体变量

struct SIMPLE {

int a;

char b;

float c;

} x;

接着定义一个指向结构体变量的指针

struct SIMPLE *p;

这个时候我们就可以使用p =&x;了。

为了方便使用我们可以给这个变量定义一个新的类型

typedefstruct SIMPLE {

int a;

char b;

float c;

} SIMPLE;

这个时候,SIMPLE就是一个新的数据类型,如果我们想定义一个结构体变量,就可以使用SIMPLE x;定义一个指向结构体的指针就可以使用SIMPLE *p;这个时候我们可以使用p =&x;

2.      结构成员

结构体中可以有很多不同的类型,如:

struct COMPLEX{

float f;

int a[20];

long *lp;

struct SIMPLE s;

struct SIMPLE sa[10];

struct SIMPLE *sp;

};

这里我们不必要担心不同的结构体中的成员名相同,因为我们的结构体是不同的。

3.      结构成员的直接访问

如果我们已经知道结构变量的名字,这里我们通过操作符(.)进行访问,比如:struct COMPLEX comp;如果我们想要访问a,则可以通过以下的方式comp.a;这里我们还可以通过下面的方式访问结构体里面的结构体的一个成员(comp.s).a;这里就是我们上面说的不同的结构体中可以定义名字相同的变量。我们甚至可以使用下面更为复杂的方式进行访问((comp.sa)[4]).c;又因为下标引用和点操作符具有相同的优先级,他们的结合性都是从左到右。

4.      结构成员的间接访问

如果我们有一个指向结构体的指针,我们想通过这个指针访问这个结构体,我们可以使用间接访问操作符。举个例子,假定一个函数的参数是个指向结构的指针,如下面的原型所示:

voidfunc(struct COMPLEX *cp );

这个函数可以使用下面这个表达式来访问这个变量所指向的结构的成员f:

(*cp).f    

但是我们平时并不使用这种方式访问一个结构体的成员变量,C语言提供了一个->操作符(也称箭头操作符)使用的方式如下:

cp-> f;

5.      结构体的自引用

结构体的自引用是否合法呢,请看下面的一个例子:

structSELF_REF1 {

int a;

int c;

structSELF_REF1 b;

}

这种类型的定义是非法的,因为成员b是另一个完整的结构,其内部还将包含其他的成员b。这样就永无止境的重复下去。

再来看下面的这种定义:

structSELF_REF1 {

int a;

int c;

structSELF_REF1 *b;

}

这个声明和上面的区别在于,声明b的时候不是声明的一个结构体,而是声明了一个指向结构体的一个指针,编译器在结构体的长度确定之前就已经知道了指针的长度,所以这种自引用是合法的。(这其实就是我们在学习数据结构的时候,使用链表的一种很好的体现)

但是我们需要警惕下面的这种声明:

typedefstruct {

int a;

int c;

SELF_FER3 *b;

}SELF_FER3;

这种声明是错误的,类型名在声明的末尾才定义,所以在结构体声明的时候,它还没有被定义。

解决的方法很简单,

typedefstructSELF_FER3_TAG {

int a;

int c;

struct SELF_FER3_TAG *b;

}SELF_FER3;

6.      不完整的声明

偶尔我们需要声明相互之间存在依赖关系的结构。即是一个结构中包含了另一个结构体的一个或者多个成员。和自引用结构体一样,至少有一个结构必须在另一个结构体内部以指针的形式存在。

这样我们就必须要使用不完整的声明,它声明一个作为结构标签的标识符。然后我们可以把这个标签于成员列表联系在一起。看下面的这个例子:

structB;

struct A{

struct B*partner;

};

struct B{

struct A*partner;

}

7.      结构体的初始化

一个位于一对花括号内部、由逗号分隔的初始值列表课用于结构体的各个成员的初始化。如果初始值列表的值不够,剩余的结构体成员将使用缺省的值进行初始化。例如:

struct INIT_EX{

int a;

short b[10];

Simple c;

} x = {10,{1,2,3,4,5},{25,’x’,1.9}}

二.  结构、指针和成员

这里建议大家到《C和指针》这本书中寻找答案吧。

三.  结构体的存储分配

1.      结构体中存在对齐的原则,即开辟结构体空间的时候并不是按照每个成员的总大小开辟存储空间,(在C++中类实例化的对象也是按照结构体的方式分配存储空间的)看下面这样的一个简单的例子:

struct ALIGN {

char a;

int b;

char c;

};

假设我们的机器是32位的,这个时候首先开辟一个字节,用于存储a,然后看下一个成员是4个字节,为了对齐,前面空3个字节,这时前面的字节数就是int字节数的倍数,接着就是从第五个字节开始开辟4个字节用于存储b,然后是c,它的字节是1个字节,前面已经有八个字节正好是1的倍数,所以直接在b的后面开辟一个字节用于存储c。

优化:我们在定义结构体的时候可以把复杂的放在前面,把简单的放在后面,比如上面的结构体可以如下定义:

struct ALIGN {

int b;

char a;

char c;

};

如果我们用sizeof测试第一个结构的时候,它所测出来的结构包括了空白的结构,即为了对齐而跳过的内存空间,它也被包含进结构体中了。

如果我们想要确定某个成员的实际存储位置,这个时候我们可以使用offsetof宏(定义于stddef.h)

offsetof(type,member)

type是结构体的类型,member是需要测试的成员名,表达式的结构是一个size_t值(无符号整型),表示这个结构成员开始的位置距离整个结构开始的位置偏移的字节数。例如对第一个结构进行下面的使用,offsetof(structALIGN,b);返回值是4。

2.      结构体的内存对齐问题(对上面问题的详细解读)

内存对齐的含义:当结构体中每次放入一个新的成员的时候,它的前面需要补充的空白的字节数。即是:先对齐,再放入

(1)    对齐数:自身字节数和编译器默认字节数中的较小值(vs:8   linux:4)

(2)    第一个成员放入的时候不需要对齐

(3)    接下来的成员放入的时候,需要对齐到对齐数的整数倍(补充空白字节,使前面的所有字节数的和为对齐数的整数倍)

(4)    上面对齐完之后需要对整个结构体进行对齐,即在所有的成员开辟完空间之后,还需要加入空白的空间,以使得整个结构体对齐,方式为:结构体的总大小是最大对齐数的整数倍。结构体的对齐数是该结构体中的最大对齐数(我们可以把结构体当成是一个数据类型,这个时候结构体也是有对齐数的,但是这个对齐数不是结构体的大小,而是结构体的最大对齐数的大小)

(5)    数组拆成单个元素进行对齐

 

修改对齐数的大小,在源文件的一开始处输入:#pragma(4),这条语句的作用是改变系统默认的对齐数,一般往小的改。

四.  作为函数参数的结构

把结构作为参数传递给一个函数是合法的,但是这种做法往往是并不适宜的。

这里我们定义一个结构体

typedefstruct

{

}Transaction;

这个结构体可能很大,我们编写一个传递结构体的一个函数

void print(Transaction trans)

{

}

如果我们在主函数中定义了一个定义了一个结构体Transaction current_trans;

然后调用函数print(current_trans);我们知道C语言参数传址调用方式要求把参数的一份拷贝传递给函数。如果这个结构体太大,我们必须把很大一个字节复制到堆栈中,以后再丢弃,这样很影响效率。

再来看下面的函数

void print(Transaction *trans)

{

}

调用的时候使用

print(¤t_trans);这时我们传递的是一个指针,指针传递的时候要比直接把结构体传递过去小的多,这样压栈的效率就高,传递指针访问结构体也是需要付出一定的代价的,就是我们在函数中要使用间接访问来访问结构体成员。但这对我们来说影响不大。

使用指针传递的时候还有一个缺陷就是,函数内可以对主函数中的结构体进行修改,如果我们想要防止在外部函数修改结构体,我们可以在函数中使用const进行修饰。

还有一点需要主要的是,结构体在传参的时候不会退化成地址,只会发生原始的拷贝,这也是我们使用指针传参的一个原因。

综上我们建议在使用结构体传递参数的时候,可以使用地址传递的方法

五.  位段

位段的声明和任何普通的结构体成员相同,但是有两个例外。首先位段成员必须声明为int、signed int或者是unsigned int类型。其次,在成员名的后面是一个冒号和一个整数,这个整数指定该位段所占用的位的数目。

这里建议用signed 或者unsigned进行声明,因为如果直接声明为int,它究竟是有符号的还是无符号的整型,这是由编译器而定的,可能会给我们造成不必要的麻烦。

 这里通过程序来给大家说明一下位段的内存开辟规则,这里是在结构体内存开辟的规则基础之上的

#include
#includetypedef struct Stu
{char name : 2;char age : 2;char sex : 5;
}Stu;int main()
{Stu student;printf("%d
", sizeof(student));system("pause");return 0;
}


这里的结果是多少呢,答案是2,这里位段的作用就是想充分的利用内存空间,首先开辟一个字节大小即是八个位,用于存储name,但是这里name只占用了两个位,然后age需要占用两个位,回去看,刚刚开辟的字节还有六个位,于是拿出两个位给了age,接着是sex,它需要五个位,但是刚刚开辟的字节只有四个位了,不够五个位,于是又开辟了一个字节,拿出其中的五个位存放sex,第一次开辟的一个字节剩余的四个位就浪费掉了。这里我们再次把结构体给改一下,再来看一下测试的结果是多少呢

typedef struct Stu
{char name : 1;int age : 2;char sex : 5;char address : 3;int add :1;
}Stu;


这里的测试结果是16,为什么呢,这是有原因的对吧,首先开辟一个字节给name,name占用了其中的一位,剩余七个位,然后遇到age,大家请注意age的类型是int型的,虽然只需要用到一个位,但是上面剩余的七个位不能给这个age,于是编译器开辟了四个字节,大家这里还容易犯的一个错误是,认为这四个字节直接在刚刚开辟的一个字节后面开辟,这是错误的。正确的方式是,需要对齐,前面开辟了一个字节,这里需要对齐到四,所以跳过了剩下了的三个字节,开辟了四个字节,这样之后就开辟了八个字节,然后下面的方式一样,在开辟一个字节用于存放sex和address,然后跳过三个字节,在开辟一个字节用于存放add,这样的结果就是16个字节,这里大家可以结合上文中的结构体对齐问题好好的探索一下。

提示

注重可移植性的程序应该注意,由于下面的这些实现相关的依赖性,位段在不同的系统中可能有不同的结果。

六.  联合体

联合体的所有成员引用的是内存中的相同位置,当我们想在不同的时候把不同的东西存储在同一个位置时,就可以使用联合体。联合体也需要对齐,它的对齐方式要适合所有的成员。

1.      变体记录

2.      联合的初始化

联合变量可以被初始化,但是这个初始值必须是联合第一个成员的类型,而且它必须位于一对花括号里面。例如

union{

int a;

float b;

char c;

} x = {5};

把x.a初始化为5.

我们不能把这个类量初始化为一个浮点值或者字符值。如果给出的初始值是任何其他类型,它就会转换(如果有可能的话)为一个整数并赋值给x.a。

 

 

 

 

 

 

 

更多相关:

  •   在前面认识C中的结构体中我介绍了结构体的基础知识,下面通过这段代码来回顾一下: 1 #include 2 #define LEN 20 3 4 struct Student{ //定义结构体 5 char name[LEN]; 6 char address[...

  • Hibernate 配置参数hibernate.hbm2ddl.auto  Hibernate中的配置文件:              参数说明:   valid...

  • 原文出处: 韩昊    1 2 3 4 5 6 7 8 9 10 作 者:韩 昊 知 乎:Heinrich 微 博:@花生油工人 知乎专栏:与时间无关的故事   谨以此文献给大连海事大学的吴楠老师,柳晓鸣老师,王新年老师以及张晶泊老师。   转载的同学请保留上面这句话,谢谢。如果还能保留文章来源就更感激不尽了。 我保证这篇文章...

  • 原文出处: 韩昊   我保证这篇文章和你以前看过的所有文章都不同,这是 2012 年还在果壳的时候写的,但是当时没有来得及写完就出国了……于是拖了两年,嗯,我是拖延症患者…… 这篇文章的核心思想就是: 要让读者在不看任何数学公式的情况下理解傅里叶分析。 傅里叶分析不仅仅是一个数学工具,更是一种可以彻底颠覆一个人以前世界观的思维...

  • 很多Linux高手都喜欢使用screen命令,screen命令可以使你轻松地使用一个终端控制其他终端。尽管screen本身是一个非常有用的工具,byobu作为screen的增强版本,比screen更加好用而且美观,并且提供有用的信息和快捷的热键。 想象一下这样一个场景:你通过Secure Shell(ssh)链接到一个服务器,并...

  • NarrowbandPrimary Synchronization Signal时域位置每1个SFN存在一个NPSSSFNSubframeSymbol长度每个SFN5最后11个symbol11个symbols频域位置NB-IOT下行带宽固定180kHz,一个PRB,12个子载波。...

  •  [h1]反斜杠只能够阻止一个字符  [h2]位于键盘的左上角,和~公用一个键。...

  • 英语的重要性,毋庸置疑!尤其对广大职场人士,掌握英语意味着就多了一项竞争的技能。那,对于我们成人来说,时间是最宝贵的。如何短时间内在英语方面有所突破,这是我们最关心的事情。英语学习,到底有没有捷径可以走,是否可以速成?周老师在这里明确告诉大家,英语学习,没有绝对的捷径走,但是可以少走弯路。十多年的教学经验告诉我们,成功的学习方法可以借...

  • 展开全部 其实IDLE提供了一个显32313133353236313431303231363533e78988e69d8331333365663438示所有行和所有字符的功能。 我们打开IDLE shell或者IDLE编辑器,可以看到左下角有个Ln和Col,事实上,Ln是当前光标所在行,Col是当前光标所在列。 我们如果想得到文件代码...

  • 前言[1]从 Main 方法说起[2]走进 Tomcat 内部[3]总结[4]《Java 2019 超神之路》《Dubbo 实现原理与源码解析 —— 精品合集》《Spring 实现原理与源码解析 —— 精品合集》《MyBatis 实现原理与源码解析 —— 精品合集》《Spring MVC 实现原理与源码解析 —— 精品合集》《Spri...

  • 【本文摘要】【注】本文所述内容为学习Yjango《学习观》相关视频之后的总结,观点归Yjango所有,本文仅作为学习之用。阅读本节,会让你对英语这类运动类知识的学习豁然开朗,你会知道英语学习方面,我们的症结所在。学习英语这类运动类知识,需要把握四个原则第一,不要用主动意识。第二,关注于端对端第三,输入输出符合实际情况第四,通过多个例子...

  • 点云PCL免费知识星球,点云论文速读。文章:RGB-D SLAM with Structural Regularities作者:Yanyan Li , Raza Yunus , Nikolas Brasch , Nassir Navab and Federico Tombari编译:点云PCL代码:https://github.co...