C++で喫茶店の1日の注文を集計してみよう(総合演習)
ここまでの C++ レッスンで学んだことを全部つないで、動くものを1つ完成させます。新しい構文は出てきません。struct・vector・参照・クラス・コンストラクタとデストラクタ・switch——すでに練習したものだけで組み立てます。
作るのは喫茶店の1日の売上集計です。商品ごとの売上を出し、棒グラフにし、時間帯ごとにまとめて、レポートにします。
この演習では、C++が何を解決した言語なのかが見えます。——vectorは自分の長さを知っている、参照を使えば複製せずに渡せる、デストラクタが後片付けを自動でやる。C言語で手作業だったことが、どれも言語の側に引き取られています。
このページではコードを実行していません。C++はコンパイルが必要で、ブラウザの中では走らせられないためです。かわりにお手本と同じコードを書けたかを、その場で判定します(判定はあなたのブラウザの中だけで行われ、どこにも送信されません)。
表示している実行結果は本物です。このページを作るときに、実際にg++でコンパイルして走らせた出力をそのまま載せています。
完成イメージ
最後まで進むと、このコードが完成します。下の実行結果は、このページを作るときに開発機で実際にコードを走らせて得たものです(このページ上では実行していません)。
===== プロ道珈琲店 本日の売上 ===== ブレンド ######-------------- 5280円 売れ筋 カフェラテ ####---------------- 3300円 トースト ##------------------ 1680円 ケーキセット ######-------------- 5280円 売れ筋 -------------------------------------------- 注文件数: 8件 売上合計: 15540円 最も売れた商品: ブレンド(5280円) --- 時間帯別 --- 朝 4220円 昼 7280円 夕方 3160円 夜 880円 ===== ここまで ===== 集計を終えました
STEP 1注文をstructで表してvectorに入れる
🎯 このステップのゴール: 構造体にメンバ関数を持たせ、vectorが長さを知っていることを確かめる
まずデータの形を決めます。注文1件が持つのは「商品名・時刻・単価・個数」の4つです。
struct Order { ... }——C言語と違ってtypedefが要りません。C++ではstructで作った名前が、そのまま型名として使えます。
std::string item;にも注目してください。C言語ではcharの配列で、長さを自分で決める必要がありました。std::stringは必要なだけ伸びます——はみ出して隣を壊す心配がありません。
そして構造体に関数を持たせられます。int amount() const { return price * count; }——これがC++のstructとC言語のstructの決定的な違いです。
末尾のconstは「このメンバ関数は自分の中身を変えません」という宣言です。付けておくと、うっかり書き換えるコードでコンパイルが止まります。読むだけの関数には必ず付けてください。
std::vector<Order>が伸び縮みする配列です。
ここがC言語との最大の違いです。orders.size()と書けば件数が返ります。件数を別の変数で持ち歩く必要も、関数に一緒に渡す必要もありません。C言語でsizeofが関数の中で効かなくて苦労したところが、まるごと消えています。
注文件数: 8件 1件目: ブレンド 480円 x3 金額: 1440円
STEP 2const参照で渡して合計する
🎯 このステップのゴール: 複製を避けつつ、書き換えないことを型で約束する
売上を合計します。int total_sales(const std::vector<Order> &orders)——この引数の書き方が、この演習でいちばん覚えて帰ってほしいところです。
&が参照です。これを付けないと——std::vector<Order> ordersと書くと——呼ぶたびにvector全体が丸ごと複製されます。8件なら気になりませんが、10万件なら10万件ぶん複製されます。
C言語ではポインタでこれを避けていました。参照はポインタと同じ効果を、*も->も書かずに得られます。中ではorders.size()とそのまま書けます。
constが2つ目の要点です。「この関数は中身を変えません」という宣言で、うっかりorders.push_back(...)と書いた瞬間にコンパイルエラーになります。
このconstには、もう1つ大事な効果があります。——呼ぶ側が安心できることです。total_sales(orders)と書いたとき、自分のordersが変えられる心配がありません。関数の中を読まなくても分かります。
ループもfor (const Order &o : orders)と参照で受けています。const Order oと書くと、1件ずつ複製されます。範囲for文ではconst 型 &で受けるのが基本形だと覚えてください。
下のコードで、参照で受けた場合と複製した場合で、元のvectorがどうなるかを確かめられます。
売上合計: 15540円 複製の中: 9件 複製のあと: 8件(変わらない) 参照のあと: 9件(増えた)
STEP 3商品名の一覧を作って、商品ごとに集計する
🎯 このステップのゴール: 重複を除いた一覧を作り、名前で絞り込んで合計する
商品ごとの売上を出します。そのためには、まず「どんな商品があるか」を知る必要があります。
unique_itemsがそれをやります。やり方は素直です——1件ずつ見て、まだ集めていない名前なら追加する。
std::vector<std::string> names;と空のvectorを作り、names.push_back(o.item)で足していきます。大きさを先に決める必要がありません——足せば伸びます。
中のforですでにあるかどうかを調べています。見つかったらbreakで抜ける——最後まで見る必要がないので、見つかった時点でやめます。
この書き方の弱点も知っておいてください。商品がN種類、注文がM件なら、調べる回数はN×Mに近づきます。数千件を超えると目に見えて遅くなります。
本来はstd::mapやstd::setを使う場面です。ここではvectorだけで書いていますが、「量が増えたらこの書き方では足りない」と知っておくことが大事です。
商品ごとの合計はtotal_of_itemです。名前で絞り込んで足すだけ——ステップ2の合計に、条件が1つ増えただけの形です。
const std::string &itemと文字列も参照で受けているのに注目してください。std::stringを値で渡すと、中の文字が複製されます。短い文字列なら気になりませんが、参照で受けるのが習慣です。
ブレンド: 5280円 カフェラテ: 3300円 トースト: 1680円 ケーキセット: 5280円
STEP 4switchで時間帯に分ける
🎯 このステップのゴール: 整数の割り算で区分を作り、switchで振り分ける
注文された時刻から、朝・昼・夕方・夜に振り分けます。
switch (hour / 3)——3で割ってから振り分けているのが工夫です。
整数の割り算で切り捨てが起きることを、逆に利用しています。9時・10時・11時はどれも9/3 = 3、10/3 = 3、11/3 = 3——3で揃います。12〜14時は4、15〜17時は5。3時間ごとの区分が、割り算1つで作れます。
ifを4つ並べるより短く、条件の書き間違いも起きにくくなります。「9時以上かつ12時未満」のような範囲を2つずつ書かなくて済むからです。
default:を必ず書いてください。朝の前(0〜8時)も夜(18時以降)も、ここで受け止めます。書かないと、当てはまらない時刻で何も返さない関数になってしまいます。
C++のswitchではbreakの書き忘れが有名な事故ですが、ここでは起きません。——各caseですぐreturnしているからです。返ってしまえば、次のcaseに流れ込みようがありません。
値を返す振り分けなら、returnで書くほうが安全です。breakを書き忘れる余地がなくなります。
時間帯ごとの合計は二重ループです。外で時間帯を回し、中で注文を回して、その時間帯のものだけ足します。ステップ3の商品ごとの集計と、まったく同じ形です。
朝: 4220円 昼: 7280円 夕方: 3160円 夜: 880円
STEP 5棒グラフを描いて、日本語の幅を揃える
🎯 このステップのゴール: stringを繰り返して作り、全角を2文字ぶんと数える
売上を文字の棒グラフにします。
std::string(filled, '#')——「#をfilled個並べた文字列」という書き方です。C言語ならforで1文字ずつ書き込むところが、1行で済みます。
長さの計算はamount * BAR_WIDTH / grand。掛け算を先に書くのが大事です——amount / grandを先にすると、整数の割り算で0になり、バーが常に空になります。
そして日本語の桁揃えです。ここはC++でも自動ではやってくれません。
std::stringのsize()は「バイト数」を返します。「ブレンド」は4文字ではなく12バイト——UTF-8では日本語1文字が3バイトだからです。
だからsize()で桁を揃えようとすると、日本語の行だけ短くなります。
pad_rightは、それを自分で数え直しています。先頭が0x80より小さければ半角1文字ぶん、そうでなければ全角2文字ぶんとして3バイト進む——UTF-8の仕組みを使った、素朴だが確実な数え方です。
これは「文字」と「バイト」が別物だということの、いちばん具体的な現れです。C++のstd::stringはバイトの並びであって、文字の並びではありません。日本語を扱うなら、必ずどこかでこの区別に出会います。
足りないぶんをstd::string(width - visible, ' ')で空白として足せば、見た目の幅が揃います。
size()が返すバイト数: 12 [ブレンド][トースト] [ブレンド ][トースト ] ############--------
STEP 6クラスにまとめて、後片付けをデストラクタに任せる
🎯 このステップのゴール: コンストラクタで始まり、スコープを抜けると自動で終わる形にする
最後の仕上げです。ここまでの部品をSalesReportというクラスにまとめます。
コンストラクタで見出しを出し、デストラクタで締めの行を出します。
SalesReport(const std::string &shop) : shop_(shop)——:のあとが初期化子リストです。本体の{}に入る前にメンバを初期化します。
本体でshop_ = shop;と書くのとは、実は違います。そちらは「空で作ってから代入し直す」——2度手間です。初期化子リストなら最初から正しい値で作られます。constメンバや参照メンバは、こちらでしか初期化できません。
~SalesReport()がデストラクタです。オブジェクトが消えるときに、自動で呼ばれます。
mainの中の{ }に注目してください。わざと波括弧で囲んであります。
この括弧を抜けた瞬間に、reportが消え、デストラクタが呼ばれます。だから「ここまで」の行が出てから「集計を終えました」が出ます。呼び出す文をどこにも書いていないのに、確実に呼ばれます。
これがC++の核心にある考え方です。ファイルを開いたら閉じる、メモリを確保したら解放する、鍵をかけたら外す——その「戻す」処理をデストラクタに置いておけば、書き忘れようがありません。途中で例外が飛んでも呼ばれます。
C言語でfreeやfcloseを書き忘れて起きていた事故が、この仕組みで消えます。
完成です。mainの波括弧を外してみてください。「ここまで」と「集計を終えました」の順番が入れ替わります——デストラクタが呼ばれる時機が変わるからです。
===== プロ道珈琲店 本日の売上 ===== ブレンド ######-------------- 5280円 売れ筋 カフェラテ ####---------------- 3300円 トースト ##------------------ 1680円 ケーキセット ######-------------- 5280円 売れ筋 -------------------------------------------- 注文件数: 8件 売上合計: 15540円 最も売れた商品: ブレンド(5280円) --- 時間帯別 --- 朝 4220円 昼 7280円 夕方 3160円 夜 880円 ===== ここまで ===== 集計を終えました
よくある質問
このページではコードを実行しないのですか?
していません。C++はコンパイルが必要で、ブラウザの中では走らせられないためです。このページではお手本と同じコードを書けたかどうかを判定する形にしています(判定はあなたのブラウザの中だけで行われ、コードはどこにも送信されません)。
ただし表示している実行結果は本物です。このページを作るときに実際にg++でコンパイルして走らせた出力を、そのまま載せています。
引数はいつも const 参照で受ければいいですか
読むだけなら、そうしてください。vector・string・自作クラスのように中身の大きいものは、値で渡すと丸ごと複製されます。
例外はint や double のような小さな型です。こちらは値で渡すほうが速いことが多く、const int &と書く意味はほとんどありません。
そして中身を変えたいときはconstを外します。std::vector<Order> &ordersと書けば、呼び出し元のvectorそのものが変わります。constの有無が、そのまま「変えるかどうか」の宣言になります。
std::string の size() が文字数と合いません
size() が返すのはバイト数だからです。UTF-8では日本語1文字が3バイトなので、「ブレンド」は4ではなく12が返ります。
std::stringはバイトの並びであって、文字の並びではありません。英数字だけなら一致するので気づきにくいのですが、日本語を扱うと必ずここに出会います。
桁を揃えたいなら、ステップ5のpad_rightのように自分で数え直すか、専用のライブラリを使います。
デストラクタは自分で呼ぶものですか
呼びません。自動で呼ばれます。
オブジェクトがスコープ({ }で囲まれた範囲)を抜けるときや、deleteされるときに、C++が呼んでくれます。
途中で例外が飛んでも呼ばれるのが大事なところです。だから「開いたら閉じる」「確保したら解放する」という後片付けをデストラクタに置いておけば、書き忘れようがなくなります。
この考え方をRAIIと呼び、C++らしい設計の中心にあるものです。
🚩 次はこのカテゴリへ
この演習で楽だったところ——vectorが長さを知っている、参照で複製せずに渡せる、デストラクタが自動で片付ける——は、どれもC言語には無い仕組みです。C言語の最終演習では、件数を自分で持ち、qsortに比較関数を渡し、sizeofが関数の中で効かない罠を通ります。比べると、C++が何を解決したのかがはっきり分かります。
C言語のレッスンへ進む