一.结构体的声明和成员类型
所谓结构,就是一些值的集合,这些值被称为成员变量。结构的每个成员都可以是不同类型的变量,可以是标量、数组、指针,甚至是其他结构体。结构体的声明框架如下:
struct tag//结构体标签名称
{
member-list; //结构中的成员列表
}variable-list;//变量列表
举个简单的例子,假设我们要描述一个运动员的姓名,编号,年龄,性别还有成绩时,可以这样来声明一个结构体。
struct Stu
{
char name[20];//姓名
char id[20];//编号
int age;//年龄
char sex[5];//性别
int result;//成绩
};//切记此处分号不能丢
当然在一些特殊结构体声明的时候,也可以进行不完全的声明,例如:
struct
{
int a;
char b;
float c;
}x;
struct {
int a;
char b;
float c;
}a[20], *p;
这两个结构体在声明的时候都忽略了结构体标签(tag),那么 p = &x 合法吗?答案是不合法。由于在结构体声明时,缺少了标签,只有 struct ,那么编译器就会把上面的两个声明当成完全不同的类型。而这种不完全声明的结构体被称为匿名结构体类型。
二.结构体变量的定义和初始化
我们已经知道了结构体的声明方法,那么结构体又是如何定义和初始化的呢?当有了结构体的类型之后,定义变量其实非常简单。
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};//定义变量s并初始化
在定义和初始化结构体时,也可以进行结构体的嵌套:
struct Point
{
int x;
int y;
};
struct Stu
{
int data;
struct Point p;
struct stu *pp;
}s1 = { 10, { 4, 5 }, NULL }; //结构体嵌套初始化
struct Stu s1 = { 20, { 5, 6 }, NULL };//结构体嵌套初始化
三.结构体成员的访问
结构变量的成员是通过点操作符( . )访问的。点操作符接受左右两个操作数,
在这张图里我们可以看到被定义的结构体 s 有成员 name 和 age ,当访问 s 的成员时就要用到我们的点操作符了。
struct S s;
strcpy(s.name, "zhangsan");//使用.访问name成员
s.age = 20;//使用.访问age成员
切记一点,当我们给字符型的成员进行初始化时,要用到 strcpy 字符拷贝函数,不可以像数字整形一样直接进行赋值。
当我们遇到结构体变量时,用点操作符进行访问。但如果我们只有指向结构体的指针时,又该怎么访问结构体成员呢?
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;
}
如上段代码所展示的,我们可以用 * 进行解引用操作,再通过点操作符来访问结构体成员,用法为 (*结构体指针)(要访问的成员名) 。而更简单的方法则是直接用 -> 指向操作符,其用法为 结构体指针名 -> 要访问的成员名。
四.结构体传参
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); //传地址
system("pause");
return 0;
}
我们能够看到函数 print1 的参数为结构体 s, 而 print2 的参数为结构体 s 的地址,那么那种传参方式更科学呢?答案是第二种。
函数传参时的参数是需要压栈的。 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,就会导致性能的下降,所以我们在给函数传结构体的参数时,最好是传结构体的地址,让函数用结构体指针类型来接收。
五.结构体的自引用
我们知道结构体中可以有很多种不同类型的成员,当然也可以有该结构体本身类型的成员。
struct Node
{
int data;
struct Node* next;
};
这段代码就是结构体的自引用,同时这也是定义一个节点。那试想如果去掉 * ,例如 struct Node node;
,这个语句就是想引用自己的结构体变量,但是当引用自己时,每个结构体内都含有一个一样的结构体,这样就会导致无限个结构体的嵌套,这样也会导致结构体的内存大小无法计算。
六.结构体内存对齐
在了解的结构体的基本构造之后,我们应该思考一个问题,那就是结构体的内存大小是如何计算的?很多人可能会觉得大小就是结构体成员所占内存大小相加,但实则不然。结构体的大小需要根据结构体的对齐规则来进行计算。
结构体对齐规则:
- 第一个结构体成员在与结构体变量偏移量为0的地址处。
- 其他成员变量要从对齐数的整数倍的地址处开始存储,存储的大小根据其类型决定。
对齐数 = 编译器默认对齐数 与 该成员大小的较小值。
(VS中默认值为 8,Linux中默认值为 4 ) - 结构体总大小必须为所有结构体成员中最大对齐数的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体要对齐到自己成员的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
有了结构体的对齐规则,我们来看下面这段代码:
struct S1
{
char c1;//1 8 1 c1对齐数为1
int i;// 4 8 4 i对齐数为4
char c2;//1 8 1 c2对齐数为1
};
int main()
{
printf("%d\n", sizeof(struct S1));//打印结果为12
return 0;
}
代码中已经标明了每个成员的对齐数(假设在vs编译器中,默认对齐数为8)。我们通过图片来分析一下,假设这些单元格就是结构体要存放的地址。
只要我们遵循结构体的对齐规则,按部就班地根据对齐数进行内存计算,结构体的大小还是和容易得出来的。
那么为什么会存在内存对齐呢?有两个原因:
- 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
- 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 为了访问未对齐的内存,处理器需要进行两次内存访问,但是对齐的内存访问仅需要一次访问。
实际上,内存对齐其实就是用存储空间来换取更短的运行时间。所以我们应该在设计结构体的时候,尽可能的让占用空间小的成员集中在一起,这样既能满足内存对齐,又能节省空间。