结构体联合体

Posted 一个山里的少年

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了结构体联合体相关的知识,希望对你有一定的参考价值。

每天进步一点点! 

什么叫结构体?结构体是由相同或者不同的数据构成的数据集合

在此之前了解一些小知识

2.1只有结构体变量才分配地址,而结构体的定义是不分配空间的。
2.2结构体中各成员的定义和之前的变量定义一样,但在定义时也不分配空间。
2.3结构体变量的声明需要在主函数之上或者主函数中声明,如果在主函数之下则会报错
2.4c语言中的结构体不能直接进行强制转换,只有结构体指针才能进行强制转换
2.5相同类型的成员是可以定义在同一类型下的
 

1.结构体的声明与定义

1.结构体是一种自定义数据类型

struct 结构体名{

成员变量或者数组

};

特别要注意的是末尾的分号一定不能少,表示结构体设计定义结束

结构体是一种集合,他里面包含了多个变量或者数组,变量的类型可以相同也可以不同,每一个这样的变量或者数组都被称为结构体成员,我们来看一个例子:

include<stdio.h>
struct student
{
	int age;
	char name[10];
	int score;
	char sex[10];
};

stedent为结构体名,他一共有四个成员,分别是age,name ,score,sex,结构体的定义方式和我们在前面学过的数组十分相似,只是数组可以初始化,而在这里是不能初始化

2.先定义结构体类型在定义结构体变量

#include<stdio.h>
struct student
{
	int age;
	char name[10];
	int score;
	char sex[10];
};
struct student s1;//s1为全局变量
int main()
{
	struct student s2;//s2为局部变量

}

3.定义结构体类型的时候同时定义结构体变量

#include<stdio.h>
struct student
{
	int age;
	char name[10];
	int score;
	char sex[10];
}s1, s2;

int main()
{
	return 0;

}

4.定义匿名结构体

#include<stdio.h>
struct
{
	int age;
	char name[10];
	int score;
	char sex[10];
}s1;

int main()
{
	return 0;

}

但是这种方式不太好,这个结构体有一种一次性的感觉,不建议这样定义结构体

总结:

1.使用struct 关键字,表示他接下来是一个结构体

2.接下来是一个结构体类型名,可以自己选择

3.花括号,括起来了结构体成员列表,使用的都是声明的方式来描述,用;来结束描述

4.在结束花括号后的分号表示结构体设计定义的结束

5.定义的方式

1)先声明结构体类型再定义变量名
例如:struct(类型名) student(结构体) student1(变量名),student2(变量名);
定义了student1和student2为struct student类型的变量,即他们具有struct student类型的结构

(2)在声明类型的同时定义变量这种形式的定义的一般形式为:struct 结构体名{
成员列表
}变量名;

注意:

结构体里面的成员可以是基本数据类型也可以是自定义数据类型

5.结构体声明的位置,及其作用

2.结构体变量的初始化

1.结构体变量和其他变量一样可以在定义的时候指定初始值 ,结构体变量的初始化用大括号初始化

例:

#include<stdio.h>
struct student
{
	int age;
	char name[10];
	int score;
	char sex[10];
};

int main()
{
	struct student s1={20,"zhangsan",60,"男" };//初始化s1;
	printf("%d  %s  %d  %s", s1.age, s1.name, s1.score, s1.sex);//打印s1中的成员
	return 0;

}

运行结果:

结构体变量的初始化用大括号初始化

那下面这种行不行了?

#include<stdio.h>
struct student
{
	int age;
	char name[10];
	int score;
	char sex[10];
};

int main()
{
	struct student s1;
	s1={20, "zhangsan", 60, "男" };
	printf("%d  %s  %d  %s", s1.age, s1.name, s1.score, s1.sex);//打印s1中的成员
	return 0;

}

这种是不行的,s1在之前已经定义过了,下面在给他值,不是初始化了而是赋值

我们可以看到

 此时编译器报错了。在结构体变量定义后,如果仍要对成员变量赋值,此时我们只能一一赋值

