小项目:图书借阅
这一课不讲新东西,而是把前面学过的拼成一个小程序:一个社区图书室的借阅记录。书架上有几本书;有人来借,有人来还;随时能查谁借了什么。我们一步一步搭,每一步都是能运行的完整程序。
要做什么
先想清楚要记哪些东西、要做哪些事:
| 要记的 | 用什么 |
|---|---|
| 一本书:书名、作者、借出去没有 | class Book |
| 书架上的全部书 | std::vector<Book> |
| 谁借了哪几本 | std::map<std::string, std::vector<std::string>>:人名对应书名的清单 |
要做的事,每一件写成一个函数:加书、按书名找书、显示书架、借书、还书、显示谁借了什么。
第一步:Book 类
一本书有三样:书名、作者、借出去没有。成员都是 private,外面通过成员函数读;只有「借出去没有」能改:
1#include <iostream>2#include <string>3 4class Book {5public:6 Book(const std::string& title, const std::string& author)7 : title_(title), author_(author), borrowed_(false) {}8 const std::string& title() const { return title_; }9 const std::string& author() const { return author_; }10 bool borrowed() const { return borrowed_; }11 void set_borrowed(bool value) { borrowed_ = value; }12 13private:14 std::string title_;15 std::string author_;16 bool borrowed_;17};18 19void show_book(const Book& b) {20 std::cout << "《" << b.title() << "》" << b.author();21 if (b.borrowed()) {22 std::cout << "(已借出)";23 }24 std::cout << '\n';25}26 27int main() {28 Book b{"三体", "刘慈欣"};29 show_book(b);30 b.set_borrowed(true);31 show_book(b);32 return 0;33}
《三体》刘慈欣 《三体》刘慈欣(已借出)
读书名的成员函数返回 const std::string&:不复制,外面也改不了。show_book 是普通函数,只读,所以参数是 const Book&。
第二步:书架
书架就是一个 std::vector<Book>。再加三个函数:add_book 往书架上放一本,find_book 按书名找、返回下标(找不到返回 -1),show_books 一本一本显示:
1#include <iostream>2#include <string>3#include <vector>4 5class Book {6public:7 Book(const std::string& title, const std::string& author)8 : title_(title), author_(author), borrowed_(false) {}9 const std::string& title() const { return title_; }10 const std::string& author() const { return author_; }11 bool borrowed() const { return borrowed_; }12 void set_borrowed(bool value) { borrowed_ = value; }13 14private:15 std::string title_;16 std::string author_;17 bool borrowed_;18};19 20void show_book(const Book& b) {21 std::cout << "《" << b.title() << "》" << b.author();22 if (b.borrowed()) {23 std::cout << "(已借出)";24 }25 std::cout << '\n';26}27 28void add_book(std::vector<Book>& books, const std::string& title, const std::string& author) {29 books.push_back(Book{title, author});30}31 32int find_book(const std::vector<Book>& books, const std::string& title) {33 for (std::size_t i = 0; i < books.size(); i++) {34 if (books[i].title() == title) {35 return static_cast<int>(i);36 }37 }38 return -1;39}40 41void show_books(const std::vector<Book>& books) {42 for (const auto& b : books) {43 show_book(b);44 }45}46 47int main() {48 std::vector<Book> books;49 add_book(books, "三体", "刘慈欣");50 add_book(books, "活着", "余华");51 add_book(books, "围城", "钱钟书");52 show_books(books);53 std::cout << "活着:下标 " << find_book(books, "活着") << '\n';54 std::cout << "红楼梦:下标 " << find_book(books, "红楼梦") << '\n';55 return 0;56}
《三体》刘慈欣 《活着》余华 《围城》钱钟书 活着:下标 1 红楼梦:下标 -1
为什么 find_book 返回下标,不返回 Book& 或者指针?第 28 课说过:书架再加书时 vector 可能搬家,之前拿到的引用、指针就不能用了;下标永远没问题。
第三步:谁借了什么
借书记录是一个 map:键是人名,值是这个人借的书名清单(一个 vector)。先单独试试它,看清楚它的样子。点单步,看 loans 里每个名字后面挂着一排格子:
1#include <iostream>2#include <map>3#include <string>4#include <vector>5 6void show_loans(const std::map<std::string, std::vector<std::string>>& loans) {7 if (loans.empty()) {8 std::cout << "没有人借书\n";9 }10 for (const auto& kv : loans) {11 std::cout << kv.first << ":";12 for (const auto& t : kv.second) {13 std::cout << "《" << t << "》";14 }15 std::cout << '\n';16 }17}18 19int main() {20 std::map<std::string, std::vector<std::string>> loans;21 show_loans(loans);22 loans["小林"].push_back("三体");23 loans["小王"].push_back("活着");24 loans["小林"].push_back("围城");25 show_loans(loans);26 return 0;27}
没有人借书 小林:《三体》《围城》 小王:《活着》
loans["小林"].push_back(...) 用的正是第 13 课说的「方括号找不到就新建」:小林第一次借书时,map 先给他建一个空清单,再把书名放进去。这里新建正是我们要的;可只想查的时候,它就是坑:
最后一行显示什么?
1#include <iostream>2#include <map>3#include <string>4#include <vector>5 6int main() {7 std::map<std::string, std::vector<std::string>> loans;8 loans["小林"].push_back("三体");9 std::cout << "小王借了 " << loans["小王"].size() << " 本\n";10 std::cout << loans.size() << '\n';11 return 0;12}
loans["小王"] 找不到小王,就给他新建了一个空清单。现在表里有小林和小王两个人,show_loans 会多显示一行「小王:」。所以下面还书时,查人用 find。
第四步:借书
借书要先检查两件事:有没有这本书、是不是已经借出去了。都没问题,就把书标成借出,再记到这个人的清单上:
1#include <iostream>2#include <map>3#include <string>4#include <vector>5 6class Book {7public:8 Book(const std::string& title, const std::string& author)9 : title_(title), author_(author), borrowed_(false) {}10 const std::string& title() const { return title_; }11 const std::string& author() const { return author_; }12 bool borrowed() const { return borrowed_; }13 void set_borrowed(bool value) { borrowed_ = value; }14 15private:16 std::string title_;17 std::string author_;18 bool borrowed_;19};20 21int find_book(const std::vector<Book>& books, const std::string& title) {22 for (std::size_t i = 0; i < books.size(); i++) {23 if (books[i].title() == title) {24 return static_cast<int>(i);25 }26 }27 return -1;28}29 30void borrow_book(std::vector<Book>& books, std::map<std::string, std::vector<std::string>>& loans,31 const std::string& person, const std::string& title) {32 int i = find_book(books, title);33 if (i == -1) {34 std::cout << "没有《" << title << "》这本书\n";35 return;36 }37 if (books[i].borrowed()) {38 std::cout << "《" << title << "》已经借出去了\n";39 return;40 }41 books[i].set_borrowed(true);42 loans[person].push_back(title);43 std::cout << person << " 借走了《" << title << "》\n";44}45 46int main() {47 std::vector<Book> books;48 books.push_back(Book{"三体", "刘慈欣"});49 books.push_back(Book{"活着", "余华"});50 std::map<std::string, std::vector<std::string>> loans;51 borrow_book(books, loans, "小林", "三体");52 borrow_book(books, loans, "小王", "三体");53 borrow_book(books, loans, "小王", "红楼梦");54 borrow_book(books, loans, "小王", "活着");55 std::cout << "小王借了 " << loans["小王"].size() << " 本\n";56 return 0;57}
小林 借走了《三体》 《三体》已经借出去了 没有《红楼梦》这本书 小王 借走了《活着》 小王借了 1 本
books 和 loans 都要改,所以都按引用传;person 和 title 只读,按 const 引用传。出了问题就显示一句、return,后面的不做:先把不行的情况挡掉,剩下的就是正常的借书。
第五步:还书
还书反过来:先用 find 查这个人有没有借书记录(不用方括号,免得新建),再在他的清单里找这本书,用第 27 课的 erase 删掉。清单空了,就把这个人整个从 map 里删掉。下面是完整的程序,演一遍图书室的一天:
1#include <algorithm>2#include <iostream>3#include <map>4#include <string>5#include <vector>6 7class Book {8public:9 Book(const std::string& title, const std::string& author)10 : title_(title), author_(author), borrowed_(false) {}11 const std::string& title() const { return title_; }12 const std::string& author() const { return author_; }13 bool borrowed() const { return borrowed_; }14 void set_borrowed(bool value) { borrowed_ = value; }15 16private:17 std::string title_;18 std::string author_;19 bool borrowed_;20};21 22int find_book(const std::vector<Book>& books, const std::string& title) {23 for (std::size_t i = 0; i < books.size(); i++) {24 if (books[i].title() == title) {25 return static_cast<int>(i);26 }27 }28 return -1;29}30 31void borrow_book(std::vector<Book>& books, std::map<std::string, std::vector<std::string>>& loans,32 const std::string& person, const std::string& title) {33 int i = find_book(books, title);34 if (i == -1 || books[i].borrowed()) {35 std::cout << "借不了《" << title << "》\n";36 return;37 }38 books[i].set_borrowed(true);39 loans[person].push_back(title);40}41 42void return_book(std::vector<Book>& books, std::map<std::string, std::vector<std::string>>& loans,43 const std::string& person, const std::string& title) {44 auto found = loans.find(person);45 if (found == loans.end()) {46 std::cout << person << " 没有借书\n";47 return;48 }49 std::vector<std::string>& titles = found->second;50 auto it = std::find(titles.begin(), titles.end(), title);51 if (it == titles.end()) {52 std::cout << person << " 没有借《" << title << "》\n";53 return;54 }55 titles.erase(it);56 if (titles.empty()) {57 loans.erase(found);58 }59 books[find_book(books, title)].set_borrowed(false);60 std::cout << person << " 还了《" << title << "》\n";61}62 63void show_loans(const std::map<std::string, std::vector<std::string>>& loans) {64 for (const auto& kv : loans) {65 std::cout << kv.first << ":";66 for (const auto& t : kv.second) {67 std::cout << "《" << t << "》";68 }69 std::cout << '\n';70 }71}72 73int main() {74 std::vector<Book> books;75 books.push_back(Book{"三体", "刘慈欣"});76 books.push_back(Book{"活着", "余华"});77 books.push_back(Book{"围城", "钱钟书"});78 std::map<std::string, std::vector<std::string>> loans;79 borrow_book(books, loans, "小林", "三体");80 borrow_book(books, loans, "小林", "围城");81 borrow_book(books, loans, "小王", "活着");82 show_loans(loans);83 return_book(books, loans, "小王", "活着");84 return_book(books, loans, "小林", "活着");85 return_book(books, loans, "小张", "三体");86 show_loans(loans);87 return 0;88}
小林:《三体》《围城》 小王:《活着》 小王 还了《活着》 小林 没有借《活着》 小张 没有借书 小林:《三体》《围城》
点单步,看小王还书以后,loans 里他那一行整个消失了。loans.erase(found) 之后 titles 绑着的那个清单也没了,所以后面不能再用 titles:这里也确实没再用。
回头看看
一百来行,用到了这门课的很多东西:
class把一本书的数据藏起来,只留该留的成员函数(第 15 课);vector当书架,map加vector记借阅,各管各的;- 函数按引用传要改的、按
const引用传只读的(第 10 课); - 找书返回下标,不返回引用(第 28 课);
- 只想查的时候用
find,不用方括号(第 13 课); - 迭代器配
erase删掉清单里的一项(第 27 课)。
小技巧: 程序再大一些,可以把 books 和 loans 一起放进一个 class Library,让借书、还书都变成它的成员函数,main 里就不用每次都传两个容器了。
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。三道题都是给图书室加一个功能。
架上还有几本
✓写好 count_available:数一数书架上没借出去的书有几本。main 已经写好了。
架上还有 3 本
1#include <iostream>2#include <string>3#include <vector>4 5class Book {6public:7 Book(const std::string& title, bool borrowed) : title_(title), borrowed_(borrowed) {}8 const std::string& title() const { return title_; }9 bool borrowed() const { return borrowed_; }10 11private:12 std::string title_;13 bool borrowed_;14};15 16int count_available(const std::vector<Book>& books) {17 int count = 0;18 19 return count;20}21 22int main() {23 std::vector<Book> books;24 books.push_back(Book{"三体", true});25 books.push_back(Book{"活着", false});26 books.push_back(Book{"围城", false});27 books.push_back(Book{"边城", true});28 books.push_back(Book{"红楼梦", false});29 std::cout << "架上还有 " << count_available(books) << " 本\n";30 return 0;31}
按作者找书
✓写好 show_by_author:显示这位作者的每一本书,一行一本;一本也没有就显示 没有这位作者的书。
《活着》 《兄弟》 没有这位作者的书
1#include <iostream>2#include <string>3#include <vector>4 5class Book {6public:7 Book(const std::string& title, const std::string& author) : title_(title), author_(author) {}8 const std::string& title() const { return title_; }9 const std::string& author() const { return author_; }10 11private:12 std::string title_;13 std::string author_;14};15 16void show_by_author(const std::vector<Book>& books, const std::string& author) {17 18}19 20int main() {21 std::vector<Book> books;22 books.push_back(Book{"活着", "余华"});23 books.push_back(Book{"三体", "刘慈欣"});24 books.push_back(Book{"兄弟", "余华"});25 show_by_author(books, "余华");26 show_by_author(books, "鲁迅");27 return 0;28}
每人最多借两本
✓给 borrow_book 加一条规矩:这个人手里已经有 2 本,就显示 小林 已经借了 2 本 并且不借给他。
小林 借走了《三体》 小林 借走了《活着》 小林 已经借了 2 本 小王 借走了《围城》
1#include <iostream>2#include <map>3#include <string>4#include <vector>5 6void borrow_book(std::map<std::string, std::vector<std::string>>& loans,7 const std::string& person, const std::string& title) {8 loans[person].push_back(title);9 std::cout << person << " 借走了《" << title << "》\n";10}11 12int main() {13 std::map<std::string, std::vector<std::string>> loans;14 borrow_book(loans, "小林", "三体");15 borrow_book(loans, "小林", "活着");16 borrow_book(loans, "小林", "围城");17 borrow_book(loans, "小王", "围城");18 return 0;19}
这一课学会了
- 先想清楚要记什么(用哪种容器)、要做什么(写成哪些函数),再一步一步写,每一步都能运行。
- 一样东西的数据放进
class,成员private,用成员函数读和改。 - 一组东西放
vector;「谁对应什么」放map;map的值也可以是vector。 - 要改的容器按引用传,只读的按
const引用传。 - 指向
vector元素时记下标,不记引用;只查不建用find;删清单里的一项用迭代器和erase。 - 函数开头先把不行的情况挡掉(显示一句、
return),剩下的就是正常情况。