目录
前言
结构体的类型,使用结构体和结构体内存分配
1.结构体的声明
2.特殊的结构体声明
3.结构体的自引用
4.结构体类型名重定义
⭐️那么我们怎么去重定义结构体类型呢?
⭐️带有嵌套的结构体重定义
5.结构体变量的定义和初始化
⭐️结构体逐语句初始化
6.结构体的输出
7.结构体占用内存大小的计算(内存对齐)
⭐️结构体内存对齐的规则
8.修改默认对齐数
9.结构体传参
什么是结构体呢?
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
为什么要有结构体?
因为单一的数据没有办法描述我们想要描述的复杂对象,所以就出现了自定义类型结构体。比如我想要描述一个人,光有姓名是不够的,还有年龄,联系方式,性别,住址等信息来描述一个人;这些信息的数据类型不一定是一样的,而这些信息的集合就是我们用来描述复杂对象的结构体;
struct tag //结构体的标签
{
member-list;//结构体的成员变量
}variable-list;//结构体的变量
例如对一个人的描述
struct Student
{
char name[20];
int age;
char sex[10];
}stu1;//这里的stu1是全局变量
int main()
{
struct Student stu2;//这里的stu2是局部变量
return 0;
}
在声明结构的时候,可以不完全的声明
比如:
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}*p;
匿名的结构体声明,结构体的标签被省略了。这个结构体的变量只能创建在这个结构体后面且这个结构体只能用一次了;
这两个结构体都是匿名结构体类型,两个结构体的成员变量也是一模一样的。那么这两个结构体到底是不是同一个结构体呢?在VS的编译器下,编译器会不会将它们当作同一个结构体呢?能不能将结构体变量x的地址赋值给结构体指针p呢(p = &x;)?
警告:
编译器会把上面的两个声明当成完全不同的两个类型。
所以是非法的
结构体的自引用就是自己调用自己;我们先来看看下面这段代码;
struct Node { int a; struct Node next; };
我创建了一个结构体Node,结构体里面又嵌套了一个Node;
这个是不是结构体的自引用呢?
如果是,那么Node的大小是多少呢?
我们发现结构体内部嵌套一个结构体是无法计算结构体的大小的。因为我们不知道Node结构体占用多大内存空间,而里面又嵌套一个不确定大小的结构体,这样根本没办法确定这个结构体大小,说明这是一个错误的代码;
那么应该如何正确的结构体自引用呢?
结构体自引用的定义:
结构体的 自引用 (self reference) ,就是在结构体内部,包含指向自身类型结构体的指针。
struct Node
{
int date;
struct Node *next;
};
为什么结构体里面包含的指向自身类型结构体的指针就可以呢?
因为在特定的操作系统下,指针的大小是确定的,指针存放的就是地址。在x86(32位)环境下,存放内存地址需要占用4个字节,也就是32个比特位;在x64位的环境下,存放内存需要占用八个字节,也就是64个比特位;
结构体成员变量的大小都可以确定了,那么我们这个结构体的大小也就可以确定了;
typedef是用来重定义类型名的;
比如:
typedef unsigned int size;
这就是无符号整形的重定义;unsigned int 在后面代码使用的过程中就不用那么复杂,size就代表了无符号整形;
格式: typedef [需要重定义的类型] [重定义后类型的名称];
⭐️那么我们怎么去重定义结构体类型呢?
typedef struct Node { int a; char b; }Node;
上面的代码就是重定义结构体类型
⭐️带有嵌套的结构体重定义:
这是错误的
typedef struct Node { int data; Node* next; }Node;
这是正确的
typedef struct Node { int data; struct Node* next; }Node;
错误的代码里面,编译器还没识别到你重定义这个结构体类型,但是你却使用了,所以这是不行的;
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
struct Point p3 = {x, y};//初始化:定义变量的同时赋初值。
struct Stu //类型声明
{
char name[15];//名字
int age; //年龄
};
struct Stu s = {"zhangsan", 20};//初始化
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化
int main()
{
struct Point p4;//局部变量
}
上面代码中的变量,main函数里面的是局部变量,main函数外面的都是全局变量;
注意:结构体变量定义和结构体声明可以同时进行也可以分开进行,关键的是:同时进行的时候定义的结构体变量是全局变量;
struct Student
{
char name[20];
int age;
};
struct Student1
{
char* name;
int age;
};
int main()
{
//给结构体Student创建对象n1,逐语句赋值
struct Student n1 ;
strcpy(n1.name,"张三");
n1.age = 18;
//给结构体Student1创建对象p1,逐语句赋值
struct Student1 p1;
p1.name = "李四";
p1.age = 20;
return 0;
}
上面代码中结构体Student的char name[20];逐语句赋值的时候是不可以写成n1.name=“ ”;这种形式的,看到好多其他的博客上有这样子去写,但是自己去试了一下是不行的,要用strcpy函数来修改或者赋值。
因为name是一个数组,数组名就是数组的首元素地址,地址里面存放一个字符串是不对的,如果name是一个指针,指针指向字符串的首字母地址,那么就可以找到整个字符串,所以指针的话就可以这样子写;
struct Student
{
char name[20];
int age;
char class[20];
};
int main()
{
struct Student stu1 = { "zhw",21,"电子信息工程三班" };
//结构体变量输出
printf("%s %d %s",stu1.name,stu1.age,stu1.class);
printf("\n");
//指针输出两种形式
struct Student* ps = &stu1;
printf("%s %d %s", (*ps).name, (*ps).age, (*ps).class);
printf("\n");
printf("%s %d %s", ps->name, ps->age, ps->class);
return 0;
}
代码运行结果:
这三种方式都可以将结构体类型数据打印出来;
结构体内存大小是如何计算的呢?我们来看下面的代码:
//结构体类型内存对齐
struct student
{
char a;
char b;
int c;
};
struct student1
{
char a;
int c;
char b;
};
int main()
{
printf("%d\n%d", sizeof(struct student), sizeof(struct student1));
return 0;
}
请问上面的结果输出的结果是什么呢?
struct student 的类型占用内存大小是1+1+4吗?
struct student1的类型占用内存大小是1+4+1吗?
我们看下运行的结果:
student和student1的成员变量是一样的,但是内存占用大小却有所差异;为什么会是8和12?这个是怎么来的呢?下面我给大家分析分析
这里就涉及到结构体内存对齐的知识点;
结构体内存对齐有4条规则:
1.结构体的第一个成员直接对齐到相对于结构体变量起始位置为0的偏移处。
2.从第二个成员开始,要对齐到某个【对齐数】的的整数倍的偏移处。
对齐数:结构体成员自身大小和默认对齐数的较小值
在vs环境下,默认对齐数是8;
在linux环境下不设默认对齐数(对齐数是结构体成员的自身大小)。
3.结构体的总大小必须是最大对齐数的整数倍。
结构体每个成员都有一个对齐数,其中最大的对齐数就是最大对齐数。
4.如果嵌套了结构体的情况
嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
看规则可能看得有点懵,,那就看看下面的图解吧~
上图结构体struct student占用内存空间的图解;
1.char a 是结构体的第一个成员,放在0偏移处;(第一条规则)
2.char b 占用的大小是1个字节, 默认对齐数是8,1<8,所以对齐数的大小是1,需要放在1的整数倍偏移处。所以放在偏移量为1的地方;(第二条规则)
3.int c 占用的空间是4个字节,默认对齐数是8,因为4<8,所以对齐数是4,需要放在4的整数倍偏移处。偏移2,3都不是4的整数倍(2,3空间就被浪费掉了),所以int 从4偏移处开始存;4 5 6 7就是存放int c占用的空间;(第二条规则)
结构体整体占用的是0~7,所以是8个字节
上图结构体struct student1占用内存空间的图解;
1.char a 是结构体的第一个成员,放在0偏移处;(第一条规则)
2.int c 占用的空间是4个字节,默认对齐数是8,因为4<8,所以对齐数是4,需要放在4的整数倍偏移处。偏移1,,2,3都不是4的整数倍(1,2,3空间就被浪费掉了),所以int 从4偏移处开始;4 5 6 7就是存放int c占用的空间;(第二条规则)
3.char b 占用的大小是1个字节, 默认对齐数是8,1<8,所以对齐数的大小是1,需要放在1的整数倍偏移处,所以可以放在8偏移处;(第二条规则)
4.由于0~8占用了9个字节,9不是最大对齐数4的倍数,所以要继续开辟空间;0~11占用了12个字节,是最大对齐数4的倍数,所以结构体整体大小是12;(第三条规则)
这里的第四条规则没用上,但是也是以此类推的;
⭐️为什么存在内存对齐?
大部分的参考资料都是如是说的:
1. 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说:
结构体的内存对齐是拿空间来换取时间的做法那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起
#include
#pragma pack(8)//设置默认对齐数为8
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)//设置默认对齐数为1
struct S2
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
//输出的结果是什么?
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}
用#pragma pack()预处理指令来修改默认对齐数,一般修改的数是1,2,4,尽量不要修改一些奇奇怪怪的数,另外不能修改成0;0到底是要怎么对齐呢,编译器都不知道了;
struct S
{
int data[1000];
int num;
};
struct S s = {{1,2,3,4}, 1000};
//结构体传参
void print1(struct S s)
{
printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{
printf("%d\n", ps->num);
}
int main()
{
print1(s); //传结构体
print2(&s); //传地址
return 0;
}
一般情况下用地址传参,指针接收会比较好;
因为像第一种传参,形参是实参的一份临时拷贝。
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
麻烦给博主点个赞,给个关注吧~蟹蟹!!!麻烦给博主点个赞,给个关注吧~蟹蟹!!!
麻烦给博主点个赞,给个关注吧~蟹蟹!!!麻烦给博主点个赞,给个关注吧~蟹蟹!!!