第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_castとreinterpret_castは最後の手段
6.3 インライン関数
マクロ関数の代替として、インライン関数が提供されています:
/* C言語のマクロ関数:危険 */
#define SQUARE(x) ((x) * (x))
SQUARE(i++); // 副作用が2回発生
// C++のインライン関数:安全
inline int square(int x) {
return x * x;
}
square(i++); // 副作用は1回のみ
---
まとめ
本章で学んだこと:
これらの機能は、Cからの改善として設計されました。次章では、C++の中核機能であるクラスとオブジェクトについて学びます。
---
練習問題
std::coutとprintfの違いを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");