#include<stdio.h>
#include<string.h>
struct student
{
	int age;
	char name[10];
	int score;
	char sex[10];
};

int main()
{
	struct student s1;
	strcpy(s1.name, "zhangsan");
	s1.age = 40;
	s1.score = 60;
	strcpy(s1.sex, "男");
	printf("%d  %s  %d  %s", s1.age, s1.name, s1.score, s1.sex);//打印s1中的成员
	return 0;

}

运行结果:

 2.结构体中嵌套结构体的初始化

结构体中中嵌套结构体的初始化我们依然使用大括号进行初始化

#include<stdio.h>
struct techer
{
	int age;
	char name[10];
};
struct student
{
	int age;
	char name[10];
	int score;
	char sex[10];
	struct techer s2;
};

int main()
{
	struct student s1 = { 20,"zhangsan",70,"男",{20,"老师"} };//初始化s1;
	printf("%d  %s  %d  %s %s", s1.age, s1.name, s1.score, s1.sex,s1.s2.name);//打印s1中的成员
	return 0;

}

3.访问结构体成员

访问结构体成员主要用两种

1.使用成员访问运算符 。结构体变量.成员变量

2.使用->运算符。结构体指针->成员变量名

举个例子:

#include<stdio.h>
struct techer
{
	int age;
	char name[10];
};
struct student
{
	int age;
	char name[10];
	int score;
	char sex[10];
	struct techer s2;
};

int main()
{
	struct student s1 = { 20,"zhangsan",70,"男",{20,"老师"} };//初始化s1;
	printf("%d  %s  %d  %s %s", s1.age, s1.name, s1.score, s1.sex,s1.s2.name);//打印s1中的成员
	return 0;

}

2.结构体指针访问成员变量

#include<stdio.h>
struct t
{
	int age;
	char name[12];
	char c;
};
int main()
{
	struct t s1 = { 20,"zhansan",'c' };//定义一个结构体变量s1并将其初始化为对应的值
	struct t* p1 = &s1;
	printf("%s %d", p1->name, p1->age);
	return 0;
}

运行结果:

#include<stdio.h>
#include<string.h>
struct t
{
	int age;
	char name[12];
	char c;
};
int main()
{
	
	struct t s1 = { 20,"zhansan",'c' };//定义一个结构体变量s1并将其初始化为对应的值
	struct t* p1 = &s1;
	printf("%s %d", p1->name, p1->age);
	return 0;
}

总结:

1.结构体整体赋值只限定于定义变量的时候并将其初始化,在使用的过程中只能对其一 一赋值,这一点和数组是类似的。

2.结构体是一种自定义类型,是创建变量的模板,不占用空间,结构体变量才包含了实实在在的数据,需要空间来存储

4.结构体传参

1.可以把结构体作为函数参数,传参的类型和其他变量或指针类似

第一种传参方式,传结构体用结构体来接收

#include<stdio.h>
#include<string.h>
struct t
{
	int age;
	char name[12];
	char c;
};
void print(struct t s2)
{
	printf("%d %s", s2.age, s2.name);
}
int main()
{
	
	struct t s1 = { 20,"zhansan",'c' };//定义一个结构体变量s1并将其初始化为对应的值
	print(s1);
	return 0;
}

运行结果:

 方法二:

传结构体变量的地址:

#include<stdio.h>
#include<string.h>
struct t
{
	int age;
	char name[12];
	char c;
};
void print(const struct t* s2)
{
	printf("%d %s", s2->age,s2->name);
}
int main()
{
	
	struct t s1 = { 20,"zhansan",'c' };//定义一个结构体变量s1并将其初始化为对应的值
	print(&s1);
	return 0;
}

运行结果:

两种传参方式都能达到目的,那哪一种传参方式更好 了答案是第二种。原因是第一种是值传递,会创建一个临时变量将实参的值拷贝给形参。如果结构体太大,传递的效率就会很低,空间的浪费很大,但是第二种传递的方式是传地址,只需要拷贝4个或者8个字节。如果要传参尽量选择第二种

