C++ の成り立ちと標準化
C++ 言語の開発は 1979 年、ベル研究所において Bjarne Stroustrup 氏によって始まりました。当時の複雑なシステム開発、特にシミュレーションや OS 構築において、C 言語の拡張性や保守性に課題を感じたことがきっかけです。
1983 年までに、C 言語にオブジェクト指向の要素であるクラスや継承などの概念を取り入れた「C with Classes」が設計され、正式に C++ と命名されました。その後、学界や産業界での採用が進み、標準ライブラリやテンプレートの整備が行われました。
標準化作業は 1989 年に ANSI と ISO の合同委員会によって開始され、1994 年に最初の草案が提出されました。この過程で、HP 实验室によって開発された STL(Standard Template Library)の採用が決定し、言語仕様の大幅な拡張となりました。最終草案は 1997 年に承認され、1998 年に ISO/ANSI 標準として正式に発行されました。
基本的なプログラム構造
C 言語と C++ における最も基本的な出力プログラムの違いを確認します。C 言語では標準入出力ヘッダーを使用しますが、C++ ではストリームライブラリを利用します。
C 言語の例:
#include <stdio.h>
int main(void) {
printf("System Initialized\n");
return 0;
}
C++ の例:
#include <iostream>
int main() {
std::cout << "System Initialized" << std::endl;
return 0;
}
命名空間(namespace)の仕組み
命名空間の必要性
大規模なプロジェクトでは、変数や関数、クラス名がグローバルスコープに存在すると名前の衝突が発生するリスクがあります。命名空間は識別子を論理的にグループ化し、名前汚染や衝突を防ぐために導入されました。
定義とスコープ
namespace キーワードを用いて定義し、中括弧内にメンバーを記述します。これはグローバルスコープとは独立した領域を生成し、同じ名前の変数を異なる空間で定義することが可能になります。
C++ におけるスコープには、関数内、グローバル、命名空間、クラスなどがあり、コンパイラはこれらの域に基づいて識別子を解決します。命名空間はグローバル領域でのみ定義可能であり、ネストさせることもできます。また、複数のファイルにまたがって同名の命名空間を定義した場合、それらは同一の空間として結合されます。
アクセス方法
命名空間内のメンバーにアクセスするには、主に以下の 3 通りの方法があります。
- 完全限定名での指定:
Namespace::Member形式でアクセス。プロジェクトでは推奨される方法です。 - using 宣言: 特定のメンバーのみを展開して使用。
- using ディレクティブ: 命名空間内のすべてのメンバーを展開。
using namespace std;など。小規模な練習では便利ですが、大規模開発では衝突リスクのため推奨されません。
標準入出力ストリーム
<iostream> ヘッダーは、標準入力ストリーム(std::cin)と標準出力ストリーム(std::cout)を定義しています。これらはそれぞれ istream クラスと ostream クラスのオブジェクトです。
出力時に使用される std::endl は改行文字の挿入に加え、出力バッファのフラッシュを行います。演算子 << は流し込み(挿入)、>> は取り出し(抽出)を表します。
C 言語の printf/scanf と異なり、C++ のストリームは変数の型を自動的に識別して処理を行います。これは関数オーバーロードによって実現されており、ユーザー定義型への拡張性も高いのが特徴です。これらは std 命名空間に属するため、適切なアクセス指定が必要です。
デフォルト引数
関数の宣言または定義において、パラメータに初期値を設定することができます。呼び出し時に実参が省略された場合、このデフォルト値が使用されます。
デフォルト引数には「完全デフォルト」と「一部デフォルト」があります。一部のみ設定する場合は、右側の引数から連続して設定する必要があり、飛び飛びに設定することはできません。また、関数呼び出し時には左から順に実参を渡す規則があります。
関数の宣言と定義が分離している場合、デフォルト値は宣言側にのみ記述し、定義側には記述しないのが規則です。
関数オーバーロード
C++ では、同一スコープ内で同名の関数を複数定義することが可能です。ただし、パラメータの型や个数、順序が異なる必要があります。これにより、同じ機能名で異なるデータ型を処理する柔軟な設計が可能になり、多態性の一種として機能します。C 言語では同一スコープ内の同名関数は許可されていません。
参照(Reference)の概念
定義と特性
参照は既存の変数に対する別名であり、新しいメモリ領域は確保されません。元の変数と同じアドレスを共有します。
型名 & 別名 = 対象変数;
記号 & はアドレス演算子と共用されていますが、文脈によって区別されます。参照には以下の特徴があります。
- 定義時に必ず初期化が必要。
- 1 つの変数に対して複数の参照を作成可能。
- 一度バインドされた対象を変更することはできない。
const 参照
const 修飾された参照は、参照先の値を変更できないようにします。これは権限の縮小となるため、通常の変数にも const 参照をバインドできますが、その逆(const 変数に通常参照)は権限拡大となるため許可されません。
int main() {
const int maxScore = 100;
const int &scoreRef = maxScore; // 可能
int current = 50;
const int &readOnly = current; // 可能(権限縮小)
const int &tempRef = (current + 10); // 一時オブジェクトへの const 参照は可能
return 0;
}
式の評価結果など、名前を持たない一時オブジェクトが存在する场合、C++ ではそれらは const 性を持ちます。そのため、一時オブジェクトを参照する場合は const 参照を使用する必要があります。
ポインタと参照の違い
両者は類似した機能を持ちますが、以下の点で異なります。
- メモリ: 参照は別名でありメモリを占有しない(概念上)、ポインタはアドレスを格納する変数。
- 初期化: 参照は必須、ポインタは任意(ただし未初期化は危険)。
- 再バインド: 参照は不可、ポインタは可能。
- アクセス: 参照は直接、ポインタは逆参照(*)が必要。
- sizeof: 参照は参照先の型サイズ、ポインタはアドレスサイズ(環境依存)。
- 安全性: 参照は null になり得ず、ポインタは null や野指針のリスクがある。
inline 関数
inline 修飾子を用いると、コンパイラに対して関数呼び出し箇所に関数本体を展開するよう提案できます。これによりスタックフレームの生成コストが削減され、実行効率が向上する可能性があります。
ただし、これはあくまでコンパイラへのヒントであり、実際の展開は実装に依存します。複雑な関数や再帰関数では無視されることが多いです。C 言語のマクロ関数に比べ、型チェックが行われデバッグが容易であるため、マクロの代替として利用されます。
inline 関数は定義が複数の翻訳単位で共有される必要があるため、宣言と定義を分離せずヘッダーファイルなどに記述するのが一般的です。
nullptr の導入
従来の C++ において、null ポインタを表す NULL マクロは整数の 0 または void* に定義されていました。これにより、関数オーバーロード解決において意図しないバージョンが呼び出される問題が発生することがありました。
// NULL が 0 と定義されている場合、int 版が呼ばれるリスクがある
void func(int);
void func(char*);
func(NULL);
C++11 では nullptr キーワードが導入されました。nullptr は専用の型を持ち、ポインタ型への暗黙変換は可能ですが、整数型へは変換できません。これにより、型安全な null 表現が可能になりました。