第2章:C++基本構文

はじめに

C++はCの拡張として設計されましたが、多くの新しい構文と概念が追加されています。本章では、C++固有の基本構文を学びます。これらはCには存在しないか、Cでは別の方法で実現されていた機能です。

各機能について、「なぜこの機能が必要だったのか」という設計背景から始め、実装と使用方法を解説します。

---

1. 入出力ストリーム(iostream)

1.1 stdio.hの問題点

C言語の標準入出力(printf/scanf)には、設計上の問題がありました:

/* C言語のprintf:型安全でない */
int x = 42;
printf("%s", x);  // 実行時エラー:intを%sで出力

この問題点:

  • 型安全性の欠如: フォーマット指定子と引数の型が一致しなくてもコンパイルエラーにならない
  • 拡張性の欠如: ユーザー定義型を直接出力できない
  • ロケール対応の困難: 国際化が難しい
  • 1.2 ストリームの設計思想

    C++のiostreamは、演算子オーバーロードオブジェクト指向を活用して、これらの問題を解決しました。

    ストリームの基本概念:

    データ → ストリーム → 出力先
            ↑
        (抽象化層)
    

    ストリームはデータの流れを抽象化します。出力先がコンソールでもファイルでも文字列でも、同じインターフェースで扱えます。

    1.3 基本的な使い方

    #include <iostream>
    
    int main() {
        // 標準出力
        std::cout << "Hello, World!" << std::endl;
    
        // 変数の出力(型安全)
        int x = 42;
        double pi = 3.14159;
        std::cout << "x = " << x << ", pi = " << pi << std::endl;
    
        // 標準入力
        int number;
        std::cout << "Enter a number: ";
        std::cin >> number;
        std::cout << "You entered: " << number << std::endl;
    
        // 標準エラー出力
        std::cerr << "Error message" << std::endl;
    
        return 0;
    }
    

    1.4 << と >> 演算子

    <<>> は元々ビットシフト演算子ですが、iostreamではストリーム挿入/抽出演算子としてオーバーロードされています:

    // 左シフト演算子(ビット操作)
    int a = 1 << 3;  // a = 8
    
    // ストリーム挿入演算子
    std::cout << "Hello";  // "Hello"を出力
    

    この設計の利点:

  • チェイン可能: std::cout << a << b << c;
  • 型安全: コンパイル時に型チェック
  • 拡張可能: ユーザー定義型にも対応可能

1.5 フォーマット出力

#include <iostream>
#include <iomanip>  // マニピュレータ用

int main() {
    int num = 42;
    double pi = 3.14159265359;

    // 16進数表示
    std::cout << std::hex << num << std::endl;  // 2a

    // 10進数に戻す
    std::cout << std::dec << num << std::endl;  // 42

    // 精度指定
    std::cout << std::fixed << std::setprecision(2) << pi << std::endl;  // 3.14

    // 幅指定
    std::cout << std::setw(10) << num << std::endl;  // "        42"

    return 0;
}

1.6 printfとの比較

| 観点 | printf | iostream | |------|--------|----------| | 型安全性 | なし | あり | | 拡張性 | 困難 | 容易 | | パフォーマンス | 高速 | やや遅い(同期) | | 可読性 | フォーマット文字列 | チェイン |

パフォーマンスについて: iostreamはCのstdioと同期するため、やや遅くなります。高速化するには:

std::ios_base::sync_with_stdio(false);  // C標準入出力との同期を解除
std::cin.tie(nullptr);  // cinとcoutの紐付けを解除

---

2. 名前空間(namespace)

2.1 名前衝突問題

大規模なプログラムでは、異なるライブラリ間で同じ名前が衝突する問題がありました:

/* library_a.h */
int init();  // ライブラリAの初期化関数

/* library_b.h */
int init();  // ライブラリBの初期化関数

/* main.c:どちらのinit()を呼ぶ? */
#include "library_a.h"
#include "library_b.h"

int main() {
    init();  // コンパイルエラー:ambiguous
    return 0;
}

C言語での対処法はプレフィックスでした:

int libA_init();
int libB_init();

しかし、これは名前が長くなり、タイプ量が増えます。

2.2 名前空間の概念

C++の名前空間は、識別子を論理的にグループ化する機能です:

namespace LibraryA {
    int init();
    void process();
}

namespace LibraryB {
    int init();
    void process();
}

int main() {
    LibraryA::init();  // LibraryAのinit
    LibraryB::init();  // LibraryBのinit
    return 0;
}

2.3 std名前空間

C++標準ライブラリの全てのシンボルはstd名前空間に属しています:

std::cout
std::cin
std::string
std::vector
std::endl

なぜstdなのか?

  • standard(標準)の略
  • 標準ライブラリとユーザーコードを明確に区別

2.4 using宣言とusing指令

名前空間を毎回書くのが面倒な場合:

// using宣言:特定のシンボルのみ
using std::cout;
using std::endl;

cout << "Hello" << endl;  // std::不要

// using指令:名前空間全体
using namespace std;