5.结构体数组

所谓的结构体数组,指的是结构体里面的每一个元素都是结构体,在实际中常用来表示拥有相同数据结构的群体,比如一个班的学生

定义结构体数组和定义结构体变量的方式类似定义时可以初始化

例:

#include<stdio.h>
#include<string.h>
struct student
{
	int age;
	char name[12];
	char c;
};

int main()
{
	
	struct student arr[3] = { {20,"zhangsan",'c'},{30,"lisi",'d'},{40,"wangwu",'p'} };
	printf("%d %d %d", arr[0].age, arr[1].age, arr[2].age);
}

运行结果:

 6.结构体指针

顾名思义就是指向结构体的指针,方式和定义整型指针那些类似,

定义格式: struct 结构体名*+结构体指针名字

例:struct student *struct _pointer;

struct_pointer=&s1;

访问成员时使用->运算符

struct_pointer->+成员名

#include<stdio.h>
#include<string.h>
struct student
{
	int age;
	char name[12];
	char c;
};

int main()
{
	
	struct student s1 = { 22,"zhangli",'c' };
	struct student* struct_pointer = &s1;
	printf("%d", struct_pointer->age);
}

运行结果:

 注意:

结构体和结构体变量是两个不同的概念:结构体是一种数据类型,是一种创建变量的模板,编译器不会为它分配内存空间,就像 int、float、char 这些关键字本身不占用内存一样;结构体变量才包含实实在在的数据,才需要内存来存储。下面的写法是错误的,不可能去取一个结构体名的地址,也不能将它赋值给其他变量:

下列写法是错误的

#include<stdio.h>
#include<string.h>
struct student
{
	int age;
	char name[12];
	char c;
};

int main()
{
	
	struct student* p = &student;
	struct student* s = student;
}


7.获取结构体成员

通过结构体指针可以获取结构体成员,一般形式为:

(*pointer).memberName

或者:

pointer->memberName

第一种写法中,.的优先级高于*,(*pointer)两边的括号不能少,如果去掉括号写成*pointer.memberName,那么就等效于*(pointer.memberName),这样意义就不对了。

第二种写法中,->是一个新的运算符,习惯称它为“箭头”,有了它,可以通过结构体指针直接取得结构体成员,这也是->在C语言中的唯一用途。

上面两种写法是等效的,我们通常采用第二种写法,这样更加直观。
 


例:

#include<stdio.h>
#include<string.h>
struct student
{
	int age;
	char name[12];
	char c;
};

int main()
{
	
	struct student s1 = { 2,"zhansan",'e' };
	struct student* pointer = &s1;
	printf("%d\\n", pointer->age);
	printf("%d", (*pointer).age);
}

运行结果:

 7.结构体变量的引用(输入和输出)

结构体变量的输入scanf和输出printf和其他变量的操作是一样的

1.值得注意的是.的优先级是比较高的

2.(如果结构体的成员本身是一个结构体,则需要继续用.运算符,直到最低一级的成员。

#include<stdio.h>
#include<string.h>
struct student
{
	int age;
	char name[12];
	char c;
	struct Date
	{
		int year;
		int month;
		int day;
	}brirthday;
}stu1;

int main()
{
	printf("%d", stu1.brirthday);//这样写是错误的,因为brirthday是一个结构体
	scanf("%d", &stu1.brirthday.month);//正确;

	
}

8.结构体内存对齐(重点)

  结构体中的内存对齐是用空间换时间的一种内存操作。

    一.结构体对齐的规则

     1、 数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。

     2、结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。

     3、结合1、2可推断:当#pragma pack的n值等于或超过所有 数据成员长度的时候,这个n值的大小将不产生任何效果。

     1) 结构体变量的首地址是其最长基本类型成员的整数倍;

     备注:编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找 内存地址能是该基本数据类型的整倍的位置,作为结构体的首地址。将这个最宽的基本数据类型的大小作为上面介绍的对齐模数。

     2)结构体每个成员相对于结构体首地址的 偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);

