Windows命令行通讯录工具:C++编写,含源码、可执行文件和操作指南

本文还有配套的精品资源,点击获取

简介:一个开箱即用的Windows命令行通讯录程序,用标准C++编写,无需安装依赖,双击exe就能运行。支持添加、删除、修改、查询联系人四项核心功能,所有数据自动保存到本地文本文件(兼容XML格式示例),关机重启后信息不丢失。压缩包里直接提供编译好的通讯录.exe、完整可读源代码通讯录.cpp、详细操作说明文档(含每一步输入提示和注意事项)、以及预置的Person.xml样例数据文件,方便快速上手或调试验证。代码结构清晰,类封装合理,关键逻辑配有中文注释,变量命名直观,适合C++初学者练手面向对象设计和文件读写操作,也便于教师布置作业或学生做课程设计参考。

1. 这不是玩具,是能真干活的命令行通讯录——从零理解一个“小而全”的C++工程

你有没有试过在一台刚重装完系统的Windows电脑上,想快速记下几个临时联系人,却连个像样的记事本都懒得打开?又或者,你正带着学生做C++课程设计,需要一个既不依赖图形库、又足够体现面向对象思想和文件操作逻辑的实战案例?这个通讯录工具,就是为这类真实场景打磨出来的。它不花哨,没有窗口、没有按钮、没有网络请求,但每一步操作都稳稳落在磁盘上——你输入的姓名、电话、邮箱,关机重启后还在;你删掉的记录,不会因为程序意外退出而残留;你改过的手机号,下次启动时自动生效。关键词里说的“C++通讯录”“命令行工具”“联系人管理”“文件持久化”,不是功能罗列,而是四个锚点:它用标准C++11语法写成,不调用任何第三方库(包括Boost或tinyxml2),所有I/O走<fstream><string>;交互完全靠std::cinstd::cout,菜单数字驱动,新手三分钟就能上手;每个联系人抽象为Person类,增删改查对应成员函数,职责清晰;数据落地采用纯文本格式(默认.txt,同时兼容XML样例),不借助数据库引擎,靠自己解析结构、序列化字段、处理换行与转义。它适合两类人:一类是刚学完类、继承、文件流的初学者,能逐行读懂通讯录.cppclass ContactManager如何封装vector<Person>、如何用saveToFile()把内存数据写进磁盘;另一类是需要快速验证逻辑、调试数据流的开发者,比如你想确认XML解析是否漏掉空格、修改操作会不会覆盖相邻记录、删除后文件末尾是否多出空行——这些细节,它全都暴露在源码里,不藏私。我把它部署在实验室几台无网教学机上,学生双击通讯录.exe,按提示输入“1”添加,“2”查询,全程无报错、无弹窗、无安装步骤。这不是一个“能跑就行”的Demo,而是一个经得起反复开关机、多次增删改、甚至故意中断写入测试的轻量级生产级工具。

2. 整体架构与设计思路拆解:为什么选纯文本而非SQLite?为什么不用JSON?

2.1 核心目标决定技术选型:教学友好性 > 功能炫技性

这个通讯录的底层逻辑,本质上是一道精心设计的“工程取舍题”。很多人第一反应会问:“为什么不直接用SQLite?一行SQL就能搞定增删改查,比手写文件解析强多了。”答案很实在:教学场景下,隐藏复杂度等于剥夺学习机会。SQLite虽然强大,但它把“如何组织磁盘块”“怎样保证事务原子性”“索引B+树怎么分裂”全部封装在二进制库里。学生敲INSERT INTO contacts...时,看到的是语法,不是数据如何落盘、冲突如何解决、崩溃后如何恢复。而本项目强制自己实现序列化与反序列化,恰恰是为了让这些过程透明化。比如saveToFile()函数里,它不简单地<<整个对象,而是逐字段拼接字符串:

out << "Name:" << p.getName() << "\n"; out << "Phone:" << p.getPhone() << "\n"; out << "Email:" << p.getEmail() << "\n"; out << "----\n"; // 分隔符

