C语言自定义类型讲解 — 结构体

枚举:http://t.csdn.cn/GNP51

联合体:http://t.csdn.cn/xPGVu

前面学到了C语言中的内置类型:char,int…

C语言中还有一种类型是自定义类型,其中包括了结构体,联合体和枚举。
那么今天来学习自定义类型中的结构体类型。

本章重点

  • 结构体类型的声明
  • 结构体的自引用
  • 结构体变量的定义和初始化
  • 结构体的内存对齐
  • 结构体传参
  • 结构体实现位段(位段的填充&可移植性)

目录

结构体

1.结构体的声明

1.1 结构基础知识

1.2 结构的声明

1.3 特殊的声明

1.4 结构的自引用

1.5 结构体变量的定义和初始化

1.6 结构体内存对齐

结构体的对齐规则

为什么存在内存对齐

1.7 修改默认对齐数

1.8 结构体传参

2.位段

1.什么是位段

2.位段的内存分配

位段是如何分配空间的

3.位段的跨平台问题

4.位段的应用


结构体

1.结构体的声明

1.1 结构基础知识

结构是一些值的集合,这些值称为成员变量,结构的每个成员可以是不同类型的变量。

1.2 结构的声明

下里面来定义一个描述学生的类型:

struct stu
{
	//学生的相关属性
	char name[20];//成员name
	int age;//成员age
} s1,s2;

 结构体标签一般是关于结构体是用来干什么的,s1和s2是 struct stu 类型的变量。

注意:结构体只是创造出来的一中类型,不用初始化。

上面的代码也可以这样写,省去变量列表,也是没问题的。

struct stu
{
	//学生的相关属性
	char name[20];//成员name
	int age;//成员age
};

若此时结构体在main函数里面,s1和s2是局部变量;若在main外面是全局变量

#include<stdio.h>

int main()
{
	struct stu
	{
		//学生的相关属性
		char name[20];//成员name
		int age;//成员age
	} s1, s2;//局部变量
	return 0;
}
#include<stdio.h>

struct stu
{
	//学生的相关属性
	char name[20];//成员name
	int age;//成员age
} s1, s2;//全局变量

int main()
{
	
	return 0;
}

struct stu 是声明的一个结构体类型,我们还可以拿他创建一个s3变量。所创建出来的s3是局部变量。

#include<stdio.h>

struct stu
{
	//学生的相关属性
	char name[20];//成员name
	int age;//成员age
} s1, s2;//全局变量

int main()
{
	struct stu s3;//局部变量

	return 0;
}

外面可以在创建结构体的同时创建变量。也可以在main里利用结构体创建变量。但是一定要注意的是结构体的分号不能丢


1.3 特殊的声明

在声明的时候,可以不完全声明

结构体的名字可以省略,变成 匿名结构体类型

//匿名结构体类型
struct
{
	//学生的相关属性
	char name[20];//成员name
	int age;//成员age
} s1, s2;

匿名结构体的使用

//匿名结构体类型
struct
{
	//学生的相关属性
	char name[20];//成员name
	int age;//成员age
} s1;

int main()
{

	return 0;
}

s1是用匿名结构体创建的变量。

注意:这个变量只能使用一次 - 声明类型的时候所创建的变量s1以后就用不了。

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

struct
{
	int a;
	char b;
	float c;
} a[20], *p;

上面的两个结构体在声明的时候省略掉了结构体标签

那么问题来了?

在上面代码的基础上,下面的代码合法吗?

p = &x;

代码演示:

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

struct
{
	int a;
	char b;
	float c;
} a[20], *p;

int main()
{
	p = &x;
	return 0;
}

代码虽然跑过了,但是会报警告。

原因:因为编译器会把上面的两个声明当做是两个完全不同的类型

1.4 结构的自引用

在结构中包含是一个类型为该结构本身的成员

数据结构是数据在内存中的存储结构,有线性,和树形(二叉树)。
其中线性又分为顺序表和链表。
其中顺序表是数据在内存中连续存放的,通过找到第一个数据就可以找到后面的数据。
但是链表的数据不是连续存放的,那么就需要通过上一个数据能找到下一个数据,那么这是后可以通过结构体的自引用

但是,下列结构体的自引用是错误的

错误写法:

struct Dode
{
	int data;
	struct Dode next;//自引用
};

int main()
{
	sizeof(struct Dode);
	return 0;
}

未来定义一个结点的时候,这个结点既可以放一个数值,又可以放下一个结点。这样就可以做到1结点找到2结点,2结点找到3结点,直到找到5结点,但是这样写存在问题

结构体里面有一个date,还有一个next;而next又是struct Dode类型的,所以next也有有个next,在这里会一直套下去,sizeof无法计算大小,程序会报错。

正确写法:

可以在一个结点中存放数据和下一个结点的地址

