前言
设计程序时,最重要的步骤之一是选择表示数据的方法。在许多情况下,简单变量甚至是数组还不够。为此,C提供了结构变量(structure variable)提高你表示数据的能力,它能让你创造新的形式。在学习数据结构之前,C中的结构体必然是要掌握的,不然会非常的难受。
一、结构体的声明
- 结构声明描述了一个结构的组织布局。
- 建立结构声明
struct tag
{
member-list;
}variable-list;
例如:
struct Stu
{
char name[20];
int age;
char sex[5];
char id[20];
}; //这里的分号不能丢
上面的例子是用来描述一个学生的基本情况,该声明描述了一个由三个字符数组和一个 int 类型变量组成的结构。该声明并未创建实际的数据对象,只描述了该对象由什么组成。(有时,们把结构声明称为模板,因为它勾勒出结构是如何存储数据的。)
我们来分析一些细节,首先是关键字 struct ,它表明跟在其后的是一个结构,tag 是一个可选的标记(该例子中是Stu),稍后程序中可以使用该标记引用该结构。(也就是进行结构体的定义)
在结构声明中,用一对花括号括起来的是结构成员列表。里面的成员变量都用自己的声明来描述。右花括号后面的分号是声明所必需的,表示结构布局定义结束。
- 特殊的声明
在声明结构的时候,可以不完全的声明
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}*p;
上面的两个结构在声明的时候省略掉了结构体标签(tag)。那么问题来了:在上面代码的基础下,下面的代码合法吗?
p = &x;
显而易见,编译器报出了警告,发出警告的原因是编译器会把上面的两个声明当成完全不同的两个类型,所以是非法的。
- 结构的自应用
在结构中包含一个类型为该结构本身的成员是否可以呢?下面先演示一下错误示范
struct Node
{
int data;
struct Node next;
};
为什么上面的是错误的呢,因为在结构体中有个成员变量是结构体,但是还没定义就已经使用了,编译器根本不指定它的存在。
那么正确的自引用又是什么样的呢?
struct Node
{
int data;
struct Node* next;
};
相信正在学数据结构的小伙伴对这个一定非常眼熟,这个结构中有一个成员变量是结构体指针 next ,这个结构体指针指向的是具有结构体类型的一块空间。
二、结构体变量的定义和初始化
- 定义结构变量
结构有两层含义。一层含义是“结构布局”,刚才已经讨论过了。结构布局告诉编译器如何表示数据,但是它并未让编译器为数据分配空间。那么下一步就是创建一个结构变量,即使结构的另一层含义。
struct Point
{
int x;
int y;
};
struct Point p;
编译器在执行这行代码时便创建了一个结构变量 p ,编译器使用 Point 模板为该变量分配空间。在结构体变量的定义中,struct Point 所起的作用相当于一般声明中的 int 或 float 。从本质上看,Point 结构声明创建了一个名为 struct Point 的新类型。
就计算机而言,上面的声明其实是以下声明的简化。换言之,声明结构的过程和定义结构变量的过程可以组合成一个步骤。
struct Point
{
int x;
int y;
}p;
- 初始化结构
既然有了结构体变量,那么我们是否也可以进行初始化呢?
当然可以,不然我们怎么使用结构体嘞。
struct Stu
{
char name[15];
int age;
};
struct Stu s1 = { "zhangsan",20 };
struct Stu
{
char name[15];
int age;
}s2 = {"lisi",19};
上面的代码就是在定义结构体的时候进行初始化以及在声明类型的同时进行定义和初始化。
还有一种特殊的初始化,那就是结构体嵌套初始化
struct Node
{
int data;
struct Stu s3;
};
struct Node n = { 20,{"wangwu",18} };
一个结构体中的成员变量是另一个结构体,那么也可以对齐进行嵌套定义。
三、访问结构成员
- 结构体变量访问成员
结构变量的成员是通过点操作符(.)访问的。点操作符接受两个操作数。左操作数是结构体变量名,右操作数是结构体成员变量。
struct Stu
{
char name[15];
int age;
}s = { "zhangsan",20 };;
int main()
{
printf("%s %d", s.name, s.age);
return 0;
}
- 结构体指针访问指向变量的成员
有时候我们得到的不是一个结构体变量,而是指向一个结构体的指针。
那该如何访问成员。
struct Stu
{
char name[20];
int age;
};
void print(struct Stu* ps)
{
printf("name : %s age : %d\n", (*ps).name, (*ps).age);
//使用结构体指针访问指向对象的成员
printf("name : %s age : %d\n", ps->name, ps->age);
}
int main()
{
struct Stu s = { "zhangsan", 20 };
print(&s);//结构体地址传参
return 0;
}
使用 -> 运算符,该运算符由一个连接号后跟一个大于号组成。指向结构的指针后面的 -> 运算符和结构变量名后面的 . 运算符的工作方式相同。
四、结构体传参
- 函数的参数把值传递给函数。每个值都是一个数字-----可能是 int 类型、float 类型,可能是 ASCII 字符码,或者是一个地址。然而,一个结构比一个单独的值复杂,所以难怪以前的C实现不允许把结构作为参数传递给函数。当前的实现已经移除了这个限制,ANSI C 允许把结构作为参数使用。所以程序员可以选择是传递结构本身,还是传递指向结构的指针。
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;
}
上面的 print1 和 print2 函数哪个好些?
答案是:首选print2函数。
原因:
函数传参的时候,参数是需要压栈的。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
结论:
结构体传参的时候,要传结构体的地址。
五、结构体内存对齐(难点)
-
计算结构体的大小是一个特别热门的考点:结构体内存对齐
(补充)
对齐数:编译器默认的一个对齐数与该成员大小的较小值。(VS中默认的值为8)(gcc环境下,没有默认对齐数,对齐数就是成员自身大小) -
首先得掌握结构体的对齐规则:
1、 结构体的第一个成员永远都放在0偏移处。
2、从第二个成员开始,以后每个成员都要对齐到某个对齐数的整倍处(偏移量)。
3、当成员全部存放进去后,结构体的总大小必须是所有成员的对齐数的整数倍,如果不够,则浪费其空间。
4、如果嵌套了结构体,嵌套的结构体要对齐到自身成员的最大对齐数的整数倍处,整个结构体的大小,必须是最大对齐数的整数倍,最大对齐数包含嵌套的结构体成员中的对齐数。 -
例1:
struct S1
{
char c1;
int i;
char c2;
};
char 的对齐数是1,int 的对齐数是4,由于 int 位于两个 char 的中间,因为结构体的总大小必须是所有成员的对齐数的整数倍,所以最后的结果是12,根据上图很明显可以看到这样做浪费了一半的空间。
- 例二:
struct S2
{
double d;
char c;
int i;
};
这个例子和上个例子的区别在于double的对齐数是8,再按照对齐规则来算就好了。下面,我们来看看结构体嵌套问题。
- 例三:
在结构体中嵌套了上个例子的结构体,那这样的结构体大小怎么算呢
struct S3
{
char c;
struct S2 s3;
double d;
};
因为对齐数是编译器默的一个对齐数与该成员大小的较小值,所以在VS中,当大小大于8,那么其对齐数就是8,再根据规则便可简单算出了。
-
为什么存在内存对齐?
1、平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2、性能原因:
数据结构(尤其是栈)应该是尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说:结构体的内存对齐是在拿空间来换取时间的做法。 -
那在设计结构体的时候,我们既要满足对齐,又要节省空间,那如何可以做到呢?
答案是让占用空间小的成员集中在一起。
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char C1;
char C2;
int i;
};
S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有一些区别,让占用空间小的成员尽量集中在一起,就是尽量让同类型的成员变量在一起。
- 修改默认对齐数
结构在对齐方式不合适的时候,我们可以自己更改默认对齐数。
#include <stdio.h>
#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;
}
总结
C结构提供在相同的数据对象中存储多个不同类型数据项的方法。可以使用标记来标识一个具体的结构模板,并声明该类型的变量。通过成员点运算符( . )可以使用结构模板中的标签来访问结构的各个成员。
如果有一个指向结构的指针,可以用该指针和间接成员运算符(->)代替结构名和点运算符来访问结构的各成员。和数组不同,结构名不是结构的地址,要在结构名前使用 & 运算符才能获得结构的地址。
一贯以来,与结构相关的函数都使用指向结构的指针作为参数。现在的C允许把结构作为参数传递,作为返回值和同类型结构之间赋值。然而,传递结构的地址通常更有效。