C语言之结构体详解
文章目录
1. 结构体类型的声明
struct tag
{
member-list;
}variable-list;
结构体标签:tag
结构体类型:struct tag
成员列表:member-list
结构体变量列表:variable-list
例如:
struct Stu
{
char name[20];
int age;
float score;
};//分号不能丢
2. 结构体变量的创建和初始化
#include <stdio.h>
struct Stu
{
char name[20];
int age;
float score;
}s1;
int main()
{
//按照顺序初始化
struct Stu s2 = { "zhangsan",18,90.1f };
printf("%s %d %.2f\n", s2.name, s2.age, s2.score);
//按照指定顺序初始化
struct Stu s3 = { .score = 82.4f,.name = "zhangsan",.age = 20 };
printf("%s %d %.2f\n", s3.name, s3.age, s3.score);
return 0;
}
在结构体创建的时候,在变量列表创建的变量是全局变量,s1也就是全局变量
struct Stu 为结构体类型,和int创建变量一样(int n = 0;),struct Stu s2创建结构体变量
结构体变量在赋值的时候需要加上大括号,再根据成员列表的顺序,输入对应的值
结构体变量初始化的时候,也可以不按顺序初始化,这时候就需要用到 ( . )结构体访问操作符
3. 结构体的特殊声明
在声明结构体的时候,可以不完全声明
#include <stdio.h>
struct
{
char a;
int b;
}x;
struct
{
char x;
int y;
}*p;
int main()
{
p = &x;
return 0;
}
上述两个结构体声明省去了结构体标签(tag)
在对第一个结构体变量取地址的时候,就会报警告
编译器会将上面声明的两个结构体当成两个不同类型的类型,所以是非法的 匿名的结构体,如果没有对结构体重命名的话,基本上只能使用一次
重命名如下:
#include <stdio.h>
typedef struct
{
char x;
int y;
}S;
int main()
{
S s1 = { 1,2 };
return 0;
}
使用typedef关键字将匿名结构体重命名为S
4. 结构体的自引用
结构体的自引用
代码一:
struct Node
{
int data;
struct Node next;
};
上述代码结构体的自引用是错误的,因为⼀个结构体中再包含⼀个同类型的结构体变量,这样结构体变量的⼤⼩就会⽆穷的⼤,是不合理的
正确的自引用:
struct Node
{
int data;//数据域 存放数据
struct Node *next;//指针域 存放下一个数据的地址
};
由于指针的大小是固定的,在32位平台下,为4字节,在64为平台下,为8字节,这样就确保了结构体变量的大小
在内存中,有些数据不是连续存放的,要想找到下一个数据,可以使用指针的方式
匿名结构体的自引用
typedef struct
{
int data;
Node* next;
}Node;
匿名结构体的自引用是不可行的,虽然使用typedef关键字重命名了匿名结构体,但是在重命名之前,Node是在重命名之后才产生的,但是在Node产生之前就已经使用Node创建了结构体指针变量,这是不行的
5. 结构体内存对齐
⾸先得掌握结构体的对⻬规则:
- 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处
- 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。
对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值。
- VS 中默认的值为 8
- Linux中 gcc 没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩
- 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的
整数倍。 - 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构
体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。
5.1 练习一
#include <stdio.h>
struct s1
{
char c1;
char c2;
int i;
};
struct s2
{
char c1;
int i;
char c2;
};
int main()
{
printf("struct s1 = %zd\n", sizeof(struct s1));
printf("struct s2 = %zd\n", sizeof(struct s2));
return 0;
}
代码运行结果如下:>
struct s1 = 8
struct s2 = 12
解释:
struct s1 = 8
- 结构体的第一个成员是 char c1; 会放在偏移量为0的位置,第一个字节,当对于自己偏移量就是0,所以只占一格
- 结构体的第二个成员是 char c2; char 为1个字节,在VS中默认的对齐数为8,取较小值 1,同时在偏移量中找到1的整数倍,也就是偏移量为1的位置,所以占一个字节
- 结构体的第三个成员是 int i; int 为4个字节,相对于默认值8,取较小值4,同时在偏移量中找到4的整数倍,跳过偏移量2 3 找到4,同时int 占四个字节,所以占4个字节
- 总共占了8个字节,在结构体中的对齐数有1 1 4,取最大值4,判断现在是否为最大对齐数4的整数倍
- 最终struct s1的大小为8个字节
struct s2 = 12
- 结构体的第一个成员是 char c1; 会放在偏移量为0的位置,第一个字节,当对于自己偏移量就是0,所以只占一格
- 结构体中的第二个成员是 int i; int 为4个字节,和默认对齐数8相比,取较小值4,在偏移量中找到4的整数倍,跳过1 2 3 ,找到偏移量为4的位置,int 为4个字节,所以占4格
- 结构体中的第三个成员是char c2; char 为1个字节,和默认对齐数8相比,取较小值1,在偏移量中找到1的整数倍,也就是偏移量为8的位置,char占一格
- 总共占了9个字节,相比结构体中的对齐数,1 1 4,取最大对齐数4,判断是否为整数倍,不是则取整数倍,也就是12字节,所以结构体的大小为12字节
5.2 练习三
#include <stdio.h>
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
printf("struct s4 = %zd\n", sizeof(struct S4));
return 0;
}
代码运行结果:>
struct s4 = 32
解释:
按照练习一的方法得出struct S3 的大小为16
struct S4
结构体嵌套时,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,上述代码中在S4中嵌套了一个S3,计算S3的偏移量使用的是S3中最大的对齐数,也就是8字节
- char会放在偏移量为0的位置,也就是一个字节
- 嵌套的结构体最大对齐数为8,和默认数一致,取偏移量为8的整数倍,也就是在8的位置,S3结构体的大小为16个字节,所以占16格
- double占8个字节,和默认对齐数一致,取偏移量为8的整数倍,也就是在24的位置,占8个字节
- 总共占了32个字节,S4中的最大对齐数为8字节,是8的整数倍
- 所以struct S4的大小为32个字节
6. 为什么存在内存对⻬?
- 平台原因 (移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。 - 性能原因:
数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两个8字节内存块中。
平台原因:在C语言没有明确规定int类型的数据是无符号还是有符号的,这取决于编译器,不同的编译器会有不同的解释
性能原因:结构体的内存对⻬是拿空间来换取时间的做法
TIPS:在设计结构体的时候,我们既要满⾜对⻬,⼜要节省空间,可以将占用空间小的成员尽量聚集在一块
例如:
struct s1
{
char c1;
char c2;
int i;
};
struct s2
{
char c1;
int i;
char c2;
};
相比较于s2,s1的占用空间较小一点
7. 修改默认对齐数
#program这个预处理指令,可以修改编译器的默认对齐数
#include <stdio.h>
struct S
{
char a;
int b;
};
#pragma pack(1) //修改默认对齐数为1
struct s
{
char a;
int b;
};
#pragma pack() //取消设置的对齐数,还原为默认对齐数
int main()
{
printf("%zd", sizeof(struct S));
printf("%zd\n", sizeof(struct s));
return 0;
}
代码运行结果:>
8
5
struct S中有两个成员 char占一个字节,位于偏移量为0的位置, int 为 4个字节,位于偏移量为4的位置占4字节,所以共占了8个字节,为最大对齐数4的整数倍,所以大小为8字节
struct s也是两个成员,但对齐数为1,也是无论什么类型的数据,都取对齐数为1的整数倍,也就是其大小为所以数据类型的大小相加,也就是5个字节
8. 结构体传参
#include <stdio.h>
struct S
{
char arr[1000];
int num;
};
//结构体传参
void print1(struct S s)
{
printf("%d", s.num);
}
//结构体地址传参
void print1(struct S* p)
{
printf("%d", p->num);
}
int main()
{
struct S s = { {1,2,3,4},1000 };
print1(s); //传结构体
print2(&s); //传结构体的地址
return 0;
}
结构体的传参可以使用传结构体或传结构体地址的方式,但是相较于传结构体,传结构体地址这种方式更优
函数中的形参相当于传入参数的临时拷贝,在传结构体的时候,函数中也开辟了一块临时空间创建结构体,上述代码中结构体的大小,相当大,此时传结构体地址的话,无论什么类型的地址,大小都是4/8字节,取决于是32位还是64位平台,此时大大节约了内存空间
原因:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递⼀个结构体对象的时候,结构体过⼤,参数压栈的的系统开销⽐较⼤,所以会导致性能的下降。
总结:
结构体在传参的时候,选择传结构体地址传参