struct Dode
{
	int data;
	struct Dode* next;
};

int main()
{
	sizeof(struct Dode);
	return 0;
}

这个时候结点中存放的是一个数据和下一个结点的地址,用来存放数据的叫数据域,存放下一个结点的地址的叫指针域。

 这个时候我们就可以把一个一个的数据串联起来,形成一个链表了。

总结:

结构体里面包含一个同类型的结构体是不行的,要包含一个的结构体指针。

1.5 结构体变量的定义和初始化

有了结构体类型,那该如何定义变量呢?

struct point
{
	int x;
	int y;
}p1 = { 2, 3 };

        用结构体定义变量就像是用图纸盖房子,即使是有了图纸也不能直接盖房子,还需要有砖头、水泥等材料和工具才行。结构体就相当于是图纸,所创建的成员变量就相当于是砖头和水泥。利用这几样定义了一个p1变量,这叫定义;在定义变量的时候赋值,这叫初始化。上面2,3就是给p1赋的值,可以把p1看成一座房子,而2,3则是房子里的家具。

定义:

struct point
{
	int x;
	int y;
}p1;//声明结构体的同时定义p1变量

int main()
{
	struct point p2;//定义结构体变量p2
}

可以在声明结构体类型的时候定义变量,也可以用所声明的类型定义变量。

初始化:

struct stu
{
	char name[20];
	int age;
} p3 = { 1,2,3 };

int main()
{
	struct stu s = { "lisi", 20 };
	return 0;
}

在定义变量之后紧跟着赋值。

1.6 结构体内存对齐

结构体的大小如何计算?

代码结果:

这两段代码只是成员变量的顺序变了其他的都一样,那为什么结果不一样呢?

结构体的对齐规则

  1. 第一个成员在与结构体变童偏移量为0的地址处。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数=编译器默认的-一个对齐数与该成员大小的较小值。(VS中默认的值为8)
  3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

代码1:

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

int main()
{
	printf("%d\n", sizeof(struct S1));
	return 0;
}

代码结果:

  1. 根据对起规则知S1中的c1存放在偏移量为0处。占用一个字节即0.
  2. 因为int类型为4个字节小于8,那么该成员S1中的 i 的对齐数是4,即 i 要存放在4的倍数的偏移量处,即存放在偏移量为4处。占用了四个字节即4~7
  3. 因为char类型为1个字节小于8,那么该成员i的对齐数是1,即i要存放在1的倍数的偏移量处,即存放在偏移量为8处。占用一个字节即8。
  4. 计算上面的字节数为9,不是该结构体总大小为最大对齐数4的倍数,那么要继续增加到12,那么结构体的大小就是12。

对齐规则很重要!很重要!很重要!

代码2:


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

int main()
{
	printf("%d\n", sizeof(struct S2));
	return 0;
}

代码结果:

  1. 根据对起规则知c1存放在偏移量为0处。占用一个字节即0.
  2. 因为char类型为1个字节小于8,那么该成员i的对齐数是1,即i要存放在1的倍数的偏移量处,即存放在偏移量1处。占用一个字节即1。
  3. 因为int类型为4个字节小于8,那么该成员i的对齐数是4,即i要存放在4的倍数的偏移量处,即存放在偏移量为4处。占用了四个字节即4~7
  4. 计算上面的字节数为8,是该结构体总大小为最大对齐数4的倍数,结构体的大小就是8。

下面给出两个练习题,来练习巩固前面讲的对齐规则。要注意的是练习2中的S4有一个成员是结构体(S3),S4的大小就包括了S3的大小。

练习1:

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

int main()
{
	printf("%d\n", sizeof(struct S3));
	return 0;
}

代码结果是,16 

练习2:

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

struct S4
{
	char c1;
	struct S3 s3;
	double d;
};

int main()
{
	printf("%d\n", sizeof(struct S3));
	printf("%d\n", sizeof(struct S4));
	return 0;
}

代码结果是,32

为什么存在内存对齐

 

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

不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取

某些特定类型的数据,否则抛出硬件异常。

2.性能原因:

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

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

 

总结:结构体的内存对齐是拿空间来换取时间的做法。

在设计结构体的时候,尽量让占用空间小的成员尽量集中在一起,因为这样既可以满足对齐,又可以节省空间。

在使用的时候,尽量用S2来代替S1的写法,因为可以节省更多的空间。

1.7 修改默认对齐数

 #pragma 这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数。

一般修改默认对齐数都为2的倍数。

struct S
{
	int i;
	double d;
};

int main()
{
	printf("%d\n", sizeof(struct S));
	return 0;
}

代码结果:

默认对齐数是8,所以代码结果是16

#pragma pack(4)//将对齐数改为4

struct S
{
	int i;
	double d;
};
#pragma pack()//将对齐数改为默认值8