备注:为结构体的一个成员开辟空间之前,编译器首先检查预开辟空间的首地址相对于结构体首地址的偏移是否是本成员的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充一定的字节,以达到整数倍的要求,也就是将预开辟空间的首地址后移几个字节。

     3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要, 编译器会在最末一个成员之后加上填充字节(trailing padding)。

备注:结构体总大小是包括填充字节,最后一个成员满足上面两条以外,还必须满足第三条,否则就必须在最后填充几个字节以达到本条要求。

     4) 结构体内类型相同的连续元素将在连续的空间内,和 数组一样。

     5) 如果结构体内存在长度大于处理器位数的元素,那么就以处理器的倍数为对齐单位;否则,如果结构体内的元素的长度都小于处理器的倍数的时候,便以结构体里面最长的 数据元素为对齐单位。

2.为什么要设置结构体内存对齐?

一、硬件原因:加快CPU访问的速度
我们大多数人在没有搞清楚CPU是如何读取数据的时候,基本都会认为CPU是一字节一字节读取的,但实际上它是按照块来读取的,块的大小可以为2,4,8,16。块的大小也称为内存读取粒度。
假设CPU没有内存对齐,要读取一个4字节的数据到一个寄存器中,(假设读取粒度为4),则会出现两种情况
1、数据的开始在CPU读取的0字节处,这刚CPU一次就你能够读取完毕
2、数据的开始没在0字节处,假设在1字节处吧,CPU要先将0~3字节读取出来,在读取4~7字节的内容。然后将0~3字节里的0字节丢弃,将4~7字节里的5,6,7字节的数据丢弃。然后组合1,2,3,4的数据。
由此可以看出,CPU读取的效率不是很高,可以说比较繁琐。
但如果有内存对齐的话:
由于每一个数据都是对齐好的,CPU可以一次就能够将数据读取完成,虽然会有一些内存碎片,但从整个内存的大小来说,都不算什么,可以说是用空间换取时间的做法。
二、平台原因:
不是所有的硬件平台都可以访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些类型的数据,否则抛出硬件异常。

3.结构体大小的计算

计算结构体的大小需要按照上面的规则:

在次说明一下:

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

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

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

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

下面我们来实践一下,光说不练,假把式

#include<stdio.h>
#include<string.h>
struct student
{
	int age;
	char a;
}stu1;

int main()
{
	printf("%d", sizeof(stu1));
	return 0;
}


上面这个结构体的大小是多少了?

首先按照上面的规则第一个元数放到偏移量为0的地方,第一个元素的类型是整型,大小为4个字节,而第二个元素的a的类型是char,大小是1个字节。而编译器默认的最大对对齐数是8,两者取最小值为1,那么a就要放到1的倍数下。此时a和age一共占用了5个字节,但所有成员里面最大对齐数为4也就是age,5并不是4的倍数,所以编译器会浪费3个字节的空间对齐,所以这个结构体的大小为8

运行结果:

 下面我们再来看一道题

#include<stdio.h>
#include<string.h>
struct student
{
	char c1;
	char c2;
	int i;
}stu1;

int main()
{
	printf("%d", sizeof(stu1));
	return 0;
}

运行结果:

结果是8,我们来分析一下为什么结果是 8??
c1是char型,占一个字节,第一个成员即 c1 在结构体变量偏移量为0 的地址处。
c2是char型,占一个字节,要对齐到对齐数的整数倍的位置。对齐数 = 编译器默认的一个对齐数与该成员大小中的较小值,vs中默认值是8,取较小值1,char类型的对齐数是1,所以对齐到1 的整数倍,那就是偏移量为1开始的地址空间。
i是int类型,占四个字节,要对齐到对齐数的整数倍的位置。int类型的对齐数就是 4,所以对齐到4 的整数倍。
我们来看一下内存分布图:

内存分布图:

那么这一个结果又是多少了?

#include<stdio.h>
#include<string.h>
struct student
{
	char c1;
	
	int i;
	char c2;
}stu1;

