文章目录
- 前言
- 一、结构体
- 二、 结构体声明
- 三、 特殊的声明----匿名结构体类型
- 四、 结构体的自引用
- (1)数据结构
- (2)结构体的自引用
- 五、 结构体变量的定义和初始化
- 六、 结构体内存对齐
- `1. 结构体的对齐规则`
- (1)结构体大小案例1
- (2)结构体大小案例2
- (3)结构体大小案例3
- 2. 为什么存在内存对齐?
- 1. 平台原因(移植原因)
- 2.性能原因:
- 3. 总体来说
- 七、修改默认对齐数
- 八、结构体传参
- 九、位段
- 1. 什么是位段
- 2. 位段的内存分配
- 3. 位段的跨平台问题
- 4. 位段的使用
- 总结
前言
C语言自定义类型中结构体、结构体声明、结构体自引用、结构体变量的定义和初始化、结构体内存对齐,结构体传参,位段等的介绍
一、结构体
- 结构是一些值的集合,这些值称为成员变量,结构的每个成员可以是不同类型的变量。
二、 结构体声明
-
- 结构体类型的模板
struct tag
{
member-list;
}variable-list;
- struct 是 结构体类型的关键字
- tag 是结构体类型的标签
- member-list是 结构体的成员
- variable-list是 基于当前结构类型创建的变量
- 例如
struct Point
{
int x;
int y;
};
struct Stu
{
char name[20];
int age;
char tele[12];
}s1,s2;
-
- 结构体嵌套声明
struct Score
{
float math;
float english;
float chinese;
int C;
};
struct Person
{
char name[20];
int grade;
struct Score s;
};
三、 特殊的声明----匿名结构体类型
- 匿名结构体类型
- 定义结构体类型时可以带标签,也可以不带标签。
- 这种特殊的声明只能使用一次,即 基于当前结构体类型创建的变量。
struct
{
int n;
char ch;
float pi;
}num1, num2;
- 程序中只能使用基于这个结构体类型创建的 num1 和 num2变量,不能重新创建变量。
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], * p;
- 上述代码,两个匿名结构体的成员一样。
- 第二个struct 和 * 结合表示 p是个指针变量
- 但是编译器认为 &x 和 p 是两个不同类型的指针。
四、 结构体的自引用
(1)数据结构
- 数据结构就是数据在内存中的存储结构。
- 数据结构有 线形(顺序表、链表等)、树形(二叉树等)等
- 链表如下:
(2)结构体的自引用
struct Node
{
int date;
struct Node* next;
};
- 结构体中包含同类型的结构体指针,就叫做结构体自引用
typedef
typedef struct Node
{
int date;
struct Node* next;
}Node;
// 此时
struct Node n1;
Node n2;
// 等价的
五、 结构体变量的定义和初始化
- 在创建类型的同时,进行初始化。
struct Point
{
int x;
int y;
}p1 = {3,4};
- 使用声明的结构体类型进行初始化
struct Point
{
int x;
int y;
};
int main()
{
struct Point p2 = { 1 , 2 };
return 0;
}
- 嵌套结构体类型的初始化
struct Score
{
float math;
float english;
float chinese;
int C;
};
struct Person
{
char name[20];
int grade;
struct Score s;
};
#include <stdio.h>
int main()
{
struct Person p1 = { "zhangsan", 6, {99.5, 60.5,100.0, 98} };
printf("%s %d %f %f %f %d", p1.name, p1.grade, p1.s.math, p1.s.english, p1.s.chinese, p1.s.C);
// 输出结果为 zhangsan 6 99.500000 60.500000 100.000000 98
return 0;
}
六、 结构体内存对齐
1. 结构体的对齐规则
- 第一个成员在与结构体变量的偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
- VS 中默认的值为 8。
- 只有 VS 编译器有默认对齐数,其他编译器上的对齐数就是成员大小。 - 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
offsetof 它是一个宏,它是用来 求 结构体成员在类型中距离起始位置的偏移量 offsetof( type, member)
使用offsetof 宏 需要调用头文件 <stddef.h>
(1)结构体大小案例1
#include <stdio.h>
#include <stddef.h>
struct S1
{
char c1;
int i;
char c2;
};
int main()
{
printf("%d\n", sizeof(struct S1));// 12
printf("%d\n", offsetof(struct S1, c1)); // 0
printf("%d\n", offsetof(struct S1, i)); // 4
printf("%d\n", offsetof(struct S1, c2)); // 8
return 0;
}
(2)结构体大小案例2
#include <stdio.h>
#include <stddef.h>
struct S2
{
char c1;
char c2;
int i;
};
int main()
{
printf("%d\n", sizeof(struct S2)); // 8
printf("%d\n", offsetof(struct S2, c1)); // 0
printf("%d\n", offsetof(struct S2, c2)); // 1
printf("%d\n", offsetof(struct S2, i)); // 4
return 0;
}
(3)结构体大小案例3
#include <stdio.h>
#include <stddef.h>
struct S2
{
char c1;
char c2;
int i;
};
struct S3
{
char c1;
struct S2 s2;
double d;
};
int main()
{
printf("%d\n", sizeof(struct S3)); // 24
printf("%d\n", offsetof(struct S3, c1)); // 0
printf("%d\n", offsetof(struct S3, s2)); // 4
printf("%d\n", offsetof(struct S3, d)); // 16
return 0;
}
-
因为如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
-
char c1位于 偏移量 0 处。
-
由上可知, struct S2 s2 自己的最大对齐数是 4 , 所以它距离起始地址的偏移量为 4,大小为 8 个字节。
-
double d 位于 8 个倍数处, 即 偏移量为 16 的位置。大小 为 8 个字节。
-
整个结构体大小为 所有最大对齐数的整数倍,所以 为 8 的倍数,即 24 个字节大小。
-
由此可见,结构体的内存对齐会有空间的浪费。
2. 为什么存在内存对齐?
1. 平台原因(移植原因)
不是所有的硬件平台都能访问任意地址上的任意数据的;
某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对其的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
3. 总体来说
结构体的内存对齐是拿 空间 换取 时间 的做法。
在设计结构体的时候,我们既要满足对齐,又要省空间, 如何做到:
让空间小的成员尽量集中在一起
七、修改默认对齐数
使用 #pragma 这个预处理命令,可以修改默认对齐数
#pragma pack(1) // 设置默认对齐数为 8
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack() // 取消设置的默认对齐数, 还原为默认
- 结构在对齐方式不合适的时候,我们可以自己更改默认对齐数。
八、结构体传参
#include <stdio.h>
struct S
{
int data[1000];
int count;
};
void print1(struct S ss)
{
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%d ", ss.data[i]);
}
printf("%d\n", ss.count);
}
void print2(struct S* ss)
{
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%d ", ss->data[i]);
}
printf("%d\n", ss->count);
}
int main()
{
struct S s1 = { {1,2,3}, 100 };
print1(s1); // 1 2 3 100
print2(&s1); // 1 2 3 100
return 0;
}
两种结构体传参都可以,但是传值调用,函数形参会重新开辟空间,降低性能。
但是 传址调用 只接收地址,不重新开辟空间,性能较好。
- 函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
- 如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能下降。
- 总之,结构体传参的时候,要传结构体的地址。
九、位段
1. 什么是位段
位段的声明和结构是类似的,有两个不同:
- 位段的成员必须是 int、unsigned int、signed int、或者char即整型家族。
- 位段的成员后边有一个冒号和数字。
#include <stdio.h>
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
int main()
{
printf("%d\n", sizeof(struct A)); // 8
return 0;
}
- 位段 的成员变量 : 后边的数字指的是 这个变量占用 的比特位的个数。
2. 位段的内存分配
1. 位段的成员可以是 int unsigned int signed int 或者是 char(属于整形家族)类型
2. 位段的空间上是按照需要以4个字节(int)或者 1 个字节(char)的方式来开辟
3. 位段涉及很多不确定的因素,位段是不跨平台的,注重可移植性的程序应该避免使用位段。
3. 位段的跨平台问题
1. int 位段 被当成有符号数还是无符号数是不确定的。
2. 位段中最大的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出现问题)。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准上未定义。
4. 当一个结构体包含两个位段,第二个位段成员比较大,无法容纳与第一个位段剩余的位时,
是舍弃剩余的位还是利用,这是不确定的。
- 跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
4. 位段的使用
- 位段的在网络中有较好的作用,如 ip数据包。
总结
C语言自定义类型中结构体、结构体声明、结构体自引用、结构体变量的定义和初始化、结构体内存对齐,结构体传参,位段等的介绍