int main()
{
	printf("%d\n", sizeof(struct S));
	return 0;
}

代码结果: 

修改对齐数后,代码输出12.

1.8 结构体传参

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

void print1(struct S ss)
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%d ", ss.date[i]);
	}
	printf("%d\n", ss.num);
}

void print2(struct S* ps)
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%d ", ps -> date[i]);
	}
	printf("%d\n", ps -> num);
}

int main()
{
	struct S s = { {1,2,3}, 100 };
	print1(s);
	print2(&s);
	return 0;
}

代码结果:

 print1 和 print2 哪个函数的效果更好呢?

答案是:print1

函数传参的时候。参数是需要压栈的,会有时间和空间上的系统开销。如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降。

结论:结构体传参的时候,要传结构体的地址。

参数具体是怎样压栈的,我在其它的文章中有做详细讲解,下面是链接。

函数的栈帧的开辟与销毁:http://t.csdn.cn/2HSWx

2.位段

位段是有结构体实现的

1.什么是位段

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

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

注意:成员后面的数字不能超过类型的大小。若a是 int 类型,数字最大是32。

实际上 char 也可以是位段的成员,因为字符在底层存储的是 ASCII 码,并且只要是整形家族就都可以。

例1:

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

位段的位其实是比特位的意思。

a后面的数字表示,a只需要2个比特位。

a后面的数字表示,b只需要5个比特位。

a后面的数字表示,c只需要10比特位。

a后面的数字表示,d只需要30个比特位。

4个成员明明是 int 类型,应该是32比特位,那2、5、10、30又是什么意思呢?

比如说_a不需要那么多的空间,所以只分配了2个比特位的空间,也就是用多少取多少。

这里也就可以看出,位段其实是用来节省空间的。

例2:

#include<stdio.h>

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

int main()
{
	printf("%d\n", sizeof(struct A));
	return 0;
}

代码结果:

1个整形4个字节,4个整形应该是16个字节,那结果为什么会是8呢?4个成员一共是47个比特位,那给6个字节,就够用了,应该输出的是6,但是结果也不对。

注意:所谓的节省空间并不是极致的节省空间,而是适当的节省空间。如果不使用位段的话,那将会开辟16个字节的空间。

具体是什么原因。在下面的位段的内存分配有做详细介绍。

2.位段的内存分配

  1. 位段的成员可以是int unsigned int signed int 或者是char (属于整形家族) 类型
  2. 位段的空间上是按照需要以4个字节( int)或者1个字节( char)的方式来开辟的。
  3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

前面例2的解释:因为先开辟4个字节的空间,一共是32个比特位。4个成员一共有47个比特位,分配完之后还剩下15个比特位没有空间存放了,所以在开辟4个字节给剩下的15个比特位使用,所以才是8个字节。

一般都是位段的类型都会是同一种类型,要不然的话,这个位段会非常复杂。

位段是如何分配空间的

例3:

struct S
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};

int main()
{
	printf("%d\n", sizeof(struct S));
	return 0;
}

情况1:

  1. 看见是char,所以先开辟1个字节,a占3个比特位,还剩下5个比特位。
  2. b有4个比特位,给b分配后还剩1个比特位,不够c分配了,所以把剩下的1个比特位舍弃。
  3. 开辟1个字节,给c分配完之后还剩下3个,不够d分配了,还要再开辟1个字节给d。

如果输出3则说明是情况1,空间不够的话会直接开辟新的空间,多余出来的比特位会直接浪费掉。

情况2:

  1. 开辟1个字节,给a分配完以后,还剩5个比特位。
  2. 给b分配完后,还剩1个比特位,再开辟1个字节,加上前面剩下的就还有9个比特位。
  3. 给c用完后还剩4个比特位,这个时候可以正好给d分配。

如果输出2则说明是情况2,空间不够的话会直接再开辟1个字节的空间,之前剩下的空间也会继续参与分配。

代码结果:

例4:

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

存储前的内存:

存储后的内存:

  1.  a的二进制序列是1010,但是a只有3个比特位的空间,所以只会存放010。
  2. 这个字节还剩下5个比特位,而b二进制序列是1100,剩下的空间可以放得下。
  3. 这个时候不够存c了,需要再开辟一个空间。
  4. c的二进制是011,但是不够5个比特位所以要补0,变成00011。
  5. 因为只剩下3个比特位的空间,不够d使用了,所以要再开辟1个空间
  6. 4的二级制序列是0100,空间正好够用。 

3.位段的跨平台问题

  1. int位段被当成有符号数还是无符号数是不确定的。
  2. 位段中最大位的数目不能确定。(1 6位机器最大16, 32位机器最大32,写成27, 在16位机器会出问题。
  3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
  4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时, 是舍弃剩余的位还是利用,这是不确定的。

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

4.位段的应用

  • 13
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

与大师约会

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值