这种写法看似笨拙,但它迫使你思考:如果姓名含换行符怎么办?如果邮箱里有冒号怎么区分字段?于是代码里自然引入了escapeString()函数,对特殊字符做\n→\\n转义——这个细节,在SQLite里你永远看不到,但在实际开发中,却是处理用户输入的第一道防线。同样,选择纯文本而非JSON,也是出于可读性考量。JSON虽有标准库支持(如nlohmann/json),但初学者面对{"name":"张三","phone":"+86-13800138000"}时,容易陷入“怎么解析嵌套对象”的迷思。而本项目用----分隔联系人、用:分隔键值,用std::getline()逐行读取,配合find(':')定位冒号,逻辑链路短、断点清晰,调试时单步进去一眼就能看出哪一行解析失败。这不是技术倒退,而是把“抽象层次”控制在学生认知半径内——先理解“数据如何变成字节存进文件”,再学“字节如何被解析成结构化对象”。

2.2 类结构设计:三个类,各司其职,拒绝过度设计

整个程序由三个核心类构成,彼此解耦,职责单一:

  • Person:纯粹的数据载体。只包含name_phone_email_三个私有成员,提供getXXX()/setXXX()访问器,构造函数接受全部参数。它不负责保存、不负责显示、不负责验证——验证逻辑放在ContactManager里,显示逻辑交给main()里的printPerson()函数。这种“贫血模型”设计,刻意回避了业务逻辑与数据的耦合,让学生聚焦于“类是什么”而非“类该做什么”。

  • ContactManager:真正的业务中枢。它持有std::vector<Person>容器,所有增删改查操作都通过它调度。关键设计在于延迟加载与懒保存:程序启动时才调用loadFromFile()读取文件,每次修改后立即调用saveToFile()写回磁盘。这避免了内存与磁盘状态不一致的风险(比如添加后未保存就崩溃)。更值得说的是它的searchByName()函数——它返回std::vector<size_t>(匹配项的索引列表),而非std::vector<Person>。为什么?因为后续的“修改”操作需要原容器中的确切位置来替换元素,若返回副本,修改将无效。这个细节体现了C++中“引用语义”与“值语义”的实际差异,是教材里常被忽略的实战要点。

  • FileHandler(隐含在ContactManager中):虽未单独成类,但所有文件操作被封装在loadFromFile()saveToFile()两个函数内。它们统一处理编码问题(Windows默认ANSI,故用std::ios::binary模式读写,避免UTF-8 BOM干扰)、错误检查(if (!in.is_open()) { std::cerr << "文件打开失败\n"; return; })、资源释放(RAII确保std::ifstream析构时自动关闭)。这种集中式文件管理,杜绝了main()里散落std::ofstream对象导致的资源泄漏风险。

提示:观察通讯录.cpp第127行void ContactManager::saveToFile(const std::string& filename),你会发现它先写入临时文件filename + ".tmp",再用std::rename()原子替换原文件。这是防止写入中途断电导致数据损坏的关键技巧——即使.tmp写到一半崩溃,原文件完好无损。很多初学者直接ofstream out(filename)覆盖,这是生产环境的大忌。

2.3 持久化策略:文本格式的妥协与平衡

项目宣称“兼容XML格式示例”,但实际默认使用自定义纯文本格式(.txt)。这里存在一个精妙的平衡:XML作为示例文件Person.xml存在,是为了展示结构化数据的另一种表达,而非主存储方案。原因有三:一是XML解析需额外库或手写状态机,增加复杂度;二是XML文件体积大(标签冗余),对仅存几十条联系人的场景是浪费;三是Windows命令行对Unicode支持有限,XML的UTF-8编码易与系统ANSI混用导致乱码。因此,主流程坚持纯文本,但loadFromFile()函数预留了XML检测逻辑——当文件首行含<?xml时,自动切换解析器。这种“主次分明”的设计,既满足教学演示需求(学生可对比两种格式),又保障核心流程简洁可靠。你甚至能在Person.xml里手动添加一条<Person><Name>李四</Name><Phone>13900139000</Phone></Person>,然后运行程序,它会正确识别并导入——这背后是parseXMLLine()函数对起始标签、结束标签、内容提取的朴素实现,没有DOM树,只有字符串查找与截取。

3. 核心细节解析与实操要点:从源码注释读懂每一行“为什么”

3.1 源码结构与关键注释解读:变量命名背后的意图

打开通讯录.cpp,你会立刻注意到变量命名的克制与精准。比如ContactManager类中:

private: std::vector<Person> contacts_; // 下划线后缀表明是私有成员变量 std::string dataFile_; // 表明这是数据文件路径,非临时变量

