23.0、C语言——结构体的剖析与使用
结构体的声明:
结构体的基础知识:
结构是一些值的集合,这些值称为成员变量,结构的每个成员与数组不同,结构体的每个成员可以是不同的类型变量【但是成员变量类型不可以是自己的结构体类型】;
比如声明一个学生结构体类型->
struct Student {
char name[20];
char tele[12];
char sex[10];
short age;
}
在main()函数中定义的结构体变量都是局部变量;
全局结构体变量,定义如下:
struct Student {
char name[20];
char tele[12];
char sex[10];
short age;
}s2,s3,s4;
struct Student s5;
特殊声明方式:
匿名结构体类型 ->
struct {
int x;
int y;
int z;
}a;
1. 向上面代码描述的是匿名结构体类型,那么这种类型由于没有结构体名字,所以只能在声明结构体类型的后面直接定义结构体变量,在其他地方无法定义结构体变量;
2. 当然这种声明的方式不建议大家去使用,这里只做了解即可;
3. 结构体类型的指针只能存放自己结构体类型的对象地址;
结构体的自引用方式 ->
struct Node {
int data;
struct Node* next;
};
1. 其实就是数据结构中的链式存储方式;
2. 结构体中一部分存储数据,一部分存储一个指向下一个结构体元素的指针;
结构体变量的定义和初始化:
typedef struct Student {
char name[20];
char tele[12];
char sex[10];
short age;
}Student;
int main() {
struct Student s1 = {"小澜","1355841253","男",18};
printf("%s%s%s%d",s1.name,s1.tele,s1.sex,s1.age);
return 0;
}
当然结构体类型中也可以包含其他的结构体类型:
typedef struct Student {
char name[20];
char tele[12];
char sex[10];
short age;
}Student;
typedef struct People {
Student s;
int a;
int b;
int c;
}People;
int main() {
People p = { {"小澜","1355841253","男",18},1,2,3 };
printf("%s%s%s%d",p.s.name,p.s.tele,p.s.sex,p.s.age);
return 0;
}
结构体内存对齐:
struct S1 {
char c1;
int a;
char c2;
};
struct S2 {
char c1;
char c2;
int a;
};
int main() {
struct S1 s1 = { 0 };
printf("%d\n",sizeof(s1));
struct S2 s2 = { 0 };
printf("%d\n", sizeof(s2));
return 0;
}
计算一下这两个结构占用内存空间的大小 ->
考点 如何计算?首先得掌握结构体的对齐规则:
1. 第一个成员在与结构体变量偏移量为 0 的地址处;
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处;【对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值;】
VS中默认的值为 8 ;【那么有些编译器没有默认对齐数(例如 gcc 编译器),这时候变量的大小就是每次的对齐数】
3. 结构体总大小为最大齐数(每个成员变量都有一个对齐数)的整数倍;
4. 如果嵌套了结构体的情况,嵌套的 结构体a 对齐到自己结构体中变量的最大对齐数的整数倍处【比如说:该 结构体a 中定义了 int a; 且这个 a 的对齐数是该 结构体a 中最大的,那么该 结构体a 就按照 4 的倍数来算】,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍;
计算s1的内存大小如下图所示:
1. s1 算出来的大小是 12;
2. 那么结构体类型变量 s2 的大小计算方法也同理能算出来是 8;
3. 可以看到 s1 和 s2 的成员变量都是一样的,只是定义的顺序不一样,最终导致结构体占用的内存空间大小不一样;
4. 可以用 #pragma pack( int ) 去修改 默认的对齐数;如果int参数什么都不写就代表取消设置的默认对齐数;
offsetof ( ) 宏
offsetof 其实是宏,这里先做了解即可,在之后宏的学习章节中再给大家详细介绍;
size_t offsetof ( structName , memberName );
1. 返回该变量在该结构体中的偏移量;【使用的时候需要引入头文件 #include <stddef.h>】
2. 第一个参数为 -> 结构体类型,第二个参数为 -> 该结构体的变量;
3. 实例代码如下所示:
struct S {
char c;
int i;
double d;
};
int main() {
printf("%d\n", offsetof(struct S ,c));
printf("%d\n", offsetof(struct S, i));
printf("%d\n", offsetof(struct S, d));
return 0;
}
输出的结果为:0 , 4 , 8
为什么存在内存对齐?
大部分的参考资料都是这么说的:
1. 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常;
2. 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐;原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问;
总的来说:
结构体的内存对齐是拿 空间 来换取 时间 的做法
结构体值传递:
struct S {
int a;
char b;
short c;
};
void init(struct S tmp) {
tmp.a = 1;
tmp.b = 'a';
tmp.c = 2;
}
int main() {
struct S s = { 0 };
init(s);
printf("a=%d,b=%c,c=%d",s.a,s.b,s.c);
return 0;
}
如上代码,将对象 s 传递给 init()函数 并且修改结构体对象 s 的值,但是由于这是值传递,所以 tmp 是一个临时拷贝的变量,所以修改的也只是 tmp 临时拷贝变量中的值,并没有真正的修改到 s 的值;
结构体址传递 ->
struct S {
int a;
char b;
short c;
};
void init(struct S* p) {
(*p).a = 1;
(*p).b = 'a';
(*p).c = 2;
}
int main() {
struct S s = { 0 };
init(&s);
printf("a=%d,b=%c,c=%d",s.a,s.b,s.c);
return 0;
}
1. 如上代码,传址方式将 s 的地址传递到 init() 函数中通过 s 的地址去修改 s 的值,是可以修改成功的;
2. 如果我们要传递 结构体对象 给函数,尽量使用传址的方式,因为这样更加节省空间节省资源;
3. 如果用传值的方式,当我们的变量很大的时候,会导致需要拷贝一个很大的临时变量,很占用栈的内存空间;
总结:
1. 函数传参的时候,参数是需要压栈,会有时间和空间上的开销;
2. 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降;
3. 所以当结构体传参的时候要传结构体的地址;