C语言的结构体你了解吗

目录
  • 结构体内存对齐
  • 结构体传参
  • 结构体实现位段
    • 什么是位段
    • 位段在内存中的存储
    • 位段的问题
  • 总结

结构体内存对齐

当我们创建一个结构体变量时,内存就会开辟一块空间,那么在创建结构体变量时内存到底是怎么开辟空间的呢?会开辟多大的空间呢?我们来看一下下面的代码

struct S
{
	int i;
	char c;
	char b;
};
struct G
{

	char c;
	int i;
	char b;
};
int main()
{
	struct S u;
	struct G g;
	printf("%d\n", sizeof(u));
	printf("%d\n", sizeof(g));
	return 0;
}

在这个代码中,我们创建了两个结构体类型,并用这两个类型创建了两个结构体变量,当变量被创建的时候,内存就会为这些变量开辟空间,而在这两个变量中,我们都创建了两个char类型的成员和一个int类型的成员,不同的是这三个成员的排列顺序不同,然后我们来打印一下这两个变量所占的字节,来看一下有什么不同

我们发现,这两个结构体变量虽然里面的元素类型一样,但是他们的大小并不一样,而他们之间的区别就是结构体成员的排列顺序不同,所以我们可以知道,结构体成员的排列顺序是会影响结构体的大小的,那么他到底是怎么影响的呢,这就和结构体的创建规则有关。

结构体在创建时要进行内存对齐,对齐的规则是:

1.第一个成员在与结构体变量偏移量为0的地址处。

2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的值为8

3.结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。

4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

下面我们来根据对齐规则来算一个下上面的两个结构体的大小:

struct S
{
	int i;
	char c;
	char b;
};

首先,我们先看struct S,第一个成员是int类型,占4个字节,它的对齐数就是4,第二个是char类型,占一个字节,对齐数是1,一个放在1的整数位上,也就是可以接着往下放,那就是在int的4个字节后面紧跟着来存放,第三个也是char类型,对齐数也是1,那么也可以接着放在后面,三个成员的最大对齐数是4,根据对齐规则,结构体的大小是最大对齐数的整数倍,而我们刚刚算完这三个占的大小已经是6个字节了,所以为了对齐,我们要浪费两个字节的空间,使这个结构体的大小为8个字节,是最大对齐数的2倍。

struct G
{

	char c;
	int i;
	char b;
};

我们再来看第二个,这里,我们的第一个元素变成了char类型,占一个字节,但是第二个成员是int类型,对齐数为4,根据对齐规则要对齐到4的整数倍处,那就只能放在结构体开始储存的位置往后4个字节的地址处,加上它本身占4个字节,这时候我们在内存中就使用了8个字节,然后在存入第三个成员char类型,对齐数是1,可以直接在后面存放,最大对齐数还是4,但是我们已经使用了9个字节,所以这时候的大小应该是12个字节,4的3倍。

以上的结果也是符合刚刚的运行结果的。

下面我们来看一个结构体里嵌套了一个结构体时的例子:

struct G
{

	char c;
	int i;
	char b;
};
struct S
{
	char a;
	int i;
	struct G c;

};

我们在结构体struct S中嵌套了一个结构体struct G,根据对齐规则,我们嵌套的结构体一个对齐到自己的最大对齐数的整数倍上,那就是4的整数倍,而第一个成员是char,第二个是int,int对齐到4的整数倍上,那前两个成员就占了8个字节,而struct G正好对齐到int的后面,大小我们刚刚算出来是12个字节,加起来就是20个字节,下面我们来验证一下

与运行结果相同,那么我们的计算就没有问题。

那么我们结构体在储存时为什么要进行内存对齐呢?主要有以下两个原因。

1.平台原因(移植原因):

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

2.性能原因:

数据结构(尤其是栈)应该尽可能地在自然边界上对齐。

原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

跟主要的可能是第二种原因,为了增加处理器的访问效率,我们选择了用浪费空间的方式,来使我们的处理器可以一次性的访问到我们需要的元素,用空间来换时间。

根据我们的对齐规则我们也可以发现,如果我们想要在创建结构体变量时节省空间,我们应该尽量让小的成员集中在一起,这样可以减少空间的浪费,节省空间。

在规则中我们看到,计算每个成员的对齐数时要选择默认对齐数与该成员大小的较小值,在vs编译器中这个默认对齐数是8,而在有的编译器中没有默认对齐数,如gcc编译器的默认对齐数就是成员自身的大小,当然,这个默认对齐数也是可以该的,而我们如何来修改默认对齐数呢,我们看下面的代码

struct H
{
	char c1;
	double d;
	char c2;
};
int main()
{
	struct H h;
	printf("%d\n", sizeof(h));
	return 0;
}

根据我们的对齐规则,这个结构体的大小应该是24个字节,我们来运行一下看看结果