int main()
{
	printf("%d", sizeof(stu1));
	return 0;
}

运行结果:

结果是12,来看一下过程?
c1是char型,占一个字节,对应到结构体变量偏移量为0 的地址处。
i是int型,占四个字节,对齐数就是4,对齐到4的整数倍位置处,即偏移量为4开始的地址空间。
c2是char型,占一个字节,对齐到1 的整数倍,那就是下一个地址空间,对齐到偏移量为8的地址空间。
结构体总大小为最大对齐数的整数倍,所以为对齐数4的整数倍,现在已经用了9个字节的空间,那么总大小就是12个字节空间。所以输出结果是12。
 

内存图:

下面我们在来看一个例子结构体嵌套结构体的大小又应该如何计算

例:

#include<stdio.h>
struct s3
{
	double d;
	char c;
	int i;
};
struct s4
{
	char c1;
	struct s3 s;
	double d;
};
int main()
{
	struct s4 t;
	printf("%d ", sizeof(t));
}

运行结果:

 结果是32,我们来看一下分析:
容易得出struct S3占16个字节。请读者自行分析,如果不会的话可以问我哦。
那我们来看一下struct S4的大小,struct S4中有三个成员变量,第一个char型,占一个字节,对齐到偏移量为0的地址处。第二个成员是结构体嵌套使用,结构体S3变量s3,刚才已经得出占16个字节,所以第二个成员对齐数是16,又因为对齐数是编译器默认数与成员对齐数中的较小值,vs默认对齐数是8,取较小值8,所以对齐到偏移量为8的地址空间。处。第三个成员是double型,占8个字节,对应到8的整数倍即偏移量24的地址处。
结构体总大小是最大对齐数8的整数倍,所以是32。
来看一下内存分布图:

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

简单理解#pragma
作为较为复杂的预处理指令之一,它的作用为更改编译器的编译状态以及为特定的编译器提供特定的编译指示,这些指示是具体针对某一种(或某一些)编译器的,其他编译器可能不知道该指示的含义又或者对该指示有不同的理解,也即是说,#pragma的实现是与具体平台相关的。可以简单将其理解为该预处理指令是开发者和编译器交互的一个工具。

#pragma pack指令说明
由于内存的读取时间远远小于CPU的存储速度,这里用设定数据结构的对齐系数,即牺牲空间来换取时间的思想来提高CPU的存储效率。

这里先说编译器的对齐配置。以vc6为例,vc6中的编译选项有 /Zp[1|2|4|8|16] ,/Zp1表示以1字节边界对齐,相应的,/Zpn表示以n字节边界对齐。n字节边界对齐的意思是说,一个成员的地址必须安排在成员的尺寸的整数倍地址上或者是n的整数倍地址上,取它们中的最小值。也就是:
min ( sizeof ( member ), n)
实际上,1字节边界对齐也就表示了结构成员之间没有空洞。
/Zpn选项是应用于整个工程的,影响所有的参与编译的结构。

例子:

#include<stdio.h>
#pragma pack(1)
struct s4
{
	char c1;
	double d;
};
int main()
{
	struct s4 t;
	printf("%d ", sizeof(t));
	
}

运行结果:

答案为9,我们通过#pragma修改了默认对齐数,修改为1,所有成员的大小和1取最小值,那么所有成员的对齐数为1,也就是挨着放的,9所以大小为9

offfsetof及其实现

offsetof是一个库函数在<stddef.h>中,其作用是计算成员变量在结构体中的偏移量·

函数原型:

 如何使用了:

#include<stdio.h>

#include<stddef.h>
struct s4
{
	char c1;
	double d;
};

int main()
{
	struct s4 t;
	printf("%d ", offsetof(struct s4, d));
	
}

运行结果:

原理已经在上面说过了在这里就不过多赘述

实现offsetof,他实际是一个宏 

代码实现:

#include<stdio.h>
#include<stddef.h>
#define OFFSETOFF(struct_type ,member)(int)&((struct_type*)0)->member
struct s4
{
	char c1;
	int a;
	double d;
};

