文章目录
- 一、内置类型与自定义类型
- 1.1 内置类型(基本数据类型)
- 1.2 自定义类型
- 二、结构体
- 2.1 结构体的声明
- 2.2 结构体变量的创建和初始化
- 2.3 结构体的特殊声明
- 2.4 结构体的自引用
- 三、结构体内存对齐
- 3.1 对齐规则
- 3.2 为什么存在内存对齐
- 3.3 修改默认对齐数
- 四、结构体传参
- 五、结构体实现位段
- 5.1 什么是位段
- 5.2 位段的内存分配
- 5.4 位段的跨平台问题
- 5.5 位段使用的注意事项
书山有路勤为径,学海无涯苦作舟。
创作不易,宝子们!如果这篇文章对你们有帮助的话,别忘了给个免费的赞哟~
一、内置类型与自定义类型
在C语言中,有内置类型(也称为基本数据类型)和自定义类型(结构体)两种类型。
1.1 内置类型(基本数据类型)
- 整型(Integer types):用于表示整数值,包括:
- int:通常表示整数,取决于编译器和系统架构,一般为4字节。
- short int:短整数,通常为2字节。
- long int:长整数,通常为4字节或8字节。
- long long int:长长整数,通常为8字节。
- 字符型(Character type):
- char:用于表示单个字符或小整数值,通常为1字节。
- 浮点型(Floating-point types):用于表示实数,包括:
- float:单精度浮点数,通常为4字节。
- double:双精度浮点数,通常为8字节。
- long double:扩展精度浮点数,大小不定,通常大于8字节。
- 空类型(Void type):
- void:表示无类型,常用于函数返回类型或指针类型。
这些内置类型是C语言提供的基本数据类型,用于表示基本数据,如整数、浮点数、字符等。
1.2 自定义类型
在C语言中,除了内置的基本数据类型外,还可以通过结构体(Structures)和枚举类型(Enums)来定义自定义类型。
- 结构体(Structures)
结构体是一种用户自定义的数据类型,用于组合不同类型的数据成员。它允许将多个不同类型的变量组合在一起,形成一个新的数据类型,以便更方便地操作相关数据。
- 枚举类型(Enums)
枚举类型是一种用户自定义的数据类型,用于定义一组相关的命名常量。它允许将一组有限的取值集合在一起,形成一个新的数据类型,以便更清晰地表示程序中的意图。
二、结构体
2.1 结构体的声明
在C语言中,定义结构体使用 struct 关键字,结构体的形式如下:
struct 结构体名 {
数据类型 成员名1;
数据类型 成员名2;
// 更多成员...
};
【示例】:描述⼀个学⽣
struct Stu
{
char name[20]; // 姓名
int age; // 年龄
char set[5]; // 性别
int id; // 学号
}; // 分号不能丢
2.2 结构体变量的创建和初始化
初始化结构体变量:有几种方法可以初始化结构体变量:
- 按照结构体成员的顺序初始化:
#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;
}
2.3 结构体的特殊声明
在声明结构的时候,可以不完全的声明。
匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
注意
:
- 匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使用一次。
- p = &x; 这种写法是不合法的,编译器会把上面的两个声明当成完全不同的两个类型,所以是非法的。
2.4 结构体的自引用
结构体中的成员不仅可以是内置的数据类型,还可以是这个结构体本身,也就是结构体中包含指向相同类型结构体的指针或引用的情况。这种自引用的数据结构通常称为递归数据结构。
比如说定义一个链表的结点:
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个字节,这是为什么呢?
这说明结构体中的成员不是随便放的,这里面是有一定规则的,这就是结构体的内存对齐。
3.1 对齐规则
首先得掌握结构体的对齐规则:
- 结构体的第⼀个成员对齐到和结构体变量起始位置偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的⼀个对齐数与该成员变量大小的较小值。
- VS 中默认的值为 8
- Linux中 gcc 没有默认对齐数,对齐数就是成员自身的大小
- 结构体总大小为最大对齐数(结构体中每个成员变量都有⼀个对齐数,所有对齐数中最大的)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
解析:对照规则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的倍数。
3.2 为什么存在内存对齐
-
平台原因 (移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。 -
性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成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个字节)。
3.3 修改默认对齐数
#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进行修饰。
原因:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递⼀个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降
结论:
结构体传参的时候,要传结构体的地址。
五、结构体实现位段
5.1 什么是位段
位段的声明和结构是类似的,有两个不同:
- 位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以选择其他类型。
- 位段的成员名后边有一个冒号和一个数字。
【示例】:
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
A就是一个位段类型。
5.2 位段的内存分配
- 位段的成员可以是 int 、unsigned int 、signed int 或者是 char 等类型。
- 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
【示例】:
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;
}
5.4 位段的跨平台问题
- int 位段被当成有符号数还是无符号数是不确定的。
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题)。
- 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。
- 当⼀个结构包含两个位段,第⼆个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
总结:
跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。
5.5 位段使用的注意事项
位段的几个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配一个地址,一个字节内部的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;
}