如果我们要把它的默认对齐数改为4,那么我们再来重新计算一下,首先第一个char类型占一个字节,然后double类型占8个字节,但是对齐数为4,对齐到4的倍数,就可以在第4个字节的位置开始存储,这时候前两个只占12个字节,最后一个char占一个字节,最大对齐数为4,大小为4的倍数,应该为16,我们来验证一下

根据运行的结果我们可以看到,确实是改变了默认对齐数

修改默认对齐数的方法就是在结构体类型前加上#pragma pack(n),n表示修改后的默认对齐数的值(一般都是2的次方数,当改为1时,表示不存在对齐),在结构体类型的后面加上#pragma pack()表示取消修改。

#pragma pack(4)
struct H
{
	char c1;
	double d;
	char c2;
};
#pragma pack()

如上面的代码,表示我们只把#pragma pack(4)~#pragma pack()之间的结构体类型的默认对齐数改为了4。

结构体传参

在学习函数的时候我们曾经学到,函数在调用时有两种方法,一种是传值调用,一种是传址调用C语言–函数,我们来看下面的代码。

struct S {
	int data[1000];
	int num;
};

//结构体传参
void print1(struct S s)
{
	printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{
	printf("%d\n", ps->num);
}
int main()
{
	struct S s = { {1,2,3,4}, 1000 };
	print1(s); //传结构体
	print2(&s); //传地址
	return 0;
}

在这个代码中,我们的print函数的目的是打印结构体变量中的一个成员,print1传参时传的就是结构体变量的值,print2传的就是结构体变量的地址,他们的不同点在于传值的时候,我们的形参是实参的一份临时拷贝,也就是说,当我把结构体变量的以传值的方式传参给函数时,当我们调用这个函数,内存就会把这个结构体拷贝一份,当我们的结构体变量比较小时还好,但是当这个结构体变量里的成员非常多,占据的空间非常大时,就会导致系统开销比较大,性能下降,而如果我们使用传址的方式,我们一个地址的大小也就4或者8个字节,就没有上面的问题,所以在结构体传参时,我们尽量要传地址,既可以节省时间,也可以节省空间。

结构体实现位段

什么是位段

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

1.位段的成员必须是 char、int、unsigned int 或signed int 。
2.位段的成员名后边有一个冒号和一个数字。

struct A
{
	int a : 2;
	int b : 5;
	int c : 10;
	int d : 30;
};

这里的A就是位段,每个成员后面的数字代表他们需要的二进制位。

位段在内存中的存储

1.位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型

2.位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。

3.位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

我们来举个例子

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

我们来研究这个位段在内存中是如何储存的

首先,我们看成员a,它占3个二进制位,是一个char类型,我们需要创建出一个字节的空间,也就是8个二进制位,a要走了3个,我们假设在存储是,我们存储的顺序是与小段存储的方式类似,也是从右向左存储的,那么a在内存中存储的位置应该是后面的三个二进制位

低地址->高地址
  ********
       aaa

假设一个代表内存中的一个二进制位,这八个代表一个字节,这3个a上面对应的*就是a在这一个字节中所占的空间。
b需要4个二进制位,而我们刚刚创建的一个字节中还有5个二进制位,所以我们把b的4个二进制位放在a的后面。

低地址->高地址
  ********
   bbbbaaa

这就是a与b在内存中的存储情况,而这是c需要个二进制位,我们只剩下一个二进制位了,而c又是一个char类型,所以我们需要再创建一个字节的空间,而当我们创建好了新的空间,c的个二进制位应该怎么储存呢,我们是接着第一个字节把剩下的二进制位用完还是在我们新创建的字节里重新储存呢,假设内存在存储时选择直接浪费掉哪个二进制位,在新的空间进行储存,这时内存中的存储分布应该是这样

低地址->高地址
  ********  ********
   bbbbaaa     ccccc

存储d时,d需要4个二进制位,第二个字节中的二进制位也不够,所以我们再创建一个字节,存储d

低地址->高地址
  ********  ********  ********
   bbbbaaa     ccccc      dddd

如果我们的假设没错的话,这一个就是a,b,c,d这4个成员在内存中的储存位置,然后我们又对这4个成员进行了赋值

a=10=>1010(二进制数)=>010(3个二进制位)

b=12=>1100(二进制数)=>1100(4个二进制位)

c=3=>11(二进制数)=>00011(3个二进制位)

d=4=>100(二进制数)=>0100(4个二进制位)

当我们用上面的数据对我们刚刚的位段进行赋值,那么这个位段在内存中存储的内容应该是这样的

低地址->高地址
  0bbbbaaa  000ccccc  0000dddd
  01100010  00000011  00000100(二进制)
     62        03        04   (十六进制)

根据我们的计算,我们发现,如果位段按照我们刚刚的假设来存储,那么在内存中存储的内容应该是62 03 03,那我们现在来调试一下看看

结果与我们推断的一样,那么就说明当前的编译器位段的存储是按照我们假设的方式来存储的。

位段的问题

但是位段在C语言中的规定又有许多不确定的地方,

1.int 位段被当成有符号数还是无符号数是不确定的。

2.位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机 器会出问题。

3.位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。

4.当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是 舍弃剩余的位还是利用,这是不确定的。

不同的平台可能对上述的问题有不同的规定,所以位段是不能跨平台的

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

位段的使用环境是在我们传输一个数据包时,可以使用位段使数据包在不能压缩的情况下,所占的空间最小。

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注我们的更多内容!

(0)

相关推荐

  • C语言中结构体实例解析

    目录 一.结构体定义 二.实例演示 结构体作为函数参数 结构体指针 三.typedef struct 和 struct的区别 1.声明不同 2.访问成员变量不同 3.重新定义不同 总结 一.结构体定义 C语言结构体由一系列相同或者不同类型的数据构成的集合,结构体类型就是以struct关键字定义的数据类型. 结构体的格式如下: struct 结构名称 { 结构体所包含的数据成员,包括变量数组等 } 结构变量 ;//结构变量可以指定一个或多个 举例: struct Student { char na

  • C语言复数的加减及输出结构体

    一. #include<stdio.h> typedef struct complex { int real; //实部 int imag; //虚部 }complex; /* 功能:复数加法 参数:两个复数 返回值:两个复数的和 */ complex complexadd(complex x,complex y) { complex sum; sum.real = x.real + y.real; sum.imag = x.imag + y.imag; return sum; } /* 功能

  • C语言结构体指针引用详解

    目录 指向结构体变量的指针 指向结构体数组的指针 结构体指针,可细分为指向结构体变量的指针和指向结构体数组的指针. 指向结构体变量的指针 前面我们通过"结构体变量名.成员名"的方式引用结构体变量中的成员,除了这种方法之外还可以使用指针. 前面讲过,&student1 表示结构体变量 student1 的首地址,即 student1 第一个项的地址.如果定义一个指针变量 p 指向这个地址的话,p 就可以指向结构体变量 student1 中的任意一个成员. 那么,这个指针变量定义成

  • C语言结构体的全方面解读

    目录 前言 一.结构体的声明与定义 1.结构体的声明 2.结构成员的类型 3.结构体的定义 二.初始化结构体 三.访问结构体成员 四.结构体嵌套 五.结构体指针 六.结构体传参 总结 前言 C语言提供了不同的数据类型,比如说int.float.double.char等,不同的类型决定了一个变量在内存中应该占据的空间以及表现形式. 但是,当我们定义一个人的时候,人的不同属性就比较难用同一个数据类型来定义了,因为人的身高.年龄.体重等属性往往需要不同数据类型,在这个时候,我们便引入结构体这个概念.

  • C语言结构体内存对齐详解

    目录 实例一: 分析:存储结构图如下 实例二: 分析:存储结构如下 实例三: 分析:存储结构如下 实例四: 分析:存储结构图如下 总结 1.结构体内存对齐是指当我们创建一个结构体变量时,会向内存申请所需的空间,用来存储结构体成员的内容.我们可以将其理解为结构体成员会按照特定的规则来存储数据内容. 2.结构体的对齐规则 (1)第一个成员在相比于结构体变量存储起始位置偏移量为0的地址处. (2)从第二个成员开始,在其自身对齐数的整数倍开始存储(对齐数=编译器默认对齐数和成员字节大小的最小值,VS编译

  • C语言结构体中内存对齐的问题理解

    目录 前言 思考 结构体在内存中开辟空间时内存对齐的规则 为什么存在内存对齐 1.平台的原因 2.性能的原因 前言 学C的同学应该知道~ 想精通C语言就不得不面对—指针与内存 续上次指针的进阶,这一章我来聊一聊C语言内存对齐的问题 学习结构体的你有没有注意过结构体向系统申请的内存为多少呢的 思考 #include<stdio.h> typedef struct s1 { char a; char b; int c; }s1; typedef struct s2 { char a; int c;

  • C#语言基础——结构体和枚举类型全面解析

    一.结构体(struct) 结构类型是用户自己定义的一种类型,它是由其他类型组合而成的,可包含构造函数.常数.字段.方法.属性.索引器.运算符.事件和嵌套类型的值类型.结构在几个重要方面不同于类:结构为值类型而不是引用类型,并且结构不支持继承. 用结构的主要思想是用于创建小型的对象,如Point和FileInfo等等.这可以节省内存,因为没有如类对象所需的那样有额外的引用产生.例如,当声明含有成千上万个对象的数组时,这会引起极大的差异. 结构体是一个变量组,将一组变量放在一起,其实就是一个自定义

  • 详解C语言的结构体中成员变量偏移问题

    c语言中关于结构体的位置偏移原则简单,但经常忘记,做点笔记以是个记忆的好办法 原则有三个: a.结构体中的所有成员其首地址偏移量必须为器数据类型长度的整数被,其中第一个成员的首地址偏移量为0, 例如,若第二个成员类型为int,则其首地址偏移量必须为4的倍数,否则就要"首部填充":以此类推 b.结构体所占的总字节数即sizeof()函数返回的值必须是最大成员的长度的整数倍,否则要进行"末尾填充": c.若结构体A将结构体B作为其成员,则结构体B存储的首地址的偏移量必须

  • Go语言中结构体方法副本传参与指针传参的区别介绍

    GO语言结构体方法跟结构体指针方法的区别 首先,我定了三个接口.一个结构和三个方法: type DeptModeA interface { Name() string SetName(name string) } type DeptModeB interface { Relocate(building string, floor uint8) } type Dept struct { name string building string floor uint8 Key string } fun

  • C语言使用结构体实现简单通讯录

    C语言用结构体实现一个通讯录,通讯录可以用来存储1000个人的信息,每个人的信息包括: 姓名.性别.年龄.电话.住址 提供方法: 1. 添加联系人信息 2. 删除指定联系人信息 3. 查找指定联系人信息 4. 修改指定联系人信息 5. 显示所有联系人信息 6. 清空所有联系人 代码实现: 头文件: #ifndef __HEAD_H__ ////防止头文件被多次调用 #define __HEAD_H__ #include<stdio.h> #include<string.h> #in

  • C语言中结构体、联合体的成员内存对齐情况

    前言 最近项目进行中,遇到一个小问题,在数据协议传输过程中,我为了方便解析,就定义了一个结构体,在数据的指针传入函数的时候,我用定义好的结构体进行强制转化,没想到一直解析失败,调试很久,终于反应过来,在用结构体指针对数据强制转换时,定义结构体我没有注意到数据对齐,因为在底层实现中,我传入的数据buffer是排列整齐的,而强制转化的结构体格式中,我定义的时候没有使用__attribute__((__packed__))或者__packed强制数据对齐,导致结构体成员真实排列会按照成员中最大的变量的

  • C语言中结构体和共用体实例教程

    目录 一.实验目的 二.实验内容 三.实验记录 3.1 候选人选票统计 3.2 print函数 3.3 链表 总结 一.实验目的 掌握结构体类型变量的定义和使用: 掌握结构体类型数组的概念和应用: 掌握链表的概念,初步学会对链表进行操作: 掌握共用体的概念与使用: 掌握指向结构体变量的指针. 掌握指向结构体数组的指针的应用. 二.实验内容 编写下列程序,然后上机调试运行. 对候选人得票的统计程序.设有3个候选人,每次输入一个得票的候选人的名字,要求最后输出各人得票结果. 编写一个函数print,

  • C语言中结构体与内存对齐实例解析

    1.结构体类型 C语言中的2种类型:原生类型和自定义类型,结构体类型是一种自定义类型. 2.结构体使用时先定义结构体类型再用类型定义变量 -> 结构体定义时需要先定义结构体类型,然后再用类型来定义变量. -> 也可以在定义结构体类型的同时定义结构体变量. // 定义类型 struct people { char name[20]; int age; }; // 定义类型的同时定义变量. struct student { char name[20]; int age; }s1; // 将类型st

  • Go语言之结构体与方法

    目录 一.结构体 1.结构体的定义与使用 2.定义并赋初值 3.匿名结构体(只使用一次,没有名字) 4.结构体的零值 5.结构体的指针 6.匿名字段(字段没有名字,只有类型) 7.嵌套结构体(结构体中套结构体) 8.字段提升 9.结构体相等性 二.方法 1.方法的定义和使用 2.有了函数为啥还需要方法? 3.指针接收器与值接收器 5.匿名字段的方法(方法提升) 6.在方法中使用值接收器 与 在函数中使用值参数 7.在方法中使用指针接收器 与 在函数中使用指针参数 8.非结构体上绑定方法 一.结构

  • Go语言空结构体详解

    目录 前言 什么是空结构体 特点 地址相同 大小为0 原理探究 使用场景 集合(Set) channel中信号传输 总结 前言 在使用 Go 语言开发过程中,我们不免会定义结构体,但是我们定义的结构体都是有字段的,基本不会定义不包含字段的 空结构体.你可能会反过来问,没有字段的空结构体有什么用呢?那么我们这篇文章就来研究下空结构体吧! 注:本文基于go 1.14.4 分析 什么是空结构体 我们说不包含任何字段的结构体叫做空结构体,可以通过如下的方式定义空结构体: 原生定义 var a struc

随机推荐