前言
C语言中本身包含了许多数据类型,但并不能够总是满足需要。自定义类型允许使用者创造出特定的且适合需要的类型。本文主要介绍结构体、位段、枚举与联合。
1. 结构体
结构体是一些值的集合,这些值的类型可以相同,也可以不同,称为结构体的成员变量。与数组相似但不同。结构体是常用的自定义类型。
1.1 结构体的声明
关键字struct
普通声明
struct tag{
member_list;
}veriable_list;
例如描述一个学生的信息的结构体类型:
struct student{
char name[20];
char num[15];
double score;
};
隐式声明
隐式声明:省略结构体标签的结构体声明。
struct {
member_list;
}veriable_list;
- 隐式声明的结构体由于没有名字只能在声明时才能定义变量,在之后不能够定义变量。
- 每个隐式声明的结构体类型都是不相同的,即使是成员变量完全相同的情况下。
例如:
#include <stdio.h>
struct {
int a;
char b;
}c;
struct {
int a;
char b;
}*p;
int main() {
p = &c;
return 0;
}
1.2 结构体的自引用
一个结构体中包含本身(结构体)的指针作为结构体成员。
struct tag{
int data;
struct tag* next;
};
使用typedef 对结构体进行重命名 正确写法:
typedef struct Node{
int data;
struct Node* next;
}Node;
错误写法:
typedef struct Node{
int data;
Node* next;
}Node;
1.3 结构体变量的定义和初始化
在声明结构体的同时定义变量和对变量初始化
struct student{
char name[20];
int num;
}s1;
struct student{
char name[20];
int num;
}s1, s2 = {"sunwukong", 1001};
struct Node
{
int data;
struct student s;
struct Node* next;
}n = {10, {"tangsheng", 1002}, NULL};
先声明结构体类型在定义变量和对变量初始化
struct student{
char name[20];
int num;
};
struct student s1;
struct student{
char name[20];
int num;
};
struct student s1;
struct student s2 = {"sunwukong", 1001};
struct Node
{
int data;
struct student s;
struct Node* next;
};
struct Node n = {10, {"tangsheng", 1002}, NULL};
1.4 结构体变量的大小 - 结构体内存对齐
结构体是一些值的集合,定义一个结构体变量时,在内存中会分配一片连续的内存空间作为结构体变量的空间。那么这片连续的空间究竟是多大呢,这里需要直到结构体内存对齐的知识才能正确知道结构体变量的大小。
结构体对齐规则
- 第一个成员在与结构体变量偏移量为0的地址处。
- 其他成员要对齐到某个数字(对齐数)的整数倍的地址处。
某一成员的对齐数 = 编译器默认的一个对齐数(如果有的话)与该成员大小的较小值。
visual studio 2019编译器默认对齐数是8。 3. 结构体总大小是所有成员变量对齐数中的最大对齐数的整数倍。 3. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体变量的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
例子:
- 相同成员的两个结构体,但是成员的顺序不同也会导致结构体变量的大小不同。
- 结构成员所占内存小的集中放在前面会使结构体变量的大小更小。
#include <stdio.h>
struct S1{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
int main() {
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}
运行结果: 嵌套结构体的大小:
#include <stdio.h>
struct S1 {
char c1;
int i;
char c2;
};
struct S2
{
char c1;
struct S1 s2;
double d;
};
int main() {
printf("%d\n", sizeof(struct S2));
return 0;
}
运行结果:
内存对齐产生的原因
- 平台原因
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则出现硬件异常。
- 性能原因
数据结构(尤其是栈)应该尽可能的在自然边界上对齐。因为为了访问未对齐的内存,处理器需要两次内存访问;而对齐的内存仅需要一次访问。
这是空间换时间的方法。 定义结构体类型时让占用空间小的成员变量尽量集中在一起,用来减少内存对齐带来的空间的浪费。
修改默认对齐数
#pragma 是预处理指令,#pragma pack() 可以修改它后面代码的默认对齐数(如果有的话),直到再次出现#pragma pack() 结束对默认对齐数的修改。
#include <stdio.h>
#pragma pack(4)
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()
#pragma pack(1)
struct S2
{
char c1;
int i;
char c2;
};
#pragma pack()
int main()
{
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}
运行结果:
1.5 结构体传参
传值(结构体)
#include <stdio.h>
struct student {
char name[20];
int num;
};
void Print(struct student stu) {
printf("%s %d\n", stu.name, stu.num);
}
int main() {
struct student s = { "sunwukong", 10001 };
Print(s);
return 0;
}
运行结果:
传地址
#include <stdio.h>
struct student {
char name[20];
int num;
};
void Print(const struct student* p) {
printf("%s %d\n", p->name, p->num);
}
int main() {
struct student s = { "sunwukong", 10001 };
Print(&s);
return 0;
}
运行结果:
在传地址与传值调用都可以完成任务时,传地址调用相比传值调用更好。
- 因为函数传参时,参数是需要压栈的,会有时间和空间上的系统开销。
- 在传递一个结构体对象时,如果结构体过大的话,参数压栈的系统开销也会较大,将会导致性能的下降。
结构体传参时主选传地址。
2. 位段 -结构体拓展
结构体具有实现位段的能力。也就是说二者比较像。
2.1 初识位段
位段的声明
与结构体声明类似,也有不同:
- 位段的成员只能是整型家族的成员(包括char);
- 位段的成员后面有一个冒号和一个数字。这个数字表示该成员占内存的几个bit(位)。
struct S{
int a:2;
int b:4;
int c:16;
};
2.2 位段的内存分配
- 位段的成员属于整形家族,如
int、unsigned int、signed int、char 。 - 位段的空间上是按照需要以四个字节
int 或一个字节char 的方式来开辟的。 - 位段涉及很多不确定因素,是不跨平台的,注重可移植的程序应该避免使用位段。
visual studio 2019下的位段空间开辟举例
#include <stdio.h>
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;
printf("%d\n", sizeof(s));
return 0;
}
2.3 位段的跨平台问题
- 位段中
int 是有符号还是无符号是未定义的,与普通情况下int 是有符号的不同。 - 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32)最大位如果是25那么在16位机器上编译不通过,在32位机器上正常运行。
- 位段中的成员在内存中是从左向右分配,还是从右向左分配标准尚未定义。
- 当一个结构包含两个位段成员,第二个位段成员比较大,第一个位段剩余的位无法容纳第二个位段成员时,是舍弃剩余的位还是利用是不确定的。
与结构相比,位段可以达到同样的效果,但是可以很好地节省空间,只是有跨平台的问题存在。 跨平台的问题不是说包含位段的程序不能够跨平台,只是说要写出适应该平台的位段代码。
2.4 位段的应用
网络中减少数据包的大小。
3. 枚举 - 可以列举的常量
枚举,顾名思义可以一一列举,并且是常量。 生活中有许多事物可以一一列举:星期、月份等……
3.1 枚举类型的定义
与结构体相似: 关键字enum
enum tag{
constant_list,
}veriable_list;
- 枚举常量的是有值的,这些值默认从0开始递增,相邻枚举常量之间默认相差1。
- 也可以对枚举常量赋初值,这样被赋值的枚举常量及之后的枚举常量的值都会随着初值而改变,它之前的还是默认值。
例子:
#include <stdio.h>
enum color {
RED,
ORANGE,
YELLOW,
GREEN,
CYAN = 10,
BLUE,
PURPLE
}c1;
int main() {
c1 = RED;
enum color c2 = BLUE;
printf("%d\n", c1);
printf("%d\n", c2);
return 0;
}
3.2 枚举的优点
#define 也可以定义常量,实现与枚举相同的效果。但是枚举在此功能上相比#define 有着几个优点:
- 增加代码的可读性和可维护性;
- 与
#define 定义的标识符相比枚举有类型检查,更加严谨。 - 防止了命名污染,把常量封装了起来。
- 便于调试。
- 使用方便,一次可以定义多个常量。
3.3 枚举的使用
#include <stdio.h>
enum week {
MONDAY = 1,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY,
SUNDAY
};
int main() {
enum week a = MONDAY;
enum week b = 1;
return 0;
}
4. 联合(共用体)
与结构体类似,但是成员共用一块内存空间。
4.1 联合类型的定义
联合类型包含一系列成员,这些成员共用同一块空间。
union tag{
member_list;
}veriable_list;
不能在定义联合变量的同时的其初始化。
#include <stdio.h>
union un {
int a;
char b;
}c;
int main() {
c.a = 10;
union un d;
return 0;
}
4.2 联合大小的计算
联合的大小至少是最大成员的大小。 当最大成员的大小不是最大对齐数的整数倍时,就要对齐到最大对齐数的整数倍。 最大对齐数参考结构体。
visual studio 2019 举例
#include <stdio.h>
union Un1 {
char c[5];
int i;
};
union Un2 {
short c[7];
int i;
};
int main() {
printf("%d\n", sizeof(union Un1));
printf("%d\n", sizeof(union Un2));
return 0;
}
union Un1 :
char c[5] 所占大小是5 个字节,单个元素是char ,大小是1 ,默认对齐数是8 ,故其对齐数就是1 。i 所占大小是4 个字节,默认对齐数是8 ,故对齐数是4 。- 最大成员大小是
5 个字节,最大对齐数是4 ,故联合的大小是8 字节
union Un2 :
short c[7] 所占大小是14 个字节,单个元素是short ,大小是2 ,默认对齐数是8 ,故其对齐数就是2 。i 所占大小是4 个字节,默认对齐数是8 ,故对齐数是4 。- 最大成员大小是
14 个字节,最大对齐数是4 ,故联合的大小是16 个字节。
结语
本节主要介绍了自定义类型相关的结构体、位段、枚举、联合。了解并熟悉这些自定义类型可以帮助理解数据结构等相关的知识。
END
|