Kodewick
第 30 课,共 30 课目录

小项目:图书借阅

第 30 课,共 30 课 · 第 7 部分 · 再往前 · 约 20 分钟

这一课不讲新东西,而是把前面学过的拼成一个小程序:一个社区图书室的借阅记录。书架上有几本书;有人来借,有人来还;随时能查谁借了什么。我们一步一步搭,每一步都是能运行的完整程序。

要做什么

先想清楚要记哪些东西、要做哪些事:

要记的用什么
一本书:书名、作者、借出去没有class Book
书架上的全部书std::vector<Book>
谁借了哪几本std::map<std::string, std::vector<std::string>>:人名对应书名的清单

要做的事,每一件写成一个函数:加书、按书名找书、显示书架、借书、还书、显示谁借了什么。

第一步:Book 类

一本书有三样:书名、作者、借出去没有。成员都是 private,外面通过成员函数读;只有「借出去没有」能改:

main.cpp
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 一本一本显示:

main.cpp
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 里每个名字后面挂着一排格子:

main.cpp
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}

第四步:借书

借书要先检查两件事:有没有这本书、是不是已经借出去了。都没问题,就把书标成借出,再记到这个人的清单上:

main.cpp
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 里删掉。下面是完整的程序,演一遍图书室的一天:

main.cpp
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 里就不用每次都传两个容器了。

练一练

写好代码,点检查。提示随便看,实在想不出来再看答案。三道题都是给图书室加一个功能。

练习 1

架上还有几本

写好 count_available:数一数书架上没借出去的书有几本。main 已经写好了。

要显示
架上还有 3 本
exercise1.cpp
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}
输出
点「运行」或按 Ctrl+Enter
练习 2

按作者找书

写好 show_by_author:显示这位作者的每一本书,一行一本;一本也没有就显示 没有这位作者的书。

要显示
《活着》
《兄弟》
没有这位作者的书
exercise2.cpp
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}
输出
点「运行」或按 Ctrl+Enter
练习 3

每人最多借两本

给 borrow_book 加一条规矩:这个人手里已经有 2 本,就显示 小林 已经借了 2 本 并且不借给他。

要显示
小林 借走了《三体》
小林 借走了《活着》
小林 已经借了 2 本
小王 借走了《围城》
exercise3.cpp
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}
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • 先想清楚要记什么(用哪种容器)、要做什么(写成哪些函数),再一步一步写,每一步都能运行。
  • 一样东西的数据放进 class,成员 private,用成员函数读和改。
  • 一组东西放 vector;「谁对应什么」放 map;map 的值也可以是 vector。
  • 要改的容器按引用传,只读的按 const 引用传。
  • 指向 vector 元素时记下标,不记引用;只查不建用 find;删清单里的一项用迭代器和 erase。
  • 函数开头先把不行的情况挡掉(显示一句、return),剩下的就是正常情况。