结构体简介:结构体是一种复杂类型,C语⾔已经提供了内置类型,如:char、short、int、long、float、double等,但是只有这些内置类 型还是不够的,假设我想描述学⽣,描述⼀本书,这时单⼀的内置类型是不⾏的。描述⼀个学⽣需要 名字、年龄、学号、⾝⾼、体重等;描述⼀本书需要作者、出版社、定价等。C语⾔为了解决这个问 题,增加了结构体这种⾃定义的数据类型,让程序员可以⾃⼰创造适合的类型。
本节要点如下:
1.结构体的声明 2.结构体变量定义及其初始化 3.结构体成员的访问
4.匿名结构体类型 5.结构体的自引用 6.结构体内存对齐 7.结构体传参
1.结构体的声明
基本格式
struct tag
{
member-list;
};
举例:
struct stu
{
char* name;
int age;
char* sex;
double score;
};//分号不能丢(一般vs自动打出来)
2.结构体变量定义及其初始化
1.结构体变量的定义
struct person
{
char* name;
int age;
char* sex;
}p1;//声明结构体变量的同时直接定义变量
struct person p2;//定义一个结构体变量
2.结构体变量的初始化
struct person p3 = { "zhangsan",20,"男" };
//按照顺序初始化
struct person p4 = { .age = 20,.name = "wangwu",.sex = "男" };
//不按照顺序初始化
3.嵌套结构体定义及其初始化
struct Node
{
int data;
struct person p1;
struct Node* next;
}s = { 20,{"lihua",18,"女"},NULL };
//嵌套结构体初始化,记得嵌套的那个用大括号括起来
这里面还定义了一个结构体指针,这其实是数据结构链表的相关思路,后续再说吧
3.结构体成员的访问(两种方式)
01.用" . "操作符的直接访问(结构体变量)
举例
struct stu ps1 = { "liming",18,"女",91.5 };
printf("%s %d %s %.1lf", ps1.name, ps1.age, ps1.sex, ps1.score);
运行结果
liming 18 女 91.5
02.用" -> "操作符的间接访问(结构体指针)
举例
struct stu ps1 = { "liming",18,"女",91.5 };
struct stu* ps = &ps1;
printf("%s %d %s %.1lf", ps->name, ps->age, ps->sex, ps->score);
运行结果
liming 18 女 91.5
特别注意事项:
寻址操作符(->)与点操作符(.)优先级都要大于寻址操作,所以必要情况下
应该先转型,后寻址,例子如下:
int struct_cmp_by_name(const void* p1, const void* p2)
{
return strcmp(((struct stu*)p1)->name, ((struct stu*)p2)->name);
return strcmp(*((struct stu*)p1)->name, *((struct stu*)p2)->name);
}
这里我们举出的例子是qsort函数比较结构体字符串大小传过去的那个函数
详见函数指针与qsort函数
前者是用->操作符间接访问,后者使用.操作符直接访问
4.匿名结构体类型
1.2 结构的特殊声明
在声明结构的时候,可以不完全的声明。
⽐如:
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
编译器会把上⾯的两个声明当成完全不同的两个类型,所以是⾮法的。 匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使⽤⼀次。
所以我的建议是不要经常使用这种匿名结构体类型
5.结构体的自引用
例如当我们定义一个链表的节点的时候
struct Node
{
int data;
struct Node next;
};
思考这样合理么???,并思考下面代码的执行结果
int main()
{
printf("%zd ",sizeof(struct Node));
return 0;
}
当我们执行这段代码的时候,我们的编译器就会报错,其实也很好理解,如果可以这样定义,一个结构体内部嵌套自己接着又会嵌套自己,无穷无尽,所以本质上实际算不出来大小,也就是我们无法嵌套自己定义结构体
但是我们可以通过嵌套一个自身的结构体指针
struct Node
{
int data;
struct Node* next;
};这种就是可以的
这样子就是合法的,具体如何实现链表等到后续再说吧
6.结构体内存对齐(计算结构体的大小)
01.有四大结构体内存对齐的规则
1. 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处
2. 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。 对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值。VS 中默认的值为 8 - Linux中 gcc 没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩
3. 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的 整数倍.
4. 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构 体的整体⼤⼩就是所有最⼤对⻬数( 而维持含嵌套结构体中成员的对⻬数)的整数倍。
理解了这几大规则,我们直接举一个例子
struct S1
{
char c1;
int i;
char c2;
};
可得知该结构体大小是12,那为什么是12呢?
我先来解释以下这个图,左边的数字是相对于初始位置的偏移量,比如第一块空间就是0,也就是偏移量是0,黑色是未利用的空间
因为第一个成员为char类型大小也就是1, 自动跟起始位置对齐, 也就是图中的蓝格子, 第二个成员为int 类型,大小为4字节,而 vs默认的对齐数是8,取小的那个也就是4,填充在4的整数倍也就是从偏移量为4处开始填充4字节(红色), 接下来的类型是char 为1字节, 而系统默认是8, 取小的那个也就是1,所以填充1的倍数,所以填充8即可,但是我们最终的总大小是最大对齐数的倍数,填充到这里也就填充了9个字节,不是4(这里的最大对齐数),继续填充至11,此时填充了12个格子,也就是12bytes,就是最终大小
总结一下
第一个紧贴初始位置填充,剩下的看偏移量,但是最后算的是填充的格子
02.offsetof计算偏移量
#include<stdio.h>
#include<stddef.h>
//offsetof()这个宏列属于头文件#include<stddef.h>
struct S1
{
char c1;
int i;
char c2;
};
int main(){
printf("%d %d %d", offsetof(struct, c1), offsetof(struct, i), offsetof(struct, c2));
return 0;
}
程序运行结果是 0 4 8 与上面的图保持一致
03.#pragema pack (number)
修改默认对齐数为number
#include<stdio.h>
#include<stddef.h>
#pragma pack(1)//括号里面是多少就把默认对齐数置为多少
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()//恢复默认对齐数
int main(){
printf("%d %d %d", offsetof(struct, c1), offsetof(struct, i), offsetof(struct, c2));
return 0;
}
运行结果为0 1 5(其实也就是按着连续存放就行)
04.为什么要内存对齐
以下是大部分文献的解释:
1. 平台原因 (移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定 类型的数据,否则抛出硬件异常。 2. 性能原因: 数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要 作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地 址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以 ⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两 个8字节内存块中。 总体来说:结构体的内存对⻬是拿空间来换取时间的做法。
7.结构体传参
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函数。
原因: 函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。 如果传递⼀个结构体对象的时候,结构体过⼤,参数压栈的的系统开销⽐较⼤,所以会导致性能的下降。
结论: 结构体传参的时候,要传结构体的地址。