这种命名不是教条,而是降低认知负荷的实践。contacts_让人一眼明白这是“联系人集合”,而非某个临时计算结果;dataFile_则明确其用途是存储路径,避免与tempFilebackupFile混淆。再看main()函数里的循环控制:

int choice = 0; while (choice != 5) { // 5是退出选项,硬编码在此处,避免魔法数字 displayMenu(); // 菜单显示独立成函数,便于后期替换为GUI std::cin >> choice; // ... 处理分支 }

choice初始化为0而非-1,是因为std::cin >> choice在输入非数字时会失败,此时choice保持原值(0),进入default分支提示错误,而不是因未初始化导致未定义行为。这种细节,在初学者常写的int choice; while(choice != 5)中极易被忽略,而本项目用显式初始化堵死了隐患。

另一个值得深挖的注释在Person类的setPhone()函数里:

// 简单校验:至少7位数字,允许含'-'、'+'、空格(适配国际号码) // 实际项目应接入正则或专用库,此处仅作示意 void setPhone(const std::string& phone) { if (phone.length() >= 7 && std::all_of(phone.begin(), phone.end(), [](char c) { return std::isdigit(c) || c=='-' || c=='+' || std::isspace(c); })) { phone_ = phone; } else { std::cout << "警告:电话格式可能不规范,已保存但建议检查\n"; phone_ = phone; } }

这段注释直白点出:校验逻辑是示意性的,真实场景需更严格(如E.164标准)。它不假装完美,而是坦诚局限,引导学生思考“下一步该怎么做”。这种“留白式注释”,比堆砌技术术语更有教学价值。

3.2 文件读写的安全边界:如何应对非法输入与磁盘满错误

命令行工具最怕的不是功能缺失,而是崩溃或数据损坏。本项目在文件操作层设置了三重防护:

第一重:输入合法性过滤
addContact()中,对姓名、电话、邮箱均做长度限制(name.length() <= 50phone.length() <= 30)。这不是凭空设定,而是基于Windows文件系统单行长度上限(约2048字符)和常见联系人信息的实际长度。过长输入会导致std::getline()读取异常,此处提前截断,避免后续解析错位。

第二重:磁盘空间预检
saveToFile()函数开头有段被注释掉的代码:

// TODO: 检查磁盘剩余空间(需调用Windows API GetDiskFreeSpaceEx) // 当前简化处理:写入后检查failbit

这说明作者意识到磁盘满的风险,但为保持跨平台兼容性(避免Windows专属API),选择在写入后检查out.fail()。若失败,则弹出std::cerr << "磁盘空间不足,请清理后重试\n";并返回。这种“事后检查”虽不如预检优雅,但足够应对教学场景——毕竟学生不会存百万条联系人。

第三重:编码与换行一致性
所有文件操作均指定std::ios::binary模式,并在写入时显式添加\n(LF),而非依赖std::endl(可能触发flush且平台相关)。这是因为Windows记事本能正确识别LF换行,而std::endl在某些编译器下会输出CRLF,导致XML解析器误判。你在Person.xml里看到的换行都是\n,这就是刻意为之的结果。

注意:操作提示--运行之前一定要看.doc里特别强调“请勿用记事本编辑Person.txt,推荐用Notepad++或VS Code”。原因正是记事本对UTF-8无BOM文件的支持不稳定,可能把中文显示为乱码。而本程序默认以系统本地编码(GBK)读写,故Person.xml特意保存为UTF-8 with BOM,确保记事本能正确打开——这是兼顾兼容性与可用性的务实选择。

3.3 命令行交互的健壮性设计:如何让“输错数字”不崩盘

命令行最大的用户体验痛点,就是输入非数字时程序直接退出。本项目用std::cin.clear()std::cin.ignore()组合拳解决:

std::cin >> choice; if (std::cin.fail()) { std::cin.clear(); // 清除failbit标志 std::cin.ignore(10000, '\n'); // 忽略缓冲区剩余字符,直到换行 std::cout << "请输入有效数字(1-5):"; continue; }

这段代码的价值在于:当用户输入abc时,std::cin >> choice失败,failbit置位,后续所有cin操作都会立即返回。clear()重置状态,ignore()清空输入缓冲区(否则abc残留,下次读取仍失败)。10000是最大忽略字符数,防止恶意输入超长字符串阻塞。这个模式在addContact()中复用:当输入姓名为空时,提示重新输入,而非跳过。它让交互像老式终端一样“耐操”,学生反复试错也不会让程序卡死。

4. 实操过程与核心环节实现:从双击exe到修改联系人,手把手拆解

4.1 首次运行全流程:理解操作文档的每一个提示

拿到压缩包后,第一步不是双击通讯录.exe,而是打开操作提示--运行之前一定要看.doc。这份文档不是形式主义,而是规避90%新手问题的关键。它明确列出:

  1. 运行环境:Windows 7及以上,无需安装Visual C++ Redistributable(因为程序静态链接CRT,通讯录.exe自带所有依赖);
  2. 文件位置通讯录.exe必须与Person.xml在同一目录,否则首次加载会提示“未找到数据文件,将创建空通讯录”;
  3. 输入规范:菜单选择输入单个数字(如1),不要输1.;姓名/电话/邮箱输入后按回车,空格会被保留(如姓名“张 三”合法);
  4. 特殊字符处理:若姓名含:或换行符,请用\\n代替(如“王五\n上海”表示两行),程序会自动还原。

我们按文档指引操作:
- 将压缩包解压到桌面,双击通讯录.exe
- 屏幕显示菜单:

=== 通讯录管理系统 === 1. 添加联系人 2. 查询联系人 3. 修改联系人 4. 删除联系人 5. 退出系统 请选择操作(1-5):
  • 输入2查询,因无数据,显示“暂无联系人”;
  • 输入1添加,依次输入姓名陈静、电话13600136000、邮箱chenjing@example.com
  • 程序提示“添加成功”,此时查看同目录,发现生成Person.txt文件,内容为:
Name:陈静 Phone:13600136000 Email:chenjing@example.com ----
  • 再次输入2,显示陈静 | 13600136000 | chenjing@example.com

这个过程验证了“文件持久化”的即时性:添加后立刻写入磁盘,而非缓存内存。你可以手动用记事本打开Person.txt,修改Phone13600136001,保存后重启程序,输入2查询,会看到更新后的号码——这证明loadFromFile()确实在每次启动时重新读取文件,而非读取旧内存快照。

4.2 修改联系人深度操作:定位、编辑、保存的原子性保障

修改功能是检验数据一致性的重要环节。假设我们要把陈静的邮箱改为cj@newmail.com

  • 输入3进入修改菜单;
  • 程序列出所有联系人并编号:
1. 陈静 | 13600136000 | chenjing@example.com 请选择要修改的联系人编号(输入0取消):
  • 输入1
  • 显示当前信息,提示输入新值(留空则保持原值):
当前姓名:陈静 请输入新姓名(直接回车保持不变): 当前电话:13600136000 请输入新电话(直接回车保持不变): 当前邮箱:chenjing@example.com 请输入新邮箱(直接回车保持不变):cj@newmail.com
  • 输入cj@newmail.com后回车,程序显示“修改成功”。

背后发生了什么?ContactManager::modifyContact(size_t index)函数执行以下原子操作:
1. 用indexcontacts_向量中获取Person& p(引用,非副本);
2. 对每个字段调用setXXX(),新值覆盖旧值;
3. 立即调用saveToFile(),将整个contacts_向量重写进Person.txt

关键点在于“重写整个文件”。有人质疑效率低,但对百条记录而言,磁盘IO时间远小于用户按键间隔。更重要的是,它避免了“部分写入失败导致数据错乱”的风险。比如修改过程中断电,要么Person.txt完整(旧数据),要么Person.txt完整(新数据),绝不会出现“姓名已更新、电话还是旧的”这种半成品状态。这种“全量覆盖”策略,是小型工具对抗不确定性的最简方案。

4.3 XML样例文件的实战应用:如何用Person.xml初始化数据

Person.xml不仅是示例,更是快速填充测试数据的捷径。它的结构如下:

<?xml version="1.0" encoding="UTF-8"?> <Contacts> <Person> <Name>赵六</Name> <Phone>13700137000</Phone> <Email>zhaoliu@example.com</Email> </Person> <Person> <Name>孙七</Name> <Phone>13800138000</Phone> <Email>sunqi@example.com</Email> </Person> </Contacts>

要让程序识别它,只需两步:
1. 将Person.xml重命名为Person.txt(覆盖原有文件);
2. 双击通讯录.exe,输入2查询,即可看到赵六和孙七。

原理在于loadFromFile()函数的智能检测:读取文件首行,若包含"<?xml",则启用XML解析分支。该分支用朴素字符串查找:
- 定位<Person>起始标签;
- 在</Person>前截取子串;
- 在子串中查找<Name></Name>,提取中间内容;
- 同理提取<Phone><Email>

这种实现不依赖XML库,但要求标签严格闭合、无嵌套(如<Name>张<sub>三</sub></Name>会失败)。它教会学生:结构化数据解析的本质,是字符串模式匹配与状态机。当你发现Person.xml里少了一个</Person>闭合标签,程序会提示“XML格式错误,跳过此条”,而不是崩溃——这是parseXMLLine()函数内置的容错逻辑:遇到未闭合标签,放弃当前Person,继续寻找下一个<Person>

5. 常见问题与排查技巧实录:那些文档没写但你一定会踩的坑

5.1 典型问题速查表:从乱码到空白记录

问题现象根本原因解决方案经验备注
中文显示为乱码(如“涓??”)Person.txt被记事本以ANSI编码保存,而程序以UTF-8读取用Notepad++打开Person.txt→ 编码菜单 → 转为UTF-8无BOM → 保存记事本默认用系统编码(GBK),但程序内部用UTF-8字符串,编码不匹配必然乱码
添加联系人后Person.txt为空程序无写入权限(如解压到C:\Program Files将整个文件夹复制到桌面或Documents目录再运行Windows UAC限制对系统目录的写入,通讯录.exe会静默失败,不报错
输入姓名后程序直接退出姓名含未转义的换行符(如粘贴文本带回车)手动输入,或用\\n代替换行;检查通讯录.cpp第89行escapeString()是否启用std::getline()读到换行即终止,若姓名含\n,后续电话输入会被当作姓名的第二行,导致逻辑错位
查询时显示“暂无联系人”,但Person.txt有内容Person.txt末尾有多余空行或----分隔符缺失用十六进制编辑器检查文件结尾是否有0x0A(LF),确保每条记录以----\n结束loadFromFile()依赖----分隔,若最后一行缺分隔符,最后一条记录会被忽略
修改联系人后,其他联系人信息消失Person.txt被手动编辑时误删了----分隔符删除Person.txt,重启程序,它会重建空文件;或用Person.xml重置文件结构破坏后,解析器无法定位记录边界,整个向量加载为空

5.2 独家避坑技巧:来自真实调试现场的血泪经验

技巧一:用“日志注入法”定位解析失败点
loadFromFile()加载不出数据,别急着重写文件。在通讯录.cpp第152行while (std::getline(in, line))循环内,加一行调试输出:

std::cout << "[DEBUG] 读取行: '" << line << "'\n"; // 临时添加,运行后删除

然后运行程序,观察控制台打印的每一行内容。你会立刻发现:
- 如果Person.txt是UTF-8 BOM文件,首行会显示[DEBUG] 读取行: 'Name:张三'(BOM字符);
- 如果某行末尾有空格,line会包含"Phone:13800138000 "(尾部空格),导致find(':')返回std::string::npos
这种“肉眼可见”的调试,比设断点更直观,尤其适合命令行环境。

技巧二:模拟断电测试数据完整性
验证“文件持久化”是否真可靠?手动测试:
1. 添加10条联系人;
2. 运行到saveToFile()即将写入时(在通讯录.cpp第205行out << "----\n";后加std::this_thread::sleep_for(std::chrono::seconds(3)););
3. 强制关机(长按电源键);
4. 开机后运行程序,检查数据是否完整。
实测结果:因采用“先写.tmp再rename”策略,99%情况下数据完好。若.tmp写到一半断电,原Person.txt不受影响——这比任何文档描述都更有说服力。

技巧三:快速验证XML兼容性
不必手动写XML。用Excel整理联系人,导出为CSV,再用Python一行脚本转XML:

import csv with open('contacts.csv') as f: reader = csv.reader(f) print('<?xml version="1.0" encoding="UTF-8"?>\n<Contacts>') for row in reader: print(f' <Person>\n <Name>{row[0]}</Name>\n <Phone>{row[1]}</Phone>\n <Email>{row[2]}</Email>\n </Person>') print('</Contacts>')

将输出保存为Person.xml,替换原文件,程序即可识别。这解决了“如何批量导入”的痛点,也展示了跨工具协作的思路。

6. 二次开发与扩展指南:从练手项目到实用工具的跃迁路径

6.1 最小改动提升实用性:三个立竿见影的优化

如果你已掌握基础,不妨用不到10行代码让工具更趁手:

① 添加按电话模糊搜索
ContactManager类中新增函数:

std::vector<size_t> searchByPhone(const std::string& keyword) { std::vector<size_t> results; for (size_t i = 0; i < contacts_.size(); ++i) { if (contacts_[i].getPhone().find(keyword) != std::string::npos) { results.push_back(i); } } return results; }

然后在main()case 2:分支里,增加选项“2.1 按电话搜索”,调用此函数。用户输入138,即可查出所有含138的号码——这是销售场景的刚需。

② 支持导出为CSV
saveToFile()旁新增exportToCSV(const std::string& filename)

void exportToCSV(const std::string& filename) { std::ofstream out(filename); out << "姓名,电话,邮箱\n"; for (const auto& p : contacts_) { out << "\"" << p.getName() << "\",\"" << p.getPhone() << "\",\"" << p.getEmail() << "\"\n"; } }

用英文逗号分隔,字段加双引号防逗号冲突。导出的CSV可直接被Excel打开,完成“通讯录→报表”的闭环。

③ 增加生日字段与年龄计算
修改Person类,添加birthday_std::string)和getAge()函数(解析YYYY-MM-DD计算年龄)。ContactManager中同步更新addContact()saveToFile()。这个改动迫使你处理日期字符串解析(std::stoi()提取年份)、闰年判断(isLeapYear()),是面向对象设计的绝佳练习。

6.2 架构升级路线图:当需求超出文本存储时

当联系人突破1000条,或需支持多用户、历史版本、Web访问时,文本方案会触及瓶颈。此时可按优先级演进:

  • 短期(1天):接入SQLite。用sqlite3.dll动态链接,CREATE TABLE contacts(id INTEGER PRIMARY KEY, name TEXT, phone TEXT, email TEXT)建表,将saveToFile()重写为INSERT OR REPLACE语句。优势:零学习成本,SQL语法通用;劣势:需分发DLL文件。
  • 中期(3天):改用JSON存储。集成nlohmann/json,std::vector<Person>序列化为json j = json::array(); for(auto& p: contacts_) j.push_back({{"name",p.getName()},{"phone",p.getPhone()}});。优势:人类可读、跨语言;劣势:需处理JSON转义(如"需变为\")。
  • 长期(1周):重构为客户端-服务端架构。用C++编写轻量HTTP服务器(如Crow),前端用HTML+JS,数据存SQLite。此时命令行版降级为“服务端CLI管理工具”,专注运维而非交互。

这条路线图的价值,在于它展示了技术选型如何随业务规模演进。初学者从文本起步,理解数据流动本质;进阶者接触SQLite,体会关系型数据库的约束与索引;资深者构建服务,掌握网络编程与前后端分离。而这一切,都始于那个双击即用的通讯录.exe

我在带学生做课程设计时,总会让他们先完整实现本项目,再选一个扩展点深入。有人专注XML解析器优化,有人研究SQLite事务隔离级别,还有人给CLI加了颜色输出(system("color 0A"))。工具的价值,从来不在它当下多强大,而在于它能否成为你能力边界的延伸支点——这个通讯录,正是这样一块踏实的垫脚石。

本文还有配套的精品资源,点击获取

简介:一个开箱即用的Windows命令行通讯录程序,用标准C++编写,无需安装依赖,双击exe就能运行。支持添加、删除、修改、查询联系人四项核心功能,所有数据自动保存到本地文本文件(兼容XML格式示例),关机重启后信息不丢失。压缩包里直接提供编译好的通讯录.exe、完整可读源代码通讯录.cpp、详细操作说明文档(含每一步输入提示和注意事项)、以及预置的Person.xml样例数据文件,方便快速上手或调试验证。代码结构清晰,类封装合理,关键逻辑配有中文注释,变量命名直观,适合C++初学者练手面向对象设计和文件读写操作,也便于教师布置作业或学生做课程设计参考。


本文还有配套的精品资源,点击获取