栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

C语言常见的自定义数据类型(1)——结构体

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

C语言常见的自定义数据类型(1)——结构体

目录

1、结构体

1.1 结构体的定义

1.2 结构体的自引用

1.3 结构体类型的重命名

1.4 结构体的嵌套

2、结构体大小的计算

2.1 结构体内存对齐

2.2 嵌套结构体大小的计算

2.3 offsetof函数

2.4 修改默认对齐数

2.5 结构体内存对齐的意义

3、结构体传参

①结构体传参

②结构体地址传参

4、位段

4.1 什么是位段

4.2 位段的内存分配

4.3 位段的跨平台问题

4.4 总结:


在C语言中,常见的数据类型有整型int、浮点型float、字符类型char等,而仅仅用这些简单的数据类型来描述现实世界是远远不够的,如描述一本书时,要想通过定义一个变量涵盖这本书的名字,出版社,作者等信息,只用简单的数据类型是不能实现的,因此,C语言还规定了几个常见的自定义类型:结构体、枚举、联合体。

1、结构体

1.1 结构体的定义

C语言中,可以使用结构体来实现存放一组不同类型的数据。结构体也可以认为是一些值的集合,这些值称为成员变量,结构体的每个成员可以是不同类型的变量。

struct tag
{
	member list;//成员列表
}variable list;//变量列表

如对一个学生的姓名、年龄、成绩进行描述时定义的结构体:

#include
struct Stu
{
	char name[20];//成员变量
	int age;
	double score;
};

int main()
{
	struct Stu s1;//定义一个结构体变量s1
return 0;
}

1.2 结构体的自引用

如果有五个节点分别为1 ,2 ,3, 4 ,5 ...... 如果要像顺序表一样,按照一个线性的关系把它存起来,即通过1可以找到2,通过2找到3 ...... ,以此类推。可以把2节点的地址存到1节点,把3节点的地址存到2节点 ...... ,以此类推,在最后一个节点处存放空指针。如下定义了一个链表式的结构体:

#include
struct Node
{
	int data;//数据域
	struct Node* next;//指针
};
int main()
{
	struct Node n;

return 0;
}

1.3 结构体类型的重命名

与其他数据类型一样,结构体类型的重命名同样可以用typedef来实现:

#include
typedef struct Node
{
	int data;//数据域
	struct Node* next;//指针
}Node;

int main()
{
	struct Node n1;
	Node n2;
	//n2和n1的类型是一样的

return 0;
}

1.4 结构体的嵌套

如下列代码,在结构体 struct Node 里又嵌套了结构体 struct Book:

#include
struct Book
{
	char name[20];
	float price;
	char id[12];
}s={"加油",55.5f,"haha001"};

struct Node
{
	struct Book b;
	struct Node* next;
};

int main()
{
	//struct Book s2={"努力",55.3f,"yeah002"};//也可以这样创建结构体变量
	struct Node n={{"gogogo",89.2f,"lyd"},NULL};

return 0;
}

2、结构体大小的计算

有如下两个结构体 S1、S2 :

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

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

它们的成员列表相同,而排列顺序不同,由简单数据类型的大小可知,char 类型占一个字节,int 类型占4个字节,这样我们可以简单估算两个结构体的大小为6个字节,实际上真的会是这样吗?下面利用 sizeof 来进行检验计算:

#include
struct S1
{
	char c1;
	int i;
	char c2;
};
struct S2
{
	char c1;
	char c2;
	int i;
};
int main()
{
	struct S1 s;
	struct S2 s2;
	printf("%dn",sizeof(s)); //12
	printf("%dn",sizeof(s2)); //8

return 0;
}

可以发现,在内存中S1结构体占12个字节大小,S2结构体占8个字节的大小,那么是什么原因导致这样的结果呢?

2.1 结构体内存对齐

结构体对齐规则:

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

2、其他成员变量要对齐到对齐数的整数倍的地址处。

(对齐数 = 编译器默认的一个对齐数与该成员大小的较小值(VS默认的值为8))

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

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

这样,我们再看上面的S1和S2结构体:

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

