在C语言中,有内置类型(也称为基本数据类型)和自定义类型(结构体)两种类型。
这些内置类型是C语言提供的基本数据类型,用于表示基本数据,如整数、浮点数、字符等。
在C语言中,除了内置的基本数据类型外,还可以通过结构体(Structures)和枚举类型(Enums)来定义自定义类型。
结构体是一种用户自定义的数据类型,用于组合不同类型的数据成员。它允许将多个不同类型的变量组合在一起,形成一个新的数据类型,以便更方便地操作相关数据。
枚举类型是一种用户自定义的数据类型,用于定义一组相关的命名常量。它允许将一组有限的取值集合在一起,形成一个新的数据类型,以便更清晰地表示程序中的意图。
在C语言中,定义结构体使用 struct 关键字,结构体的形式如下:
struct 结构体名 {
数据类型 成员名1;
数据类型 成员名2;
// 更多成员...
};
【示例】:描述⼀个学⽣
struct Stu
{
char name[20]; // 姓名
int age; // 年龄
char set[5]; // 性别
int id; // 学号
}; // 分号不能丢
初始化结构体变量:有几种方法可以初始化结构体变量:
#include<stdio.h>
int main()
{
struct Stu s = { "张三", 19, "男", "202201170248" };
printf("%s\n", s.name);
printf("%d\n", s.age);
printf("%s\n", s.set);
printf("%s\n", s.id);
return 0;
}
2. 按照指定的顺序初始化 前面在说操作符时我们讲过,可以通过点操作符来访问结构体成员,这里同样可以通过点操作符来给结构体成员进行初始化。
int main()
{
struct Stu s = { .age = 19, .id = "202201170248", .name = "张三", .set = "男" };
printf("%s\n", s.name);
printf("%d\n", s.age);
printf("%s\n", s.set);
printf("%s\n", s.id);
return 0;
}
在声明结构的时候,可以不完全的声明。
匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
注意
:
结构体中的成员不仅可以是内置的数据类型,还可以是这个结构体本身,也就是结构体中包含指向相同类型结构体的指针或引用的情况。这种自引用的数据结构通常称为递归数据结构。
比如说定义一个链表的结点:
struct Node
{
int data;
struct Node next;
};
注意
:这种自引用是错误的,因为⼀个结构体中再包含⼀个同类型的结构体变量,这样结构体变量的大小就会无穷的大,是不合理的。即无法确定 sizeof(struct Node) 的大小。
正确的自引用方式:
struct Node
{
int data;
struct Node* next;
};
在结构体自引用使用的过程中,夹杂了 typedef 对匿名结构体类型重命名,也容易出现引入问题。
typedef struct
{
int data;
struct Node* next;
}Node;
这种也是错误的,因为Node是对前面的匿名结构体类型的重命名产生的,但是在匿名结构体内部提前使用Node类型来创建成员变量,这是不行的。
解决方案如下:定义结构体不要使用匿名结构体了
typedef struct Node
{
int data;
struct Node* next;
}Node;
【示例】:计算结构体的大小。
struct S
{
char c1; // 占1个字节
int i; // 占4个字节
char c2; // 占1个字节
};
int main()
{
struct S s = { 0 };
printf("%zd\n", sizeof(s));
return 0;
}
在代码中我们看到结构体中有两个char和一个int,那他的大小就是6个字节,但结果真的是这样吗?
运行之后发现是12个字节,这是为什么呢? 这说明结构体中的成员不是随便放的,这里面是有一定规则的,这就是结构体的内存对齐。
首先得掌握结构体的对齐规则:
解析:对照规则1,第一个成员对齐到和结构体变量起始位置偏移量为0,也就是图中为0的位置(char占1个字节),其余的成员变量对齐到对齐数整数倍的位置(int占4个字节,VS的默认值为8,4小于8,即这里的对齐数为4),也就是4的整数倍(图中序号4)开始存,第三个成员变量也一样(char占1个字节小于8,即对齐数是1)。最后结构体总大小是最大对期数(第一个和第三个对齐数都是1,第二个对齐数是4)的整数倍,也就是4的倍数,由于已经占了9个字节,所以下一个4的倍数就是12,这里总共浪费了6个字节的空间大小。
【练习1】
struct S1
{
char c1; // 占1个字节
char c2; // 占1个字节
int i; // 占4个字节
};
【练习2】
struct S2
{
double d;
char c;
int i;
};
【练习3】 结构体中嵌套结构体
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
解析:这里就要对应规则4,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,S3中最大的对齐数是8,即要对齐到8的整数倍处。结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍,也是8的倍数。
总体来说:结构体的内存对齐是拿空间来换取时间的做法。
设计结构体的时候,我们既要满足对齐,又要节省空间
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
S1 和 S2 类型的成员一模一样,但是 S1 和 S2 所占空间的大小有了一些区别(S1占12个字节,S2占8个字节)。
#pragma 这个预处理指令,可以改变编译器的默认对齐数。 【示例】:
#include<stdio.h>
#pragma pack(1) //设置默认对⻬数为1
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack() //取消设置的对⻬数,还原为默认
int main()
{
struct S1 s1;
printf("%zd\n", sizeof(s1));
return 0;
}
结构体在对齐方式不合适的时候,我们可以自己更改默认对齐数。
【示例1】:
#include<stdio.h>
struct S
{
int arr[1000];
int num;
double d;
};
void print1(struct S s)
{
for (int i = 0; i < 5; i++)
{
printf("%d ", s.arr[i]);
}
printf("\n");
printf("%d\n", s.num);
printf("%lf\n", s.d);
}
int main()
{
struct S s = { {1,2,3,4,5}, 100,3.14 };
print1(s);
return 0;
}
【示例2】:
struct S
{
int arr[1000];
int num;
double d;
};
void print2(const struct S* ps)
{
for (int i = 0; i < 5; i++)
{
printf("%d ", ps->arr[i]);
}
printf("\n");
printf("%d\n", ps->num);
printf("%lf\n", ps->d);
}
int main()
{
struct S s = { {1,2,3,4,5}, 100,3.14 };
print2(&s);
return 0;
}
示例1和示例2中首选示例2,因为示例传参时是将结构体在拷贝一份给形参,本身这个结构体所占的空间就比较大,在拷贝一份太占用空间,不太合适,而示例2传的是一个指针,可以通过这个指针直接访问这个结构体,不需要额外创建多余空间,当然,为了结构体内容不被修改,可以加一个const进行修饰。 原因:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。 如果传递⼀个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降
结论: 结构体传参的时候,要传结构体的地址。
位段的声明和结构是类似的,有两个不同:
【示例】:
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
A就是一个位段类型。
【示例】:
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
return 0;
}
总结: 跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。
位段的几个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配一个地址,一个字节内部的bit位是没有地址的。 所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段的成员输入值,只能是先输入放在一个变量中,然后赋值给位段的成员。
【示例】:
struct S
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
int main()
{
struct S s = { 0 };
// scanf("%d", &s._b); // 这是错误的
// 正确的示范
int b = 0;
scanf("%d", &b);
s._b = b;
return 0;
}