コーディング · プログラミング言語
C++入門チュートリアル(初心者向け)
結論
C++はコンパイル型の静的型付け言語で、Cの速度と低水準の制御に、クラス、テンプレート、大規模な標準ライブラリを組み合わせています。hello.cpp を g++ -std=c++20 hello.cpp -o hello でコンパイルし、./hello で実行します。モダンC++では、生の配列の代わりに std::string、std::vector、std::map を使い、手動の delete の代わりにスマートポインタを使い、RAIIによってリソースを自動的に解放します。
C++とは何か、何に使われるか
C++は1979年にBell研究所のBjarne Stroustrupが「C with Classes」として開発を始めた言語です。ISOによって標準化されており、C++11から「モダンC++」の時代が始まり、C++17、C++20、C++23が続きました。C++は性能と制御が重要な分野で使われています。ゲームエンジン(Unreal Engine)、Webブラウザ(Chrome、Firefox)、データベース、取引システム、組み込み機器、コンパイラ、機械学習のランタイムなどです。基本思想はゼロオーバーヘッド抽象化で、使わない機能にはコストがかかりません。
コンパイラのインストールと Hello, world の実行
LinuxとmacOSではGCC(g++)またはClang(clang++)、WindowsではMicrosoft Visual C++(Visual Studio経由)を使います。規格のバージョンは明示的に指定しましょう。
#include <iostream>
int main() {
std::cout << "Hello, world\n";
return 0;
}g++ -std=c++20 -Wall -Wextra hello.cpp -o hello
./hello
# Hello, worldstd::cout は標準出力ストリームで、<< で書き込みます。標準ライブラリのものはすべて名前空間 std にあります。
変数、型、auto
C++にはCと同じ組み込み型があり、さらに bool がキーワードとして加わり、参照や std::string などのライブラリ型があります。auto は初期化子からコンパイラに型を推論させます。波括弧初期化 {} は、縮小変換を拒否します。
| 型 | 例 | 備考 |
|---|---|---|
int, long long | int n{42}; | 符号付き整数 |
double | double x{3.14}; | 64ビット浮動小数点数 |
bool | bool ok{true}; | true または false |
char | char c{'A'}; | 1バイト |
std::string | std::string s{"hi"}; | <string> で提供される、伸縮する文字列 |
auto | auto v = 2.5; | 推論された型。ここでは double |
T& | int& r = n; | 参照:n の別名 |
#include <iostream>
#include <string>
int main() {
int count{7};
auto avg = count / 2.0; // double, 3.5
std::string name{"Ada"};
name += " Lovelace";
const int& ref = count; // read-only alias of count
std::cout << name << " " << name.size() << " " << avg << " " << ref << "\n";
// Ada Lovelace 12 3.5 7
}制御構文と関数
制御構文はCと同じで、範囲ベースの for ループが加わります。大きなオブジェクトはコピーを避けるため const 参照で渡し、関数がオブジェクトを変更する場合は非 const 参照で渡します。
#include <iostream>
#include <string>
#include <vector>
int square(int x) { return x * x; }
void addSuffix(std::string& s, const std::string& suffix = "!") {
s += suffix;
}
int main() {
int score = 72;
if (score >= 90) std::cout << "A\n";
else if (score >= 70) std::cout << "B\n"; // B
else std::cout << "C\n";
std::vector<int> nums{1, 2, 3};
for (int n : nums) std::cout << square(n) << " "; // 1 4 9
std::cout << "\n";
std::string word = "hi";
addSuffix(word);
std::cout << word << "\n"; // hi!
}クラス
クラスはデータメンバーとメンバー関数をまとめます。コンストラクタがオブジェクトを初期化し、private メンバーは外部のコードから隠されます。struct は、メンバーがデフォルトで public である点を除いてクラスと同じです。
#include <iostream>
#include <string>
#include <utility>
class Account {
public:
explicit Account(std::string owner) : owner_(std::move(owner)) {}
void deposit(double amount) {
if (amount > 0) balance_ += amount;
}
double balance() const { return balance_; }
const std::string& owner() const { return owner_; }
private:
std::string owner_;
double balance_ = 0.0;
};
int main() {
Account acc{"Ann"};
acc.deposit(50);
acc.deposit(-5); // ignored
std::cout << acc.owner() << ": " << acc.balance() << "\n"; // Ann: 50
}継承(class Savings : public Account)と virtual 関数により、実行時ポリモーフィズムが実現します。オーバーライドする関数には override を付けると、コンパイラが検査してくれます。
標準ライブラリのコンテナとアルゴリズム
標準ライブラリ(STLとも呼ばれます)は、イテレータを通じて連携するコンテナとアルゴリズムを提供します。
| コンテナ | 似たもの | 用途 |
|---|---|---|
std::vector<T> | 動的配列 | 標準的なシーケンスコンテナ |
std::array<T, N> | 固定長配列 | サイズがコンパイル時に分かっている場合 |
std::map<K, V> | ソート済み辞書 | キーが順序付き、検索はO(log n) |
std::unordered_map<K, V> | ハッシュテーブル | 平均O(1)の高速な検索 |
std::set<T> | ソート済みの重複なし集合 | 所属判定、順序付きの出力 |
#include <algorithm>
#include <iostream>
#include <map>
#include <string>
#include <vector>
int main() {
std::vector<int> nums{3, 1, 2, 5};
std::sort(nums.begin(), nums.end());
auto evens = std::count_if(nums.begin(), nums.end(),
[](int n) { return n % 2 == 0; });
std::cout << nums.front() << " " << nums.back() << " " << evens << "\n"; // 1 5 1
std::map<std::string, int> ages{{"bob", 25}, {"ann", 31}};
ages["cy"] = 40;
for (const auto& [name, age] : ages) { // structured bindings
std::cout << name << "=" << age << " "; // ann=31 bob=25 cy=40
}
std::cout << "\n";
}[](int n) { ... } はラムダ式で、その場に書く無名関数です。テンプレートのおかげで、1つの std::vector があらゆる要素型で使えます。自分で書くには template <typename T> を使います。
特徴的な機能:RAIIとスマートポインタ
RAII(Resource Acquisition Is Initialization、リソース取得は初期化)とは、リソースをオブジェクトが所有するという考え方です。コンストラクタが取得し、デストラクタが解放します。オブジェクトがスコープを抜けるとき、例外が投げられた場合でもデストラクタが自動的に実行されるため、メモリ、ファイル、ロックがリークしません。スマートポインタは、このRAIIをヒープメモリに適用したものです。
#include <iostream>
#include <memory>
struct Noisy {
int id;
explicit Noisy(int i) : id(i) { std::cout << "create " << id << "\n"; }
~Noisy() { std::cout << "destroy " << id << "\n"; }
};
int main() {
auto a = std::make_unique<Noisy>(1); // sole owner
{
auto b = std::make_shared<Noisy>(2); // shared, reference-counted
auto c = b; // two owners now
std::cout << "owners: " << b.use_count() << "\n";
} // destroy 2 happens here
std::cout << "end of main\n";
} // destroy 1 happens here出力:create 1、create 2、owners: 2、destroy 2、end of main、destroy 1。モダンなコードでは、new や delete を自分で書くことはほとんどありません。
例外によるエラー処理
例外オブジェクトを throw し、const 参照で catch します。標準の例外型は <stdexcept> にあります。例外を投げない関数には noexcept を付けられます。
#include <iostream>
#include <stdexcept>
#include <string>
int parseAge(const std::string& text) {
int age = std::stoi(text); // throws std::invalid_argument on "abc"
if (age < 0) throw std::out_of_range("negative age");
return age;
}
int main() {
try {
std::cout << parseAge("41") << "\n"; // 41
std::cout << parseAge("abc") << "\n";
} catch (const std::invalid_argument&) {
std::cout << "not a number\n"; // not a number
} catch (const std::exception& e) {
std::cout << "error: " << e.what() << "\n";
}
}想定内の失敗に対しては、例外を投げる代わりに std::optional<T>(C++17)や std::expected<T, E>(C++23)を返すコードベースも多くあります。
小さな完成プログラム:単語の出現頻度
#include <algorithm>
#include <cctype>
#include <fstream>
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>
int main(int argc, char* argv[]) {
if (argc != 2) {
std::cerr << "usage: " << argv[0] << " FILE\n";
return 1;
}
std::ifstream in(argv[1]);
if (!in) {
std::cerr << "cannot open " << argv[1] << "\n";
return 1;
}
std::unordered_map<std::string, int> counts;
std::string word;
while (in >> word) {
std::string clean;
for (unsigned char ch : word)
if (std::isalpha(ch)) clean += static_cast<char>(std::tolower(ch));
if (!clean.empty()) ++counts[clean];
}
std::vector<std::pair<std::string, int>> top(counts.begin(), counts.end());
std::sort(top.begin(), top.end(),
[](const auto& a, const auto& b) { return a.second > b.second; });
for (size_t i = 0; i < top.size() && i < 5; ++i)
std::cout << top[i].second << " " << top[i].first << "\n";
}g++ -std=c++20 -O2 words.cpp -o words でコンパイルし、./words notes.txt で実行します。in がスコープを抜けるとファイルは自動的に閉じられます。これもRAIIです。
初心者がよくやる間違い
- ヘッダファイルに
using namespace std;を書く。それをインクルードするすべてのファイルが汚染されます。 const&で足りるのに、std::vectorなどの大きなオブジェクトを値渡しする。int iとv.size()の比較のように、符号付きと符号なしを混ぜて比較する。警告を有効にすると気づけます。push_backの後もstd::vectorへのイテレータや参照を持ち続ける。要素が移動している可能性があります。std::make_uniqueやstd::vectorなどの所有する型を使わず、生のnewとdeleteを使う。
次のステップ
言語とライブラリのリファレンスとしてcppreference.comを、良い書き方の指針としてC++ Core Guidelines(Bjarne StroustrupとHerb Sutterが編集)を使いましょう。Standard C++ Foundationのサイトisocpp.orgには、おすすめの書籍を載せた入門ページがあります。プロジェクトのビルドにはCMakeを学び、続いてムーブセマンティクス、テンプレート、constexpr、std::thread による並行処理へ進みます。
よくある質問
C++の前にCを学ぶべきですか?
必須ではありません。モダンC++の教え方は std::string、std::vector、クラスから始め、生ポインタやCスタイルの配列は後で扱います。ただし、Cを先に学ぶとメモリの理解には役立ちます。
どのC++規格を使うべきですか?
C++17とC++20は現行のGCC、Clang、MSVCで十分にサポートされており、C++23のサポートも広がっています。-std=c++20 のようなフラグで明示的に選びましょう。
ポインタと参照の違いは何ですか?
参照は、作成時に束縛する必要があり、付け替えたりnullにしたりできない別名です。ポインタはアドレスを保持し、nullにでき、別の対象を指すように変更できます。
C++は習得が難しいですか?
言語の規模が大きいため、PythonやGoより学習曲線は長くなります。モダンな機能、標準コンテナ、スマートポインタに絞って使えば、典型的な落とし穴の大半を避けられます。