对结构体S1:假设这个结构体在一个位置处开辟空间要存储,第一个成员c1在与结构体变量偏移量为0的地址处,即在0处;对第二个成员变量,对齐到对齐数的整数倍的地址处,4与8比较4较小,4是对齐数,所以i放到(4的倍数)与结构体变量偏移量为4的地址处,占4个字节,c2为1个字节,1与8比较1较小,1是对齐数,所以c2放到与结构体变量偏移量为8的地址处。又因为结构体总大小为最大对齐数的整数倍,4与1相比,4为最大对齐数,所以现在总共占了9个字节的大小,不是4的倍数,所以还要继续开辟3个字节的空间,即12个字节的空间,所以这个结构体的大小为12个字节。

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

同理对结构体S2:假设这个结构体在一个位置处开辟空间要存储,第一个成员c1在与结构体变量偏移量为0的地址处,即在0处;对第二个成员变量,对齐到对齐数的整数倍的地址处,1与8比较1较小,1是对齐数,所以c2放到(1的倍数)与结构体变量偏移量为1的地址处,占1个字节,i为4个字节,4与8比较4较小,4是对齐数,所以i放到与结构体变量偏移量为4的地址处。又因为结构体总大小为最大对齐数的整数倍,4与1相比,4为最大对齐数,现在总共占了8个字节的大小,是4的倍数,所以这个结构体的大小为8个字节。

2.2 嵌套结构体大小的计算

看下面代码,在结构体S4中嵌套了结构体S3,求S4结构体的大小:

#include
#include
struct S3
{
	double d;
	char c;
	int i;
};
struct S4
{
	char c1;
	struct S3 s3;
	double d;
};

int main()
{
	printf("%dn",sizeof(struct S4));//32

return 0;
}

根据结构体对齐规则第四条,如果有嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

c1大小为1字节,放到与结构体变量偏移量为0的地址处,S3可知为16字节大小,对齐到8(嵌套的结构体对齐到自己的最大对齐数的整数倍处)地址处,d大小为8字节,对齐到24地址处,此时总共占了32字节,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍,32是8的整数倍,所以S4大小即为32字节。

2.3 offsetof函数

——计算结构体成员相对于起始位置的偏移量

size_t offsetof( structName, memberName );

该函数的两个参数分别为结构体名和成员名,头文件为:

#include
#include
struct S3
{
	double d;
	char c;
	int i;
};

struct S2
{
	char c1;//1
	char c2;//1
	int i;///4
};

int main()
{
	printf("%un",offsetof(struct S3,d)); //0
	printf("%un",offsetof(struct S3,c)); //8
	printf("%un",offsetof(struct S3,i)); //12
	//printf("%dn",sizeof(struct S3)); //16

	printf("%un",offsetof(struct S2,c1)); //0
	printf("%un",offsetof(struct S2,c2)); //1
	printf("%un",offsetof(struct S2,i)); //4
return 0;
}

2.4 修改默认对齐数

上面介绍到,在VS编译器中默认对齐数为8(linux环境下没有默认对齐数,此时自身的大小就是其对齐数),在对齐方式不合适的时候,我们可以更改默认的对齐数。

利用#pragma pack( )修改默认对齐数:

#include

#pragma pack(4) //设置
//#pragma pack(1)----9    改成1时实际上就是按照不对齐方式存储的
struct S
{
	char c;
	double d;
};

#pragma pack() //取消

int main()
{
	struct S s;
	printf("%dn",sizeof(s));//12

return 0;
}

可知,当修改默认对齐数为1时,结构体大小变为9字节,此时就是按照不对齐方式存储的;当修改默认对齐数为4时,此时结构体大小变为12字节。

2.5 结构体内存对齐的意义

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

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

2、性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。为了访问未对齐的内存,处理器需要作两次内存访问,而对齐的内存访问仅需要一次访问就可以。

总的来说,结构体内存对齐是拿空间来换取时间的做法。在上面的代码中,改了下成员的顺序,开辟的空间就不一样了。所以,在设计结构体的时候,如果考虑既要满足对齐,又要节省空间,可以让占用空间小的成员尽量集中在一起(这样在一定程度上浪费的空间就会更少)。

3、结构体传参

比较下列结构体传参的两种方式:

①结构体传参
#include
struct S
{
	int data[1000];
	int num;
};

void print1(struct S s)
{
	printf("%dn",s.num);
}

int main()
{
	print1(s);//传结构体

return 0;
}

