c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第四式】自定义类型详解(结构体、枚举、联合)
c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第四式】自定义类型详解(结构体、枚举、联合)
【心法】
【第零章】c语言概述
【第一章】分支与循环语句
【第二章】函数
【第三章】数组
【第四章】操作符
【第五章】指针
【第六章】结构体
【第七章】const与c语言中一些错误代码
【禁忌秘术】
【第一式】数据的存储
【第二式】指针
【第三式】字符函数和字符串函数
【第四式】自定义类型详解(结构体、枚举、联合)
文章目录
- c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第四式】自定义类型详解(结构体、枚举、联合)
- 前言
- 一、结构体
- 1. 结构的基础知识
- 2. 结构的声明
- 3. 特殊的声明
- 4. 结构的自引用
- 5. 结构体变量的定义和初始化
- 6. 结构体内存对齐
- 7. 修改默认对齐数
- 8. 结构体传参
- 二、位段
- 1. 什么是位段
- 2. 位段的内存分配
- 3. 位段的跨平台问题
- 4. 位段的应用
- 三、枚举
- 1. 枚举类型的定义
- 2. 枚举的优点
- 3. 枚举的使用
- 四、联合
- 1. 联合类型的定义
- 2. 联合的特点
- 3. 联合大小的计算
- 4. 联合休的使用实例:
- 五、使用结构体实现一个通讯录
- 总结
前言
c语言中虽然定义得有许多的类型,如char、int、double等等,但是现实生活中需要描述的问题,仅仅使用这些已有类型是不够的,所以在c语言中,程序员可以根据自己的需要,定义出自己的类型结构,以满足需求。
本文会对c语言中的结构体进行详细介绍,包括:
- 结构体:
- 结构体类型的声明
- 结构的自引用
- 结构体变量的定义和初始化
- 结构体内存对齐
- 结构体传参
- 结构体实现位段(位段的填充&可移植性)
- 枚举
- 枚举类型的定义
- 枚举的优点
- 枚举的使用
- 联合
- 联合类型的定义
- 联合的特点
- 联合的使用
一、结构体
1. 结构的基础知识
结构是一些值的集合,这些值被称为成员变量。结构的每个成员可以是不同类型的变量。
与数组作区分:
虽然数组也是一组值的集合,但数组的这组值的类型相同;
2. 结构的声明
struct Tag
{
member-list;
} variable-list;
// 使用关键字struct来声明一个结构体
// Tag是结构体的名字
// {}中成员变量
// {}后面的是结构体变量
例如用结构体来描述一个学生
struct Stu
{
char name[20]; // 名字
int age; // 年龄
char sex[5]; // 性别
char id[10]; // 学号
}; // 分号不能省略
3. 特殊的声明
除了上面的声明方式外,在结构体的声明时可以不完全声明
// 还可以不声明结构体的名字,定义一个匿名结构体
struct
{
member-list;
} variable-list;
// 但是通过这种方法定义的结构体,就只有在定义这里创建的几个结构体变量,之后无法定义一个新的变量,因为没有名字。
struct
{
int a;
short b;
char c;
double d;
} x;
// 这个结构体只有x这一个变量,无法定义新变量
struct
{
int a;
short b;
char c;
double d;
} *p;
// 这个结构体只有p这一个指针变量,无法定义新变量
上面的两个结构体都省略了结构体标签Tag,那么问题来了:
// 在上面代码的基础上下面代码合法吗?
p = &x;
非法
因为编译器会将上面的两个声明当作两个完全不同的类型,所以这种行为是非法的。
4. 结构的自引用
结构体中的成员变量类型还可以是其它的结构体:
struct A
{
int a;
short b;
char c;
float d;
};
struct B
{
int data;
struct A a;
};
那么有一个问题,一个结构体的成员变量能否是它自身呢?
struct Node
{
int data;
struct Node next;
};
// 这可行吗?
// 如果可行,这个结构体在内存中占多大空间呢?
// 即sizeof(struct Node)是多少
显然这种行为是不可行的,结构体中的成员变量是这个结构体,那么这个结构体在内存中占多大空间就无法确认;
正确的引用方式:
struct Node
{
int data;
struct Node* next;
};
// 通过指针来实现结构体的自引用
// 指针的大小是根据机器的平台决定的,这是一个确定的值,所以这个结构体在内存中占据的空间是确定的
注意,当我们使用typedef来重命名结构体时,通常都会使用不完全的结构体声明
如:
typedef struct
{
int data;
char ch;
} Test;
// 这是一个匿名结构体,并将其重命名为Test
那么下面代码正确吗?
typedef struct
{
int data;
Test* node;
} Test;
不行!
因为,是先有了这个结构体,才能将其重命名为Test,但是在结构体的成员变量中先使用Test,这样就产生了冲突,所以这种方式是不合法的;
应该使用下面的方式进行结构体的自引用;
typedef struct Node
{
int data;
struct Node* node;
} Node;
5. 结构体变量的定义和初始化
有了结构体类型,之后应该如何定义变量呢?
#include <stdio.h>
struct Point
{
int x;
int y;
} p1; // 声明类型的同时定义一个变量p1
struct Point p2; // 定义变量p2,struct Point是这个类型的名字
// 初始化
struct Point p3 = { x, y };
// 这里的x, y表示int类型的数值
// 同数组相同,一组元素使用{}初始化赋值,可以不完全初始化,后续的成员变量会被初始化为0
struct Stu // 声明类型
{
char name[20]; // 名字
int age; // 年龄
};
struct Stu stu = { "zhangsan" ,22 }; // 初始化
struct Node
{
int data;
struct Point p;
struct Node* next;
} n1 = { 10, { 4, 5 }, NULL }; // 结构体嵌套初始化
struct Node n2 = { 20, { 3, 7 }, &n1 }; // 结构体嵌套初始化
6. 结构体内存对齐
通过上面的学习,我们已经掌握了结构体的基本使用了,但是一个结构体在内存中占据多大空间呢?它的成员变量的空间是如何分布的呢?
下面我们就来看看结构体内存对齐:
#include <stdio.h>
int main()
{
struct S1
{
char c1;
int i;
char c2;
};
printf("S1 = %d\n" , sizeof(struct S1));
struct S2
{
char c1;
char c2;
int i;
};
printf("S2 = %d\n", sizeof(struct S2));
struct S3
{
double d;
char c;
int i;
};
printf("S3 = %d\n", sizeof(struct S3));
struct S4
{
char c;
struct S3 s3;
double d;
};
printf("S4 = %d\n", sizeof(struct S4));
return 0;
}
S1和S2的成员变量类型相同,只是排序不同,它们在内存中占据的空间相同吗?
一个结构体在内存中占据空间的大小,是成员变量占据内存空间的大小之和吗?
运行结果:
可以看到,S1和S2的大小不同;
结构体占据的内存空间大小也不等于它的成员变量占据空间之和;
那么,结构体变量的内存大小应该如何计算呢?
首先,我们先了解结构体的对齐规则
:
第一个成员在与结构体变量偏移量为0的地址处;
其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处;
对齐数 = 编译器默认的一个对齐数 与 该成员大小的 较小值
- VS中默认为8,例如一个int类型的成员变量的对齐数为4,一个char类型的成员变量的对齐数为1,一个空间大于8的结构体类型的对齐数为8;
结构体的总大小为最大对齐数(每个成员都有一个对齐数)的整数倍,这个结构体的对齐数就是它的最大对齐数;
如果嵌套了结构体,这个嵌套的结构体对齐到它的最大对齐数的整数倍处,结构体整体大小就是所有最大对齐数(包括嵌套的结构体的对齐数)的整数倍;
那么为什么要在内存对齐呢?(大部分资料)
- 平台原因:
不是所有的硬件平台都能够访问任意地址上的任意数据;某些硬件平台只能在某些地址处取某些特定类型的数据,否则会抛出异常; - 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐;
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
例如,一个int类型,在32位机器上,一次读取能够获取4个字节,如果这个int类型的数据在内存上不对齐,就不能拿到完整的数据,需要多读取一次;
总的来说,结构体的内存对齐就是拿空间换时间
的做法;
那么相同的成员类型,不同的排序顺序,结构体占据的内存空间也可能是不同的,为了节省空间,应该:让占空间小的成员尽量集中在一起
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
// s1和S2的成员一模一样,但是它们占据的内存空间大小是不一样的
练习
现在对这部分最开始给出的4个结构体占据的空间进行详细分析:
结构体的第一个成员的偏移量为0,所以c1在这个结构体的最前面占一个字节,int类型占4个字节,所以一个int类型的成员的对齐数为4,它的起始位置的偏移量为4的整数倍,所以此时需要先空3个字节,i从第四个字节开始存放,char类型占一个字节,对齐数为1,地址地址的偏移量为1的整数倍,所以可以接着继续放,最后因为结构体的总大小为所有成员的最大对齐数的整数倍,此时的最大对齐数为4,所以此时需要再补3个字节,补成12字节,所以一个S1类型的变量占12个字节;
分析同上,不再赘述;
第一个成员c占1个字节,从0偏移处开始,第二个成员是一个结构体,由上面的分析得知,S3占16个字节,所以它的对齐数为8,此时需要填充7个字节,从第8字节开始,最后是一个对齐数为8的double类型,开始位置的偏移量应为8的倍数,所以S4这个类型占32个字节的空间;
7. 修改默认对齐数
使用#pragma
这个预处理指令可以修改默认对齐数;
#include <stdio.h>
#pragma pack(8) // 设置默认对齐数为8
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack() // 取消设置的默认对齐数,还原为默认的8 -- VS中
#pragma pack(1) // 将默认对齐数设为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;
}
分析:
运行结果:
结构体在对齐方式不合适的时候,可以自己更改默认对齐数;
百度面试题:
写一个宏,计算结构体中某变量相对于首地址的偏移,并给出说明,参考offsetof
宏的实现
#define OFFSETOF(type, member) (unsigned int)(&(((type*)0)->member))
// 将0作为一个结构体类型的指针,通过指针->访问到对应成员member,再取地址,
// 因为这个结构体类型的地址为0,所以此时这个成员的地址就相当于它的偏移量,再将这个地址转为无符号整数,
// 这样就能获取到一个结构体类型中某成员变量相对于首地址的偏移
#include <stdio.h>
struct Stu
{
char name[20];
int age;
char sex[5];
char id[10];
};
int main()
{
printf("%d\n", OFFSETOF(struct Stu, age));
return 0;
}
运行结果:
注意,这里的数组的对齐数是这个数组中元素的对齐数,它是一个char类型的数组,所以这个数组的对齐数为1
这与嵌套结构体的对齐数相同,结构体的对齐数为最大对齐数,是这个结构中的成员变量的最大的对齐数,数组中元素相同,所以最大对齐数就是数组元素的对齐数;
8. 结构体传参
直接看代码,下面的代码1和代码2哪个更好
#include <stdio.h>
struct S
{
int data[1000];
int num;
};
struct S s = { { 1,2,3,4 }, 1000 };
// 代码1
void print1(struct S s)
{
printf("%d\n", s.num);
}
// 代码2
void print2(struct S* s)
{
printf("%d\n", s->num);
}
int main()
{
print1(s); // 传结构体
print2(&s); // 传指针
return 0;
}
代码1是传值调用,代码2是传址调用;
我们都知道,
传值调用,是将实参拷贝一份,函数对临时变量进行处理;
传址调用,是将实参的地址传给函数,函数能通过指针,直接对参数进行处理;
传值相比传址会多做一次拷贝,存在额外开销,当结构体类型占据空间很庞大时,这个额外开销也就随着增大;
所以从性能角度出发,代码2的传址调用更好;
所以在结构体传参时,要传结构体的地址
二、位段
1. 什么是位段
位段的声明和结构体是类似的,只有两个不同:
- 位段的成员必须是
int
、unsigned int
、signed int
、char
、unsigned char
、signed char
- 位段的成员名后面有一个冒号
:
和一个数字
比如:
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
A就是一个位段类型,那么位段A占多大的内存呢?
8个字节;
怎么来的呢?
每个成员的:
后面的数字表示这个成员占多少个比特位;
int表示每次分配空间都分配一个int类型的大小,也就是4个字节,然后使用这4个字节为成员分配空间,直到剩余空间不足,继续分配一个int类型的大小;
在这个例子中,先位段分配4个字节,成员_a占2个bit,此时还剩30个bit;成员_b占5个bit,此时还剩25个bit;成员_c占10个bit,此时还剩15个bit;成员_d需要30个bit,剩余的空间不足,所以再分配4个字节,此时要怎么使用呢?
是使用了剩余的15bit,再从新的32bit中分配15bit;还是直接从新的32bit中分配30bit?
正确的是后者。
所以这个位段占的空间大小为8个字节;
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;
}
注意:
位段的成员的长度是有限制的,
char类型的成员长度不能超过8;
int类型的成员长度不能超过32;(32位或64位机器下)
3. 位段的跨平台问题
- int位段被当成有符号数还是无符号数是不确定的;
- 位段中最大位的数目是不确定。(16位机器下,最大为16,写成27,在16位机器中是会出问题的);
- 位段中的成员在内存中从左向右,还是从右向左分配标准尚未定义;
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的;
总结
与结构相比,位段可以达到同样的效果,但是可以更好的节省空间,但是会存在跨平台问题;
4. 位段的应用
像计算机网络中的IP数据报的格式,就是使用了位段。这样节省了传递的报文的长度,使得报文的有效数据更高。具体就不展开介绍了。
注意
因为位段的成员的开始地址并不是一个字节的起始位置,只有字节是有地址的,比特位是没有地址的,所以&s._a
这种行为是错误的,要输入值到一个位段的成员,是不能直接使用scanf
函数的,只能将值输入到一个变量中,通过赋值,将这个值传给位段成员;
三、枚举
枚举顾名思义就是一一列举。
比如,一周有7天,从星期一到星期天是有限的7天,可以一一列举;
月份有12个月,也可以一一列举;
这里就能使用枚举了。
1. 枚举类型的定义
enum Day // 星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Color // 颜色
{
Red,
Green,
Blue
};
enum Month // 月份
{
Jan,
Feb,
Mar,
Apr,
May,
Jun,
Jul,
Aug,
Sep,
Oct,
Nov,
Dec
};
以上定义的enum Day
、enum Color
、enum Month
都是枚举类型。
{}中的内容是枚举类型的可能取值,也叫做枚举常量
。
这些可能的取值是可确定的,默认从0开始,依次递增1,在定义的时候也可以赋初值。
如:
enum Color // 颜色
{
Red, // 0
Green, // 1
Blue // 2
};
enum Color // 颜色
{
Red = 1, // 1
Green = 3, // 3
Blue // 4
};
枚举是一种类型(枚举类型),它的成员是常量(枚举常量),枚举常量的类型是int;
2. 枚举的优点
为什么要用枚举呢?
- 增加代码的可读性和可维护性;
- 和
#define
定义的标识符比较,枚举有类型检查,更加严谨; - 防止命名污染(封装);
- 便于调试;
- 使用方便,可一次定义多个常量;
例如:前面我们写过的计算器
void menu()
{
printf("*******************\n");
printf("*** 1.Add 2.Sub ***\n");
printf("*** 3.Mul 4.Div ***\n");
printf("*** 0.exit ***\n");
printf("*******************\n");
}
enum Op
{
exit,
add,
sub,
mul,
div
};
int main()
{
int input;
printf("请选择:>");
scanf("%d", &input);
// 代码1
switch (input)
{
case 0:
// 退出
break;
case 1:
Add();
break;
case 2:
Sub();
break;
case 3:
Mul();
break;
case 4:
Div();
break;
default:
// 选错
break;
}
// 代码2
switch (input)
{
case exit:
// 退出
break;
case add:
Add();
break;
case sub:
Sub();
break;
case mul:
Mul();
break;
case div:
Div();
break;
default:
// 选错
break;
}
return 0;
}
这段代码中,代码1如果不看menu函数,估计没人知道case 1这些分支表示什么意思,但是代码2中使用了枚举类型,就能一眼看出每个分支是干什么的,增加了代码的可读性;
#define
的功能是直接替换,并不会因为类型不同而产生不同的操作;
除此之外,#define
定义的标识符常量是全局的,而枚举类型的值只能被该类型的变量访问;
从源程序到可执行程序再到执行经过的过程为:
#define
的替换是在预编译过程中完成的,调试无法发现#define
带来的错误;
3. 枚举的使用
使用上面的Color类型:
int main()
{
enum Color color1 = Blue;
enum Color color2 = 2; // err
return 0;
}
直接将int类型赋值给枚举类型是错误的,虽然Blue这个枚举常量的值也是2,但是它的类型的enum Color这个枚举类型,c语言是会对枚举类型进行类型检查的;
四、联合
1. 联合类型的定义
联合也是一种特殊的自定义类型;
这种类型定义的变量也包含一系列的成员,特征是这些成员都共用一块内存空间(所以,联合也叫共用体);
比如:
#include <stdio.h>
union Un
{
char ch;
int i;
} ;
union Un1
{
double d;
char ch;
int i;
};
int main()
{
printf("%d\n", sizeof(union Un));
printf("%d\n", sizeof(union Un1));
return 0;
}
运行结果:
2. 联合的特点
联合的成员共用一块内存空间,这样一个联合变量的大小,至少也是最大成员的大小,因为联合至少要能保存它最大成员的值;
下面代码输出什么?
#include <stdio.h>
union Un
{
int i;
char c;
};
int main()
{
union Un un;
printf("%p\n", &un);
printf("%p\n", &(un.i));
printf("%p\n", &(un.c));
return 0;
}
分析:
因为联合体变量un有两个成员i、c,这个联合变量占4个字节,通过内存地址的使用都是从低位到高位,所以i占整4个字节,c占地址最小的1个字节;
运行结果:
下面代码的输出又是什么呢?
int main()
{
union Un un;
un.i = 0x11223344;
un.c = 0x55;
printf("%x\n", un.i);
return 0;
}
分析:
运行结果:
面试题
判断当前计算机的大小端存储
之前实现方式是将一个int类型的变量赋值为1,
如果机器是大端字节序,那么它在内存中的存储为(低到高):00 00 00 01;
如果机器是小端字节序,那么它在内存中的存储为(低到高):01 00 00 00;
取这个int类型变量的地址,将其转换为char*类型,访问这个指针指向的内存空间的内容(处于低位的第一字节):
如果为0,则为大端;如果为1,则为小端;
这种方法和联合的使用刚好相符,所以此时可以使用联合类型,就不需要强制类型转换了;
具体代码如下:
#include <stdio.h>
union Un
{
char c;
int i;
};
int main()
{
union Un un;
un.i = 1;
if (un.c)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
3. 联合大小的计算
- 联合的大小至少是最大成员的大小;
- 当最大成员大小不是最大对齐数的整数倍时,就要对齐到最大对齐数的整数倍;
举个例子:
#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;
}
分析:
根据上面的规则,
Un1中最大对齐数是i的对齐数4,最大成员的大小是c数组的大小5,此时5不是4的整数倍,所以此时的联合的大小为8;
Un1中最大对齐数是i的对齐数4,最大成员的大小是c数组的大小14,此时14不是4的整数倍,所以此时的联合的大小为16;
运行结果:
4. 联合休的使用实例:
使用联合体是可以节省空间的,例如:
现在要举办一个活动,要上线一个礼品兑换单,礼品兑换单有三种商品:图书、杯子、衬衫。
每种商品都有:库存量、价格、商品类型和商品类型相关的其他信息。
图书:书名、作者、页数;
杯子:设计;
衬衫:设计、可选颜色、可选尺寸;
如果不认真思考,可能就会直接使用下面类似的结构:
struct gift_list
{
// 公共属性
int stock_number; // 库存量
double price; // 价格
int item_type; // 商品类型
// 特殊属性
// 图书
char title[20]; // 书名
char author[20]; // 作者
int num_pages; // 页数
// 杯子
char design[30]; // 设计
// 衬衫
// 与杯子共用设计成员属性
int colors; // 颜色
int sizes; // 尺寸
};
这样的结构设计非常简单,但结构中包含了所有礼品的属性,对于一个礼品来说有些属性是不需要的,使用这个结构记录数据是会浪费内存的;
上面的礼品中只有一些属性是共用,对于那些特殊属性就可以使用联合体来保存,以此来节省一些内存;
所以上面的代码可以改成:
struct gift_list
{
// 公共属性
int stock_number; // 库存量
double price; // 价格
int item_type; // 商品类型
// 使用匿名联合体,在声明时定义一个变量item
union
{
// 这个联合体中的成员是三个匿名结构体的变量,book、cup、shirt
struct
{
// 图书
char title[20]; // 书名
char author[20]; // 作者
int num_pages; // 页数
} book;
struct
{
// 杯子
char design[30]; // 设计
} cup;
struct
{
// 衬衫
char design[30]; // 设计
int colors; // 颜色
int sizes; // 尺寸
} shirt;
} item;
// 特殊属性
};
五、使用结构体实现一个通讯录
contact.c
#define _CRT_SECURE_NO_WARNINGS
#include "contact.h"
void initContact(Contact* contact)
{
assert(contact);
memset(contact->data, 0, MAXNUM * sizeof(Peoinfo));
contact->count = 0;
}
static int isFull(const Contact* contact)
{
if (contact->count == MAXNUM)
{
return 1; // 通讯录已满
}
return 0; // 通讯录未满
}
void Add(Contact* contact)
{
assert(contact);
if (isFull(contact))
{
printf("通讯录已满,无法再增加信息\n");
return;
}
getchar();
// 没有做溢出检查
printf("输入名字:>");
gets((contact->data[contact->count]).name);
printf("输入年龄:>");
scanf("%d", &((contact->data[contact->count]).age));
getchar();
printf("输入性别:>");
gets((contact->data[contact->count]).sex);
//getchar();
printf("输入电话:>");
gets((contact->data[contact->count]).phone_number);
//getchar();
printf("输入地址:>");
gets((contact->data[contact->count]).address);
(contact->count)++;
}
int isEmpty(const Contact* contact)
{
if (contact->count == 0)
{
return 1; // 通讯录为空
}
return 0;
}
void Print(const Contact* contact)
{
assert(contact);
if (isEmpty(contact))
{
printf("通讯录为空\n");
return;
}
printf("%-20s%-7s%-7s%-20s%-30s\n", "名字", "年龄", "性别", "电话", "地址");
int i = 0;
for (i = 0; i < contact->count; i++)
{
printf("%-20s%-7d%-7s%-20s%-30s\n", (contact->data[i]).name,
(contact->data[i]).age,
(contact->data[i]).sex,
(contact->data[i]).phone_number,
(contact->data[i]).address);
}
}
static int search_by_name(const Contact* contact)
{
assert(contact);
if (isEmpty(contact))
{
printf("通讯录为空\n");
return;
}
char buffer[20] = { 0 };
printf("输入要查找人的名字:>");
getchar();
gets(buffer);
int i = 0;
for (i = 0; i < contact->count; i++)
{
// 找到了,只能找到第一个相符的人
if (strcmp((contact->data[i]).name, buffer) == 0)
{
return i;
}
}
return -1; // 未找到
}
void Delete(Contact* contact)
{
// 找到指定人
// 通过名字查找
int ret = search_by_name(contact);
if (ret == -1)
{
printf("没有这个人\n");
return;
}
(contact->count)--;
int i = 0;
for (i = ret; i < contact->count; i++)
{
strcpy((contact->data[i]).name, (contact->data[i + 1]).name);
(contact->data[i]).age = (contact->data[i + 1]).age;
strcpy((contact->data[i]).sex, (contact->data[i + 1]).sex);
strcpy((contact->data[i]).phone_number, (contact->data[i + 1]).phone_number);
strcpy((contact->data[i]).address, (contact->data[i + 1]).address);
}
}
void Modify(Contact* contact)
{
assert(contact);
// 找到指定人
// 通过名字查找
int ret = search_by_name(contact);
if (ret == -1)
{
printf("没有这个人\n");
return;
}
printf("进行修改\n");
// 没有做溢出检查
printf("输入名字:>");
gets((contact->data[ret]).name);
printf("输入年龄:>");
scanf("%d", &((contact->data[ret]).age));
getchar();
printf("输入性别:>");
gets((contact->data[ret]).sex);
//getchar();
printf("输入电话:>");
gets((contact->data[ret]).phone_number);
//getchar();
printf("输入地址:>");
gets((contact->data[ret]).address);
}
void Search(const Contact* contact)
{
assert(contact);
int ret = search_by_name(contact);
if (ret == -1)
{
printf("没有这个人\n");
return;
}
printf("%-20s%-7s%-7s%-20s%-30s\n", "名字", "年龄", "性别", "电话", "地址");
printf("%-20s%-7d%-7s%-20s%-30s\n", (contact->data[ret]).name,
(contact->data[ret]).age,
(contact->data[ret]).sex,
(contact->data[ret]).phone_number,
(contact->data[ret]).address);
}
void Sort(Contact* contact)
{
assert(contact);
// 使用冒泡排序
int i = 0;
int j = 0;
for (i = 0; i < (contact->count) - 1; i++)
{
for (j = 0; j < (contact->count) - i - 1; j++)
{
if (strcmp((contact->data[j]).name, (contact->data[j + 1]).name) > 0)
{
char buffer[30] = { 0 };
// 交换名字
strcpy(buffer, (contact->data[j]).name);
strcpy((contact->data[j]).name, (contact->data[j + 1]).name);
strcpy((contact->data[j + 1]).name, buffer);
// 交换年龄
int tmp = (contact->data[j]).age;
(contact->data[j]).age = (contact->data[j + 1]).age;
(contact->data[j + 1]).age = tmp;
// 交换性别
strcpy(buffer, (contact->data[j]).sex);
strcpy((contact->data[j]).sex, (contact->data[j + 1]).sex);
strcpy((contact->data[j + 1]).sex, buffer);
// 交换电话
strcpy(buffer, (contact->data[j]).phone_number);
strcpy((contact->data[j]).phone_number, (contact->data[j + 1]).phone_number);
strcpy((contact->data[j + 1]).phone_number, buffer);
// 交换地址
strcpy(buffer, (contact->data[j]).address);
strcpy((contact->data[j]).address, (contact->data[j + 1]).address);
strcpy((contact->data[j + 1]).address, buffer);
}
}
}
}
contact.h
#pragma once
#include <stdio.h>
#include <string.h>
#include <assert.h>
// 定义结构体、标识符常量
#define MAXNUM 1000 // 通讯录中能保存的人数
typedef struct
{
int age; // 年龄
char name[20]; // 名字
char sex[7]; // 性别
char phone_number[20]; // 电话号码
char address[30]; // 地址
} Peoinfo; // 保存人的信息
typedef struct
{
Peoinfo data[MAXNUM]; // 通讯录可以保存1000个人的信息
int count; // 当前通讯录中的人数
} Contact;
// 声明函数
// 初始化通讯录
void initContact(Contact*);
// 增加人的信息
void Add(Contact*);
// 打印通讯录
void Print(Contact*);
// 删除指定人的信息
void Delete(Contact*);
// 修改指定人的信息
void Modify(Contact*);
// 查找指定人的信息
void Search(Contact*);
// 排序通讯录
void Sort(Contact*);
test.c
#include "contact.h"
void menu()
{
printf("*******************************\n");
printf("*** 1.增加信息 2.删除信息 ***\n");
printf("*** 3.修改信息 4.查找信息 ***\n");
printf("*** 5.排序信息 6.打印信息 ***\n");
printf("*** 0.退出程序 ***\n");
printf("*******************************\n");
}
enum Op
{
exit, // 退出程序
add, // 增加信息
delete, // 删除信息
modify, // 修改信息
search, // 查找信息
sort, // 排序信息
print // 打印信息
};
int main()
{
int input;
// 通讯录
Contact contact;
// 初始化通讯录
initContact(&contact);
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case exit:
printf("退出通讯录\n");
break;
case add:
Add(&contact);
break;
case delete:
Delete(&contact);
break;
case modify:
Modify(&contact);
break;
case search:
Search(&contact);
break;
case sort:
Sort(&contact);
break;
case print:
Print(&contact);
break;
default:
printf("没有这个选项\n");
break;
}
}while(input);
return 0;
}
其中contact.c中的排序函数可以使用回调函数来实现让用户提供排序方法,不一定要用名字作排序;
总结
本文对c语言中会用到的结构体类型进行了详细解读,并在最后给出了一个使用结构体实现的通讯录;