总言
C语言:自定义类型介绍。
文章目录
- 总言
- 1、结构体
- 1.1、结构体声明
- 1.1.1、基本声明方式
- 1.1.2、特殊的声明:不完全声明
- 1.2、结构体自引用
- 1.2.1、结构体自引用说明
- 1.2.2、typdef对结构体重命名
- 1.3、结构体变量的定义和初始化
- 1.4、结构体变量大小计算(重点)
- 1.4.1、问题引入
- 1.4.2、结构体对齐规则
- 1.4.2.1、例题一
- 1.4.2.2、例题二
- 1.4.2.3、例题三
- 1.4.2.4、例题四
- 1.4.3、相关宏:用于计算结构体成员到起始位置的偏移量
- 1.4.4、为什么会存在内存对齐?
- 1.4.5、如何设计结构体、如何修改默认对齐数
- 1.4.5.1、结构体设计注意事项
- 1.4.5.2、修改默认对齐数
- 1.4.5.3、实现一个用于计算偏移量的宏
- 1.5、结构体传参
- 2、段位
- 2.1、什么是段位
- 2.2、段位的内存分配
- 2.3、位段不能跨平台使用说明以及实际应用举例
- 3、枚举
- 3.1、什么是枚举、枚举常量
- 3.2、枚举优点及使用
- 4、联合(共用体)
- 4.1、什么是联合
- 4.2、联合体具有什么特点
- 4.3、联合体的大小计算
1、结构体
1.1、结构体声明
1.1.1、基本声明方式
基本格式如下:
struct tag //结构体名称
{
member - list; //结构体成员
}variable - list; //用该结构体定义出的一个结构体变量:此处为全局变量
实际例子:
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}; //分号不能丢
注意事项:
1、struct Stu{};
大括号外分号不能舍弃;
2、我们定义出的结构体变量可以在全局作用域,也可以在局部作用域。
struct Stu
{
char name[20];
int age;
char sex[5];
char id[20];
}st1,st2; //全局结构体变量定义:方式一
struct Stu st3; //全局结构体变量定义:方式二
int main()
{
struct Stu st4; //局部结构体变量定义
}
1.1.2、特殊的声明:不完全声明
1)、使用说明
结构体在声明的时候省略掉了结构体标签(tag),这样的结构体称之为匿名结构体。
struct //此处没有结构体名称
{
member - list;
}variable - list;
实例如下:
要使用这样的结构体,需要在声明时就创建相应的结构体变量,比如下述的st1
和st2
。由于没有名称,后续就不能使用该结构体创建新的结构体变量。
struct
{
char name[20];
int age;
char id[20];
}st1,st2;
2)、一个问题:两个成员变量完全一致的匿名结构体,是否是同一个结构体?
实例演示:
如下是两个成员变量一致的结构体,分别用其定义结构体变量x
、结构体数组a[20]
、结构体指针* p
。
struct //A
{
int a;
char b;
float c;
}x;
struct //B
{
int a;
char b;
float c;
}a[20], * p;
问题:下述赋值是否正确?
p = &a;
回答:编译器会把上面的两个声明当成完全不同的两个类型,故这种赋值是非法的。
1.2、结构体自引用
1.2.1、结构体自引用说明
1)、问题引入:结构体中能否包含结构体自身类型的成员变量?该如何声明这样的结构体?
写法举例一: 下述写法是否可行?
struct Node
{
int data;
struct Node next;
};
上述自引用写法存在一个问题,该结构体大小如何计算?
分析: 对struct Node
本身,其包含一个int
类型的变量,即4字节,再加上一个结构体自身变量,而这个结构体自身又包含这样一个int
类型的变量和其其本身的变量,如此循环反复,则该结构体类型大小是无法统计的。
结果: 在结构体中直接定义结构体变量本身是错误写法。
写法举例二:
struct Node
{
int data;//数据域
struct Node* next;//指针域
};
说明: 可行,使用结构体指针既能做到对结构体本身自引用,又能明确得出结构体大小。故上述这种写法是一种正确的结构体自引用的写法。
2)、如何对匿名对象进行结构体自引用?
根据1)中内容可知,对结构体自引用时,要使用对应的结构体指针,注意结构体指针的类型。由此产生一个疑惑,对于匿名结构体而言,由于没有结构体名称,那么该如何在匿名结构体中实现自引用呢?还是说匿名结构体本身不能自引用?
此处需要用到"typdef对结构体重命名"相关知识。
1.2.2、typdef对结构体重命名
1)、typdef在结构体中使用方式+该基础下结构体自引用说明
typedef struct Node
{
int data;
}Node;
int main()
{
Node s1; //两种写法都正确
struct Node s2;
}
如上述,typedef
关键字的作用是重命名变量,那么此处的含义是将结构体类型struct Node
重命名为Node
。那么定义结构体时就能起到简化作用。
问题1: 有了上述的基础认知,当我们在结构体中自引用时,对应的结构体指针的类型该如何写?
typedef struct Node
{
int data;
(这里类型是什么?)* next;
}Node;
回答: 同1.2.1中说明一致,可使用struct Node* next;
来声明结构体指针。
问题2: 既然我们使用了typedef定义结构体, 那么以下这种写法是否可行?
typedef struct Node
{
int data;
Node* next;
}Node;
回答: 错误。此处Node
是对结构体重命名后的名称,这就涉及到一个现有鸡还是现有蛋的问题。因此不能用上述方法声明。
2)、typdef在匿名结构体中使用方式+匿名结构体自引用说明
有了1)中的认知,那么对于匿名结构体的处理理解起来就方便许多。
问题1:能否使用typedef对匿名结构体重命名?能。
typedef struct
{
int data;
}Node;
int main()
{
Node s1;
Node s2;
return 0;
}
问题2:能否在typdef的基础上,对匿名结构体进行自引用?不能。
typedef struct
{
int data;
Node* next;
}Node;
这里涉及的问题同样是先有鸡还是先有蛋。
1.3、结构体变量的定义和初始化
此部分基于该结构体来讲解说明
struct Book //一本书
{
char name[20]; //书名
float price; //价格
char id[12]; //编号
};
1)、单独定义一个结构体变量
struct Book
{
char name[20];
float price;
char id[12];
};
struct Book B0;//定义
struct Book B1 ={ "偶然的创造",75.00 ,97870};//定义并初始化
int main()
{
struct Book A0;//定义
struct Book A1 = { "过于喧嚣的孤独",35.00 ,97875 };//定义并初始化
return 0;
}
2)、在结构体声明的同时定义结构体变量
struct Book
{
char name[20];
float price;
char id[12];
}B0; //在声明中定义结构体变量
struct Book
{
char name[20];
float price;
char id[12];
}B1 = { "细说宋朝",78.00,97872};//在声明中定义结构体变量并为其初始化
3)、存在结构体嵌套时如何初始化结构体变量?
struct Book
{
char name[20];
float price;
char id[12];
};
struct Node
{
struct Book b;
struct Node* next;
};
如上,现在又有一个结构体Node
,其内部有一个成员变量是struct Book
,问,如何为Node
初始化?
struct Book
{
char name[20];
float price;
char id[12];
};
struct Node
{
struct Book b;
struct Node* next;
};
struct Node N1;
struct Node N2 = { {"逆龄大脑",69.00, 97875},NULL };
int main()
{
struct Node N3 = { {"人类简史",68.00, 97872},NULL };
return 0;
}
1.4、结构体变量大小计算(重点)
1.4.1、问题引入
如下述两个成员变量类型相同的结构体,S1
、S2
,问它们的大小?
struct S1
{
char c1;//1
int i;//4
char c2;//1
};
struct S2
{
char c1;//1
char c2;//1
int i;//4
};
int main()
{
struct S1 s1;
struct S2 s2;
printf("%d\n", sizeof(s1));
printf("%d\n", sizeof(s2));
return 0;
}
以下为实际运行结果,由下述结果我们可以发现两个现象:
1、结构体大小不一定与结构体成员类型大小之和相等;
2、成员类型相同,但成员顺序不同的两个结构体,其大小不一定相同。
这说明对于结构体这种自定义类型的变量,其有相应的存储规则(大小计算方式)。
1.4.2、结构体对齐规则
相关规则如下:
1.4.2.1、例题一
1)、规则演示一:
#include<stdio.h>
struct S1
{
char c1;//1
int i;//4
char c2;//1
};
int main(void)
{
printf("%d\n", sizeof(struct S1));
return 0;
}
1.4.2.2、例题二
2)、规则演示二:
#include<stdio.h>
struct S2
{
char c1;//1
char c2;//1
int i;//4
};
int main(void)
{
printf("%d\n", sizeof(struct S2));//8
return 0;
}
1.4.2.3、例题三
3)、规则演示三:
#include<stdio.h>
struct S3
{
double d;
char c;
int i;
};
int main(void)
{
printf("%d\n", sizeof(struct S3));//16
return 0;
}
解析:vs环境,32位机
一个double,8字节,偏移量0-7;
一个char,1字节,偏移量8;
一个int,4字节,偏移量12-15;
大小为16,为最大对齐数4的倍数。
1.4.2.4、例题四
4)、规则演示四:
嵌套的结构体对齐到自己的最大对齐数的整数倍处
结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
#include<stdio.h>
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main(void)
{
printf("%d\n", sizeof(struct S4));//32
return 0;
}
1.4.3、相关宏:用于计算结构体成员到起始位置的偏移量
相关宏介绍
相关演示如下:
struct S1
{
char c1;//1
int i;//4
char c2;//1
};
struct S2
{
char c1;//1
char c2;//1
int i;//4
};
#include <stddef.h>
int main()
{
printf(" %u\n", offsetof(struct S1, c1));
printf(" %u\n", offsetof(struct S1, i));
printf(" %u\n", offsetof(struct S1, c2));
printf("\n");
printf(" %u\n", offsetof(struct S2, c1));
printf(" %u\n", offsetof(struct S2, c2));
printf(" %u\n", offsetof(struct S2, i));
return 0;
}
1.4.4、为什么会存在内存对齐?
结构体的内存对齐是拿空间来换取时间的做法。
1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
当参与访问数据时:
1.4.5、如何设计结构体、如何修改默认对齐数
1.4.5.1、结构体设计注意事项
1)、结构体设计注意事项
根据上述所学内容,在设计结构体时,我们既要满足对齐,又要节省空间,相对适合的方法是:让占用空间小的成员尽量集中在一起。
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
int main(void)
{
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}
1.4.5.2、修改默认对齐数
1)、基本修改方法:#pragma pack(对齐数值)
#pragma pack(4)//设置默认对齐数为4
struct S1
{
char c;
double d;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main(void)
{
printf("%d\n", sizeof(struct S1));
return 0;
}
2)、偏移量设置为0、偏移量设置为1
#pragma pack( 1 )
,即没有浪费空间,为数据实际类型大小
#pragma pack( 0 )
,恢复编译器默认对齐方式
#pragma pack(1)//设置默认对齐数为1
struct S2
{
char c;
double d;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main(void)
{
printf("%d\n", sizeof(struct S2));
return 0;
}
3)、一些说明
结构在对齐方式不合适的时候,我么可以自己更改默认对齐数,通常在有需要时按照2的倍数来设置,但不要随意设置默认对齐数。
1.4.5.3、实现一个用于计算偏移量的宏
要求:写一个宏,计算结构体中某变量相对于首地址的偏移,并给出说明
1.5、结构体传参
1)、基本说明
结构体传参分为传值传参和传址传参,要注意二者实参如何写、形参如何接受、如何访问等问题。(由于之前初始C语言和操作符详解中有涉及说明,此处不做过多介绍)
但需要注意的是,函数传参时,参数需要压栈,会有时间和空间上的系统开销。如果传递的结构体对象过大,参数压栈的系统开销比较大,会导致性能下降。因此,相对而言,使用结构体传参的时候,比较建议使用传址传参。
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;
}
2、段位
2.1、什么是段位
位段,与结构体类似,但有细微区别:
1.位段的成员必须是 char、int、unsigned int 或signed int 。
2.位段的成员名后边有一个冒号和一个数字。
1)、使用说明
段位:如下,A就是一个位段类型。
struct A //声明和结构体类似
{
int _a : 2;//注意格式:一个冒号和一个数字
int _b : 5; //注意类型
int _c : 10;
int _d : 30;
};
结构体:如下,AA就是一个结构体类型。
struct AA
{
int _a;
int _b;
int _c;
int _d;
};
对比:
2)、段位后跟随的数字含义理解
int _a : 2;
_a : 2
,这里的2的计数单位是比特位,表示两个二进制数为:2bit。两个bit位可表示的二进制数有:00、01、10、11,对应到十进制就是0、1、2、3。
位段的创建是为了满足一些场景需求,比如一个场景只需0-3范围内的数字,如果创建一个整型int变量,则32个比特位可表示的数字范围很大,会浪费很多不必要的空间。
2.2、段位的内存分配
1)、基本说明
1. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
2)、案例演示:vs下位段存储方式推测与验证
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;
}
2.3、位段不能跨平台使用说明以及实际应用举例
1)、位段不能跨平台使用
1. int 位段被当成有符号数还是无符号数是不确定的。
2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
2)、位段实际应用
网络协议:图源百度。
3、枚举
3.1、什么是枚举、枚举常量
1)、基本定义说明
枚举,顾名思义,将有可能的取值一一列举,通常用在可能值较少的变量上。枚举的声明方式与结体、联合类似。
使用枚举声明三原色:
enum Color //enum Color为枚举类型
{
RED,
GREEN,
BLUE
};
使用枚举声明性别:
enum Sex //enum Sex为枚举类型
{
MALE,
FEMALE,
SECRET
};
使用枚举声明星期:
enum Day //enum Day为枚举类型
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
2)、枚举常量
对于枚举,{}
中的内容是枚举类型的可能取值,也叫枚举常量。这些可能取值都是有值的,默认从0开始,一次递增1。
int main()
{
printf(" %d\n", RED);
printf(" %d\n", GREEN);
printf(" %d\n", BLUE);
printf("\n");
printf(" %d\n", MALE);
printf(" %d\n", FEMALE);
printf(" %d\n", SECRET);
printf("\n");
printf(" %d\n", Mon);
printf(" %d\n", Tues);
printf(" %d\n", Wed);
printf(" %d\n", Thur);
printf(" %d\n", Fri);
printf(" %d\n", Sat);
printf(" %d\n", Sun);
printf("\n");
}
注意,我们可以在定义枚举类型时为对应枚举常量赋初值(初始化),其后续值仍旧遵守递增1的规律。
enum Day
{
Mon=7,
Tues,
Wed,
Thur=1,
Fri,
Sat,
Sun
};
int main()
{
printf(" %d\n", Mon);
printf(" %d\n", Tues);
printf(" %d\n", Wed);
printf(" %d\n", Thur);
printf(" %d\n", Fri);
printf(" %d\n", Sat);
printf(" %d\n", Sun);
printf("\n");
}
3.2、枚举优点及使用
1)、关于使用枚举常量的优点说明
1. 增加代码的可读性和可维护性
2. 和#define
定义的标识符比较枚举有类型检查,更加严谨。
3. 防止了命名污染(封装)
4. 便于调试(#define
在预编译时已经做了处理,而我们调试时是在编译步骤进行调试的,因信息偏差会出现调试有出入的情况。)
5. 使用方便,一次可以定义多个常量
2)、枚举的使用
enum Color//颜色
{
RED = 1,
GREEN = 2,
BLUE = 4
};
int main()
{
enum Color clr2 = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
clr2 = 5;
}
4、联合(共用体)
4.1、什么是联合
联合也是一种特殊的自定义类型。这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。
演示如下:
union Un
{
char c;
int i;
};
int main()
{
//联合变量的定义
union Un un;
//计算连个变量的大小
printf("%d\n", sizeof(un));
printf("%p\n", &un);
printf("%p\n", &un.c);
printf("%p\n", &un.i);
return 0;
}
4.2、联合体具有什么特点
由4.2可知,联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员),此外,由于空间共用,当给一个联合体成员赋新值时,会改变其它成员的值。
1)、演示实例一:
union Un
{
char c;
int i;
};
int main()
{
//联合变量的定义
union Un un;
//下面输出的结果是什么?
un.i = 0x11223344;
printf("%x\n", un.i);
un.c = 0x55;
printf("%x\n", un.i);
return 0;
}
2)、实际应用演示:计算大小端
int test()
{
union Un
{
char c;
int i;
};
union Un u;
u.i = 1;
return u.c;
}
int main()
{
if (1 == test())
{
printf("小端\n");
}
else
printf("大端\n");
}
4.3、联合体的大小计算
相关规则:
联合的大小至少是最大成员的大小。
当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
演示代码如下:
union Un0
{
char c;
int i;
};
union Un1
{
char c[5];
int i;
};
union Un2
{
short c[7];
int i;
};
int main()
{
//下面输出的结果是什么?
printf("%d\n", sizeof(union Un0));
printf("%d\n", sizeof(union Un1));
printf("%d\n", sizeof(union Un2));
}