②结构体地址传参
#include
struct S
{
	int data[1000];
	int num;
};

void print2(struct S* ps)
{
	printf("%dn",ps->num);
	printf("%dn",(*ps).num);
}

int main()
{
	print1(&s);//传结构体地址

return 0;
}

以上两种方式都可以实现对结构体成员信息的打印。不过,函数在传参的时候,参数是需要压栈的,会有时间和空间上的系统开销。第一种方式把整个结构体传参,如果结构体过大,参数压栈的时候系统开销比较大,会导致系统性能的下降;而第二种方式只需要传一个指针,而指针的大小是固定的,4字节(32位平台)或者8字节(64位平台),所以结构体传参的时候,尽量传结构体的地址。

4、位段

了解了结构体,就不得不提结构体实现位段的能力了。

4.1 什么是位段

位段的声明和结构体是类似的,注意有两处不同:

1. 位段的成员必须是 int 、unsigned int 或 signed int (char 其实也可以)

2. 位段的成员名后边有一个冒号和一个数字,数字代表占用 bit 位的数量

比较结构体和位段的不同:

结构体:

//结构体
struct A
{
	int _a;
	int _b;
	int _c;
	int _d;
};

位段:

//位段
struct B
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};

对于结构体A,如果不考虑对齐,结构体A需要4个整型即16个字节的内存;对于位段B,_a这个成员只需要占2个bit位,_b这个成员占5个bit位,_c这个成员占10个bit位,_d这个成员占30个bit位,总共只要47个bit位,所以只用两个整型空间即可存下:

#include
//结构体
struct A
{
	int _a; //一个整型的取值范围为INT_MIN ~ INT_MAX
	int _b;
	int _c;
	int _d;
};

//位段
struct B
{
	int _a : 2; //2个比特位 只能放 00 01 10 11 四个数, 如果有一个变量正好只有这几个取值,可以考虑用位段
	int _b : 5;
	int _c : 10;
	int _d : 30;
};
int main()
{
	printf("%dn",sizeof(struct A)); // 16   
	printf("%dn",sizeof(struct B)); // 8    ---- 确实只需要两个整型的空间
return 0;
}

注意:

结构体A中的整型成员_a,取值范围为INT_MIN ~ INT_MAX;

位段B中的成员_a由于只占两个bit位的大小,意味着只能放 00 ,01 ,10, 11 四个数。

所以在定义结构体的时候,如果发现某些成员是不需要那么大的空间的,只需要少量的bit位就够了,这个时候使用位段在一定程序上节省了空间。

4.2 位段的内存分配

struct A
{
    int _a : 2;
    int _b : 5;
    int _c : 10;
    int _d : 30;
};

对这个位段(均为int),在内存存储的时候,先开辟了一个int类型的空间,即4个字节,先存_a,占了2个bit位,还有30个,再存_b,又占了5个bit位,还剩25个bit位,再存_c,还剩15个bit位,再存_d的时候,空间不够了,考虑再开辟一个int类型的空间,就能放下_d了。

此时考虑一个问题:放_d的时候是接着后面放还是在新开辟的那个int类型里的空间去放?

假设存储的时候从低位开始,空间不够了在新开辟的空间去存,不够的空间浪费掉,最后推算的应该是需要3个字节,下面用一个例子去验证:

#include
struct S
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};
int main()
{
	struct S s = {0};
	printf("%dn",sizeof(s)); // 3

return 0;
}

由打印结果可知,我们的推断是正确的,确实是3字节。

4.3 位段的跨平台问题

在使用位段的时候,同时要注意:

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

2. 位段中最大位的数目也是不确定的 (16 位机器最大16,32 位机器最大 32,如果写成27,在16位机器会出问题);

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

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

由于以上的不确定性,造成了位段有跨平台的问题。

4.4 总结:

1. 位段的成员可以是int 、 unsigned int 、  signed int 或是char(属于整型家族)类型
2. 位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的

3. 在VS环境下:位段的在内存中的空间开辟与存储规则是:当存不下的时候开辟空间,放的时候直接在新开辟的空间里去存,之前放不下的空间就浪费掉了

4. 与结构相比,位段可以很好的节省空间,但是有跨平台的问题所在,注重可移植的程序应该避免使用位段

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/862734.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号