4. 自定义类型
4.1 结构体
定义:一些值得集合,这些值称为成员变量。结构体每个成员可以是不同类型的变量。
4.1.1 结构体类型声明
-
普通声明
struct Person { char name[10]; int age; float height; }; struct Person { char name[10]; int age; float height; }p1, p2; // 声明结构体类型并声明结构体变量p1、p2
-
特殊声明(匿名结构体)
省略标签,不完全声明结构体类型。只能声明一次变量。
struct { char name[10]; int age; float height; }p; // 声明匿名结构体类型 并声明结构体变量 struct { char name[10]; int age; float height; }*p; // 声明匿名结构体类型 并声明结构体指针变量
那么上面的两个结构体变量能否写成如下形式?
ps = &s;
警告:编译器将两种声明当成完全不同的两个类型
4.1.2 结构体的自引用
一个结构体中可以包含另一个另一个结构体类型。那结构体中能否包含本身结构体类型?
// 答案:不能
// 原因:形成递归 内存大小无法确定
自引用
该结构体变量能找到同类型的另一个结构体变量,声明中包含同类型结构体的指针,而不是同类型结构体的变量。
// 正确自引用
struct Node
{
int data;
struct Node* next;
}
// 错误自引用
typedef struct
{
int data;
Node* next;
}Node;
// 通过类型重命名得到的Node 不能再结构体声明中直接使用
4.1.3 结构体变量的定义和初始化
-
定义
struct Person { char name[10]; int age; float height; }p1; // 方式一 int main() { struct Person p2; // 方式二 return 0; }
-
初始化
struct Person { char name[10]; int age; float height; }p1 = {"listen", 20, 185.0}; // 方式一 int main() { struct Person p2 = { "turbo", 25, 178.5 }; // 方式二 return 0; }
-
结构体嵌套初始化
struct Person { char name[10]; int age; float height; } struct Student { char id[20]; float score; struct Person p; }s1 = { "1914121006", 63.95, {"listen", 20, 185.0} }; // 方式一 int main() { struct Student s2 = { "2114134566", 78.50, {"turbo", 19, 178.5} }; // 方式二 return 0; }
4.1.4 结构体内存对齐
-
内存对齐规则
- 第一个成员在与结构体变量偏移量为0的地址处
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
对齐数=编辑器默认的一个对齐数与该成员大小的较小值(VS中默认的值为8,linux系统中没有默认对齐数) - 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
- 嵌套结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有对齐数(含嵌套结构体的对齐数)中的最大值的整数倍
-
实例1
struct S1 { char c1; int i; char c2; }; struct S2 { char c1; char c2; int i; }; int main() { printf("%d\n", sizeof(struct S1)); // 12 printf("%d\n", sizeof(struct S2)); // 8 } // 输出结果: // 12 // 8
-
实例2
struct S1 { char c1; int i; char c2; }; struct S2 { int i; char c; struct S1 s; }; int main() { printf("%d\n", sizeof(struct S2)); // 20 } // 输出结果: // 20
-
为什么存在内存对齐 — 空间换时间
- 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
- 硬件原因:经过内存对齐之后,可以减少CPU访问内存的次数和运算,提高效率。
-
修改默认对齐数
#pragma pack(8) // 设置默认对齐数为8 #pragma pack() // 取消设置的默认对齐数 还原为默认值 // 一般设置的默认对齐数为2的倍数
-
补充
通过实例1可得,调整结构体成员顺序,将较小的成员放在一块,可以在一定程度上减小结构体的大小。
4.1.5 结构体传参
struct Data
{
int id;
char data[1000];
};
void print1(struct Data d)
{
printf("%d --> %s\n", d.id, d.data);
}
void print1(struct Data *d)
{
printf("%d --> %s\n", d->id, d->data);
}
int main()
{
struct Data d = { 0 };
print1(d);
print2(&d);
return 0;
}
print1
和print2
哪个函数更好?
答案:print2
原因:如果传递的是结构体变量,函数传参时,将拷贝结构体,将造成一定程度空间上和时间上的系统开销;而传递的是结构体指针,指针变量也就4或8字节,并不会造成过大的系统开销。此外,传递结构体指针,还能在函数体内修改结构体变量的成员值;如果不想修改,只需在函数形参前加上const
4.1.6 百度笔试题
- 要求:写一个宏,计算结构体中某变量相对于首地址的偏移量,并给出说明
- 考察:
offsetof
宏的实现
4.2 位段(或位域)
4.2.1 什么是位段
定义结构体时,指定成员变量所占用的二进制位。数据存储时,并不需要一个完整的字节,只需要占用一个或几个字节即可,因此来节省空间。
- 位段的成员必须是int、unsigned int 或 signed int
- 位段的成员名后边有一个冒号和一个数字
- 位域在本质上就是一种结构类型,不过其成员是按二进位分配的
例如
struct bs{
int a:8;
int b:2;
int c:1;
};
// a占8位 b站2位 c站1位
4.2.2 位域定义
struct 位域结构名
{
type [member_name] : width ;
type [member_name] : width ;
type [member_name] : width ;
...
};
// type: 只能为 int、unsigned int、signed int 决定如何解释位域的值
// member_name:位域名称
// width:位域中位的数量 宽度必须小于或等于指定类型的位宽度
4.2.3 位域的使用
位段的使用和结构体变量相同。
位域变量名.位域名
位域指针变量名->位域名
-
一个位域存储在同一个字节中,如一个字节所剩空间不够存放另一位域时,则会从下一单元起存放该位域。也可以有意使某位域从下一单元开始。
struct bs{ int a:4; int :4; /* 空域 */ int b:4; /* 从下一单元开始存放 */ int c:4; } // a 占第一字节的 4 位,后 4 位填 0 表示不使用 // b 从第二字节开始,占用 4 位,c 占用 4 位
-
位域的宽度不能超过它所依附的数据类型的长度,成员变量都是有类型的,这个类型限制了成员变量的最大长度,: 后面的数字不能超过这个长度。
-
位域可以是无名位域,这时它只用来作填充或调整位置。无名的位域是不能使用的。
struct bs{ int a:4; int :3; /* 空域 3不能使用 */ int b:2; int c:1; }
-
位域成员往往不占用完整的字节,有时候也不处于字节的开头位置,因此使用
&
获取位域成员的地址是没有意义的,C语言也禁止这样做。地址是字节(Byte)的编号,而不是位(Bit)的编号。
4.2.2 位段的内存分配
-
几点说明:
- 位段的成员可以是int、 unsigned int、 signed int、char、signed char、unsigned char (整形家族) 类型(其中char、signed char、unsigned char 类型C标准中并没有规定,仅仅是VS编译器对其进行了扩充)
- 位段的空间上是按照需要以4byte(int)或者1byte(char)的方式开辟
- 位段涉及很多不确定因素,位段是不跨平台的,注意可移植程序应避免使用位段
- C语言标准并没有规定位域的具体存储方式,不同的编译器有不同的实现,但它们都尽量压缩存储空间
-
位段的大小是多少呢?
struct bs { int a : 4; int : 3; int b : 2; int c : 1; }; int main() { printf("%d\n", sizeof(struct bs)); // 4 return 0; } // 输出结果: // 4
-
基本存储规则
- 当相邻成员的类型相同时,如果它们的位宽之和小于类型的 sizeof 大小,那么后面的成员紧邻前一个成员存储,直到不能容纳为止;如果它们的位宽之和大于类型的 sizeof 大小,那么后面的成员将从新的存储单元开始,其偏移量为类型大小的整数倍。
- 当相邻成员的类型不同时,不同的编译器有不同的实现方案,GCC会压缩存储,而 VC/VS 不会(与不指定位宽时的存储方式相同)。
- 如果成员之间穿插着非位域成员,那么不会进行压缩。
- VS中,内存分配如图所示
实例:位段S的内存分配。
4.2.3 位段的跨平台问题
- int位段被当成有符号数还是无符号数是不确定的
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,如果在16位机器中写成大于16的数则会出现问题)
- 位段中成员在内存中从左向右分配,还是从右向左分配标准尚未定义
- 当一个结构体包含两个位段,第二个位段成员较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的
4.2.4 位段的应用
TCP头部报文
4.2.5 习题
代码输出结果
#include <stdio.h>
#include <string.h>
int main()
{
unsigned char puc[4];
struct tagPIM
{
unsigned char ucPim1;
unsigned char ucData0 : 1;
unsigned char ucData1 : 2;
unsigned char ucData2 : 3;
} *pstPimData;
pstPimData = (struct tagPIM*)puc;
memset(puc, 0, 4);
pstPimData->ucPim1 = 2;
pstPimData->ucData0 = 3;
pstPimData->ucData1 = 4;
pstPimData->ucData2 = 5;
printf("%02x %02x %02x %02x\n", puc[0], puc[1], puc[2], puc[3]); // 02 29 00 00
return 0;
}
4.2 枚举
C 语言中的一种基本数据类型。将可能的值一一列举。
// 比如:星期 1~7
// 性别 男 女 其他
// 月份 1~12
4.2.1 枚举类型的定义
enum Day // 星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex // 性别
{
Male,
Female,
Other
};
// 第一个枚举成员的默认值为整型的 0
// 后续枚举成员的值在前一个成员上加 1
// 如果将第一个枚举的成员变量定义为1
// 那么第二个就为2 一次类推 自增1
// 没有指定枚举元素的值 默认为前一个元素+1
// C语言中 枚举类型是被当做 int 或者 unsigned int 类型来处理
4.2.2 枚举变量的定义
-
先定义类型,后定义枚举变量
enum Sex // 性别 { Male, Female, Other }; enum Sex s;
-
定义类型的同时定义枚举变量
enum Sex // 性别 { Male, Female, Other } s;
-
省略枚举名称,直接定义枚举变量
enum // 性别 { Male, Female, Other } s;
4.2.2 枚举的优点
- 增加代码的可读性和可维护性
- 和#define定义的标识符比较枚举有类型检查,更加严谨 枚举是一种类型
- 防止命名污染(封装)
- 便于调试。#define在预编译时替换,在调试时只有被替换值
- 使用方便,一次定义多个常量(相比于#define)
4.2.3 枚举的使用
enum Sex// 性别
{
Male,
Female,
Other
}s;
int main()
{
enum Sex s = Female;
printf("%d\n", s); // 1
printf("%d\n", sizeof(s)); // 4
return 0;
}
4.2.4 枚举遍历
enum Day // 星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
int main()
{
enum Day day;
for (day = Mon; day <= Sun; day++)
{
printf("%d\n", day);
}
return 0;
}
// 输出结果:
// 0
// 1
// 2
// 3
// 4
// 5
// 6
4.3 联合体(或共用体)
4.3.1 联合类型的定义
是一种特殊的数据类型,允许在相同的内存位置存储不同的数据类型。可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。共用体提供了一种使用相同的内存位置的有效方式。
-
格式
union [union tag] { member definition; member definition; member definition; ... } [union variables]; // [] 内的值是可选的 // union定义与结构体定义相似
-
实例
union Data { char c; int i; } d;
4.3.3 共用体使用
共用体成员的访问,使用成员访问操作符
.
// 实例
union Data
{
char c;
int i;
} d;
int main()
{
d.i = 10000000;
printf("%d\n", d.i); // 10000000
printf("%d\n", d.c); // -128
d.c = 'a';
printf("%d\n", d.i); // 9999969
printf("%d\n", d.c); // 97
return 0;
}
- 应用—判断机器大小端字节序
-
字符指针
int sysinfo() { // 返回值 0 -- 大端存储 // 返回值 1 -- 小端存储 int a = 1; return *(char*)(&a); } int main() { printf("%d\n", sysinfo()); // 1 return 0; }
-
共用体
int sysinfo() { // 返回值 0 -- 大端存储 // 返回值 1 -- 小端存储 union Data { char c; int i; } d; d.i = 1; return d.c; } int main() { printf("%d\n", sysinfo()); // 1 return 0; }
-
4.3.3 联合的特点
联合体成员是共用一块内存空间,一个联合变量的大小,至少是最大成员的大小。
union Data
{
char c;
int i;
} d;
int main()
{
printf("%p\n", &d);
printf("%p\n", &d.c);
printf("%p\n", &d.i);
return 0;
}
4.3.4 联合大小的计算
- 联合的大小至少是最大成员的大小
- 当最大成员大小不是最大对齐数的正数倍时,对齐到最大对齐数的整数倍
union U1
{
char c[5]; // 对齐数 1 占用 5byte
int i; // 对齐数 4 占用 4byte
};
// 最大共用体成员占用内存5byte 最大对齐数4 所以共用体总占用内存 8byte (4byte × 2 > 5byte)
union U2
{
short c[7]; // 对齐数 2 占用 14byte
int i; // 对齐数 4 占用 4byte
};
// 最大共用体成员占用内存14byte 最大对齐数4 所以共用体总占用内存 16byte (4byte × 4 > 14byte)
int main()
{
printf("%d\n", sizeof(union U1)); // 8
printf("%d\n", sizeof(union U2)); // 16
}
4.3.5 练习
代码输出结果。
int main()
{
union
{
short k;
char i[2];
}*s, a;
s = &a;
s->i[0] = 0x39;
s->i[1] = 0x38;
printf("%x\n", a.k);
return 0;
}
对于a.k,short类型,在vs中小端存储,数值低位存储在低地址内存中,数值高位存储在高地址中。所以s.k=0x3839
4.4 实战-通讯录
- 要求
- 通讯录中能够存放1000个人的信息
- 信息:名字、年龄、性别、电话、地址
- 增加人的信息
- 删除指定人名的信息
- 修改指定人名的所有信息
- 查找指定人名的信息
- 排序通讯录的信息
- 通讯录中能够存放1000个人的信息