一、结构体结构体类型的声明结构的自引用结构体变量的定义和初始化结构体内存对齐结构体传参
一、结构体
1.1 结构体的声明
结构体是一些值的集合,这些值称为结构体的成员变量,这些成员变量可以是不同类型的数据,这与数组就有了差异。
下面是结构体声明的方式。
struct S//S叫做结构体的tag 也就是标签
{
//成员列表
}//变量列表;
举个例子,假设要创建描述一个人的结构体。
struct people
{
char name[20];
int age;
double height;
double weight;
}zhangsan;
当然,你不一定要在创建结构体时就创建变量,但注意变量列表后面的分号不能丢,即使你没有创建变量。
struct people
{
char name[20];
int age;
double height;
double weight;
}zhangsan;
int main()
{
struct people wangwu;
return 0;
}
在main函数内部创建变量也没有问题
如果你觉得创建变量时总是带着struct和tag太麻烦 可以用类型重命名,例如:
typedef struct people
{
char name[20];
int age;
double weight;
double height;
}people;
int main()
{
people wangwu;
return 0;
}
这里用typedef重新命名结构体为people 然后直接用people创建了一个wangwu变量,这样是合法的。
可能有人会问,直接省略掉struct的tag——people可以吗?像这样:
struct
{
char name[20];
int age;
double weight;
double height;
};
这样也是可以的,这种结构体叫做匿名结构体,匿名结构体如果要创建变量一定要在创建结构体的同时声明变量。
在主函数内部做不到创建变量,而且匿名结构体的变量一般只使用一次,两个成员类型完全相同的结构体VS仍然认为它是不同类型的结构体。
struct
{
int a;
char b;
float c; }x;
struct
{
int a;
char b;
float c;
}a[20], *p;
p = &x;//这是非法的,因为两个匿名结构体是不同类型。
1.2结构体的自引用
先提出一个问题:这样的代码合法吗?如果合法,sizeof(struct Node)又是多少?
struct Node
{
int data;
struct Node next;
};
这是非法的创建方式,因为struct Node的大小依赖于next的大小,next的大小也依赖于struct Node它们两个是互相影响的关系,如果非要计算会陷入一个死循环,无线套娃。所以这个结构体的大小是未知的。
正确的方式:
struct Node
{
int data;
struct Node* next;
};
通过创建结构体指针类型的变量,这个变量的大小是确定的。
1.3结构体变量的初始化
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};//结构体嵌套初始化
1.4结构体的大小计算——内存对齐
先阐述一下内存对齐的规则:
1.结构体的第一个成员总在偏移量为0的位置处。
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的 较小值 。VS默认的对齐数是 8,linux环境下没有默认对齐数,成员自身的大小就是它的对齐数。3.结构体整体的大小对齐到最大成员大小的整数倍处
4.如果出现了嵌套的结构体,内部结构体对齐到自身最大成员大小的整数倍处,
整个结构体的大小对齐到所有(包括内部嵌套的结构体)最大对齐数的整数倍处。
第二条规则中的对齐数就是将成员自己的大小和默认的对齐数比较,较小值的整数倍就是当前这个成员要对齐到的位置。
第三条规则的意思是,所有的成员与默认对齐数比较后得到的所有对齐数中,得到一个最大值,结构体大小要对齐到它的整数倍处。
比如:
struct s
{
char p;
int i;
};
第一个成员char总是在偏移量为0的位置,它占了1个字节的空间
而第二个成员int的大小是4个字节,它和默认对齐数8比较得到的较小值是4,从而它要对齐到偏移量为4的整数倍处,如图。
这就是这个结构体的内存分布,偏移量为1~3位置的内存就被浪费了,从而这个结构体的大小为8个字节。
1.5内存对齐的作用
1. 平台原因 ( 移植原因 ) :不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。2. 性能原因 :数据结构 ( 尤其是栈 ) 应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
意思就是,有些硬件平台上一次性只读取特定长度的数据,比如32位平台上一次在内存中读取四个字节也就是32个bit位的数据,如果不考虑内存对齐的话,会造成一种数据的二次读取,浪费时间,像这样:
一次读取只读到了char和int的前三个字节,要完整的读到int还需要读取一次。
如果内存对齐了,时间就会减少很多,也就是说内存对齐是一种用空间换取时间的方式。
NOTE:在vs中你可以选择修改默认的对齐数,这需要用到一个宏#pragma pack(修改后的对齐数)
1.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;
}
如果形参以结构体的形式接收,在函数中访问结构体的对象要用‘.’,比如s.age或是s.height
如果形参以结构体的指针形式接收,在函数中访问结构体对象用箭头,比如s->age
但两个传参方式我们要选择哪一种呢?答案是选择第二种。
因为函数在传参的过程中参数是需要压栈的,如果传递的是结构体,