a. 结构体类型的声明
结构是一些值得集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量
struct tag
{
member-list;
}variable-list;
例如描述学生
struct Student
{
// 成员变量
char name[20];// 姓名
char tele[12];//电话
char sex[10];// 性别
int age;// 年龄
};// ;不可以省略
如何使用:创建的结构体变量
struct Student s1;
也可以在声明的时候创建变量,但是是全局变量,如下:
struct Student
{
// 成员变量
char name[20];// 姓名
char tele[12];//电话
char sex[10];// 性别
int age;// 年龄
} s4, s5, s6; // 这是全局变量
匿名结构体类型(不建议使用)
struct
{
int a;
int b;
float c;
}x;// 因为没有结构体名字,因此创建变量只能这样创建
b. 结构体的自引用
以链表为例
struct Node
{
int data;
struct Node n;
};
// 上面的定义是有问题的,因为如果此时
sizeof(struct Node);
// 这里怎么求Node所占空间呢,嵌套着定义是计算不出占用空间的,未来是不能创建这种变量的
因此,虽然结构体中可以声明各种类型的成员变量,但是不能声明自己本身的结构体成员变量
那如何定义链表呢?
虽然我们不能定义结构体变量,因为结构体变量的大小是无法确定的。但是指针变量的大小是确定的,都是4个字节。所以我们 可以定义结构体变量指针,用于指向下一个节点的位置。
struct Node
{
int data;
struct Node* next;
};
注意点:
有时候我们经常使用typedef
来简化结构体的声明;但是在这里最好还是不要简化;
typedef struct Node
{
int data;
//Node* next; 这里其实是还没有对这个结构体重命名为Node,就使用了Node这个简化的名称。所以这个struct是不可以省略的
struct Node* next;
}Node;
// 另一种情形是,匿名结构体
tepedef struct
{
int data;
Node* next;// 这里和上面原因是一样的,并且这个Node是个匿名结构体
}Node;
所以我们在搞不清楚的情况下,最好不要偷懒省略这些
c. 结构体变量的定义和初始化
初始化:定义变量的同时赋初值
struct Point
{
int x;
int y;
}p1; // 声明类型的同时定义变量p1
struct Point p2; // 定义结构体变量p2
struct Point p3 = {x, y}; // 初始化
d. 结构体内存对齐
struct S1
{
char c1;
int a;
char c2;
};
struct S2
{
char c1;
char c2;
int a;
};
sizeof(s1); // 12
sizeof(s2); // 8
如何计算结构体内存大小呢?
结构体对齐规则
第一个成员在与结构体变量偏移量为0的地址处。也就是说结构体变量的所占空间最开始的空间就是第一个成员变量。(结构体变量从哪开始,第一个成员就放哪)
其他成员变量要对其到某个数字(对齐数)的整数倍的地址处
对齐数
编译器默认的一个对齐数与该成员大小的较小值。VS中默认的值为8。对于数组来说,对齐数是数组中元素的大小,而不是数组的总大小。
结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
上面的文字可能不好理解,现在使用图解的方法来说明
s1结构体变量的大小
s2结构体内存大小
需要注意的是,这里是在vs编译器中,默认对齐数是8,而在gcc中,是没有默认的这个数字的!!!这里的较小值就是其本身的大小
嵌套结构体的计算
struct S3
{
double d; // 对齐数是8
char c; // 对齐数是1
int i; // 对齐数是4
}; // 根据上面不难算出这里的S3的大小是16
struct S4
{
char c1;
// 这里嵌套了结构体,根据上面的规则4,如果有嵌套结构体
// 那么这里的对齐数,是嵌套的这个结构体s3里面成员变量的最大对齐数
// 而不是整个s3所占用的空间大小!!!
struct S3 s3;
double d;
};
为什么存在内存对齐????
平台原因(移植原因)
不是所有的硬件平台都能访问任意地址上的任意数据的;
某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
比如,有些平台规定取整形数据,我们只能在4的倍数上取,如果不是4的倍数,就异常
性能原因
数据结构(尤其是栈),应该尽可能的在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;
而对齐的内存访问仅仅需要一次访问。
比如在32位机器上,有32根地址线,也就是说有32根数据线。在传输数据的时候,一次可以读取4个字节。如果此时不按照对齐方式
struct S { char c; int a; }
对于成员变量
a
来说c 01 00 00 | 00 00 00 00
就会被隔开了,我们需要先读取c 01 00 00
找到a
的前半部分01 00 00
,再读取一次找到后半部分00
总体来说:
结构体的内存对齐是拿空间换时间的做法。
如何设计成员变量占用更少的空间
让占用空间小的成员尽量集中在一起
修改默认对齐数
#pragma
这个预处理命令,就可以修改默认对齐数#include <stdio.h> #pragma pack(4) // 设置默认对齐数是4 struct s { char c; double d; }; #pragma pack() // 取消设置的默认对齐数 也就是这个范围内是4 // 更改后的大小是16 -- > 12
offsetof
计算成员变量的偏移量。这个不是函数,是宏
size_t offsetof (structName, memberName)
使用前引入头文件stddef.h
printf(offsetof(struct s, c));
就打印出来了其偏移量。
e. 结构体传参
struct S
{
int a;
char c;
double d;
}
struct S s = {0};
// s.a = 100;
// s.c = 'w';
// s.d = 3.14;
Init(s); // 实现初始化函数,完成上面的赋值
// 如果使用的是结构体来接收,如下这样
void Init(struct S tmp)
{
tmp.a = 100;
tmp.c = 'w';
tmp.d = 3.14;
}
// 并不能修改s的内容,因为是值传递,只是对s的一个临时拷贝。相当于创建了一个副本进行操作。
// 调试时可以看出 &s 和 &tmp 地址是不一样的
// 如果函数内部想改变函数外部的值,就要传地址
void Init(struct S* ps)
{
ps->a = 100;
ps->c = 'w';
ps->d = 3.14;
}
// 调用时传递地址
Init(&s);
// 另外对于打印函数,不需要改变结构体内部内容的时候,传递值也是可以的。
平时我们使用结构体时,最好使用传递地址的形式,因为结构体一般占用内存空间比较大,时间空间效率比较低
另外,函数传参时,参数需要压栈。(参数传递是从右向左传的)。先为形参开辟空间,拷贝实参的数据进去,然后为该函数开辟栈空间。如果结构体过大,为形参开辟空间,压栈的系统开销比较大,会导致性能下降