cout << "Hello" << endl;  // 全てstd::不要

注意: using namespace std;はヘッダファイルでは使うべきではありません:

// bad_header.h
#pragma once
using namespace std;  // 危険!このヘッダをincludeした全ファイルに影響

// good_header.h
#pragma once
// using namespace stdを書かない
std::string getMessage();  // 明示的に書く

2.5 名前空間のネストと無名名前空間

// ネストした名前空間
namespace Company {
    namespace Product {
        namespace Module {
            void function();
        }
    }
}

// C++17以降の簡略記法
namespace Company::Product::Module {
    void function();
}

// 無名名前空間(内部リンケージ)
namespace {
    int internal_variable;  // このファイル内でのみ有効
    void internal_function() {}
}

無名名前空間は、C言語のstaticと同様に、シンボルをファイル内に限定します。

---

3. 参照(Reference)

3.1 エイリアスとしての参照

参照は、既存の変数に対する別名(エイリアス)です。ポインタに似ていますが、より安全で直感的です。

int x = 10;
int& ref = x;  // refはxの別名

ref = 20;      // xも20になる
std::cout << x << std::endl;  // 20

3.2 ポインタとの違い

| 特性 | ポインタ | 参照 | |------|---------|------| | 初期化 | 後からでも可能 | 宣言時に必須 | | 再代入 | 可能 | 不可能 | | null | 可能 | 不可能(未定義動作) | | 間接参照 | *ptr | 自動 | | アドレス取得 | ptr | &ref(元の変数のアドレス) |

int a = 10, b = 20;

// ポインタは再代入可能
int* ptr = &a;
ptr = &b;  // OK

// 参照は再代入不可能
int& ref = a;
ref = b;  // これはaにbの値を代入(参照先は変わらない)

3.3 参照の内部実装

参照は、コンパイラレベルではポインタとして実装されることが多いです:

int x = 10;
int& ref = x;
ref = 20;

// 概念的には以下と同等
int* const ptr = &x;
*ptr = 20;

しかし、言語レベルでは参照はポインタではありません。参照は「そのオブジェクト自体」として振る舞います。

3.4 関数の引数としての参照

C言語では、関数で変数を変更するにはポインタを渡す必要がありました:

/* C言語 */
void swap(int* a, int* b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main() {
    int x = 1, y = 2;
    swap(&x, &y);  // アドレスを渡す
    return 0;
}

C++では参照を使うことで、より自然に書けます:

// C++
void swap(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}

int main() {
    int x = 1, y = 2;
    swap(x, y);  // 変数をそのまま渡す
    return 0;
}

3.5 const参照

大きなオブジェクトをコピーせずに渡したいが、変更されたくない場合:

// コピーが発生(非効率)
void printVector(std::vector<int> vec) {
    // ...
}

// 参照で受け取り(効率的、しかし変更される危険)
void printVector(std::vector<int>& vec) {
    // ...
}

// const参照(効率的かつ安全)
void printVector(const std::vector<int>& vec) {
    // vecを変更するとコンパイルエラー
}

重要な規則: 引数を変更しない関数では、const参照を使うべきです。

---

4. const修飾子

4.1 const変数

const int MAX_SIZE = 100;  // 変更不可
MAX_SIZE = 200;  // コンパイルエラー

C言語のマクロ(#define)との違い:

/* C言語 */
#define MAX_SIZE 100  // 単なるテキスト置換、型なし

// C++
const int MAX_SIZE = 100;  // 型付き、スコープあり

4.2 constポインタの組み合わせ

ポインタとconstの組み合わせは混乱しやすいです:

int x = 10;
int y = 20;

// 1. ポインタ自体が変更可能、指す先も変更可能
int* ptr1 = &x;
ptr1 = &y;   // OK
*ptr1 = 30;  // OK

// 2. ポインタ自体が変更可能、指す先は変更不可
const int* ptr2 = &x;  // または int const* ptr2
ptr2 = &y;   // OK
*ptr2 = 30;  // エラー

// 3. ポインタ自体が変更不可、指す先は変更可能
int* const ptr3 = &x;
ptr3 = &y;   // エラー
*ptr3 = 30;  // OK

// 4. ポインタ自体も指す先も変更不可
const int* const ptr4 = &x;
ptr4 = &y;   // エラー
*ptr4 = 30;  // エラー

覚え方: constは左側のものを修飾する(最初の場合は例外的に右側)

4.3 constメンバ関数

クラスのメンバ関数をconstにすると、その関数内でメンバ変数を変更できません:

class Point {
    int x, y;

public:
    // constメンバ関数:メンバを変更しない
    int getX() const { return x; }
    int getY() const { return y; }

    // 非constメンバ関数:メンバを変更する
    void setX(int newX) { x = newX; }
};

constオブジェクトからは、constメンバ関数のみ呼び出せます:

const Point p(10, 20);
p.getX();   // OK
p.setX(30); // エラー:非constメンバ関数は呼べない

4.4 const正確性(const correctness)

C++では「const正確性」を維持することが重要です:

  • 変更しないものはconstにする
  • const参照で渡す
  • constメンバ関数を適切に定義する

class String {
    char* data;
    size_t length;

public:
    // const正確なインターフェース
    size_t size() const { return length; }
    const char* c_str() const { return data; }
    char& operator[](size_t i) { return data[i]; }
    const char& operator[](size_t i) const { return data[i]; }
};

---

5. 関数オーバーロード

5.1 同名関数の定義

C言語では、同じ名前の関数を複数定義できませんでした:

/* C言語:コンパイルエラー */
int add(int a, int b);
double add(double a, double b);  // エラー:redefinition

C++では、引数の型や数が異なれば同名の関数を定義できます:

// C++:OK
int add(int a, int b) {
    return a + b;
}

double add(double a, double b) {
    return a + b;
}

int add(int a, int b, int c) {
    return a + b + c;
}

5.2 名前マングリング

コンパイラは、関数の引数情報を含んだ「マングリング名」を生成します:

int add(int, int);      // → _Z3addii
double add(double, double);  // → _Z3adddd
int add(int, int, int); // → _Z3addiii

確認方法(Linux/Mac):

g++ -c functions.cpp
nm functions.o  # マングリング名が見える
c++filt _Z3addii  # デマングル:add(int, int)

5.3 オーバーロード解決規則

コンパイラは以下の優先順位でオーバーロードを解決します:

  • 完全一致
  • 型昇格(int → long、float → double)
  • 標準変換(int → double)
  • ユーザー定義変換

void foo(int);
void foo(double);
void foo(long);

foo(42);      // 完全一致:foo(int)
foo(3.14);    // 完全一致:foo(double)
foo(42L);     // 完全一致:foo(long)
foo('a');     // 型昇格:foo(int)(charはintに昇格)
foo(3.14f);   // 型昇格:foo(double)(floatはdoubleに昇格)

5.4 デフォルト引数

関数の引数にデフォルト値を指定できます:

void greet(const std::string& name = "World") {
    std::cout << "Hello, " << name << "!" << std::endl;
}

greet();         // Hello, World!
greet("Alice");  // Hello, Alice!

注意: デフォルト引数は右側からのみ指定可能

void func(int a, int b = 10, int c = 20);  // OK
void func(int a = 5, int b, int c = 20);   // エラー

---

6. その他の基本機能

6.1 bool型

C言語には真のブール型がありませんでした(C99で追加):

/* C言語 */
int is_valid = 1;  // 0か非0で判断

C++には組み込みのbool型があります:

bool is_valid = true;
bool is_empty = false;

if (is_valid) {
    // ...
}

6.2 新しいキャスト演算子

C言語のキャストは危険でした:

/* C言語:なんでもキャストできる */
int* ptr = (int*)some_function_pointer;  // 危険

C++では4種類のキャスト演算子があります:

// static_cast:コンパイル時にチェックされる変換
double d = 3.14;
int i = static_cast<int>(d);  // double → int

// dynamic_cast:実行時に継承関係をチェック
Derived* d = dynamic_cast<Derived*>(base_ptr);

// const_cast:const/volatileの除去(危険)
const int* cptr = &x;
int* ptr = const_cast<int*>(cptr);

// reinterpret_cast:ビットパターンの再解釈(最も危険)
int* ptr = reinterpret_cast<int*>(0x12345678);

使用指針

  • まずstatic_castを試す
  • 継承関係のダウンキャストにはdynamic_cast
  • const_castreinterpret_castは最後の手段
  • 6.3 インライン関数

    マクロ関数の代替として、インライン関数が提供されています:

    /* C言語のマクロ関数:危険 */
    #define SQUARE(x) ((x) * (x))
    SQUARE(i++);  // 副作用が2回発生
    

    // C++のインライン関数:安全
    inline int square(int x) {
        return x * x;
    }
    square(i++);  // 副作用は1回のみ
    

    ---

    まとめ

    本章で学んだこと:

  • iostream: 型安全で拡張可能な入出力
  • 名前空間: 名前衝突を防ぐグループ化機能
  • 参照: ポインタより安全なエイリアス
  • const: 不変性を保証する修飾子
  • 関数オーバーロード: 同名関数の多重定義
  • 新しいキャスト: より安全な型変換
  • これらの機能は、Cからの改善として設計されました。次章では、C++の中核機能であるクラスとオブジェクトについて学びます。

    ---

    練習問題

  • std::coutprintfの違いを3つ挙げ、それぞれの利点を説明してください。
  • 以下のコードの問題点を指摘し、修正してください:
void swap(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
}

  • 以下のconstの使い方の違いを説明してください:
const int* ptr1;
int* const ptr2;
const int* const ptr3;

  • 名前マングリングとは何か、なぜ必要なのかを説明してください。
  • 以下の関数呼び出しで、どのオーバーロードが選ばれるか答えてください:
void func(int);
void func(double);
void func(const char*);

func(42);
func(3.14);
func(0);
func("hello");