int main()
{
	printf("%d ", OFFSETOFF(struct s4, a));
	
}

第一个参数是结构体的名字,第二个参数是结构体成员的名字,返回的是该成员在结构体中的偏移量

这个宏的实现巧妙之处在于,将起始的对象的地址强制赋为0, 即是(struct_type*)0这里.那么返回的(struct_type*)0->member即是成员的偏移量了.


注意:这个结构并没有申请内存空间,却要去访问其成员,按常理不是会出错,因为访问了未申请的内存空间.

但是,在这里我们实际并没有访问结构成员的内存空间,只是返回其地址值,我们用的是取址运算符.而这里的值是编绎器在编绎阶段便已经计算好的。hs

联合体

什么是联合体?在C语言中,变量的定义是分配存储空间的过程。一般的,每个变量都具有其独有的存储空间,那么可不可以在同一个内存空间中存储不同的数据类型(不是同事存储)呢?

答案是可以的,使用联合体就可以达到这样的目的。联合体也叫共用体,在C语言中定义联合体的关键字是union。
 

1.定义一个联合体的格式和结构体类似

union 联合体名{

成员列表

};

成员列表中有若干成员,成员形式一般为:类型说明符 成员名。

与结构体,枚举一样,联合体也是一种构造类型

 2.联合体定义的方法

方法一:先创建模板后定义变量

#include<stdio.h>
union student{
	int a;
	char c;
};
int main()
{
	union student s1;
	return 0;
}

方法二:创建模板和变量

#include<stdio.h>
union student
{
	int a;
	char c;
}s1;
int main()
{
	
	return 0;
}

方法三:省略联合体名,也就是匿名联合体

#include<stdio.h>
union 
{
	int a;
	char c;
}s1;
int main()
{
	
	return 0;
}

联合体的初始化和结构体基本一样,在这里就不过多赘述

2.联合体的大小计算

当没有定义#pragma pack()这种指定value字节对齐时,他的计算规则时联合体中最大成员所占的内存的大小,并且必须为最大类型所占字节的倍数

下面我们来看一个例子:

#include<stdio.h>
union 
{
	char s[10];
	int a;
}s1;
int main()
{
	printf("%d", sizeof(s1));
	return 0;
}

运行结果:

 在这个联合体中最大的成员的大小为10也就是那个数组,但是最大类型所占字节数的大小为4.并不是4的整数倍所以编译器会浪费2个字节,所以大小为12个字节

3.使用联合体判断大小端:

联合体的概念和特征:union维护足够的空间来存放多个数据成员中的“一种”,而不为每一个数据成员都配置空间,在union中所有的成员共用同一个空间,同一时间只存储一个数据成员,最大的特征就是所有的数据成员具有相同的起始地址即联合体的基地址。

2)计算机中字节存储主要有两种:大端模式(Big_endian)和小端模式(Little_endian),从英文名字上可以明白,大端模式是从低地址开始,高位结束,(即高地址存地位,低地址存高位);小端模式是从高地址开始,低地址结束(与大端相反,)。

3)利用union中所有数据成员具有同样的起始地址的特点,通过一个int成员存储1,然后通过char成员来读取,即可巧妙地得出数据存放的方式,若通过char成员(即读取起始位置上的第一个字节)读取,若得出值为1,则说明是小端模式。

 

代码如下:

#include<stdio.h>
union stu
{
	int a;
	char c;
};
int main()
{
	union stu s;
	s.a = 1;
	if (s.c == 1)
	{
		printf("小端");
	}
	else
	{
		printf("大端");
	}
	return 0;
}

 博主实力有限如有错误请在评论区留言,如果觉得写的不错可以点个赞,谢谢!

以上是关于结构体联合体的主要内容,如果未能解决你的问题,请参考以下文章

结构体struct和联合体union(联合)有啥区别呢?

C89:论结构体/枚举体/联合体的使用

C 联合体

结构体嵌套联合体字节对齐问题

详解C语言结构体枚举联合体

详解C语言结构体枚举联合体