赞
踩
正文开始
C语言为我们提供了基本的数据类型,例如int、char、float等,但我们在实际生活中的对象都是复杂的,不能仅靠一种数据简单的描述。
我们回顾一下数组,数组是一种自定义类型,比如int arr[10],它的类型就为int [10],自定义类型使我们能够更加灵活的解决问题。
而结构体同样是一种自定义类型。而结构体就实现了对一个对象进行多方面描述的功能。
struct tag
{
member-list;//成员列表
};
举个栗子,例如要描述一个学生,需要他的名字、年龄、性别、学号,就可以这样定义
struct Student
{
char name[20];//姓名
int age;//年龄
char sex[5];//性别
char id[20];//学号
};//分号不能丢
注:
对于结构体的理解:结构体就是一个自定义的类型,也就是说,当我们创建一个结构体后,它就可以类似于int、char这些数据类型一样拿来用。
在声明结构体时,可以不完全的声明,例如:
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
匿名结构体类型定义时,只能在定义结构体时声明结构体变量,否则是不能声明结构体变量的。
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
(p == &x)成立吗?答案是不成立,因为这是匿名结构体类型,就算里面的成员变量完全一样,编译器仍会把它们当成完全不同的类型。匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使用一次。
在结构体内部包含一个该结构体本身的成员是否可行呢?
比如这样:
struct Node
{
int data;
struct Node next;
}
这样写显然是不行的,因为一个结构体内在包含一个同类型的结构体变量,这样就会形成一个无限的套娃,结构体变量的大小就会变成无穷大,是不合理的。我们可以通过使用结构体指针变量来自引用
例如这样就是合法的:
struct Node
{
int data;
struct Node* next;
//包含了下一个节点的地址
//而不是下一个节点的內容
//这样就避免了无限套娃的情况
}
在结构体自引用使用的过程中,若使用了typedef对匿名结构体类型重命名,也容易产生问题,例如:
typedef struct
{
int data;
Node* next;
}Node;
上述代码是将一个匿名结构体类型重命名为了Node,并且在匿名结构体类型中包含了该结构体类型的变量next,但是这是不合法的,因为在匿名结构体内部提前使用了 Node 类型来创建成员变量。所以,强烈建议定义结构体不要使用匿名结构体
既然结构体是一种类型,那么我们就可以使用这种类型来创建变量,创建变量有两种方式:
//结构体类型创建 struct Student { char name[20];//名字 int age;//年龄 char sex[5];//性别 char id[20];//学号 }; int main() { //结构体变量的创建 struct Student a; //创建了类型为struct Student的变量a return 0; }
//结构体类型创建
struct Student
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}a;
//结构体变量创建
//这时创建的结构体变量是一次性的
int main()
{
return 0;
}
结构体变量的初始化有两种情况,一种是按照结构体成员列表顺序初始化,一种是指定顺序初始化。例如:
struct Student
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};
int main()
{
//按照结构体成员列表顺序初始化
struct Student a = { "张三", 20, "男", "20232022" };
//指定顺序初始化
struct Student b = {.age = 18, .name = "李四", .sex = "女", .id = "20235600"}
return 0;
}
注:
struct Student { char name[20];//名字 int age;//年龄 char sex[5];//性别 char id[20];//学号 }; int main() { struct Student a; //err 这样是不允许的 //a = { "张三", 20, "男", "20232022" }; //只能单独初始化 a.name = "张三"; a.age = 20; return 0; }
struct Student
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};
int main()
{
struct Student a = { 0 };
return 0;
}
结构体变量通过操作符.来访问成员:
结构体变量.成员名
例如:
#include <stdio.h>
struct Text
{
int x;
int y;
};
int main()
{
struct Text p = { 1,2 };
printf("x:%d\ny:%d\n", p.x, p.y);
//p.x访问成员x
//p.y访问成员y
return 0;
}
运行结果:
指针可以指向结构体类型,我们可以通过一个结构体指针变量来间接访问结构体成员
(*结构体指针).成员名
结构体指针->成员名
#include <stdio.h> struct Text { int x; int y; }; int main() { struct Text p = { 1,2 }; struct Text * pp = &p; pp->x = 4; (*pp).y = 5; printf("x=%d\ny=%d\n", pp->x, pp->y); return 0; }
运行结果:
注:当使用 (*结构体指针).成员名时,括号不能省去,因为.的优先级要大于 *
结构体在内存中的存储方式同一般的类型颇有不同,它遵循结构体内存对齐规则
- 结构体的第一个成员对齐到结构体变量起始位置偏移量为0的地址处
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
- 对齐数 = 编译器默认的一个对齐数 与 该成员变量大小的较小值
- VS 中默认值为8
- Linux 中 gcc 没有默认对齐数,对齐数就是成员自身的大小- 结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大值)的整数倍
- 如果嵌套了结构体,嵌套的结构体成员到自己的成员中最大对齐数的整数倍,结构体的整体大小就是所有最大对齐数(包含嵌套结构体中成员的对齐数)的整数倍
相信大家现在都是一脸懵吧,别急,我来举个例子:
#include <stdio.h>
struct S1
{
char c1;
int i;
char c2;
};
int main()
{
printf("%d\n", sizeof(struct S1));
return 0;
}
我们来探讨一下在VS中运行结果是什么:
首先,根据对齐规则第一条,将char c1安置好
然后安置第二个结构体成员变量int i,它的大小为4个字节,编译器默认对齐数为8,两者较小值为4。也就是说,它的对齐数为4,需要对齐到4的整数倍的地址处,也就是这样:
随后安置第三个结构体成员变量char c2,它的大小为1个字节,编译器默认对齐数为8,两者较小值为1。也就是说,它的对齐数为1,需要对齐到1的整数倍的地址处,也就是这样:
最后根据对齐规则第3条,三个成员变量中最大的对齐值为4,所以总大小为四的整数倍,所以整个结构体的内存情况就为:
结构体类型struct S1的大小就为12个字节,我们运行以下代码验证一下:
来几个例子练习一下:
#include <stdio.h> //练习1 struct S2 { char c1; char c2; int i; }; //练习2 struct S3 { double d; char c; int i; }; int main() { printf("%d\n", sizeof(struct S2)); printf("%d\n", sizeof(struct S3)); return 0; }
运行验证一下:
当结构体内部嵌套结构体变量时,嵌套的结构体对齐数为其内部的最大对齐数,整个结构体的对齐数为所有最大对齐数(包含嵌套结构体中成员的对齐数)的整数倍。
例如:
#include <stdio.h> struct S3 { double d; char c; int i; }; //练习4-结构体嵌套问题 struct S4 { char c1; struct S3 s3; double d1; }; int main() { printf("%d\n", sizeof(struct S4)); return 0; }
运行验证一下:
总的来说,结构体的内存对齐就是拿空间来换取时间的做法。我们在设计结构体的时候,应该尽可能地将占用空间小的成员集中在一起,这样可以在一定程度上减少空间的使用。
使用#pragma pack()这个预处理指令,可以修改编译器默认的对齐数
例如:
#pramg pack(1)//设置默认对齐数为1
将结构体变量作为参数传递进函数,同样有传址调用和传值调用:
#include <stdio.h> struct S { int data[100]; int num; }; //传值调用 void print1(struct S s) { printf("%d\n", s.num); } //传址调用 void print2(struct S* ps) { printf("%d\n", ps->num); } int main() { struct S s = { { 1,2,3,4 }, 30}; print1(s); print2(&s); }
运行结果:
在我们使用结构体传参时,更推荐使用传址调用,因为函数在传参时,参数需要压栈(即在栈区申请空间来存储参数),会有时间和空间上的系统开销,如果传入的结构体对象过大,那么系统开销就大,就会导致性能的下降。
在前面我们学习了结构体的对齐规则,实现了用空间换取时间的效果,但如果我们想要节省空间,那该怎么办呢?C语言为我们提供了位段的概念,来实现节省空间的效果。
例如:
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
其中:
例如:
struct S { char a : 3; char b : 4; char c : 5; char d : 4; }; int main() { struct S s = { 0 }; s.a = 10;//二进制:1010 s.b = 12;//二进制:1100 s.c = 3;//二进制:11 s.d = 4;//二进制:100 return 0; }
这里我们假设位段申请到一块内存中,是从右向左使用;并且剩余的空间,不足下一个成员使用的时候浪费掉,那么内存分配如下:
首先,成员都是char类型,所以每次开辟一个字节的空间,开辟了第一个字节后,存放进成员 a,它共占3bit
然后存放成员 b,它共占4bit
随后要放成员 c,它共占5bit,但开辟的第一个字节已经不够用了,所以就会开辟第二块内存
最后要存放成员 d,它共占4个字节,但开辟的第二个字节已经不够用了,所以就会开辟第三块内存
这样,结构体成员的内存就都开辟出来了,共三个字节,接下来是给成员初始化;成员 a 二进制表示为1010、成员 b 二进制表示为1100、成员 c 二进制表示为 11、成员 d 二进制表示为100,按位存储进内存,多余位置为0,超出部分舍去
在对应环境下运行检验一下:
跟结构体相比,位段可以达到相同的效果,并且可以很好的节省空间,但是有跨平台的问题存在,原因如下:
位段的几个成员可能公用一个字节,由于地址是以字节为单位分配的,一个字节内部的 bit 位是没有地址的,而有些成员的起始位置并不是某个字节的起始位置,所以这些成员是没有地址的。
所以不能对位段成员使用&操作符,也就不能使用 scanf 直接给位段成员输入值,只能先输入一个放在变量里,然后赋值给位段成员
例如:
#include <stdio.h> struct S { char a : 3; char b : 4; char c : 5; char d : 4; }; int main() { struct S s = { 0 }; //scanf("%d", &s.a)//这是错误的 //正确用法 int i = 0; scanf("%d", &i); s.a = i; printf("%d", s.a); return 0; }
完
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。