背景
针对后面学习数据结构时对结构体概念的模糊,重新对模糊概念进行系统的梳理。
1 结构体声明(创建)
1.1 结构体基础知识
结构体是一些值得集合,这些值称为成员变量,结构体中每个成员变量可以是不同的类型,这也是结构体与数组的最大区别。
1.2 结构体声明
struct tag // 自定义类型名 相当于int double等等(tag可以省略)
{
member-list; //成员列表 可以是不同类型的变量
}variable-list; //定义(创建)结构体对象(变量)
例如描述一个学生
struct stu
{
char name[20]; //姓名
int age; //年龄
char sex[3]; //性别
char id[20]; //学号
}; //分号一定不能丢!!!!!
2 结构体对象(变量)的定义与初始化
2.1 不使用typedef创建结构体对象
- 如以下代码声明struct point,struct stu类型结构体,定义(创建)结构体对象或者在变量列表定义(创建)struct stu类型结构体对象时对其初始化,并且定义(创建)了struct Stu * p类型的结构体指针以及struct Stu S[20]的结构体数组。
struct point //结构体类型
{
int x;
int y;
};
struct Stu
{
int age;
char name[10];
struct point x_y; //嵌套的结构体
struct Stu* next;
}stu = { 10,"张三",{1, 2}, NULL }, * p, S[20]; //声明类型的同时定义了结构体变量stu,结构体指针变量p,结构体数组S
//并且对嵌套的结构体stu进行了初始化
int main()
{
struct Stu s1; //定义结构体变量s1
struct Stu s2 = { 20,"李四" ,{4, 5}, NULL }; //定义结构体变量的同时,对其初始化
s1.age = 10; //用.的方式对成员变量逐一赋值
//s1.name = "张三"; //不可这样,怎么可以将字符串赋给指针呢?这里可以使用strcpy()进行赋值
p = &stu;
printf("%d\n", s1.age);
printf("%d\n", stu.age);
printf("%s\n", stu.name);
printf("%d\n", p->age); //通过指向stu的结构体指针p
printf("%d %d\n", p->x_y.x, p->x_y.y);
}
- 如以下代码匿名结构体类型(省略了tag),在我们声明一个结构体的时候,可以不完全的声明。这种方式创建结构体对象的时候只能在变量列表创建。
struct
{
int age;
char name[10];
}stu1 = {10,"张三"},stu2; //声明结构体同时,定义了结构体变量stu1,stu2
int main()
{
stu2.age = 30;
printf("%d\n", stu1.age);
printf("%d\n", stu2.age);
}
2.2 使用typedef创建结构体对象
如以下代码typedef在结构体声明中的作用是:
(1)为匿名的结构体类型和结构体指针类型起别名stu1,p1。
(2)为 struct tag与 struct tag*分别起别名为stu2,p2。即stu2与p2实际上就是结构体类型与指针类型。
这样后面使用该结构体类型与结构体指针类型的时候,更加的简洁。
其中 struct tag == stu2
struct tag * == p2
typedef struct
{
int age;
char name[10];
int num[10];
}stu1, *p1;
typedef struct tag //其中tag可以省略,结果一样
{
int age;
char name[10];
int num[10];
}stu2, *p2;
int main()
{
stu1 S1 = { 10,"李四",{3,4,5,6} };
p1 h1 = &S1;
stu2 S2 = { 30,"张三",{4,5,6,7} }; // struct tag == stu
p2 h2 = &S2; //struct tag* == p; 一定要注意呀
printf("%d\n", S1.num[1]);
printf("%d\n", S2.num[1]);
printf("%s\n", S1.name); //name为数组名,表示数组的起始地址
printf("%s\n", S2.name);
printf("%d\n", h1->num[1]);
printf("%d\n", h2->num[1]);
}
当我们省略tag时,打印结果一样,但是这样写的话我们后面不能使用struct tag当结构体类型构建对象(因为这个结构体时匿名的,没有名字(tag)),只可用该结构体别名stu构建对象。stu 和 *p在这里的作用就是为这个匿名的结构体类型与结构体指针类型起了一个名字。
2.3 结构体中关于对结构体成员字符数组赋值问题的理解
如以下案例在我们对数组进行初始化的时候我们是对char name[20]初始化,没有直接对name进行初始化,因为如第二个案例中s.name = "张三"这种初始化方式就是错误的方式。因为name为数组名,数组名为数组首元素的地址,我们应该是想将“张三”赋值给name所指向的这块空间中,不能直接赋值,这里采用了strcp()函数对其进行赋值。
***//案例一***
struct stu
{
char name[20];
int age;
};
int main()
{
struct stu s = { "张三",20 }; //直接对其初始化 可以将“张三传给char name[20]”
printf("%s", s.name);
return 0;
}
*//案例二*
struct stu
{
char name[20];
int age;
};
int main()
{
struct stu s = { 0 };
t->age = 20;
//t->name[0] = 'z';
//t->name= "张三"; //这么写是错误的 name是数组名 数组名是数组首元素的地址,怎么可以把“张三放到地址里面去”
//应该是把数组放到这块地址所在的空间里面去
// t.name[10] = "张三"这样也不行 name[10]是指的name数组中的第十个元素
//将字符串放入字符数组可以用字符串拷贝函数
strcpy(t->name, "张三");
printf("%d %s", t.age, t.name); //t.name表示其首地址,故可以直接用%s打印
return 0;
}
3 结构体自引用与互引用
3.1 结构体自引用不使用typedef
错误方式
如下图所示,在结构体中包含一个类型为该结构体本身的成员可否?
如果可以这样定义,那么这个结构体大小为多少呢?
struct Node
{
int data;
struct Node next; //
};
由于struct Node结构体中包含int data和struct Node next,其结构体成员struct Node next中又会包含int data和struct Node next,这样我们可以一直无限循环下去。所以这个结构体的大小是未知的,在分配内存的时候,由于无限嵌套,也无法确定这个结构体的长度,所以这种方式是非法的。
正确方式(使用指针)
struct Node
{
int data;
struct Node* next; //
};
由于指针的大小是确定的(32位机器上指针大小为4,64位机器上指针大小为8),所以不会出现以上无限嵌套的情况。
3.2 结构体自引用使用typedef
错误方式
//第一种错误
typedef struct Node
{
int data;
Node* next; //struct Node* next
}Node;
//第二种错误
typedef struct
{
int data;
Node* next; //怎么也不行,
}Node; //
以上两种结构体自引用的时使用的虽然都是指针,但是我们是先声明(创建)结构体struct Node,然后进行typedef。故创建结构体成员next(类型为Node),结构体类型Node是未定义的。,所以我们在结构体自引用的时候使用typedef,不可以在声明结构体时使用别名来构建结构体成员!!!。
正确方式
typedef struct Node
{
int data;
struct Node* next; //struct Node* next
}Node;
3.2 结构体的互引用
错误方式
typedef struct tag_a{
int value;
B *bp; /* 类型B还没有被定义 */
} A;
typedef struct tag_b{
int value;
A *ap;
} B;
错误原因:对于A结构体而言,B结构体还没声明就被使用。同理B结构体也一样。
正确方式
在 struct tag_a结构体中使用 struct tag_b 结构体类型时,需提前使用struct tag_b结构体的不完整声明
struct tag_b; /* 使用结构体的不完整声明(incomplete declaration) */
struct tag_a
{
struct tag_b *bp;
int value;
}A;
struct tag_b{
struct tag_a *ap;
int value;
}B;
4 结构体内存对齐(重点)
4.1内存对齐规则
如何计算一个结构体大小,首先要知道结构体对齐规则
结构体对齐规则:
-
第一个结构体成员在该结构体变量偏移量为0的地址处。
-
其他成员变量大小(结构体第二个成员变量及以后)要对其到==某个数字(对齐数)==的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数与该结构体成员类型大小的较小值。
vs中默认对齐数为8
Linux中没有默认对齐数,对齐数就是自身成员的大小 -
结构体的总大小为最大对齐数(每个成员变量都有一个对齐数,取齐数的最大值)的整数倍。
-
如果嵌套了结构体的情况,嵌套的结构体大小对齐到自己的最大对齐数的整数倍处,被嵌套的结构体对齐数为自身结构体中的最大对齐数。(如结构体S2中嵌套结构体S1,则按照S1的自身的最大对齐数(最大元素类型)和S2中各元素类型取较大者对齐)
结构体大小计算具体案例
案例一
//练习1
struct s1
{ //结构体成员类型大小/默认对齐数/对齐数
char c1; //1/8/1
int i; //4/8/4 //对齐到4的倍数
char c2; //1/8/1
};
printf("%d\n",sizeof(struct s1)); //12
c1为char类型,大小为1个字节,默认对齐数为8,其大小与1的倍数对齐,故c1位于0偏移处
i为int类型,大小为4个字节,默认对齐数为8,其大小与4的倍数对齐,故i位于4~7偏移处
c2为char类型,大小为1个字节,默认对齐数为8,其大小与1的倍数对齐,故c2位于8偏移处
而结构体的总大小为最大对齐数(4)的整数倍,由图中可知,为12
案例二
//练习2
struct s1
{ //结构体成员类型大小/默认对齐数/对齐数
char c1; //1/8/1
char c2; //1/8/1 //对齐到4的倍数
int i; //4/8/4
};
printf("%d\n",sizeof(struct s1)); //8
案例三
//练习3
struct s1
{ //结构体成员类型大小/默认对齐数/对齐数
short c1[10]; //2/8/2
char c2; //1/8/1
int i; //4/8/4 最大对齐数为4
};
printf("%d\n",sizeof(struct s1)); //28
c1为short类型,大小为2个字节,默认对齐数为8,其大小与2的倍数对齐,故c1位于0~19偏移处
c2为char类型,大小为1个字节,对齐数为1对齐,其大小与1的倍数对齐,故c2位于20偏移处
i为int类型,大小为4个字节,默认对齐数为8,其大小与4的倍数对齐,故故i位于24~27偏移处
而结构体的总大小为最大对齐数(4)的整数倍,由图中可知,为28
案例四
struct s2
{ //结构体变量大小/默认对齐数/对齐数
char b1; //1/8/1
char b2; //1/8/1 //对齐到4的倍数
int i; //4/8/4
};//最大对齐数为4 s2大小为8
struct s1
{ //结构体类型大小/默认对齐数/对齐数
short c1[10]; //2/8/2 20
char c2; //1/8/1 1
int j; //4/8/4 4
struct s2 a; //对齐数:4 8 //s2中结构体成员中最大对齐数为4,故s2对齐数为4
};
printf("%d", sizeof(struct s1)); //36
c1为short类型,大小为2个字节,默认对齐数为8,其大小与2的倍数对齐。故c1位于0~19偏移处
c2为char类型,大小为1个字节,对齐数为1对齐,其大小与1的倍数对齐。故c2位于20偏移处
j为int类型,大小为4个字节,默认对齐数为8,其大小与4的倍数对齐,故c1位于24~27偏移处
a为一个结构体变量,大小为8,它的对齐数就是该结构体的最大对齐数4,故c1位于28~35偏移处
而结构体的总大小为最大对齐数(4)的整数倍,由图中可知,为36。
4.2 为什么要内存对齐
1. 平台原因(移植原因)
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因:
数据结构(尤其是栈)应该尽可能的在自然边界上面对齐。原因在于,为了访问未对齐的内存,处理器需要作用两次内存访问;而对齐的内存访问只需要一次访问。
***总体来说
结构体内存对齐就是拿空间换取时间的做法***
所有我们在创建结构体的时候,如果既要满足对齐,又要节省空间,我们应该
让占用空间小的成员尽量集中在一起
例如以下两个结构体s1与s2类型的成员一模一样,但是s1和s2所占用的空间大小去不一样,s1大小位12个字节,s2大小位8个字节。
struct s1
{
char c1;
int i;
char c2 ;
};
struct s2
{
char c1;
char c2;
int i;
};
4.3 修改默认的对齐数
使用预处理指令 #pragma pack(num) ,指定num对齐数的大小可以修改我们的默认对齐数
#include<stdio.h>
#pragma pack(8) //设置默认对齐数为8
struct s1
{
char c1; //1/8 1
int i; //4/8 4
char c2; //1/8 1
};
#pragma pack() //取消设置对齐数,还原为默认对齐数
#pragma pack(1) //设置默认对齐数为1
struct s1
{
char c1; //1/1 1
int i; //4/1 1
char c2; //1/1 1
};
#pragma pack() //取消设置对齐数,还原为默认对齐数
int main()
{
printf("%d\n",sizeof(struct s1)); //12
printf("%d\n",sizeof(struct s2)); //6 //大小为最大对齐数1的整数倍 6
return 0;
}
5 结构体传参
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;
}
对比上面两种我们应该多用传结构体变量的地址及printf2
因为函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。