2018-02-27
本書はプログラミングの経験はあるがC++は知らない読者を対象にしたC++を学ぶための本である。本書はすでに学んだことのみを使って次の知識を説明する手法で書かれた。C++コンパイラーをC++で書く場合、C++コンパイラーのソースコードをコンパイルする最初のC++コンパイラーをどうするかというブートストラップ問題がある。本書はいわばC++における知識のブートストラップを目指した本だ。これにより読者は本を先頭から読んでいけば、まだ学んでいない概念が突如として無説明のまま使われて混乱することなく読み進むことができるだろう。
C++知識のブートストラップを意識した入門書の執筆はなかなかに難しかった。ある機能Xを教えたいが、そのためには機能Yを知っていなければならず、機能Yを理解するためには機能Zの理解が必要といった具合に、C++の機能の依存関係の解決をしなければならなかったからだ。著者自身も苦しい思いをしながらできるだけ今までに説明した知識のみを使って次の知識を教えるように書き進めていった結果、意外な再発見をした。ポインターを教えた後はC++のほとんどの機能を教えることに苦労しなくなったのだ。けっきょくC++ではいまだにポインターの機能はさまざまな機能の土台になっているのだろう。
本書の執筆時点でC++は現在、C++20の規格制定に向けて大詰めを迎えている。C++20では#includeに変わるモジュール、軽量な実行媒体であるコルーチン、高級なassert機能としてのコントラクトに加え、とうとうコンセプトが入る。ライブラリとしてもコンセプトを活用したレンジ、span、flat_mapなどさまざまなライブラリが追加される。その詳細は、次に本を出す機会があるならば『江添亮の詳説C++17』と似たようなC++20の参考書を書くことになるだろう。C++はまだまだ時代に合わせて進化する言語だ。
本書の執筆はGitHub上で公開した状態で行われた。
本書のライセンスはGPLv3である。ただし、本書の著者近影はGPLv3ではなく撮影者が著作権を保持している。
本書の著者近影の撮影は、著者の古くからの友人でありプロのカメラマンである三浦大に撮影してもらった。
江添亮
C++とは何か。C++の原作者にして最初の実装者であるBjarne Stroustrupは、以下のように簡潔にまとめている。
C++は、Simulaのプログラム構造化のための機構と、Cのシステムプログラミング用の効率性と柔軟性を提供するために設計された。C++は半年ほどで現場で使えることを見込んでいた。結果として成功した。
Bjarne Stroustrup, A History of C++: 1979-1991, HOPL2
プログラミング言語史に詳しくない読者は、Simulaというプログラミング言語について知らないことだろう。Simulaというのは、初めてオブジェクト指向プログラミングを取り入れたプログラミング言語だ。当時と言えばまだ高級なプログラミング言語はほとんどなく、if else, whileなどのIBMの提唱した構造化プログラミングを可能にする文法を提供しているプログラミング言語すら、多くは研究段階であった。いわんやオブジェクト指向など、当時はまだアカデミックにおいて可能性の1つとして研究されている程度の地に足のついていない夢の機能であった。そのような粗野な時代において、Simulaは先進的なオブジェクト指向プログラミングを実現していた。
オブジェクト指向は現代のプログラミング言語ではすっかり普通になった。データの集合とそのデータに適用する関数を関連付けることができる便利なシンタックスシュガー、つまりプログラミング言語の文法上の機能として定着した。しかし、当時のオブジェクト指向というのはもっと抽象度の高い概念であった。本来のオブジェクト指向をプログラミング言語に落とし込んだ最初の言語として、SimulaとSmalltalkがある。
Simulaではクラスのオブジェクト1つ1つが、あたかも並列実行しているかのように振る舞った。Smalltalkでは同一プログラム内のオブジェクトごとのデータのやり取りですらあたかもネットワーク越しに通信をするかのようなメッセージパッシングで行われた。
問題は、そのような抽象度の高すぎるSimulaやSmalltalkのようなプログラミング言語の設計と実装では実行速度が遅く、大規模なプログラムを開発するには適さなかった。
Cの効率性と柔軟性というのは、要するに実行速度が速いとかメモリー消費量が少ないということだ。ではなぜCはほかの言語に比べて効率と柔軟に優れているのか。これには2つの理由がある。
1つ、Cのコードは直接ハードウェアがサポートする命令にまでマッピング可能であるということ。現実のハードウェアにはストレージがあり、メモリーがあり、キャッシュがあり、レジスターがあり、命令は投機的に並列実行される泥臭い計算機能を提供している。
1つ、使わない機能のコストを支払う必要がないというゼロオーバーヘッドの原則。例えばあらゆるメモリー利用がGC(ガベージコレクション)によって管理されている言語では、たとえメモリーをすべて明示的に管理していたとしても、GCのコストを支払わなければならない。GCではプログラマーは確保したメモリーの解放処理を明示的に書く必要はない。定期的に全メモリーを調べて、どこからも使われていないメモリーを解放する。この処理には余計なコストがかかる。しかし、いつメモリーを解放すべきかがコンパイル時に決定できる場合では、GCは必要ない。GCが存在する言語では、たとえGCが必要なかったとしても、そのコストを支払う必要がある。また実行時にメモリーレイアウトを判定して実行時に分岐処理ができる言語では、たとえコンパイル時にメモリーレイアウトが決定されていたとしても、実行時にメモリーレイアウトを判定して条件分岐するコストを支払わなければならない。
C++は、「アセンブリ言語をおいて、C++より下に言語を置かない」と宣言するほど、ハードウェア機能への直接マッピングとゼロオーバーヘッドの原則を重視している。
C++のほかの特徴としては、委員会方式による国際標準規格を定めていることがある。特定の一個人や一法人が所有する言語は、個人や法人の意思で簡単に仕様が変わってしまう。短期的な利益を追求するために長期的に問題となる変更をしたり、単一の実装が仕様だと言わんばかりの振る舞いをする。特定の個人や法人に所有されていないこと、実装が従うべき標準規格があること、独立した実装が複数あること、言語に利害関係を持つ関係者が議論して投票で変更を可決すること、これがC++が長期に渡って使われてきた理由でもある。
委員会方式の規格制定では、下位互換性の破壊は忌避される。なぜならば、既存の動いているコードを壊すということは、それまで存在していた資産の価値を毀損することであり、利害関係を持つ委員が反対するからだ。
下位互換性を壊した結果何が起こるかというと、単に言語が新旧2つに分断される。Python 2とPython 3がその最たる例だ。
C++には今日の最新で高級な言語からみれば古風な制約が数多く残っているが、いずれも理由がある。下位互換性を壊すことができないという理由。効率的な実装方法が存在しないという理由。仮に効率的な実装が存在するにしても、さまざまな環境で実装可能でなければ規格化はできないという理由。
C++には善しあしがある。Bjarne StroustrupはC++への批判にこう答えている。
言語には2種類ある。文句を言われる言語と、誰も使わない言語。
C++は文句を言われる方の言語だ。
プログラミング言語を学ぶには、まず書いたソースコードをプログラムとして実行できるようになることが重要だ。自分が正しく理解しているかどうかを確認するために書いたコードが期待どおりに動くことを確かめてこそ、正しい理解が確認できる。
C++は慣習的に、ソースファイルをコンパイルしてオブジェクトファイルを生成し、オブジェクトファイルをリンクして実行可能ファイルを生成し、実行可能ファイルを直接実行することで実行する言語だ。
ほかの言語では、ソースファイルをそのままパースし、解釈して実行するインタープリター形式の言語が多い。もっとも、いまとなってはソースファイルから中間言語に変換して、VM(Virtual Machine)と呼ばれる中間言語を解釈して実行するソフトウェア上で実行するとか、JIT(Just-In-Time)コンパイルしてネイティブコードを生成して実行するといった実装もあるため、昔のように単純にインタープリター型の言語ということはできなくなっている事情はある。ただし、最終的にJITコンパイルされてネイティブコードが実行される言語でも、コンパイルやコード生成はプログラマーが意識しない形で行われるため、プログラマーはコンパイラーを直接使う必要のない言語も多い。
C++はプログラマーが直接コンパイラーを使い、ソースファイルをプログラムに変換する言語だ。
ここでは、典型的なC++のソースファイルをどのようにコンパイルし実行するか、一連の流れを学ぶ。
以下のC++のソースファイルは標準出力にhelloと出力するものだ。
#include <iostream>
int main()
{
std::cout << "hello" ;
}コードの詳細な意味はさておくとして、このサンプルコードを使ってC++の実行までの流れを見ていこう。
まずは端末から作業用の適当な名前のディレクトリーを作る。ここではcppとしておこう。ディレクトリーの作成はmkdirコマンドで行える。
$ mkdir cpp
$ cd cpp
好きなテキストエディターを使って上のサンプルコードをテキストファイルとして記述する。ファイル名はhello.cppとしておこう。
$ vim hello.cpp
C++のソースファイルの名前は何でもよいが、慣習で使われている拡張子がいくつかある。本書では.cppを使う。
無事にソースファイルが作成できたかどうか確認してみよう。現在のカレントディレクトリー下のファイルの一覧を表示するにはls、ファイルの内容を表示するにはcatを使う。
$ ls
hello.cpp
$ cat hello.cpp
#include <iostream>
int main()
{
std::cout << "hello" ;
}
さて、ソースファイルが用意できたならば、いよいよコンパイルだ。
C++のソースファイルから、実行可能ファイルを生成するソフトウェアをC++コンパイラーという。C++コンパイラーとしては、GCC(GNU Compiler Collection)とClang(クラン)がある。使い方はどちらもほぼ同じだ。
GCCを使って先ほどのhello.cppをコンパイルするには以下のようにする。
$ g++ -o hello hello.cpp
GCCという名前のC++コンパイラーなのにg++なのは、gccはC言語コンパイラーの名前としてすでに使われているからだ。この慣習はClangも引き継いでいて、ClangのC++コンパイラーはclang++だ。
サンプルコードを間違いなくタイプしていれば、カレントディレクトリーにhelloという実行可能ファイルが作成されるはずだ。確認してみよう。
$ ls
hello hello.cpp
さて、いよいよ実行だ。通常のOSではカレントディレクトリーがPATHに含まれていないため、実行するにはカレントディレクトリーからパスを指定する必要がある。
$ ./hello
hello
上出来だ。初めてのC++プログラムが実行できた。さっそくC++を学んでいきたいところだが、その前にC++プログラミングに必要なツールの使い方を学ぶ必要がある。
GCCはC++のソースファイルからプログラムを生成するC++コンパイラーだ。
GCCの基本的な使い方は以下のとおり。
g++ その他のオプション -o 出力するファイル名 ソースファイル名
ソースファイル名は複数指定することができる。
$ g++ -o abc a.cpp b.cpp c.cpp
これについては分割コンパイルの章で詳しく解説する。
コンパイラーはメッセージを出力することがある。コンパイルメッセージには、エラーメッセージと警告メッセージとがある。
エラーメッセージというのは、ソースコードに文法上、意味上の誤りがあるため、コンパイルできない場合に生成される。エラーメッセージはエラーの箇所も教えてくれる。ただし、文法エラーは往々にして適切な誤りの箇所を指摘できないこともある。これは、C++の文法としては正しくないテキストファイルから、妥当なC++であればどういう間違いなのかを推測する必要があるためだ。
警告メッセージというのは、ソースコードにコンパイルを妨げる文法上、意味上の誤りは存在しないが、誤りの可能性が疑われる場合に出力される。
GCCのコンパイラーオプションをいくつか学んでいこう。
-std=はC++の規格を選択するオプションだ。C++17に準拠したいのであれば-std=c++17を指定する。読者が本書を読むころには、C++20や、あるいはもっと未来の規格が発行されているかもしれない。常に最新のC++規格を選択するオプションを指定するべきだ。
-Wallはコンパイラーの便利な警告メッセージのほとんどすべてを有効にするオプションだ。コンパイラーによる警告メッセージはプログラムの不具合を未然に発見できるので、このオプションは指定すべきだ。
--pedantic-errorsはC++の規格を厳格に守るオプションだ。規格に違反しているコードがコンパイルエラー扱いになる。
これをまとめると、GCCは以下のように使う。
g++ -std=c++17 -Wall --pedantic-errors -o 出力ファイル名 入力ファイル名
ところで、GCCのオプションはとても多い。すべてを知りたい読者は、以下のようにしてGCCのマニュアルを読むとよい。
$ man gcc
手元にマニュアルがない場合、GCCのWebサイトにあるオンラインマニュアルも閲覧できる。
先ほどのソースコードをもう一度見てみよう。冒頭に以下のような行がある。
#include <iostream>これは#includeディレクティブ(#include directive)といい、プリプロセッサー(preprocessor)の一部だ。プリプロセッサーについて詳しくは煩雑になるので巻末資料を参照してもらうとして、このコードはiostreamライブラリを使うために必要で、その意味としてはヘッダーファイルiostreamの取り込みだ。
C++の標準ライブラリを使うには、ライブラリごとに対応した#includeディレクティブを書かなければならない。それはあまりにも煩雑なので、本書では標準ライブラリのヘッダーファイルをすべて#includeしたヘッダーファイル(header file)を作成し、それを#includeすることで、#includeを書かなくて済むようにする。
そのためにはまず標準ライブラリのヘッダーファイルのほとんどすべてを#includeしたヘッダーファイル、all.hを作成する。
#include <cstddef>
#include <limits>
#include <climits>
#include <cfloat>
#include <cstdint>
#include <cstdlib>
#include <new>
#include <typeinfo>
#include <exception>
#include <initializer_list>
#include <cstdalign>
#include <stdexcept>
#include <cassert>
#include <cerrno>
#include <system_error>
#include <string>
#if __has_include(<string_view>)
# include <string_view>
#endif
#include <array>
#include <deque>
#include <forward_list>
#include <list>
#include <vector>
#include <map>
#include <set>
#include <unordered_map>
#include <unordered_set>
#include <queue>
#include <stack>
#include <iterator>
#include <algorithm>
#include <cfenv>
#include <random>
#include <numeric>
#include <cmath>
#include <iosfwd>
#include <iostream>
#include <ios>
#include <streambuf>
#include <istream>
#include <ostream>
#include <iomanip>
#include <sstream>
#include <fstream>
#if __has_include(<filesystem>)
# include <filesystem>
#endif
#include <cstdio>
#include <cinttypes>
#include <regex>
#include <atomic>
#include <thread>
#include <mutex>
#include <shared_mutex>
#include <condition_variable>
#include <future>
using namespace std::literals ;このようなヘッダーファイルall.hを作成したあとに、ソースファイルで以下のように書けば、ほかのヘッダーファイルを#includeする必要がなくなる。
#include "all.h"
// その他のコード//から行末まではコメントで、好きなテキストを書くことができる。
しかし、この最初の1行の#includeも面倒だ。そこでGCCのオプション-includeを使い、all.hを常に#includeした扱いにする。
$ g++ -include all.h -o program main.cpp
このようにすると、main.cppが以下のコードでもコンパイルできるようになる。
// main.cpp
// 面倒な#includeなどなし
int main()
{
std::cout << "hello" ;
}これでヘッダーファイルが省略できるようになった。
C++はソースファイルをコンパイルする必要がある言語だ。コンパイルには時間がかかる。コンパイルにどれだけ時間がかかっているかを計測するには、以下のようにするとよい。
$ time g++ -std=c++17 -Wall --pedantic-errors -include all.h -o program main.cpp
どうだろうか。読者の環境にもよるが、知覚できるぐらいの時間がかかっているのではないだろうか。プログラミングの習得にはコードを書いてから実行までの時間が短い方がよい。そこで本格的にC++を学ぶ前に、コンパイル時間を短縮する方法を学ぶ。
プログラムで変更しないファイルを事前にコンパイルしておくと、変更した部分だけコンパイルすればよいので、コンパイル時間の短縮になる。GCCでは、ヘッダーファイルを事前にコンパイルする特別な機能がある。標準ライブラリのヘッダーファイルは変更しないので、事前にコンパイルしておけばコンパイル時間の短縮になる。
事前にコンパイルしたヘッダーファイルのことをコンパイル済みヘッダー(precompiled header)という。
すでに作成したall.hはコンパイル済みヘッダーとするのに適切なヘッダーファイルだ。
コンパイル済みヘッダーファイルを作成するには、ヘッダーファイル単体をGCCに与え、出力するファイルをヘッダーファイル名.gchとする。ヘッダーファイル名がall.hの場合、all.h.gchとなる。
GCCのオプションにはほかのソースファイルをコンパイルするときと同じオプションを与えるほか、ヘッダーファイルがC++で書かれていることを示すオプション-x c++-headerを与える。
$ g++ -std=c++17 -Wall --pedantic-errors -x c++-header -o all.h.gch all.h
こうすると、コンパイル済みヘッダーファイルall.h.gchが生成できる。
GCCはヘッダーファイルを使うときに、同名の.gchファイルが存在する場合は、そちらをコンパイル済みヘッダーファイルとして使うことで、ヘッダーファイルの処理を省略する。
$ g++ -std=c++17 -Wall --pedantic-errors -include all.h -o program main.cpp
コンパイル済みヘッダーは1回のコンパイルにつき1つしか使うことができない。そのため、コンパイル済みヘッダーとするヘッダーファイルを定め、そのヘッダーファイル内にほかのヘッダーをすべて記述する。本書ではコンパイル済みヘッダーファイルとする元のヘッダーファイルの名前をall.hとする。
さっそくコンパイル時間の短縮効果を確かめてみよう。
$ ls
all.h main.cpp
$ g++ -std=c++17 -Wall --pedantic-errors -x c++-header -o all.h.gch all.h
$ ls
all.h all.h.gch main.cpp
$ time g++ -std=c++17 -Wall --pedantic-errors -include all.h -o program main.cpp
ここまで、我々はソースファイルをコンパイルして実行可能ファイルを生成し、プログラムを実行する方法について学んできた。これまでに学んできたことを一連のコマンドで振り返ってみよう。
$ ls
all.h main.cpp
$ cat all.h
#include <iostream>
$ cat main.cpp
int main() { std::cout << "hello"s ; }
まず、カレントディレクトリーにはall.hとmain.cppがある。この2つのファイルは実行可能ファイルを生成するために必要なファイルだ。今回、その中身は最小限にしてある。本当のall.hは、実際には前回書いたように長い内容になる。
$ g++ -std=c++17 -Wall --pedantic-errors -x c++-header -o all.h.gch all.h
$ ls
all.h all.h.gch main.cpp
次に、ソースファイルのコンパイルを高速化するために、ヘッダーファイルall.hから、コンパイル済みヘッダーファイルall.h.gchを生成する。
$ g++ -std=c++17 -Wall --pedantic-errors -include all.h -o program main.cpp
$ ls
all.h all.h.gch main.cpp program
プリコンパイル済みヘッダーファイルall.h.gchとC++ソースファイルmain.cppから、実行可能ファイルprogramを生成する。
$ ./program
hello
実行可能ファイルprogramを実行する。
これで読者はC++のプログラミングを学び始めるにあたって必要なことはすべて学んだ。さっそくC++を学んでいきたいところだが、その前にもう1つ、ビルドシステムを学ぶ必要がある。
以上のC++のソースファイルからプログラムを実行するまでの流れは、C++のプログラムとしてはとても単純なものだが、それでも依存関係が複雑だ。
プログラムの実行にあたって最終的に必要なのはファイルprogramだが、このファイルはGCCで生成しなければならない。ところでGCCでファイルprogramを生成するには、事前にall.h, all.h.gch, main.cppが必要だ。all.h.gchはall.hからGCCで生成しなければならない。
一度コンパイルしたプログラムのソースファイルを書き換えて再びコンパイルする場合はどうすればいいだろう。main.cppだけを書き換えた場合、all.hは何も変更されていないので、コンパイル済みヘッダーファイルall.h.gchの再生成は必要ない。all.hだけを書き換えた場合は、all.h.gchを生成するだけでなく、programも再生成しなければならない。
プログラムのコンパイルには、このような複雑な依存関係の解決が必要になる。依存関係の解決を人間の手で行うのはたいへんだ。例えば読者が他人によって書かれた何千ものソースファイルと、プログラムをコンパイルする手順書だけを渡されたとしよう。手順書に従ってコンパイルをしたとして、ソースファイルの一部だけを変更した場合、いったいどの手順は省略できるのか、手順書から導き出すのは難しい。するとコンパイルを最初からやり直すべきだろうか。しかし、1つのソースファイルのコンパイルに1秒かかるとして、何千ものソースファイルがある場合、何千秒もかかってしまう。たった1つのソースファイルを変更しただけですべてをコンパイルし直すのは時間と計算資源の無駄だ。
この依存関係の問題は、ビルドシステムによって解決できる。本書ではGNU Makeというビルドシステムを学ぶ。読者がこれから学ぶビルドシステムによって、以下のような簡単なコマンドだけで、他人の書いた何千ものソースファイルからなるプログラムがコンパイル可能になる。
何千ものソースファイルから実行可能ファイルを生成したい。
$ make
これだけだ。makeというコマンド1つでプログラムのコンパイルは自動的に行われる。
何千ものソースファイルのうち、1つのソースファイルだけを変更し、必要な部分だけを効率よく再コンパイルしたい。
$ make
これだけだ。makeというコマンド1つでプログラムの再コンパイルは自動的に行われる。
ところで、生成される実行可能ファイルの名前はプログラムごとにさまざまだ。プログラムの開発中は、共通の方法でプログラムを実行したい。
$ make run
これでどんなプログラム名でも共通の方法で実行できる。
ソースファイルから生成されたプログラムなどのファイルをすべて削除したい。
$ make clean
これで生成されたファイルをすべて削除できる。
テキストエディターにはVimを使っているがわざわざVimからターミナルに戻るのが面倒だ。
:make
VimはノーマルモードからMakeを呼び出すことができる。もちろん、:make runや:make cleanもできる。
依存関係はどのように表現したらいいのだろうか。GNU MakeではMakefileという名前のファイルの中に、ターゲット(targets)、事前要件(prerequisites)、レシピ(recipes)という3つの概念で依存関係をルール(rules)として記述する。ルールは以下の文法だ。
ターゲット : 事前要件
[TAB文字]レシピ
レシピは必ずTAB文字を直前に書かなければならない。スペース文字ではだめだ。これはmakeの初心者を混乱させる落とし穴の1つとなっている。忘れずにTAB文字を打とう。
問題を簡単に理解するために、以下のような状況を考えよう。
$ ls
source
$ cat source > program
この例では、ファイルprogramを生成するためにはファイルsourceが必要だ。ファイルsourceはすでに存在している。
ターゲットは生成されるファイル名だ。この場合programとなる。
program : 事前要件
レシピ
事前要件はターゲットを生成するために必要なファイル名だ。この場合sourceとなる。
program : source
レシピ
レシピはターゲットを生成するために必要な動作だ。この場合、cat source > programとなる
program : source
cat source > programさっそくこのルールを、ファイルMakefileに書き込み、makeを呼び出してみよう。
$ ls
Makefile source
$ cat Makefile
program : source
cat source > program
$ make
cat source > program
$ ls
Makefile program source
これがMakeの仕組みだ。ターゲットの生成に必要な事前要件と、ターゲットを生成するレシピを組み合わせたルールで依存関係を記述する。makeを実行すると、実行したレシピが表示される。
もう少しMakeのルールを追加してみよう。例えばファイルsourceはあらかじめ存在するのではなく、ファイルsource01, source02, source03の中身をこの順番で連結して生成するとしよう。以下のように書ける。
program : source
cat source > program
source : source01 source02 source03
cat source01 source02 source03 > sourceGNU MakeはカレントディレクトリーにあるファイルMakefileの一番上に書かれたルールを実行しようとする。programを生成するにはsourceが必要だが、sourceの生成には別のルールの実行が必要だ。Makefileはこの依存関係を自動で解決してくれる。
$ touch source01 source02 source03
$ ls
Makefile source01 source02 source03
$ make
cat source01 source02 source03 > source
cat source > program
$ ls
Makefile program source source01 source02 source03
すでにmakeを実行したあとで、もう一度makeを実行するとどうなるだろうか。
$ make
make: 'program' is up to date.
このメッセージの意味は「programは最新だ」という意味だ。makeはファイルのタイムスタンプを調べ、もしファイルprogramよりsourceのタイムスタンプの方が若い場合、つまりprogramが変更されたよりもあとにsourceが変更された場合、ルールを実行する。
試しにファイルsource02のタイムスタンプを更新してみよう。
$ touch source02
$ make
cat source01 source02 source03 > source
cat source > program
ファイルsourceは事前要件にsource02を含む。source02のタイムスタンプがsourceより若いので、sourceが再び生成される。すると、sourceのタイムスタンプがprogramのタイムスタンプよりも若くなったので、programも生成される。
もう1つ例を見てみよう。
$ touch a b c
$ ls
a b c Makefile
あるディレクトリーにファイルa, b, cがある。
Makefileは以下の内容になっている。
D : A B C
cat A B C > D
A : a
cat a > A
B : b
cat b > B
C : c
cat c > CこのMakefileを呼び出したときに作られるのはファイルDだ。ファイルDを作るにはファイルA, B, Cが必要だ。このファイルはそれぞれファイルa, b, cから生成されるルールが記述してある。
これをmakeすると以下のようにファイルA, B, C, Dが作られる。
$ ls
a b c Makefile
$ make
cat a > A
cat b > B
cat c > C
cat A B C > D
ここで、ファイルbのタイムスタンプだけを更新してmakeしてみよう。
$ touch b
$ make
cat b > B
cat A B C > D
ファイルbのタイムスタンプがファイルBより若くなったので、ファイルBがターゲットとなったルールが再び実行される。ファイルA, Cのルールは実行されない。そしてファイルBのタイムスタンプがファイルDより若くなったので、ファイルDがターゲットとなったルールが再び実行される。
makeにより、処理する必要のあるルールだけが部分的に処理されていることがわかる。
makeは適切なルールさえ書けば、依存関係の解決を自動的に行ってくれる。
Makefileにはコメントを書くことができる。#で始まる行はコメント扱いされる。
# programを生成するルール
program : source
cat source > program
# sourceを生成するルール
source : source01 source02 source03
cat source01 source02 source03 > sourceMakefileには変数を書くことができる。
変数の文法は以下のとおり。
variable = foobar
target : $(variable)
これは、
target : foobar
と書いたものと同じように扱われる。
変数は=の左側に変数名、右側に変数の内容を書く。
変数を使うときは、$(変数名)のように、$()で変数名を包む。
GNU Makeは便利なことに、いくつかの変数を自動で作ってくれる。
$@ ターゲット$@はルールのターゲットのファイル名になる。
target :
echo $@このMakefileを実行すると以下のように出力される。
$ make
echo target
$< 最初の事前要件$<はルールの最初の事前要件のファイル名になる。
target : A B C
echo $<このMakefileを実行すると以下のように出力される。
$ make
echo A
$^ すべての事前要件$^はすべての事前要件のファイル名が空白区切りされたものになる
target : A B C
echo $^このMakefileを実行すると以下のように出力される。
$ make
echo A B C
例えばターゲットを生成するために事前要件とターゲットのファイル名をレシピに書く場合、
target : prerequisite
cat prerequisite > targetと書く代わりに、
target : prerequisite
cat $< > $@と書ける。
PHONYターゲットとは、ファイル名を意味せず、単にレシピを実行するターゲット名としてのみ機能するターゲットのことだ。
hi :
echo hi
hello :
echo helloこれを実行すると以下のようになる。
$ make
echo hi
hi
$ make hi
echo hi
hi
$ make hello
echo hello
hello
makeを引数を付けずに実行すると、一番上に書かれたルールが実行される。引数としてターゲットを指定すると、そのターゲットのルールと、依存するルールが実行される。
ただし、ターゲットと同じファイル名が存在すると、ルールは実行されない。
$ touch hello
$ make hello
make: 'hello' is up to date.
GNU Makeはこの問題に対処するため、.PHONYターゲットという特殊な機能がある。これはPHONYターゲットを.PHONYターゲットの事前要件とすることで、ターゲットと同じファイル名の存在の有無にかかわらずルールを実行させられる。
hello :
echo hello
.PHONY : helloPHONYターゲットはコンパイルしたプログラムの実行や削除に使うことができる。
hello : hello.cpp
g++ -o $@ $<
run : hello
./hello
clean :
rm -rf ./hello
.PHONY : run clean以上を踏まえて、C++入門用の環境構築をしてこの章のまとめとする。
今回構築する環境のファイル名とその意味は以下のとおり。
main.cppall.h
all.h.gch
program
Makefile
使い方は以下のとおり。
makemake run
make clean
GCCに与えるコンパイラーオプションを変数にまとめる。
gcc_options = -std=c++17 -Wall --pedantic-error言語はC++17、すべての警告を有効にし、規格準拠ではないコードはエラーとする。
プログラムをコンパイルする部分は以下のとおり。
program : main.cpp all.h all.h.gch
g++ $(gcc_options) -include all.h $< -o $@
all.h.gch : all.h
g++ $(gcc_options) -x c++-header -o $@ $<実行可能ファイルprogramと、コンパイル済みヘッダーall.h.gchをコンパイルするルールだ。
PHONYターゲットは以下のとおり。
run : program
./program
clean :
rm -f ./program
rm -f ./all.h.gch
.PHONY : run cleanmakeでコンパイル。make runで実行。make cleanでコンパイル結果の削除。
Makefile全体は以下のようになる。
gcc_options = -std=c++17 -Wall --pedantic-errors
program : main.cpp all.h all.h.gch
g++ $(gcc_options) -include all.h $< -o $@
all.h.gch : all.h
g++ $(gcc_options) -x c++-header -o $@ $<
run : program
./program
clean :
rm -f ./program
rm -f ./all.h.gch
.PHONY : run cleanプログラミング言語の個々の機能の解説を理解するためには、まず言語の全体像を掴まなければならない。この章ではC++のさまざまなコードをひと通り観光していく。ここではコードの詳細な解説はしない。
以下はC++の最小のコードだ。
int main(){}暗号のようなコードで訳がわからないが、これが最小のコードだ。mainというのはmain関数のことだ。C++ではプログラムの実行はmain関数から始まる。
ソースコードにコメントを記述して、もう少しわかりやすく書いてみよう。
int // 関数の戻り値の型
main // 関数名
() // 関数の引数
{ // 関数の始まり
// 実行される処理
} // 関数の終わり//から行末まではコメントだ。コメントには好きなことを書くことができる。
このコードと1つ前のコードは、コメントの有無を別にすれば何の違いもない。このコードで使っている、intとかmainとか記号文字の1つ1つをトークン(token)と呼ぶ。C++ではトークンの間に空白文字や改行文字をいくら使ってもよい。
なので、
int main(){ }と書くこともできるし、
int main ( ) { }と書くこともできるし、紙に印刷する都合上とても読みづらくなるかもしれないが
int
main
(
)
{
}と書くこともできる。
ただし、トークンの途中で空白文字や改行文字を使うことはできない。以下のコードは間違っている。
i
nt ma in(){}// helloと改行を出力するプログラム
int main()
{
std::cout << "hello"s ;
}標準出力はプログラムの基本だ。C++で標準出力する方法はいくつもあるが、<iostream>ライブラリを利用するものが最も簡単だ。
std::coutは標準出力を使うためのライブラリだ。
<<はoperator <<という演算子だ。C++では演算子にも名前が付いていて、例えば+はoperator +となる。<<も演算子の一種だ。
"hello"sというのは文字列で、二重引用符で囲まれた中の文字列が標準出力に出力される。
セミコロン;は文の区切り文字だ。C++では文の区切りは明示的にセミコロンを書く必要がある。ほかの言語では改行文字を文脈から判断して文の区切りとみなすこともあるが、C++では明示的に文の区切り文字としてセミコロンを書かなければならない。
セミコロンを書き忘れるとエラーとなる。
int main()
{
// エラー! セミコロンがない
std::cout << "error"s
}複数の文を書いてみよう。
int main()
{
std::cout << "one "s ;
std::cout << "two "s ;
std::cout << "three "s ;
}C++はほかの多くの言語と同じように、逐次実行される。つまり、コードは書いた順番に実行される。そして標準出力のような外部への副作用は、実行された順番で出力される。このコードを実行した結果は以下のとおり。
one two three
"three two one "や"two one three "のような出力結果にはならない。
C++を含む多くの言語でa + b + cと書けるように、operator <<もa << b << cと書ける。operator <<で標準出力をするには、左端はstd::coutでなければならない。
int main()
{
std::cout << "aaa"s << "bbb"s << "ccc"s ;
}出力はaaabbbcccとなる。
二重引用符で囲まれた文字列を、文字どおり文字列という。文字列には末尾にsが付くものと付かないものがある。これには違いがあるのだが、わからないうちはsを付けておいた方が便利だ。
int main()
{
// これは文字列
std::cout << "hello"s ;
// これも文字列、ただし不便
std::cout << "hello" ;
}文字列リテラルの中にバックスラッシュを書くと、エスケープシーケンスとして扱われる。最もよく使われるのは改行文字を表す\nだ。
int main()
{
std::cout << "aaa\nbbb\nccc"s ;
}これは以下のように出力される。
aaa
bbb
ccc
バックスラッシュを文字列で使いたい場合は\\と書かなければならない。
int main()
{
std::cout << "\\n is a new-line.\n"s ;
}文字列は演算子operator +で「足す」ことができる。「文字列を足す」というのは、「文字列を結合する」という意味だ。
int main()
{
std::cout << "hello"s + "world"s ;
}iostreamは文字列のほかにも、整数や浮動小数点数を出力できる。さっそく試してみよう。
int main()
{
std::cout
<< "Integer: "s << 42 << "\n"s
<< "Floating Point: "s << 3.14 ;
}-123や0や123といった数値を整数という。3.14のような数値を浮動小数点数という。
数値を扱えるのだから、計算をしてみたいところだ。C++は整数同士の演算子として、四則演算(+-*/)や剰余(%)をサポートしている。
int main()
{
std::cout
<< 3 + 5 << " "s << 3 - 5 << " "s
<< 3 * 5 << " "s << 3 / 5 << " "s
<< 3 % 5 ;
}演算子は組み合わせて使うこともできる。その場合、演算子*/%は演算子+-よりも優先される。
int main()
{
// 7
std::cout << 1 + 2 * 3 ;
}この場合、まず2*3が計算され6となり、1+6が計算され7となる。
1+2の方を先に計算したい場合、括弧()で囲むことにより、計算の優先度を変えることができる。
int main()
{
// 9
std::cout << (1 + 2) * 3 ;
}これは1+2が先に計算され3となり、3*3が計算され9となる。
浮動小数点数同士でも四則演算ができる。剰余はできない。
int main()
{
std::cout
<< 3.5 + 7.11 << " "s << 3.5 - 7.11 << " "s
<< 3.5 * 7.11 << " "s << 3.5 / 7.11 ;
}では整数と浮動小数点数を演算した場合どうなるのだろう。さっそく試してみよう。
int main()
{
std::cout << 1 + 0.1 ;
}結果は1.1だ。整数と浮動小数点数を演算した結果は浮動小数点数になる。
そういえばC++には文字列もあるのだった。文字列と文字列は足すことができる。数値と数値も足すことができる。では数値と文字列を足すとどうなるのだろう。
int main()
{
std::cout << 1 + "234"s ;
}この結果はエラーになる。
いや待て、C++には末尾にsを付けない文字列もあるのだった。これも試してみよう。
int main()
{
std::cout << 1 + "234" ;
}結果はなんと34になるではないか。C++では謎の数学により1 + "234" = "34"であることが判明した。この謎はいずれ解き明かすとして、いまは文字列には必ず末尾にsを付けることにしよう。その方が安全だ。
さあどんどんプログラミング言語によくある機能を見ていこう。次は変数だ。
int main()
{
// 整数の変数
auto answer = 42 ;
std::cout << answer << "\n"s ;
// 浮動小数点数の変数
auto pi = 3.14 ;
std::cout << pi << "\n"s ;
// 文字列の変数
auto question = "Life, The Universe, and Everything."s ;
std::cout << question ;
}変数はキーワードautoに続いて変数名を書き、=に続いて値を書くことで宣言できる。変数の宣言は文なので、文末にはセミコロンが必要だ。
auto 変数名 = 値 ;変数名はキーワード、アンダースコア(_)で始まる名前、アンダースコア2つ(__)を含む名前以外は自由に名付けることができる。
変数の最初の値は、= 値の代わりに(値)や{値}と書いてもよい。
int main()
{
auto a = 1 ;
auto b(2) ;
auto c{3} ;
}この=, (), {}による変数の初期値の指定を、初期化という。
変数は使う前に宣言しなければならない。
int main()
{
// エラー、名前xは宣言されていない
std::cout << x ;
auto x = 123 ;
}変数の値は初期化したあとにも演算子=で変更できる。これを代入という。
int main()
{
// 変数の宣言
auto x
// 初期化
= 123 ;
// 123
std::cout << x ;
// 代入
x = 456 ;
// 456
std::cout << x ;
// もう一度代入
x = 789 ;
// 789
std::cout << x ;
}代入演算子operator =は左辺に変数名を、右辺に代入する値を書く。面白いこととして、右辺には代入する変数名そのものを書ける。
int main()
{
auto x = 10 ;
x = x + 5 ;
// 15
std::cout << x ;
}operator =は「代入」という意味で、「等号」という意味ではないからだ。x=x+5は、「xとx+5は等しい」という独創的な数学上の定義ではなく、「変数xに代入前の変数xの値に5を加えた数を代入する」という意味だ。
変数のいまの値に対して演算した結果を変数に代入するという処理はとてもよく使うので、C++にはx = x + aと同じ意味で使える演算子、operator +=もある。
int main()
{
auto x = 1 ;
// x = x + 5と同じ
x += 5 ;
}operator +=と同様に、operator -=, operator *=, operator /=, operator %=もある。
C++の変数は、専門用語を使うと「静的型付け」になる。静的型付けと対比されるのが「動的型付け」だ。もっと難しく書くと、動的型付け言語の変数は、C++で言えば型情報付きのvoid *型の変数のような扱いを受ける。
C++の変数には型がある。型というのは値の種類を表す情報のことだ。
例えば、以下は変数が動的型付けの言語JavaScriptのコードだ。
var x = 1 ;
x = "hello" ;
x = 2 ;JavaScriptではこのコードは正しい。変数xは数値型であり、文字列型に代わり、また数値型に戻る。
C++ではこのようなコードは書けない。
int main()
{
auto x = 1 ;
// エラー
x = "hello"s ;
x = 2 ;
}C++では、変数xは整数型であり、文字列型に変わることはない。整数型の変数に文字列型を代入しようとするとエラーとなる。
C++では型に名前が付いている。整数型はint、浮動小数点数型はdouble、文字列型はstd::stringだ。
int main()
{
// iはint型
auto i = 123 ;
// dはdouble型
auto d = 1.23 ;
// sはstd::string型
auto s = "123"s ;
}実は変数の宣言でautoと書く代わりに、具体的な型を書いてもよい。
int main()
{
int i = 123 ;
double d = 1.23 ;
std::string s = "123"s ;
}整数型(int)と浮動小数点数型(double)はそれぞれお互いの型の変数に代入できる。ただし、変数の型は変わらない。単に一方の型の値がもう一方の型の値に変換されるだけだ。
int main()
{
// 浮動小数点数型を整数型に変換
int a = 3.14 ;
// 3
std::cout << a << "\n"s ;
// 整数型を浮動小数点数型に変換
double d = 123 ;
// 123
std::cout << d ;
}浮動小数点数型を整数型に変換すると、小数部が切り捨てられる。この場合、3.14の小数部0.14が切り捨てられ3となる。0.9999も小数部が切り捨てられ0になる。
int main()
{
int i = 0.9999 ;
// 0
std::cout << i ;
}整数型を浮動小数点数型に変換すると、値を正確に表現できる場合はその値になる。正確に表現できない場合は近い値になる。
int main()
{
double d = 1234567890 ;
// 正確に表現できるかどうかわからない
std::cout << d ;
}整数型と浮動小数点数型の挙動についてはあとの章で詳しく解説する。また、これ以外にも型はいくらでもあるし、読者が新しい型を作り出すこともできる。これもあとの章で詳しく解説する。
「変数ぐらい知っている。さっさと教えてもらいたい。どうせC++の関数は書きづらいのだろう」と考える読者の皆さん、お待たせしました。こちらがC++の関数でございます。
int main()
{
// 関数
auto print = [](auto x)
{
std::cout << x << "\n"s ;
} ;
// 関数呼び出し
print(123) ;
print(3.14) ;
print("hello") ;
}C++では関数も変数として扱える。auto print =までは変数だ。変数の初期化として関数を書いている。より正確にはラムダ式と呼ばれる関数を値として書くための文法だ。
ラムダ式は以下のような文法を持つ。
[] // ラムダ式導入部
() // 引数
{} // 本体ラムダ式は[]で始まり、()の中に引数を書き、{}の中の文が実行される。
例えば以下は引数を2回標準出力する関数だ。
int main()
{
auto twice = [](auto x)
{
std::cout << x << " "s << x << "\n"s ;
} ;
twice(5) ;
}引数はauto 引数名で受け取れる。引数を複数取る場合は、カンマ,で区切る。
int main()
{
auto print_two = []( auto x, auto y )
{
std::cout << x << " "s << y << "\n"s ;
} ;
print_two( 1, 2 ) ;
print_two( "Pi is", 3.14 ) ;
}引数を取らないラムダ式を書く場合は、単に()と書く。
int main()
{
auto no_args = []()
{
std::cout << "Nothing.\n" ;
} ;
no_args() ;
}関数は演算子operator ()を関数の直後に書いて呼び出す。これが演算子であるというのは少し不思議な感じがするが、C++では紛れもなく演算子だ。operator +とかoperator -などと同じ演算子だ。
int main()
{
// 何もしない関数
auto func = [](){} ;
// operator ()の適用
func() ;
// これもoperator ()
func ( ) ;
}演算子operator ()は、ラムダ式そのものに対して適用することもできる。
int main()
{
// 変数fをラムダ式で初期化
auto f = [](){} ;
// 変数fを関数呼び出し
f() ;
// ラムダ式を関数呼び出し
[](){}() ;
}このコードを見ると、operator ()が単なる演算子であることがよくわかるだろう。[](){}がラムダ式でその直後の()が関数呼び出し演算子だ。
関数は値を返すことができる。関数から値を返すには、return文を使う。
int main()
{
auto plus = []( auto x, auto y )
{ return x + y ; } ;
std::cout
<< plus( 1, 2 ) << "\n"s
<< plus( 1.5, 0.5 ) << "\n"s
<< plus( "123"s, "456"s) ;
}関数はreturn文を実行すると処理を関数の呼び出し元に返す。
int main()
{
auto f = []()
{
std::cout << "f is called.\n" ;
return 0 ; // ここで処理が戻る
std::cout << "f returned zero.\n" ;
} ;
auto result = f() ;
}これを実行すると以下のようになる。
$ make
f is called.
return文以降の文が実行されていないことがわかる。
実はラムダ式は本当のC++の関数ではない。本当の関数はとても書きづらいので心して読むべきだ。
読者は本書の冒頭で使ったmain関数という言葉を覚えているだろうか。覚えていないとしても、サンプルコードに必ずと言っていいほど出てくるmainという名前は気になっていたことだろう。
int main(){}これを見ると、聡明な読者はラムダ式と似通ったところがあることに気付くだろう。
[](){}末尾の(){}が同じだ。これは同じ意味だ。()は関数の引数で、{}は関数の本体だ。
では残りの部分はどうだろうか。intは関数の戻り値の型、mainは関数の名前だ。
C++の本当の関数は以下のような文法で定義される。
int // 戻り値の型
main // 関数名
() // 関数の引数
{} // 関数の本体試しに、int型の引数を2つ取り足して返す関数plusを書いてみよう。
int plus( int x, int y )
{
return x + y ;
}
int main()
{
auto x = plus( 1, 2 ) ;
}では次に、double型の引数を2つ取り足して返す関数plusを書いてみよう。
double plus( double x, double y )
{
return x + y ;
}
int main()
{
auto x = plus( 1.0, 2.0 ) ;
}最後のstd::string型の引数を2つ取り足して返す関数plusは読者への課題とする。
これがC++の本当の関数だ。C++の関数では、型をすべて明示的に書かなければならない。型を間違えるとエラーだ。
しかも、C++の関数は、戻り値の型を正しく返さなければならない。
int f()
{
// エラー、return文がない
}もし、何も値を返さない関数を書く場合は、どの値でもないという特別な型、void型を関数の戻り値の型として書かなければならないという特別なルールまである。
void f()
{
// OK
}ただし、戻り値の型については、具体的な型の代わりにautoを書くこともできる。その場合、return文で同じ型さえ返していれば、気にする必要はない。
// void
auto a() { }
// int
auto b() { return 0 ; }
// double
auto c() { return 0.0 ; }
// std::string
auto d() { return ""s ; }
// エラー
// return文の型が一致しない。
auto e()
{
return 0 ;
return 0.0 ;
}やれやれ疲れた。この辺でひと休みして、デバッグについて考えよう。まずはコンパイルエラーについてだ。
プログラムにはさまざまなバグがあるが、コンパイルエラーは最も簡単なバグだ。というのも、プログラムのバグの存在が実行前に発覚したわけだから、手間が省ける。もしコンパイルエラーにならない場合、実行した結果から、バグがあるかどうかを判断しなければならない。
読者の中には、せっかく書いたソースコードをコンパイルしたらコンパイルエラーが出たので、運が悪かったとか、失敗したとか、怒られてつらい気持ちになったなどと感じることがあるかもしれない。しかしそれは大違いだ。コンパイラーによって読者はプログラムを実行することなくバグが発見できたのだから、読者は運が良かった、大成功した、褒められて最高の気持ちになったと感じるべきなのだ。
さあ皆さんご一緒に、
熟練のプログラマーは自分の書いたコードがコンパイルエラーを出さずに一発でコンパイルが通った場合、逆に不安になるくらいだ。
もしバグがあるのにコンパイルエラーが出なければ、バグの存在に気が付かないまま、読者の書いたソフトウェアは広く世の中に使われ、10年後、20年後に最もバグが発見されてほしくない方法で発見されてしまうかもしれない。すなわち、セキュリティ上問題となる脆弱性という形での発覚だ。しかし安心してほしい。いま読者が出したコンパイルエラーによって、そのような悲しい未来の可能性は永久に排除されたのだ。コンパイルエラーはどんどん出すとよい。
コンパイルエラーの原因は2つ。
3つだった。コンパイルエラーの原因は3つ。
4つだった。ただ、3.と4.はめったにないから無視してよい。
文法エラーとは、C++というプログラミング言語の文法に従っていないエラーのことだ。これはC++として解釈できないので、当然エラーになる。
よくある文法エラーとしては、文末のセミコロンを打ち忘れたものがある。例えば以下のコードには間違いがある。
int main()
{
auto x = 1 + 1
auto y = x + 1 ;
}これをコンパイルすると以下のようにコンパイルエラーメッセージが出力される。
$ make
g++ -std=c++17 -Wall --pedantic-error -include all.h main.cpp -o program
main.cpp: In function ‘int main()’:
main.cpp:4:5: error: expected ‘,’ or ‘;’ before ‘auto’
auto y = x + 1 ;
^~~~
main.cpp:3:10: warning: unused variable ‘x’ [-Wunused-variable]
auto x = 1 + 1
^
Makefile:4: recipe for target 'program' failed
make: *** [program] Error 1
コンパイラーのメッセージを読み慣れていない読者はここで考えることを放棄してコンピューターの電源を落とし家を出て街を徘徊し夕日を見つめて人生、宇宙、すべてについての究極の質問への答えを模索してしまうことだろう。
しかし恐れるなかれ。コンパイラーのエラーメッセージを読み解くのは難しくない。
まず最初の2行を見てみよう。
$ make
g++ -std=c++17 -Wall --pedantic-error -include all.h main.cpp -o program
1行目はシェルにmakeを実行させるためのコマンド、2行目はmakeが実行したレシピの中身だ。これはコンパイラーによるメッセージではない。
3行目からはコンパイラーによる出力だ。
main.cpp: In function ‘int main()’:
コンパイラーはソースファイルmain.cppの中の、int main()という関数について、特に言うべきことがあると主張している。
言うべきこととは以下だ。
main.cpp:4:5: error: expected ‘,’ or ‘;’ before ‘auto’
auto y = x + 1 ;
^~~~
GCCというコンパイラーのエラーメッセージは、以下のフォーマットを採用している。
ソースファイル名:行番号:列番号: メッセージの種類: メッセージの内容
ここでのメッセージの種類はerror、つまりこのメッセージはエラーを伝えるものだ。
ソースファイル名はmain.cpp、つまりエラーはmain.cppの中にあるということだ。
行番号というのは、最初の行を1行目とし、改行ごとにインクリメントされていく。今回のソースファイルの場合、以下のようになる。
1 int main()
2 {
3 auto x = 1 + 1
4 auto y = x + 1 ;
5 }
もし読者が素晴らしいテキストエディターであるVimを使っている場合、:set nuすると行番号を表示できる。
その上でエラーメッセージの行番号を確認すると4とある。つまりコンパイラーは4行目に問題があると考えているわけだ。
4行目を確認してみよう。
auto y = x + 1 ;
何の問題もないように見える。さらにエラーメッセージを読んでみよう。
列番号が5となっている。列番号というのは、行頭からの文字数だ。最初の文字を1文字目とし、文字ごとにインクリメントされていく。
123456789...
auto y = x + 1 ;
4行目は空白文字を4つ使ってインデントしているので、autoのaの列番号は5だ。ここに問題があるのだろうか。何も問題がないように見える。
この謎を解くためには、メッセージの内容を読まなければならない。
expected ‘,’ or ‘;’ before ‘auto’
auto y = x + 1 ;
^~~
これは日本語に翻訳すると以下のようになる。
‘auto’の前に','か';'があるべき
auto y = x + 1 ;
^~~
1行目はエラー内容をテキストで表現したものだ。これによると、'auto'の前に','か';'があるべきとあるが、やはりまだわからない。
2行目は問題のある箇所のソースコードを部分的に抜粋したもので、3行目はそのソースコードの問題のある文字を視覚的にわかりやすく示しているものだ。
ともかく、コンパイラーの指示に従って'auto'の前に','を付けてみよう。
,auto y = x + 1 ;
これをコンパイルすると、また違ったエラーメッセージが表示される。
main.cpp: In function ‘int main()’:
main.cpp:4:6: error: expected unqualified-id before ‘auto’
,auto y = x + 1 ;
^~~~
では';'ならばどうか。
;auto y = x + 1 ;
これはコンパイルが通るようだ。
しかしなぜこれでコンパイルが通るのだろう。そのためには、コンパイラーが問題だとした行の1つ上の行を見る必要がある。
auto x = 1 + 1
auto y = x + 1 ;
コンパイラーにとって、改行は空白文字と同じくソースファイル中の意味のあるトークン(キーワードや名前や記号)を区切る文字でしかない。コンパイラーにとって、このコードは実質以下のように見えている。
auto x=1+1 auto y=x+1;
"1 auto"というのは文法エラーだ。なのでコンパイラーは文法エラーが発覚する最初の文字である'auto'の'a'を指摘したのだ。
人間にとって自然になるように修正すると、コンパイラーが指摘した行の1つ上の行の行末に';'を追加すべきだ。
auto x = 1 + 1 ;
auto y = x + 1 ;
さて、問題自体は解決したわけだが、残りのメッセージも見ていこう。
main.cpp:3:10: warning: unused variable ‘x’ [-Wunused-variable]
auto x = 1 + 1
これはコンパイラーによる警告メッセージだ。警告メッセージについて詳しくは、デバッグ:警告メッセージの章で解説する。
Makefile:4: recipe for target 'program' failed
make: *** [program] Error 1
これはGNU Makeによるメッセージだ。GCCがソースファイルを正しくコンパイルできず、実行が失敗したとエラーを返したので、レシピの実行が失敗したことを伝えるメッセージだ。
プログラムはどうやってエラーを通知するのか。main関数の戻り値によってだ。main関数は関数であるので、戻り値がある。main関数の戻り値はint型だ。
// 戻り値の型
int
// main関数の残りの部分
main() { }main関数が何も値を返さない場合、return 0したものとみなされる。main関数が0もしくはEXIT_SUCCESSを返した場合、プログラムの実行の成功を通知したことになる。
// 必ず実行が成功したと通知するプログラム
int main()
{
return 0 ;
}プログラムの実行が失敗した場合、main関数はEXIT_FAILUREを返すことでエラーを通知できる。
// 必ず実行が失敗したと通知するプログラム
int main()
{
return EXIT_FAILURE ;
}EXIT_SUCCESSとEXIT_FAILUREはマクロだ。
#define EXIT_SUCCESS
#define EXIT_FAILUREその中身はC++標準規格では規定されていない。どうしても値を知りたい場合は以下のプログラムを実行してみるとよい。
int main()
{
std::cout
<< "EXIT_SUCCESS: "s << EXIT_SUCCESS << "\n"s
<< "EXIT_FAILURE: "s << EXIT_FAILURE ;
}文法エラーというのは厄介なバグだ。というのも、コンパイラーというのは正しい文法のソースファイルを処理するように作られている。文法を間違えた場合、ソースファイル全体が正しくないということになる。コンパイラーは文法違反に遭遇した場合、なるべく人間がよく間違えそうなパターンをヒューリスティックに指摘することもしている。そのため、エラーメッセージに指摘された行番号と列番号は、必ずしも人間にとっての問題の箇所と一致しない。
もう1つ例を見てみよう。
int main()
{
// 引数を3つ取って足して返す関数
auto f = [](auto a, auto b, auto c)
{ return a + b + c ; } ;
std::cout << f(1+(2*3),4-5,6/(7-8))) ;
}GCCによるコンパイルエラーメッセージだけ抜粋すると以下のとおり。
main.cpp: In function ‘int main()’:
main.cpp:7:40: error: expected ‘;’ before ‘)’ token
std::cout << f(1+(2*3),4-5,6/(7-8))) ;
^
さてさっそく読んでみよう。すでに学んだように、GCCのメッセージのフォーマットは以下のとおりだ。
ソースファイル名:行番号:列番号: メッセージの種類: メッセージの内容
これに当てはめると、問題はソースファイルmain.cppの7行目の40列目にある。
エラーメッセージは、「';'がトークン')'の前にあるべき」だ。
トークン(token)というのは'std'とか'::'とか'cout'といったソースファイルの空白文字で区切られた最小の文字列の単位のことだ。
抜粋されたソースコードに示された問題の箇所、つまり7行目40列目にあるトークンは')'だ。この前に';'が必要とはどういうことだろう。
問題を探るため、7行目のトークンを詳しく分解してみよう。以下は7行目と同じソースコードだが、トークンをわかりやすく分解してある。
std::cout << // 標準出力
f // 関数名
( // 開き括弧
1+(2*3), // 第1引数
4-5, // 第2引数
6/(7-8) // 第3引数
) // 開き括弧に対応する閉じ括弧
) // ???
; // 終端文字
これを見ると、閉じ括弧が1つ多いことがわかる。
意味エラーとは、ソースファイルは文法的に正しいが、意味的に間違っているコンパイルエラーのことだ。
さっそく例を見ていこう。
int main()
{
auto x = 1.0 % 1.0 ;
}このコードをコンパイルすると出力されるエラーメッセージは以下のとおり。
main.cpp: In function ‘int main()’:
main.cpp:3:18: error: invalid operands of types ‘double’ and ‘double’ to binary ‘operator%’
auto x = 1.0 % 1.0 ;
~~~~^~~~~
問題の箇所は3行目の18列目、'%'だ。
エラーメッセージは、「二項 'operator%'に対して不適切なオペランドである型'double'と'double'」とある。
前の章を読み直すとわかるとおり、operator %は剰余を計算する演算子だが、この演算子にはdouble型を渡すことはできない。
このコードはどうだろう。
// 引数を1つ取る関数
void f( int x ) { }
int main()
{
// 引数を2つ渡す
f( 1, 2 ) ;
}このようなエラーメッセージになる。
main.cpp: In function ‘int main()’:
main.cpp:7:13: error: too many arguments to function ‘void f(int)’
f( 1, 2 ) ;
^
main.cpp:2:6: note: declared here
void f( int x ) { }
^
問題の箇所は7行目。「関数'void f(int)'に対して実引数が多すぎる」とある。関数fは引数を1つしか取らないのに、2つの引数を渡しているのがエラーの原因だ。
2つ目のメッセージはエラーではなくて、エラーを補足説明するための注記(note)メッセージだ。ここで言及している関数fとは、2行目に宣言されていることを説明してくれている。
意味エラーはときとしておぞましいほどのエラーメッセージを生成することがある。例えば以下の一見無害そうなコードだ。
int main()
{
"hello"s << 1 ;
}このコードは文法的に正しいが、意味的に間違っているコードだ。このコードをコンパイルすると膨大なエラーメッセージが出力される。しかも問題の行番号特定以外、大して役に立たない。
C++コンパイラーもソフトウェアであり、バグがある。コンパイラーにバグがある場合、正しいC++のソースファイルがコンパイルできないことがある。
読者がそのようなコンパイラーの秘孔を突くコードを書くことはまれだ。しかし、もしそのようなコードを偶然にも書いてしまった場合、GCCは、
gcc: internal compiler error: エラー内容
Please submit a full bug report,
with preprocessed source if appropriate.
See <ドキュメントへのファイルパス> for instructions.
のようなメッセージを出力する。
これはGCCのバグなので、見つけた読者は適切な方法でバグ報告をしよう。
さてC++の勉強に戻ろう。この章では条件分岐について学ぶ。
条件分岐とループについて学ぶ前に、まず複合文(compound statement)やブロック(block)と呼ばれている、複数の文をひとまとめにする文について学ばなければならない。
C++では文(statement)が実行される。文については詳しく説明すると長くなるが、';'で区切られたものが文だ。
int main()
{
// 文
auto x = 1 + 1 ;
// 文
std::cout << x ;
// 空文
// 実は空っぽの文も書ける。
;
}複数の文を{}で囲むことで、1つの文として扱うことができる。これを複合文という。
int main()
{
// 複合文開始
{
std::cout << "hello\n"s ;
std::cout << "hello\n"s ;
} // 複合文終了
// 別の複合文
{ std::cout << "world\n"s ; }
// 空の複合文
{ }
}複合文には';'はいらない。
int main()
{
// ;はいらない
{ }
// これは空の複合文に続いて
// 空文があるだけのコード
{ } ;
}複合文の中に複合文を書くこともできる。
int main()
{
{{{}}} ;
}関数の本体としての一番外側'{}'はこの複合文とは別のものだが、読者はまだ気にする必要はない。
複合文は複数の文をひとまとめにして、1つの文として扱えるようにするぐらいの意味しか持っていない。ただし、変数の見え方に影響する。変数は宣言された最も内側の複合文の中でしか使えない。
int main()
{
auto a = 0 ;
{
auto b = 0 ;
{
auto c = 0 ;
// cはここまで使える
}
// bはここまで使える
}
// aはここまで使える
}これを専門用語では変数の寿命とかブロックスコープ(block-scope)という。
内側のブロックスコープの変数が、外側のブロックスコープの変数と同じ名前を持っていた場合はエラーではない。外側の変数が内側の変数で隠される。
int main()
{
auto x = 0 ;
{
auto x = 1 ;
{
auto x = 2 ;
// 2
std::cout << x ;
}
// 1
std::cout << x ;
x = 42 ;
// 42
std::cout << x ;
}
// 0
std::cout << x ;
}慣れないうちは驚くかもしれないが、多くのプログラミング言語はこのような挙動になっているものだ。
すでに読者はさまざまな数値計算を学んだ。読者は12345 + 6789の答えや、8073 * 132 / 5の答えを計算できる上、この2つの答えをさらに掛け合わせた結果だって計算できる。
int main()
{
auto a = 12345 + 6789 ;
auto b = 8073 * 132 / 5 ;
auto sum = a + b ;
std::cout
<< "a=12345 + 6789=" << a << "\n"s
<< "b=8073 * 132 / 5=" << b << "\n"s
<< "a+b=" << sum << "\n"s ;
}なるほど、答えがわかった。ところで変数aと変数bはどちらが大きいのだろうか。大きい変数だけ出力したい。この場合は条件分岐を使う。
C++では条件分岐にif文を使う。
int main()
{
auto a = 12345 + 6789 ;
auto b = 8073 * 132 / 5 ;
if ( a < b )
{
// bが大きい
std::cout << b ;
}
else
{
// aが大きい
std::cout << a ;
}
}if文は以下のように書く。
if ( 条件 )
文1
else
文2条件が真(true)のときは文1が実行され、偽(false)のときは文2が実行される。
elseの部分は書かなくてもよい。
if ( 条件 )
文1
文2その場合、条件が真のときだけ文1が実行される。条件の真偽にかかわらず文2は実行される。
int main()
{
if ( 2 < 1 )
std::cout << "sentence 1.\n" ; // 文1
std::cout << "sentence 2.\n" ; // 文2
}この例では、2が1より小さい場合は文1が実行される。文2は必ず実行される。
条件次第で複数の文を実行したい場合、複合文を使う。
int main()
{
if ( 1 < 2 )
{
std::cout << "yes!\n" ;
std::cout << "yes!\n" ;
}
}条件とか真偽についてはとてもとても深い話があるのだが、その解説はあとの章に回すとして、まずは以下の比較演算子を覚えよう。
| 演算子 | 意味 |
|---|---|
a == b |
aはbと等しい |
a != b |
aはbと等しくない |
a < b |
aはbより小さい |
a <= b |
aはbより小さい、もしくは等しい |
a > b |
aはbより大きい |
a >= b |
aはbより大きい、もしくは等しい |
真(true)というのは、意味が真であるときだ。正しい、成り立つ、正解などと言い換えてもよい。それ以外の場合はすべて偽(false)だ。正しくない、成り立たない、不正解などと言い換えてもいい。
整数や浮動小数点数の場合、話は簡単だ。
int main()
{
// 1は2より小さいか?
if ( 1 < 2 )
{ // 真、お使いのコンピューターは正常です
std::cout << "Your computer works just fine.\n"s ;
}
else
{
// 偽、お使いのコンピューターには深刻な問題があります
std::cout << "Your computer has serious issues.\n"s ;
}
}文字列の場合、内容が同じであれば等しい。違うのであれば等しくない。
int main()
{
auto a = "dog"s ;
auto b = "dog"s ;
auto c = "cat"s ;
if ( a == b )
{
std::cout << "a == b\n"s ;
}
else
{
std::cout << "a != b\n" ;
}
if ( a == c )
{
std::cout << "a == c\n" ;
}
else
{
std::cout << "a != c\n" ;
}
}では文字列に大小はあるのだろうか。文字列に大小はある。
int main()
{
auto cat = "cat"s ;
auto dog = "dog"s ;
if ( cat < dog )
{ // 猫は小さい
std::cout << "cat is smaller.\n"s ;
}
else
{ // 犬は小さい
std::cout << "dog is smaller.\n"s ;
}
auto longcat = "longcat"s ;
if ( longcat > cat )
{ // longcatは長い
std::cout << "Longcat is Looong.\n"s ;
}
else
{
std::cout << "Longcat isn't that long. Sigh.\n"s ;
}
}実行して確かめてみよう。ほとんどの読者の実行環境では以下のようになるはずだ。ほとんどの、というのは、そうではない環境も存在するからだ。読者がそのような稀有な環境を使っている可能性はまずないだろうが。
cat is smaller.
Longcat is Looong.
なるほど。"cat"sは"dog"sよりも小さく(?)、"longcat"sは"cat"sよりも長い(大きい?)ようだ。なんだかよくわからない結果になった。
これはどういうことなのか。もっと簡単な文字列で試してみよう。
int main()
{
auto x = ""s ;
// aとbはどちらが小さいのだろうか?
if ( "a"s < "b"s )
{ x = "a"s ; }
else
{ x = "b"s ; }
// 小さい方の文字が出力される
std::cout << x ;
}これを実行するとaと出力される。すると"a"sは"b"sより小さいようだ。
もっと試してみよう。
int main()
{
auto x = ""s ;
if ( "aa"s < "ab"s )
{ x = "aa"s ; }
else
{ x = "ab"s ; }
// 小さい文字列が出力される
std::cout << x ;
}これを実行すると、aaと出力される。すると"aa"sは"ab"sより小さいことになる。
文字列の大小比較は文字単位で行われる。まず最初の文字が大小比較される。もし等しい場合は、次の文字が大小比較される。等しくない最初の文字の結果が、文字列の大小比較の結果となる。
if文の中で書く条件(condition)は、条件式(conditional expression)とも呼ばれている式(expression)の一種だ。式というのは例えば"1+1"のようなものだ。式は文の中に書くことができ、これを式文(expression statement)という。
int main()
{
1 + 1 ; // 式文
}"a==b"や"a\<b"のような条件も式なので、文として書くことができる。
int main()
{
1 == 1 ;
1 < 2 ;
}C++では多くの式には型がある。たとえば"123"はint型で、"123+4"もint型だ。
int main()
{
auto a = 123 ; // int
auto b = a + 4 ; // int
auto c = 1.0 ; // double
auto d = "hello"s ; // std::string
}とすると、"1==2"や"3!=3"のような条件式にも型があるのではないか。型があるのであれば変数に入れられるはずだ。試してみよう。
int main()
{
if ( 1 == 1 )
{ std::cout << "1 == 1 is true.\n"s ; }
else
{ std::cout << "1 == 1 is false.\n"s ; }
auto x = 1 == 1 ;
if ( x )
{ std::cout << "1 == 1 is true.\n"s ; }
else
{ std::cout << "1 == 1 is false.\n"s ; }
}"if(x)"は"if(1==1)"と書いた場合と同じように動く。
変数に入れられるのであれば出力もできるのではないだろうか。試してみよう。
int main()
{
auto a = 1 == 1 ; // 正しい
auto b = 1 != 1 ; // 間違い
std::cout << a << "\n"s << b ;
}1
0
なるほど、条件が正しい場合"1"になり、条件が間違っている場合"0"になるようだ。
ではif文の中に1や0を入れたらどうなるのだろうか。
// 条件が正しい値だけ出力される。
int main()
{
if ( 1 ) std::cout << "1\n"s ;
if ( 0 ) std::cout << "0\n"s ;
if ( 123 ) std::cout << "123\n"s ;
if ( -1 ) std::cout << "-1\n"s ;
}実行結果は以下のようになる。
1
123
-1
この結果を見ると、条件として1, 123, -1は正しく、0は間違っているということになる。ますます訳がわからなくなってきた。
そろそろ種明かしをしよう。条件式の結果は、bool型という特別な型を持っている。
int main()
{
auto a = 1 == 1 ; // bool型
bool A = 1 == 1 ; // 型を書いてもよい
}int型の変数には整数の値が入る。double型の変数には浮動小数点数の値が入る。std::string型の変数には文字列の値が入る。
すると、bool型の変数にはbool型の値が入る。
bool型には2つの値がある。条件が正しいことを意味するtrueと、条件が間違っていることを意味するfalseだ。
int main()
{
bool correct = true ;
bool wrong = false ;
}bool型にこれ以外の値は存在しない。
bool型の値を正しく出力するには、std::boolalphaを出力する。
int main()
{
std::cout << std::boolalpha ;
std::cout << true << "\n"s << false ;
}true
false
std::boolalpha自体は何も出力をしない。一度std::boolalphaを出力すると、それ以降のbool値がtrue/falseで出力されるようになる。
元に戻すにはstd::noboolalphaを使う。
int main()
{
std::cout << std::boolalpha ;
std::cout << true << false ;
std::cout << std::noboolalpha ;
std::cout << true << false ;
}以下のように出力される。
truefalse10
すでに学んだ比較演算子は、正しい場合にbool型の値trueを、間違っている場合にbool型の値falseを返す。
int main()
{
// true
bool a = 1 == 1 ;
// false
bool b = 1 != 1 ;
// true
bool c = 1 < 2 ;
// false
bool d = 1 > 2 ;
}先に説明したif文の条件が「正しい」というのはtrueのことで、「間違っている」というのはfalseのことだ。
int main()
{
// 出力される
if ( true )
std::cout << "true\n"s ;
// 出力されない。
if ( false )
std::cout << "false\n"s ;
}bool型にはいくつかの演算が用意されている。
"!a"はaがtrueの場合falseに、falseの場合trueになる。
int main()
{
std::cout << std::boolalpha ;
// false
std::cout << !true << "\n"s ;
// true
std::cout << !false << "\n"s ;
}論理否定演算子を使うと、falseのときのみ実行されてほしい条件分岐が書きやすくなる。
// ロケットが発射可能かどうかを返す関数
bool is_rocket_ready_to_launch()
{
// まだだよ
return false ;
}
int main()
{
// ロケットが発射可能ではないときに実行される
if ( !is_rocket_ready_to_launch() )
{ // もうしばらくそのままでお待ちください
std::cout << "Standby...\n" ;
}
}この例では、ロケットが発射可能でない場合のみ、待つようにアナウンスする。
同じように、trueのときに実行されてほしくない条件分岐も書ける。
// ロケットが発射可能かどうかを返す関数
bool is_rocket_ready_to_launch()
{
// もういいよ
return true ;
}
int main()
{
// ロケットが発射可能なときに実行される
if ( !is_rocket_ready_to_launch() )
{ // カウントダウン
std::cout << "3...2...1...Hallelujah!\n"s ;
}
}この2つの例では、ロケットの状態が実行すべき条件ではないので、正しく何も出力されない。
bool型の値の同値比較はわかりやすい。trueはtrueと等しく、falseはfalseと等しく、trueとfalseは等しくない。
int main()
{
std::cout << std::boolalpha ;
auto print = [](auto b)
{ std::cout << b << "\n"s ; } ;
print( true == true ) ; // true
print( true == false ) ; // false
print( false == true ) ; // false
print( false == false ) ; // true
print( true != true ) ; // false
print( true != false ) ; // true
print( false != true ) ; // true
print( false != false ) ; // false
}比較演算子の結果はbool値になるということを覚えているだろうか。"1 \< 2"はtrueになり、"1 \> 2"はfalseになる。
bool値同士も同値比較ができるということは、"(1 \< 2) == true"のように書くことも可能だということだ。
int main()
{
bool b = (1 < 2) == true ;
}"(1\<2)"はtrueなので、"(1\<2)==true"は"true==true"と同じ意味になる。この結果はもちろん"true"だ。
"a && b"はaとbがともにtrueのときにtrueとなる。それ以外の場合はfalseとなる。これを論理積という。
表にまとめると以下のようになる。
| 式 | 結果 |
|---|---|
false && false |
false |
false && true |
false |
true && false |
false |
true && true |
true |
さっそく確かめてみよう。
int main()
{
std::cout << std::boolalpha ;
auto print = []( auto b )
{ std::cout << b << "\n"s ; } ;
print( false && false ) ; // false
print( false && true ) ; // false
print( true && false ) ; // false
print( true && true ) ; // true
}論理積は、「AかつB」を表現するのに使える。
例えば、人間の体温が平熱かどうかを判断するプログラムを書くとする。36.1℃以上、37.2℃以下を平熱とすると、if文を使って以下のように書くことができる。
int main()
{
// 体温
double temperature = 36.6 ;
// 36.1度以上
if ( temperature >= 36.1 )
if ( temperature <= 37.2 )
{ std::cout << "Good.\n"s ; }
else
{ std::cout << "Bad.\n"s ; }
else
{ std::cout << "Bad.\n"s ; }
}このコードは、operator &&を使えば簡潔に書ける。
int main()
{
double temperature = 36.6 ;
if ( ( temperature >= 36.1 ) && ( temperature <= 37.2 ) )
{ std::cout << "Good.\n"s ; }
else
{ std::cout << "Bad.\n"s ; }
}"a || b"はaとbがともにfalseのときにfalseとなる。それ以外の場合はtrueとなる。これを論理和という。
表にまとめると以下のようになる。
| 式 | 結果 |
|---|---|
false || false |
false |
false || true |
true |
true || false |
true |
true || true |
true |
さっそく確かめてみよう。
int main()
{
std::cout << std::boolalpha ;
auto print = []( auto b )
{ std::cout << b << "\n"s ; } ;
print( false || false ) ; // false
print( false || true ) ; // true
print( true || false ) ; // true
print( true || true ) ; // true
}論理和は、「AもしくはB」を表現するのに使える。
例えば、ある遊園地の乗り物には安全上の理由で身長が1.1m未満、あるいは1.9mを超える人は乗れないものとする。この場合、乗り物に乗れる身長かどうかを確かめるコードは、if文を使うと以下のようになる。
int main()
{
double height = 1.3 ;
if ( height < 1.1 )
{ std::cout << "No."s ; }
else if ( height > 1.9 )
{ std::cout << "No."s ; }
else
{ std::cout << "Yes."s ; }
}論理和を使うと以下のように簡潔に書ける。
int main()
{
double height = 1.3 ;
if ( ( height < 1.1 ) || ( height > 1.9 ) )
{ std::cout << "No."s ; }
else
{ std::cout << "Yes."s ; }
}論理積と論理和は短絡評価と呼ばれる特殊な評価が行われる。これは、左から右に最小限の評価をするという意味だ。
論理積では、“a && b”とある場合、aとbがともにtrueである場合のみ、結果はtrueになる。もし、aがfalseであった場合、bの結果如何にかかわらず結果はfalseとなるので、bは評価されない。
int main()
{
auto a = []()
{
std::cout << "a\n"s ;
return false ;
} ;
auto b = []()
{
std::cout << "b\n"s ;
return true ;
} ;
bool c = a() && b() ;
std::cout << std::boolalpha << c ;
}これを実行すると以下のようになる。
a
false
関数呼び出し"a()"の結果はfalseなので、"b()"は評価されない。評価されないということは関数呼び出しが行われず、当然標準出力も行われない。
同様に、論理和では、"a || b"とある場合、aとbのどちらか片方でもtrueであれば、結果はtrueとなる。もし、aがtrueであった場合、bの結果如何にかかわらず結果はtrueとなるので、bは評価されない。
int main()
{
auto a = []()
{
std::cout << "a\n"s ;
return true ;
} ;
auto b = []()
{
std::cout << "b\n"s ;
return false ;
} ;
bool c = a() || b() ;
std::cout << std::boolalpha << c ;
}結果、
a
true
"b()"が評価されていないことがわかる。
bool型の値と演算はこれで全部だ。値はtrue/falseの2つのみ。演算は==, !=, !と&&と||の5つだけだ。
読者の中には納得のいかないものもいるだろう。ちょっと待ってもらいたい。boolの大小比較できないのだろうか。boolの四則演算はできないのか。"if(123)"などと書けてしまうのは何なのか。
好奇心旺盛な読者は本書の解説を待たずしてすでに自分でいろいろとコードを書いて試してしまっていることだろう。
boolの大小比較はどうなるのだろうか。
int main()
{
std::cout << std::boolalpha ;
bool b = true < false ;
std::cout << b ;
}このコードを実行すると、出力は"false"だ。"true \< false"の結果が"false"だということは、trueはfalseより大きいということになる。
四則演算はどうか?
int main()
{
auto print = [](auto x)
{ std::cout << x << "\n"s ; } ;
print( true + true ) ;
print( true + false ) ;
print( false + true ) ;
print( false + false ) ;
}結果、
2
1
1
0
不思議な結果だ。"true+true"は"2"、"true+false"は"1"、"false+false"は"0"。これはtrueが1でfalseが0ならば納得のいく結果だ。大小比較の結果としても矛盾していない。
すでに見たように、std::boolalphaを出力していない状態でboolを出力するとtrueが1、falseが0となる。
int main()
{
std::cout << true << false ;
}結果、
10
これはbool型と整数型が変換されているのだ。
異なる型の値が変換されるというのは、すでに例がある。整数型と浮動小数点数型だ。
int main()
{
// 3
int i = 3.14 ;
std::cout << i << "\n"s ;
// 123.0
double d = 123 ;
std::cout << d << "\n"s ;
}浮動小数点数型は整数型に変換できる。その際に小数部は切り捨てられる。整数型は浮動小数点数型に変換できる。小数部はない。
これと同じように、bool型も整数型と変換ができる。
bool型のtrueを整数型に変換すると1になる。falseは0になる。
int main()
{
// 1
int True = true ;
// 0
int False = false ;
}同様に、整数型のゼロをbool型に変換するとfalseになる。非ゼロはtrueになる。
int main()
{
// false
bool Zero = 0 ;
// すべてtrue
bool One = 1 ;
bool minus_one = -1 ;
bool OneTwoThree = 123 ;
}したがって、"if (0)"は"if (false)"と等しく、"if (1)"や"if(-1)"など非ゼロな値は"if (true)"と等しい。
int main()
{
// 出力されない
if ( 0 )
std::cout << "No output.\n"s ;
// 出力される
if ( 1 )
std::cout << "Output.\n"s ;
}大小比較は単にboolを整数に変換した結果を比較しているだけだ。"true \< false"は"1 \< 0"と書くのと同じだ。
int main()
{
std::cout << std::boolalpha ;
// 1 < 0
std::cout << (true < false) ;
}同様に四則演算もbool型を整数型に変換した上で計算をしているだけだ。"true + true"は"1 + 1"と書くのと同じだ。
int main()
{
// 1 + 1
std::cout << (true + true) ;
}C++では、bool型と整数型の変換は暗黙に行われてしまうので注意が必要だ。
やれやれ、条件分岐は難しかった。この辺でもう一度ひと休みして、息抜きとしてデバッグの話をしよう。今回はコンパイラーの警告メッセージ(warning messages)についてだ。
コンパイラーはソースコードに文法エラーや意味エラーがあると、エラーメッセージを出すことはすでに学んだ。
コンパイラーがエラーメッセージを出さなかったとき、コンパイラーはソースコードには文法エラーや意味エラーを発見できず、コンパイラーは意味のあるプログラムを生成することができたということを意味する。しかし、コンパイルが通って実行可能なプログラムが生成できたからといって、プログラムにバグがないことは保証できない。
たとえば、変数xとyを足して出力するプログラムを考える。
int main()
{
auto x = 1 ;
auto y = 2 ;
std::cout << x + x ;
}このプログラムにはバグがある。プログラムの仕様は変数xとyを足すはずだったが変数xとxを足してしまっている。
コンパイラーはこのソースコードをコンパイルエラーにはしない。なぜならば上のコードは文法的に正しく、意味的にも正しいコードだからだ。
警告メッセージはこのような疑わしいコードについて、エラーとまではいかないまでも、文字どおり警告を出す機能だ。例えば上のコードをGCCでコンパイルすると以下のような警告メッセージを出す。
$ make
g++ -std=c++17 -Wall --pedantic-error -include all.h main.cpp -o program
main.cpp: In function ‘int main()’:
main.cpp:5:10: warning: unused variable ‘y’ [-Wunused-variable]
auto y = 2 ;
^
すでに説明したように、GCCのメッセージは
ソースファイル名:行番号:列番号:メッセージの種類:メッセージの内容
というフォーマットを取る。
このメッセージのフォーマットに照らし合わせると、このメッセージはソースファイルmain.cppの5行目の10列目について何かを警告している。警告はメッセージの種類としてwarningが使われる。
警告メッセージの内容は、「未使用の変数'y' [-Wunused-variable]」だ。コード中で'y'という名前の変数を宣言しているにもかかわらず、使っている場所がない。使わない変数を宣言するのはバグの可能性が高いので警告しているのだ。
[-Wunused-variable]というのはGCCに与えるこの警告を有効にするためのオプション名だ。GCCに-Wunused-variableというオプションを与えると、未使用の変数を警告するようになる。
$ g++ -Wunused-variable その他のオプション
今回は-Wallというすべての警告を有効にするオプションを使っているので、このオプションを使う必要はない。
もう1つ例を出そう。以下のソースコードは変数xの値が123と等しいかどうかを調べるものだ。
int main()
{
// xの値は0
auto x = 0 ;
// xが123と等しいかどうか比較する
if ( x = 123 )
std::cout << "x is 123.\n"s ;
else
std::cout << "x is NOT 123.\n"s ;
}これを実行すると、"x is 123.\n"と出力される。しかし、変数xの値は0のはずだ。なぜか0と123は等しいと判断されてしまった。いったいどういうことだろう。
この謎は警告メッセージを読むと解ける。
g++ -std=c++17 -Wall --pedantic-error -include all.h main.cpp -o program
main.cpp: In function ‘int main()’:
main.cpp:5:12: warning: suggest parentheses around assignment used as truth value [-Wparentheses]
if ( x = 123 )
~~^~~~~
main.cppの5行目の12列目、「真偽値として使われている代入は括弧で囲むべき」とある。これはいったいどういうことか。よく見てみると、演算子が同値比較に使う==ではなく、=だ。=は代入演算子だ。
int main()
{
auto x = 0 ;
// 代入
// xの値は1
x = 1 ;
// 同値比較
x == 1 ;
}実はif文の条件にはあらゆる式を書くことができる。代入というのは、実は代入式という式なので、if文の中にも書くことができる。その場合、式の結果の値は代入される変数の値になる。
そして思い出してほしいのは、整数型はbool型に変換されるということだ。0はfalse、非ゼロはtrueだ。
int main()
{
auto x = 0 ;
// 1はtrue
bool b1 = x = 1 ;
if ( x = 1 ) ;
// 0はfalse
bool b0 = x = 0 ;
if ( x = 0 ) ;
}つまり、"if(x=1)"というのは、"if(1)"と書くのと同じで、これは最終的に、"if(true)"と同じ意味になる。
警告メッセージの「括弧で囲むべき」というのは、括弧で囲んだ場合、この警告メッセージは出なくなるからだ。
int main()
{
auto x = 0 ;
if ( (x = 0) )
std::cout << "x is 123.\n"s ;
else
std::cout << "x is NOT 123.\n"s ;
}このコードをコンパイルしても警告メッセージは出ない。
わざわざ括弧で囲むということは、ちゃんと代入を意図して使っていることがわかっていると意思表示したことになり、結果として警告メッセージはなくなる。
この警告メッセージ単体を有効にするオプションは-Wparenthesesだ。
警告メッセージは万能ではない。ときにはまったく問題ないコードに対して警告メッセージが出たりする。これは仕方がないことだ。というのもコンパイラーはソースコード中に表現されていない、人間の脳内にある意図を読むことはできないからだ。ただし、警告メッセージにはひと通り目を通して、それが問題ない誤検知であるかどうかを確認することは重要だ。
ここまで学んできた範囲でも、かなりのプログラムが書けるようになってきた。試しにちょっとプログラムを書いてみよう。
最近肥満が気になる読者は、肥満度を把握するためにBMI(Body Mass Index)を計算して出力するプログラムを書くことにした。
BMIの計算は以下のとおり。
\[ BMI = \frac{体重_{kg}}{身長^2_{m}} \]
本書をここまで読み進めた読者ならば、このようなプログラムは簡単に書けるだろう。計算は小数点以下の値を扱う必要があるために、変数は浮動小数点数型(double)にする。掛け算はoperator *で、割り算はoperator /だ。出力にはstd::coutを使う。
int main()
{
// 身長1.63m
double height = 1.63 ;
// 体重73kg
double mass = 73.0 ;
// BMIの計算
double bmi = mass / (height*height) ;
// BMIの出力
std::cout << "BMI="s << bmi << "\n"s ;
}結果は"27.4756"となった。これだけでは太っているのか痩せているのかよくわからない。調べてみると、BMIの数値と肥満との関係は以下の表のとおりになるそうだ。
| BMI | 状態 |
|---|---|
| 18.5未満 | 痩せすぎ(Underweight) |
| 18.5以上、25未満 | 普通(Normal) |
| 25以上、30未満 | 太り気味(Overweight) |
| 30以上 | 肥満(Obese) |
ではさっそく、この表のようにBMIから肥満状態も出力してくれるように、プログラムを書き換えよう。
int main()
{
// 身長1.63m
double height = 1.63 ;
// 体重73kg
double mass = 73.0 ;
// BMIの計算
double bmi = mass / (height*height) ;
// BMIの出力
std::cout << "BMI="s << bmi << "\n"s ;
// 状態の判定をする関数
auto status = []( double bmi )
{
if ( bmi < 18.5 )
return "Underweight.\n"s ;
else if ( bmi < 25.0 )
return "Normal.\n"s ;
else if ( bmi < 30.0 )
return "Overweight.\n"s ;
else
return "Obese."s ;
} ;
// 状態の出力
std::cout << status(bmi) ;
}ここまで問題なく読むことができただろうか。ここまでのコードはすべて、本書を始めから読めば理解できる機能しか使っていない。わからない場合、この先に進む前に本書をもう一度始めから読み直すべきだろう。
上のプログラムには実用にする上で1つ問題がある。身長と体重の値を変えたい場合、ソースコードを書き換えてコンパイルしなければならないのだ。
例えば読者の身長が1.8mで体重が80kgの場合、以下のように書き換えなければならない。
int main()
{
// 身長1.63m
double height = 1.80 ;
// 体重73kg
double mass = 80.0 ;
// BMIの計算
double bmi = mass / (height*height) ;
// BMIの出力
std::cout << "BMI="s << bmi << "\n"s ;
}すると今度は身長が1.48mで体重が48kgの人がやってきて私のBMIも計測しろとうるさい。しかも昨日と今日で体重が変わったからどちらも計測したいと言い出す始末。
こういうとき、プログラムのコンパイル時ではなく、実行時に値を入力できたならば、いちいちプログラムをコンパイルし直す必要がなくなる。
入力にはstd::cinを使う。std::coutは標準出力を扱うのに対し、std::cinは標準入力を扱う。std::coutがoperator <<を使って値を出力したのに対し、std::cinはoperator >>を使って値を変数に入れる。
int main()
{
// 入力を受け取るための変数
std::string x{} ;
// 変数に入力を受け取る
std::cin >> x ;
// 入力された値を出力
std::cout << x ;
}実行結果、
$ make run
hello
hello
標準入力はデフォルトでは、プログラムを実行したユーザーがターミナルから入力する。上の実行結果の2行目は、ユーザーの入力だ。
std::cinは入力された文字列を変数に入れる。入力は空白文字や改行で区切られる。そのため、空白で区切られた文字列を渡すと、以下のようになる。
$ make run
hello world
hello
入力は複数取ることができる。
int main()
{
std::string x{} ;
std::string y{} ;
std::cin >> x >> y ;
std::cout << x << y ;
}実行結果、
$ make run
hello world
helloworld
空白文字は文字列の区切り文字として認識されるので変数x, yには入らない。
std::cinでは文字列のほかにも整数や浮動小数点数、boolを入力として得ることができる。
int main()
{
// 整数
int i{} ;
std::cin >> i ;
// 浮動小数点数
double d{} ;
std::cin >> d ;
}実行結果、
$ make run
123 1.23
数値はデフォルトで10進数として扱われる。
boolの入力には注意が必要だ。普通に書くと、ゼロがfalse, 非ゼロがtrueとして扱われる。
int main()
{
bool b{} ;
std::cin >> b ;
std::cout << std::boolalpha << b << "\n"s ;
}実行結果、
$ make run
1
true
$ make run
0
false
$ make run
123
true
$ make run
-1
true
"true", "false"という文字列でtrue, falseの入力をしたい場合、std::cinにstd::boolalphaを「入力」させる。
int main()
{
// bool型
bool b{} ;
std::cin >> std::boolalpha >> b ;
std::cout << std::boolalpha << b ;
}実行結果
$ make run
true
true
$ make run
false
false
std::boolalphaを入出力するというのは、実際には何も入出力しないので奇妙に見えるが、そういう設計になっているので仕方がない。
では標準入力を学んだので、さっそくBMIを計算するプログラムを標準入力に対応させよう。
int main()
{
// 身長の入力
double height{} ;
std::cout << "height(m)>" ;
std::cin >> height ;
// 体重の入力
double mass{} ;
std::cout << "mass(kg)>" ;
std::cin >> mass ;
double bmi = mass / (height*height) ;
std::cout << "BMI=" << bmi << "\n"s ;
}上出来だ。
標準入出力が扱えるようになれば、もう自分の好きなプログラムを書くことができる。プログラムというのはけっきょく、入力を得て、処理して、出力するだけのものだからだ。入力はテキストだったりグラフィックだったり何らかの特殊なデバイスだったりするが、基本は変わらない。
たとえば読者はまだC++でファイルを読み書きする方法を知らないが、標準入出力さえ使えれば、ファイルの読み書きはリダイレクトを使うだけでできるのだ。
int main()
{
std::cout << "hello" ;
}これは"hello"と標準出力するだけの簡単なプログラムだ。このプログラムをコンパイルしたプログラム名をprogramとしよう。標準出力の出力先はデフォルトで、ユーザーのターミナルになる。
$ ./program
hello
リダイレクトを使えば、この出力先をファイルにできる。リダイレクトを使うには"プログラム \> ファイル名"とする。
$ ./program > hello.txt
$ cat hello.txt
hello
ファイルへの簡単な書き込みは、リダイレクトを使うことであとから簡単に実現可能だ。
リダイレクトはファイルの読み込みにも使える。例えば先ほどのBMIを計算するプログラムを用意しよう。
// bmi
int main()
{
double height{ } ;
double mass { } ;
std::cin >> height >> mass ;
std::cout << mass / (height*height) ;
}このプログラム名をbmiとして、通常どおり実行すると以下のようになる。
$ ./bmi
1.63
73
27.4756
このうち、1.63と73はユーザーによる入力だ。これを毎回手で入力するのではなく、ファイルから入力することができる。つまり以下のようなファイルを用意して、
1.63
73
このファイルを例えば、"bodymass.txt"とする。手で入力する代わりに、このファイルを入力として使いたい。これにはリダイレクトとして"プログラム名 \< ファイル名"とする。
$ ./bmi < bodymass.txt
27.4756
リダイレクトの入出力を組み合わせることも可能だ。
$ cat bodymass.txt
1.63
73
$ ./bmi < bodymass.txt > index.txt
$ cat index.txt
27.4756
もちろん、このようなファイルの読み書きは簡易的なものだが、かなりの処理がこの程度のファイル操作でも行えるのだ。
プログラムが出力した結果をさらに入力にすることだってできる。
例えば、先ほどのプログラムbmiに入力するファイルbodymass.txtの身長の単位がメートルではなくセンチメートルだったとしよう。
163
73
この場合、プログラムbmiを書き換えて対処することもできるが、プログラムに入力させる前にファイルを読み込み、書き換えて出力し、その出力を入力とすることもできる。
まず、身長の単位をセンチメートルからメートルに直すプログラムを書く。
// convert
int main()
{
double height{} ;
double mass{} ;
std::cin >> height >> mass ;
// 身長をセンチメートルからメートルに直す
// 体重はそのままでよい
std::cout << height/100.0 << "\n"s << mass ;
}このプログラムをconvertと名付け、さっそく使ってみよう。
$ ./convert
163
73
1.63
73
身長の単位がセンチメートルからメートルに正しく直されている。
これをリダイレクトで使うとこうなる。
$ ./convert < bodymass.txt > fixed_bodymass.txt
$ ./bmi < fixed_bodymass.txt
27.4756
しかしこれではファイルが増えて面倒だ。この場合、パイプを使うとスッキリと書ける。
パイプはプログラムの標準出力をプログラムの標準入力とするの使い方は、"プログラム名 | プログラム名"だ。
$ ./convert < bodymass.txt | ./bmi
27.4756
ところで、すでに何度か説明なしで使っているが、POSIX規格を満たすOSにはcatというプログラムが標準で入っている。cat ファイル名は指定したファイル名の内容を標準出力する。標準出力はパイプで標準入力にできる。
$ cat bodymass.txt | ./convert | ./bmi
27.4756
現代のプログラミングというのは、すでに存在するプログラムを組み合わせて作るものだ。もし、自分の必要とする処理がすでに実装されているのであれば、自分で書く必要はない。
例えば、読者はまだカレントディレクトリー下のファイルの一覧を列挙する方法を知らない。しかしPOSIX規格を満たすOSにはlsというカレントディレクトリー下のファイルの一覧を列挙するプログラムが存在する。これを先ほどまでBMIの計算などの作業をしていたディレクトリー下で実行してみよう。
$ ls
all.h all.h.gch bmi bodymass.txt convert data main.cpp Makefile program
ファイルの一覧が列挙される。そしてこれはプログラムlsによる標準出力だ。標準出力ということは、リダイレクトしてファイルに書き込んだり、パイプで別のプログラムに渡したりできるということだ。
$ ls > files.txt
$ ls | ./program
標準入出力が扱えれば、ネットワークごしにWebサイトをダウンロードすることもできる。これにはほとんどのGNU/LinuxベースのOSに入っているcurlというプログラムを使う。
$ curl https://example.com
プログラムcurlは指定されたURLからデータをダウンロードして、標準出力する。標準出力するということは、パイプによって標準入力にできるということだ。
$ curl https://example.com | ./program
読者はC++でネットワークアクセスする方法を知らないが、すでにネットワークアクセスは可能になった。
ほかにも便利なプログラムはたくさんある。プログラミングの学び始めはできることが少なくて退屈になりがちだが、読者はもうファイルの読み書きやネットワークアクセスまでできるようになったのだから、退屈はしないはずだ。
さて、ここまでで変数や関数、標準入出力といったプログラミングの基礎的な概念を教えてきた。あと1つでプログラミングに必要な基礎的な概念はすべて説明し終わる。ループだ。
C++では、プログラムは書いた順番に実行される。これを逐次実行という。
int main()
{
std::cout << 1 ;
std::cout << 2 ;
std::cout << 3 ;
}実行結果、
123
この実行結果が"123"以外の結果になることはない。C++ではプログラムは書かれた順番に実行されるからだ。
条件分岐は、プログラムの実行を条件付きで行うことができる。
int main()
{
std::cout << 1 ;
if ( false )
std::cout << 2 ;
std::cout << 3 ;
if ( true )
std::cout << 4 ;
else
std::cout << 5 ;
}実行結果、
134
条件分岐によって、プログラムの一部を実行しないということが可能になる。
ここでは繰り返し(ループ)の基礎的な仕組みを理解するために、最も原始的で最も使いづらい繰り返しの機能であるgoto文を学ぶ。goto文で実用的な繰り返し処理をするのは面倒だが、恐れることはない。より簡単な方法もすぐに説明するからだ。なぜ本書でgoto文を先に教えるかというと、あらゆる繰り返しは、けっきょくのところif文とgoto文へのシンタックスシュガーにすぎないからだ。goto文を学ぶことにより、繰り返しを恐れることなく使う本物のプログラマーになれる。
"hello\n"と3回出力するプログラムはどうやって書くのだろうか。"hello\n"を1回出力するプログラムの書き方はすでにわかっているので、同じ文を3回書けばよい。
// 1回"hello\n"を出力する関数
void hello()
{
std::cout << "hello\n"s ;
}
int main()
{
hello() ;
hello() ;
hello() ;
}10回出力する場合はどうするのだろう。10回書けばよい。コードは省略する。
では100回出力する場合はどうするのだろう。100回書くのだろうか。100回も同じコードを書くのはとても面倒だ。読者がVimのような優秀なテキストエディターを使っていない限り100回も同じコードを間違えずに書くことは不可能だろう。Vimならば1回書いたあとにノーマルモードで"100."するだけで100回書ける。
実際のところ、100回だろうが、1000回だろうが、あらかじめ回数がコンパイル時に決まっているのであれば、その回数だけ同じ処理を書くことで実現可能だ。
しかし、プログラムを外部から強制的に停止させるまで、無限に出力し続けるプログラムはどう書けばいいのだろうか。そういった停止しないプログラムを外部から強制的に停止させるにはCtrl-Cを使う。
以下はそのようなプログラムの実行例だ。
$ make run
hello
hello
hello
hello
...
[Ctrl-Cを押す]
goto文は指定したラベルに実行を移す機能だ。
ラベル名 : 文
goto ラベル名 ;int main()
{
std::cout << 1 ;
// ラベルskipまで飛ぶ
goto skip ;
std::cout << 2 ;
// ラベルskip
skip :
std::cout << 3 ;
}これを実行すると以下のようになる。
13
2を出力すべき文の実行が飛ばされていることがわかる。
これだけだと"if (false)"と同じように見えるが、goto文はソースコードの上に飛ぶこともできるのだ。
void hello()
{
std::cout << "hello\n"s ;
}
int main()
{
loop :
hello() ;
goto loop ;
}これは"hello\n"を無限に出力するプログラムだ。
このプログラムを実行すると、
helloが呼ばれるgoto文でラベルloopまで飛ぶという処理を行う。
ひたすら同じ文字列を出力し続けるだけのプログラムというのも味気ない。もっと面白くてためになるプログラムを作ろう。例えば、ユーザーから入力された数値を合計し続けるプログラムはどうだろう。
いまから作るプログラムを実行すると以下のようになる。
$ make run
> 10
10
> 5
15
> 999
1014
> -234
780
このプログラムは、
"\>"と表示してユーザーから整数値を入力という動作を繰り返す。先ほど学んだ無限ループと同じだ。
さっそく作っていこう。
int input()
{
std::cout << ">"s ;
int x {} ;
std::cin >> x ;
return x ;
}
int main()
{
int sum = 0 ;
loop :
sum = sum + input() ;
std::cout << sum << "\n"s ;
goto loop ;
}関数inputは"\>"を表示してユーザーからの入力を得て戻り値として返すだけの関数だ。
"sum = sum + input()"は、変数sumに新しい値を代入するもので、その代入する値というのは、代入する前の変数sumの値と関数inputの戻り値を足した値だ。
このような変数xに何らかの値nを足した結果を元の変数xに代入するという処理はとても多く使われるので、C++では"x = x + n"を意味する省略記法"x += n"がある。
int main()
{
int x = 1 ;
int n = 5 ;
x = x + n ; // 6
x += n ; // 11
}さて、本題に戻ろう。上のプログラムは動く。しかし、プログラムを停止するにはCtrl-Cを押すしかない。できればプログラム自ら終了してもらいたいものだ。
そこで、ユーザーが0を入力したときはプログラムを終了するようにしよう。
int input()
{
std::cout << ">"s ;
int x {} ;
std::cin >> x ;
return x ;
}
int main()
{
int sum = 0 ;
loop :
// 一度入力を変数に代入
int x = input() ;
// 変数xが0でない場合
if ( x != 0 )
{// 実行
sum = sum + x ;
std::cout << sum << "\n"s ;
goto loop ;
}
// x == 0の場合、ここに実行が移る
// main関数の最後なのでプログラムが終了
}うまくいった。このループは、ユーザーが0を入力した場合に繰り返しを終了する、条件付きのループだ。
最後に紹介するループは、インデックスループだ。\(n\)回"hello\n"sを出力するプログラムを書こう。問題は、この\(n\)はコンパイル時には与えられず、実行時にユーザーからの入力で与えられる。
// n回出力する関数の宣言
void hello_n( int n ) ;
int main()
{
// ユーザーからの入力
int n {} ;
std::cin >> n ;
// n回出力
hello_n( n ) ;
}このコードをコンパイルしようとするとエラーになる。これは実はコンパイルエラーではなくてリンクエラーという種類のエラーだ。その理由は、関数hello_nに対する関数の定義が存在しないからだ。
関数というのは宣言と定義に分かれている。
// 関数の宣言
void f( ) ;
// 宣言
void f( )
// 定義
{ }関数の宣言というのは何度書いても大丈夫だ。
// 宣言
int f( int x ) ;
// 再宣言
int f( int x ) ;
// 再宣言
int f( int x ) ;関数の宣言というのは戻り値の型や関数名や引数リストだけで、";"で終わる。
関数の定義とは、関数の宣言のあとの"{}"だ。この場合、宣言のあとに";"は書かない。
int f( int x ) { return x ; }関数の定義は一度しか書けない。
// 定義
void f() {}
// エラー、再定義
void f() {}なぜ関数は宣言と定義とに分かれているかというと、C++では名前は宣言しないと使えないためだ。
int main()
{
// エラー
// 名前fは宣言されていない
f() ;
}
// 定義
void f() { }なので、必ず名前は使う前に宣言しなければならない。
// 名前fの宣言
void f() ;
int main()
{
// OK、名前fは関数
f() ;
}
// 名前fの定義
void f() { }さて、話を元に戻そう。これから学ぶのは\(n\)回"hello\n"sと出力するプログラムの書き方だ。ただし\(n\)はユーザーが入力するので実行時にしかわからない。すでに我々はユーザーから\(n\)の入力を受け取る部分のプログラムは書いた。
// n回出力する関数の宣言
void hello_n( int n ) ;
int main()
{
// ユーザーからの入力
int n {} ;
std::cin >> n ;
// n回出力
hello_n( n ) ;
}あとは関数hello_n(n)が\(n\)回"hello\n"sと出力するようなループを実行すればいいのだ。
すでに我々は無限回"hello\n"sと出力する方法を知っている。まずは無限回ループを書こう。
void hello_n( int n )
{
loop :
std::cout << "hello\n"s ;
goto loop ;
}終了条件付きループで学んだように、このループを\(n\)回繰り返した場合に終了させるには、if文を使って、終了条件に達したかどうかで実行を分岐させればよい。
void hello_n( int n )
{
loop :
// まだn回繰り返していない場合
if ( ??? )
{ // 以下を実行
std::cout << "hello\n"s ;
goto loop ;
}
}このコードを完成させるにはどうすればいいのか。まず、現在何回繰り返しを行ったのか記録する必要がある。このために変数を作る。
int i = 0 ;変数iの初期値は0だ。まだ繰り返し実行を1回も行っていないということは、つまり0回繰り返し実行をしたということだ。
1回繰り返し実行をするたびに、変数iの値を1増やす。
i = i + 1 ;これはすでに学んだように、もっと簡単に書ける。
i += 1 ;実は、さらに簡単に書くこともできる。変数の代入前の値に1を足した値を代入する、つまり変数の値を1増やすというのはとてもよく書くコードなので、とても簡単な演算子が用意されている。operator ++だ。
int main()
{
int i = 0 ;
++i ; // 1
++i ; // 2
++i ; // 3
}これで変数iの値は1増える。これをインクリメント(increment)という。
インクリメントと対になるのがデクリメント(decrement)だ。これは変数の値を1減らす。演算子はoperator --だ。
int main()
{
int i = 0 ;
--i ; // -1
--i ; // -2
--i ; // -3
}さて、必要な知識は学び終えたので本題に戻ろう。\(n\)回の繰り返しをしたあとにループを終了するには、まずいま何回繰り返し実行しているのかを記録する必要がある。その方法を学ぶために、0, 1, 2, 3, 4…と無限に出力されるプログラムを書いてみよう。
このプログラムを実行すると以下のように表示される。
$ make run
1, 2, 3, 4, 5, 6, [Ctrl-C]
Ctrl-Cを押すまでプログラムは無限に実行される。
ではどうやって書くのか。以下のようにする。
iを作り、値を0にするiと", "sを出力するiをインクリメントするgoto 2.この処理を素直に書くと以下のコードになる。
int main()
{
// 1. 変数iを作り、値を0にする
int i = 0 ;
loop :
// 2. 変数iと", "sを出力する
std::cout << i << ", "s ;
// 3. 変数iをインクリメントする
++i ;
// 4. goto 2
goto loop ;
}どうやら、いま何回繰り返し実行しているか記録することはできるようになったようだ。
ここまでくればしめたもの。あとはgoto文を実行するかどうかをif文で条件分岐すればよい。しかし、if文の中にどんな条件を書けばいいのだろうか。
void hello_n( int n )
{
int i = 0 ;
loop :
// まだn回繰り返し実行をしていなければ実行
if ( ??? )
{
std::cout << "hello\n"s ;
++i ;
goto loop ;
}
}具体的に考えてみよう。n == 3のとき、つまり3回繰り返すときを考えよう。
if文実行のとき、i == 0if文実行のとき、i == 1if文実行のとき、i == 2if文実行のとき、i == 3ここではn == 3なので、3回まで実行してほしい。つまり3回目まではtrueになり、4回目のif文実行のときにはfalseになるような式を書く。そのような式とは、ズバリ"i != n"だ。
void hello_n( int n )
{
int i = 0 ;
loop :
if ( i != n )
{
std::cout << "hello\n"s ;
++i ;
goto loop ;
}
}さっそく実行してみよう。
$ make run
3
hello
hello
hello
$ make run
2
hello
hello
なるほど、動くようだ。しかしこのプログラムにはバグがある。-1を入力すると、なぜか大量のhelloが出力されてしまうのだ。
$ make run
-1
hello
hello
hello
hello
[Ctrl-C]
この原因はまだ現時点の読者には難しい。この謎はいずれ明らかにするとして、いまはnが負数の場合にプログラムを0回の繰り返し分の実行で終了するように書き換えよう。
void hello_n( int n )
{
// nが負数ならば
if ( n < 0 )
// 関数の実行を終了
return ;
int i = 0 ;
loop :
if ( i != n )
{
std::cout << "hello\n"s ;
++i ;
goto loop ;
}
}goto文は極めて原始的で使いづらい機能だ。現実のC++プログラムではgoto文はめったに使われない。もっと簡単な機能を使う。ではなぜgoto文が存在するかというと、goto文は最も原始的で基礎的で、ほかの繰り返し機能はif文とgoto文に変換することで実現できるからだ。
goto文より簡単な繰り返し文に、while文がある。ここではgoto文とwhile文を比較することで、while文を学んでいこう。
無限ループをgoto文で書く方法を思い出してみよう。
int main()
{
auto hello = []()
{ std::cout << "hello\n"s ; } ;
loop :
// 繰り返し実行される文
hello() ;
goto loop ;
}このコードで本当に重要なのは関数helloを呼び出している部分だ。ここが繰り返し実行される文で、ラベル文とgoto文は、繰り返し実行を実現するために必要な記述でしかない。
そこでwhile(true)だ。while(true)はgoto文とラベル文よりも簡単に無限ループを実現できる。
while (true) 文while文は文を無限に繰り返して実行してくれる。試してみよう。
int main()
{
auto hello = []()
{ std::cout << "hello\n"s ; } ;
while (true)
hello() ;
}このコードの重要な部分は以下の2行。
while (true)
hello() ;これをgoto文とラベル文を使った無限ループと比べてみよう。
loop:
hello() ;
goto loop ;どちらも同じ意味のコードだが、while文の方が明らかに書きやすくなっているのがわかる。
goto文で学んだ、ユーザーからの整数値の入力の合計の計算を繰り返すプログラムをwhile(true)で書いてみよう。
int input()
{
std::cout << ">"s ;
int x {} ;
std::cin >> x ;
return x ;
}
int main()
{
int sum = 0 ;
while( true )
{
sum += input() ;
std::cout << sum << "\n"s ;
}
}重要なのは以下の5行だ。
while( true )
{
sum += input() ;
std::cout << sum << "\n"s ;
}これをgoto文で書いた場合と比べてみよう。
loop :
sum += input() ;
std::cout << sum << "\n"s ;
goto loop ;本当に重要で本質的な、繰り返し実行をする部分の2行のコードはまったく変わっていない。それでいてwhile(true)の方が圧倒的に簡単に書ける。
なるほど、無限ループを書くのに、goto文を使うよりwhile(true)を使った方がいいことがわかった。ではほかのループの場合でも、while文の方が使いやすいだろうか。
本書を先頭から読んでいる優秀な読者はwhile(true)のtrueはbool型の値であることに気が付いているだろう。実はwhile(E)の括弧の中Eは、if(E)と書くのとまったく同じ条件なのだ。条件がtrueであれば繰り返し実行される。falseなら繰り返し実行されない。
while ( 条件 ) 文int main()
{
// 実行されない
while ( false )
std::cout << "No"s ;
// 実行されない
while ( 1 > 2 )
std::cout << "No"s ;
// 実行される
// 無限ループ
while ( 1 < 2 )
std::cout << "Yes"s ;
}while文を使って、0が入力されたら終了する合計値計算プログラムを書いてみよう。
int input()
{
std::cout << ">"s ;
int x {} ;
std::cin >> x ;
return x ;
}
int main()
{
int sum = 0 ;
int x {} ;
while( ( x = input() ) != 0 )
{
sum += x ;
std::cout << sum << "\n"s ;
}
}重要なのはこの5行。
while( ( x = input() ) != 0 )
{
sum += x ;
std::cout << sum << "\n"s ;
}ここではちょっと難しいコードが出てくる。whileの中の条件が、"( x = input() ) != 0"になっている。これはどういうことか。
実は条件はbool型に変換さえできればどんな式でも書ける。
int main()
{
int x { } ;
if ( (x = 1) == 1 )
std::cout << "(x = 1) is 1.\n"s ;
}このコードでは、“(x=1)”と“1”が等しい“==”かどうかを判断している。“(x=1)”という式は変数xに1を代入する式だ。代入式の値は、代入された変数の値になる。この場合変数xの値だ。変数xには1が代入されているので、その値は1、つまり“(x=1) == 1”は“1 == 1”と書くのと同じ意味になる。この結果はtrueだ。
さて、このことを踏まえて、“( x = input() ) != 0”を考えてみよう。
“( x = input() )”は変数xに関数inputを呼び出した結果を代入している。関数inputはユーザーから入力を得て、その入力をそのまま返す。つまり変数xにはユーザーの入力した値が代入される。その結果が0と等しくない“!=”かどうかを判断している。つまり、ユーザーが0を入力した場合はfalse、非ゼロを入力した場合はtrueとなる。
while(条件)は条件がtrueとなる場合に繰り返し実行をする。結果として、ユーザーが0を入力するまで繰り返し実行をするコードになる。
goto文を使った終了条件付きループと比較してみよう。
loop:
if ( (x = input() ) != 0 )
{
sum += x ;
std::cout << sum << "\n"s ;
goto loop ;
}while文の方が圧倒的に書きやすいことがわかる。
\(n\)回"hello\n"sと出力するプログラムをwhile文で書いてみよう。ただし\(n\)はユーザーが入力するものとする。
まずはgoto文でも使ったループ以外の処理をするコードから。
void hello_n( int n ) ;
int main()
{
int n {} ;
std::cin >> n ;
hello_n( n ) ;
}あとは関数hello_n(n)がインデックスループを実装するだけだ。ただしnが負数ならば何も実行しないようにしよう。
goto文でインデックスループを書くときに学んだように、
n < 0ならば関数を終了iを作り値を0にするi != nならば繰り返し実行++igoto 3.をwhile文で書くだけだ。
void hello_n( int n )
{
// 1. n < 0ならば関数を終了
if ( n < 0 )
return ;
// 2. 変数iを作り値を0にする
int i = 0 ;
// 3. i != nならば繰り返し実行
while( i != n )
{ // 4. 出力
std::cout << "hello\n"s ;
// 5. ++i
++i ;
} // 6. goto 3
}重要な部分だけ抜き出すと以下のとおり。
while( i != n )
{
std::cout << "hello\n"s ;
++i ;
}goto文を使ったインデックスループと比較してみよう。
loop :
if ( i != n )
{
std::cout << "hello\n"s ;
++i ;
goto loop ;
}読者の中にはあまり変わらないのではないかと思う人もいるかもしれない。しかし、次の問題を解くプログラムを書くと、while文がいかに楽に書けるかを実感するだろう。
問題:以下のような九九の表を出力するプログラムを書きなさい。
1 2 3 4 5 6 7 8 9
2 4 6 8 10 12 14 16 18
3 6 9 12 15 18 21 24 27
4 8 12 16 20 24 28 32 36
5 10 15 20 25 30 35 40 45
6 12 18 24 30 36 42 48 54
7 14 21 28 35 42 49 56 63
8 16 24 32 40 48 56 64 72
9 18 27 36 45 54 63 72 81
もちろん、このような文字列を愚直に出力しろという問題ではない。
int main()
{
// 違う!
std::cout << "1 2 3 4 5..."s ;
}逐次実行、条件分岐、ループまでを習得した誇りある本物のプログラマーである我々は、もちろん九九の表はループを書いて出力する。
まず出力すべき表を見ると、数値が左揃えになっていることに気が付くだろう。
4 8 12
5 10 15
8は1文字、10は2文字にもかかわらず、12と15は同じ列目から始まっている。これは出力するスペース文字を調整することでも実現できるが、ここでは単にタブ文字を使っている。
タブ文字はMakefileを書くのにも使った文字で、C++の文字列中に直接書くこともできるが、エスケープ文字\tを使ってもよい。
int main()
{
std::cout << "4\t8\t12\n5\t10\t15"s ;
}エスケープ文字\nが改行文字に置き換わるように、エスケープ文字\tはタブ文字に置き換わる。
九九の表はどうやって出力すればよいだろうか。計算自体はC++では"a*b"でできる。上の表がどのように計算されているかを考えてみよう。
1*1 1*2 1*3 1*4 1*5 1*6 1*7 1*8 1*9
2*1 2*2 2*3 2*4 2*5 2*6 2*7 2*8 2*9
3*1 3*2 3*3 3*4 3*5 3*6 3*7 3*8 3*9
4*1 4*2 4*3 4*4 4*5 4*6 4*7 4*8 4*9
5*1 5*2 5*3 5*4 5*5 5*6 5*7 5*8 5*9
6*1 6*2 6*3 6*4 6*5 6*6 6*7 6*8 6*9
7*1 7*2 7*3 7*4 7*5 7*6 7*7 7*8 7*9
8*1 8*2 8*3 8*4 8*5 8*6 8*7 8*8 8*9
9*1 9*2 9*3 9*4 9*5 9*6 9*7 9*8 9*9
これを見ると、"a*b"のうちのaを1から9までインクリメントし、それに対してbを1から9までインクリメントさせればよい。つまり、9回のインデックスループの中で9回のインデックスループを実行することになる。ループの中のループだ。
while ( 条件 )
while ( 条件 )
文さっそくそのようなコードを書いてみよう。
int main()
{
// 1から9まで
int a = 1 ;
while ( a <= 9 )
{
// 1から9まで
int b = 1 ;
while ( b <= 9 )
{
// 計算結果を出力
std::cout << a * b << "\t"s ;
++b ;
}
// 段の終わりに改行
std::cout << "\n"s ;
++a ;
}
}うまくいった。
ところで、このコードをgoto文で書くとどうなるだろうか。
int main()
{
int a = 1 ;
loop_outer :
if ( a <= 9 )
{
int b = 1 ;
loop_inner :
if ( b <= 9 )
{
std::cout << a * b << "\t"s ;
++b ;
goto loop_inner ;
}
std::cout << "\n"s ;
++a ;
goto loop_outer ;
}
}とてつもなく読みにくい。
ところでいままでwhile文で書いてきたインデックスループには特徴がある。
試しに1から100までの整数を出力するコードを見てみよう。
int main()
{
int i = 1 ;
while ( i <= 100 )
{
std::cout << i << " "s ;
++i ;
}
}このコードを読むと、以下のようなパターンがあることがわかる。
int main()
{
// ループ実行前の変数の宣言と初期化
int i = 1 ;
// ループ中の終了条件の確認
while ( i <= 100 )
{
// 実際に繰り返したい文
std::cout << i << " "s ;
// 各ループの最後に必ず行う処理
++i ;
}
}ここで真に必要なのは、「実際に繰り返したい文」だ。その他の処理は、ループを実現するために必要なコードだ。ループの実現に必要な処理が飛び飛びの場所にあるのは、はなはだわかりにくい。
for文はそのような問題を解決するための機能だ。
for ( 変数の宣言 ; 終了条件の確認 ; 各ループの最後に必ず行う処理 ) 文for文を使うと、上のコードは以下のように書ける。
int main()
{
for ( int i = 1 ; i <= 100 ; ++i )
{
std::cout << i << " "s ;
}
}ループの実現に必要な部分だけ抜き出すと以下のようになる。
// for文の開始
for (
// 変数の宣言と初期化
int i = 1 ;
// 終了条件の確認
i <= 100 ;
// 各ループの最後に必ず行う処理
++i )for文はインデックスループによくあるパターンをわかりやすく書くための機能だ。例えばwhile文のときに書いた九九の表を出力するプログラムは、for文ならばこんなに簡潔に書ける。
int main()
{
for ( int a = 1 ; a <= 9 ; ++a )
{
for ( int b = 1 ; b <= 9 ; ++b )
{ std::cout << a*b << "\t"s ; }
std::cout << "\n"s ;
}
}while文を使ったコードと比べてみよう。
int main()
{
int a = 1 ;
while ( a <= 9 )
{
int b = 1 ;
while ( b <= 9 )
{
std::cout << a * b << "\t"s ;
++b ;
}
std::cout << "\n"s ;
++a ;
}
}格段に読みやすくなっていることがわかる。
C++ではカンマ','を使うことで、複数の式を1つの文に書くことができる。
int main()
{
int a = 0, b = 0 ;
++a, ++b ;
}for文でもカンマが使える。九九の表を出力するプログラムは、以下のように書くこともできる。
int main()
{
for ( int a = 1 ; a <= 9 ; ++a, std::cout << "\n"s )
for ( int b = 1 ; b <= 9 ; ++b )
std::cout << a*b << "\t"s ;
}変数もカンマで複数宣言できると知った読者は、以下のように書きたくなるだろう。
int main()
{
for ( int a = 1, b = 1 ;
a <= 9 ;
++a, ++b,
std::cout << "\n"s
)
std::cout << a*b << "\t"s ;
}これは動かない。なぜならば、for文を2つネストさせたループは、\(a \times b\)回のループで、変数aが1から9まで変化するそれぞれに対して、変数bが1から9まで変化する。しかし、上のfor文1つのコードは、変数a, bともに同時に1から9まで変化する。したがって、これは単にa回のループでしかない。a回のループの中でb回のループをすることで\(a \times b\)回のループを実現できる。
for文では使わない部分を省略することができる。
int main()
{
bool b = true ;
// for文による変数宣言は使わない
for ( ; b ; b = false )
std::cout << "hello"s ;
}for文で終了条件を省略した場合、trueと同じになる。
int main()
{
for (;;)
std::cout << "hello\n"s ;
}このプログラムは"hello\n"sと無限に出力し続けるプログラムだ。"for(;;)"は"for(;true;)"と同じ意味であり、"while(true)"とも同じ意味だ。
do文はwhile文に似ている。
do 文 while ( 条件 ) ;比較のためにwhile文の文法も書いてみると以下のようになる。
while ( 条件 ) 文while文はまず条件を確認しtrueの場合文を実行する。これを繰り返す。
int main()
{
while ( false )
{
std::cout << "hello\n"s ;
}
}do文はまず文を実行する。しかる後に条件を確認しtrueの場合繰り返しを行う。
int main()
{
do {
std::cout << "hello\n"s ;
} while ( false ) ;
}違いがわかっただろうか。do文は繰り返し実行する文を、条件がなんであれ、最初に一度実行する。
do文を使うと条件にかかわらず文を1回は実行するコードが、文の重複なく書けるようになる。
ループの実行の途中で、ループの中から外に脱出したくなった場合、どうすればいいのだろうか。例えばループを実行中に何らかのエラーを検出したので処理を中止したい場合などだ。
while ( true )
{
// 処理
if ( is_error() )
// エラーのため脱出したくなった
// 処理
}break文はループの途中から脱出するための文だ。
break ;break文はfor文、while文、do文の中でしか使えない。
break文はfor文、while文、do文の外側に脱出する。
int main()
{
while ( true )
{
// 処理
break ;
// 処理
}
}これは以下のようなコードと同じだ。
int main()
{
while ( true )
{
// 処理
goto break_while ;
// 処理
}
break_while : ;
}break文は最も内側の繰り返し文から脱出する
int main()
{
while ( true ) // 外側
{
while ( true ) // 内側
{
break ;
}
// ここに脱出
}
}ループの途中で、いまのループを打ち切って次のループに進みたい場合はどうすればいいのだろう。例えば、ループの途中でエラーを検出したので、そのループについては処理を打ち切りたい場合だ。
while ( true )
{
// 処理
if ( is_error() )
// このループは打ち切りたい
// 処理
}continue文はループを打ち切って次のループに行くための文だ。
continue ;continue文はfor文、while文、do文の中でしか使えない。
int main()
{
while ( true )
{
// 処理
continue ;
// 処理
}
}これは以下のようなコードと同じだ。
int main()
{
while ( true )
{
// 処理
goto continue_while ;
// 処理
continue_while : ;
}
}continue文はループの最後に処理を移す。その結果、次のループを実行するかどうかの条件を評価することになる。
continue文は最も内側のループに対応する。
int main()
{
while ( true ) // 外側
{
while ( true ) // 内側
{
continue ;
// continueはここに実行を移す
}
}
}最後に関数でループを実装する方法を示してこの章を終わりにしよう。
関数は関数を呼び出すことができる。
void f() { }
void g()
{
f() ; // 関数fの呼び出し
}
int main()
{
g() ; // 関数gの呼び出し
}ではもし、関数が自分自身を呼び出したらどうなるだろうか。
void hello()
{
std::cout << "hello\n" ;
hello() ;
}
int main()
{
hello() ;
}mainは関数helloを呼び出すhelloは"hello\n"と出力して関数helloを呼び出す関数helloは必ず関数helloを呼び出すので、この実行は無限ループする。
関数が自分自身を呼び出すことを、再帰(recursion)という。
なるほど、再帰によって無限ループを実現できることはわかった。では終了条件付きループは書けるだろうか。
関数はreturn文によって呼び出し元に戻る。単に'return ;'と書けば再帰はしない。そして、if文によって実行は分岐できる。これを使えば再帰で終了条件付きループが実現できる。
試しに、ユーザーが0を入力するまでループし続けるプログラムを書いてみよう。
// ユーザーからの入力を返す
int input ()
{
int x { } ;
std::cin >> x ;
return x ;
}
// 0の入力を終了条件としたループ
void loop_until_zero()
{
if ( input() == 0 )
return ;
else
loop_until_zero() ;
}
int main()
{
loop_until_zero() ;
}書けた。
ではインデックスループはどうだろうか。1から10までの整数を出力してみよう。
インデックスループを実現するには、書き換えられる変数が必要だ。関数は引数で値を渡すことができる。
void g( int x ) { }
void f( int x ) { g( x+1 ) ; }
int main() { f( 0 ) ; }これを見ると、関数mainは関数fに引数0を渡し、関数fは関数gに引数1を渡している。これをもっと再帰的に考えよう。
void until_ten( int x )
{
if ( x > 10 )
return ;
else
{
std::cout << x << "\n" ;
return until_ten( x + 1 ) ;
}
}
int main()
{
until_ten(1) ;
}関数mainは関数until_tenに引数1を渡す。
関数until_tenは引数が10より大きければ何もせず処理を戻し、そうでなければ引数を出力して再帰する。そのとき引数は\(+1\)される。
これによりインデックスループが実現できる。
関数は戻り値を返すことができる。再帰で戻り値を使うことにより面白い問題も解くことができる。
例えば、1と0だけを使った10進数の整数を2進数に変換するプログラムを書いてみよう。
$ make run
> 0
0
> 1
1
> 10
2
> 11
3
> 1010
10
> 1111
15
まず10進数と2進数を確認しよう。数学的に言うと「10を底にする」とか「2を底にする」という言い方をする。
具体的な例を出すと10進数では1,2,3,4,5,6,7,8,9,0の文字を使う。1234は以下のようになる。
\[ 1234 = 1 \times 10^3 + 2 \times 10^2 + 3 \times 10^1 + 4 \times 10^0 = 1 \times 1000 + 2 \times 100 + 3 \times 10 + 4 \times 1 \]
10進数で1010は以下のようになる。
\[ 1010 = 1 \times 10^3 + 0 \times 10^2 + 1 \times 10^1 + 0 \times 10^0 = 1 \times 1000 + 0 \times 100 + 1 \times 10 + 0 \times 1 \]
2進数では1,0の文字を使う。1010は以下のようになる。
\[ 1010 = 1 \times 2^3 + 0 \times 2^2 + 1 \times 2^1 + 0 \times 2^0 = 1 \times 8 + 0 \times 4 + 1 \times 2 + 0 \times 1 \]
2進数の1010は10進数では10になる。
では問題を解いていこう。
問題を難しく考えるとかえって解けなくなる。ここではすでに10進数から2進数への変換は解決したものとして考えよう。関数convertによってその問題は解決した。
// 2進数への変換
int convert( int n ) ;まだ我々は関数convertの中身を書いていないが、すでに書き終わったと仮定しよう。するとプログラムの残りの部分は以下のように書ける。
int convert( int n ) ;
// 入力
int input()
{
std::cout << "> " ;
int x{} ;
std::cin >> x ;
return x ;
}
// 出力
void output( int binary )
{
std::cout << binary << "\n"s ;
}
int main()
{
// 入力、変換、出力のループ
while( true )
{
auto decimal = input() ;
auto binary = convert( decimal ) ;
output( binary ) ;
}
}あとは関数convertを実装すればよいだけだ。
関数convertに引数を渡したときの結果を考えてみよう。convert(1010)は10を返し、convert(1111)は15を返す。
ではconvert(-1010)の結果はどうなるだろうか。これは-10になる。
負数と正数の違いを考えるのは面倒だ。ここでは正数を引数として与えると10進数から2進数へ変換した答えを返してくる魔法のような関数solveをすでに書き終えたと仮定しよう。我々はまだ関数solveを書いていないが、その問題は未来の自分に押し付けよう。
// 1,0のみを使った10進数から
// 2進数へ変換する関数
int solve( int n ) ;すると、関数convertがやるのは負数と正数の処理だけでよい。
solveに渡してreturnsolveに渡して負数にしてreturnint convert( int n )
{
// 引数が正数の場合
if ( n > 0 )
// そのまま関数solveに渡してreturn
return solve( n ) ;
else // 引数が負数の場合
// 絶対値を関数solveに渡して負数にしてreturn
return - solve( -n ) ;
}nが負数の場合の絶対値は-nで得られる。その場合、関数solveの答えは正数なので負数にする。
あとは関数solveを実装するだけだ。
今回、引数の整数を10進数で表現した場合に2,3,4,5,6,7,8,9が使われている場合は考えないものとする。
// OK
solve(10111101) ;
// あり得ない
solve(2) ;再帰で問題を解くには再帰的な考え方が必要だ。再帰的な考え方では、問題の一部のみを解き、残りは自分自身に丸投げする。
まずとても簡単な1桁の変換を考えよう。
solve(0) ; // 0
solve(1) ; // 1引数が0か1の場合、単にその値を返すだけだ。関数solveには正数しか渡されないので、負数は考えなくてよい。すると、以下のように書ける。
int solve( int n )
{
if ( n <= 1 )
return n ;
else
// その他の場合
}その他の場合とは、桁数が多い場合だ。
solve(10) ; // 2
solve(11) ; // 3
solve(110) ; // 4
solve(111) ; // 5関数solveが解決するのは最下位桁だ。110の場合は0で、111の場合は1となる。最も右側の桁のみを扱う。数値から10進数で表記したときの最下位桁を取り出すには、10で割った余りが使える。覚えているだろうか。剰余演算子のoperator %を。
int solve( int n )
{
if ( n <= 1 )
return n ;
else // 未完成
return n%10 ;
}結果は以下のようになる。
solve(10) ; // 0
solve(11) ; // 1
solve(110) ; // 0
solve(111) ; // 1これで関数solveは最下位桁に完全に対応した。しかしそれ以外の桁はどうすればいいのだろう。
ここで再帰的な考え方が必要だ。関数solveはすでに最下位桁に完全に対応している。ならば次の桁を最下位桁とした数値で関数solveを再帰的に呼び出せばいいのではないか。
以下はsolve(n)が再帰的に呼び出す関数だ。
solve(10) ; // solve(1)
solve(11) ; // solve(1)
solve(100) ; // solve(10)→solve(1)
solve(110) ; // solve(11)→solve(1)
solve(111) ; // solve(11)→solve(1)10進数表記された数値から最下位桁を取り除いた数値にするというのは、11を1に, 111を11にする処理だ。これは数値を10で割ればよい。
10 / 10 ; // 1
11 / 10 ; // 1
100 / 10 ; // 10
110 / 10 ; // 11
111 / 10 ; // 1110進数表記は桁が1つ上がると10倍される。だから10で割れば最下位桁が消える。ところで、我々が計算しようとしているのは2進数だ。2進数では桁が1つ上がると2倍される。なので、再帰的に関数solveを呼び出して得られた結果は2倍しなければならない。そして足し合わせる。
int solve( int n )
{
// 1桁の場合
if ( n <= 1 )
return n ; // 単に返す
else // それ以外
return
// 最下位桁の計算
n%10
// 残りの桁を丸投げする
// 次の桁なので2倍する
+ 2 * solve( n/10 ) ;
}冗長なコメントを除いて短くすると以下のとおり。
int solve( int n )
{
if ( n <= 1 )
return n ;
else
return n%10 + 2 * solve( n/10 ) ;
}再帰ではないループで関数solveを実装するとどうなるのだろうか。
引数の数値が何桁あっても対応できるよう、ループで1桁ずつ処理していくのは変わらない。
もう一度2進数の計算を見てみよう。
\[ 1010 = 1 \times 2^3 + 0 \times 2^2 + 1 \times 2^1 + 0 \times 2^0 = 1 \times 8 + 0 \times 4 + 1 \times 2 + 0 \times 1 \]
1桁目は0で、この値は\(0 \times 2^0\)、2桁目は1で、この値は\(1 \times 2^1\)になる。
一般に、\(i\)桁目の値は\(i桁目の数字 \times 2^{i-1}\)になる。
すると解き方としては、各桁の値を計算した和を返せばよい
int solve( int n )
{
// 和
int result = 0 ;
// i桁目の数字に乗ずる値
int i = 1 ;
// 桁がなくなれば終了
while ( n != 0 )
{
// 現在の桁を計算して足す
result += n%10 * i ;
// 次の桁に乗ずる値
i *= 2 ;
// 桁を1つ減らす
n /= 10 ;
}
return result ;
}再帰を使うコードは、再帰を理解できれば短く簡潔でわかりやすい。ただし、再帰を理解するためにはまず再帰を理解しなければならない。
再帰は万能ではない。そもそも関数とは、別の関数から呼ばれるものだ。関数mainだけは特別で、関数mainを呼び出すことはできない。
int main()
{
main() ; // エラー
}関数の実行が終了した場合、呼び出し元に処理が戻る。そのために関数は呼び出し元を覚えていなければならない。これには通常スタックと呼ばれるメモリーを消費する。
void f() { } // gに戻る
void g() { f() ; } // mainに戻る
int main() { g() ; }関数の中の変数も通常スタックに確保される。これもメモリーを消費する。
void f() { }
void g()
{
int x {} ;
std::cin >> x ;
f() ; // 関数を呼び出す
// 関数を呼び出したあとに変数を使う
std::cout << x ;
}このコードでは、関数gが変数xを用意し、関数fを呼び出し、処理が戻ったら変数xを使っている。このコードが動くためには、変数xは関数fが実行されている間もスタックメモリーを消費し続けなければならない。
スタックメモリーは有限であるので、以下のような再帰による無限ループは、いつかスタックメモリーを消費し尽して実行が止まるはずだ。
void hello()
{
std::cout << "hello\n" ;
hello() ;
}
int main() { hello() ; }しかし、大半の読者の環境ではプログラムの実行が止まらないはずだ。これはコンパイラーの末尾再帰の最適化によるものだ。
末尾再帰とは、関数のすべての条件分岐の末尾が再帰で終わっている再帰のことだ。
例えば以下は階乗を計算する再帰で書かれたループだ。
int factorial( int n )
{
if ( n < 1 )
return 0 ;
else if ( n == 1 )
return 1 ;
else
return n * factorial(n-1) ;
}factorial(n)は\(1 \times 2 \times 3 \times ... \times n\)を計算する。
この関数は、引数nが1未満であれば引数が間違っているので0を返す。そうでない場合でnが1であれば1を返す。それ以外の場合、n * factorial(n-1)を返す。
このコードは末尾再帰になっている。末尾再帰は非再帰のループに機械的に変換できる特徴を持っている。例えば以下のように、