クラステンプレート
C では、 int 型 の ため の 連結 リスト、 double 型 の ため の 連結 リスト といった よう に、 悪夢 の よう な 大量 の コード が 必要 に なる こと が あり まし た。 何とか コード 量 を 減らそ う と する と マクロ に 逃げ ざる を 得 ず、 そうすると 今度 は 非常 に 読み づらく、 保守 の 難しい コード に なっ て しまい ます。 クラス テンプレート を 使う と、 たった 1つ の クラス テンプレート を 用意 する だけで、 あらゆる 型 に 対応 でき ます。 実際 には、 連結 リスト の よう に、 よく 使う データ 構造 は、 標準 ライブラリ に 用意 さ れ て いる ので、 作る 必要 すら あり ませ ん。 この 章 では、 クラス テンプレート の 作り方 と 使い 方 を 説明 し た 後、 標準 ライブラリ に 用意 さ れ て いる 便利 な クラス テンプレート を いくつ か 紹介 し ます。
template < typename テンプレート パラメータ 名 >
class クラス テンプレート 名 {
メンバ の 宣言
};通常 の クラス 定義 の 先頭 に、 テンプレート で ある こと を 表す template キーワード と、 テンプレート パラメータ の 並び を 記述 し ます。 関数 テンプレート の 記述 方法 と 同じ と 考え て 良い です。
クラステンプレートの使用例
#include < iostream >
template < typename T >
class Point {
public:
Point( T x, T y) : mX( x), mY( y) {}
inline T GetX() const { return mX; }
inline T GetY() const { return mY; }
private: T mX; T mY;
};
int main() {
const Point < int > p 1( 3, 5);
const Point < double > p 2( 4.6, 2.3);
std::cout << p1.GetX() << ", " << p1.GetY() << std::endl;
std::cout << p2.GetX() << ", " << p2.GetY() << std::endl;
}標準ライブラリのクラステンプレート
std::basic_string
文字列 を 表現 する ため に、 C では char などの 文字 型 の 配列 や ポインタ を 使用 し まし た。 C の 経験 しか ない 方 には 分かり づらい こと かも 知れ ませ ん が、 C の 文字列 は、 機能的 にも 安全 面 でも、 あるいは 表現 力 の 面 でも 貧弱 です。 ほとんど の プログラミング 言語 には、 きちんと し た 専用 の 文字列 型 が 存在 する もの です が、 C に ある のは 文字 型 だけで、 これ を 配列 の 機能 によって 複数個 並べる こと で、 文字列 らしき もの を 作っ て いる わけ です。 std:: basic_string は、「 きちんと し た 専用 の 文字列 型」 を 目指し た もの だ と いえ ます。 なお、 std:: basic_string を 使用 する には、 string という 名前 の 標準 ヘッダ を インクルード し ます。 C ++ の 標準 ヘッダ の 名前 には、 拡張子 が 付か ない の でし た。 string. h と し て しまう と、 C の 標準 ヘッダ に なっ て しまう ので 注意 し て 下さい。 実際 の ところ、 std:: basic_string は、 この 名称 の まま 使う こと は 少なく、 std:: string という 短め の 名前 で 使用 する こと が ほとんど です。
#include < iostream >
#include < string >
int main() {
std:: string s1; // デフォルトコンストラクタ。 空文字列 で 初期化
std:: string s2(" abc"); // "abc" で 初期化
std:: cout << s1 << std:: endl;
std:: cout << s2 << std:: endl;
s1 = "xyz"; // = 演算子 で 代入 できる
std:: cout << s1 << std:: endl;
s1 = s2; // = 演算子 で コピー できる
std:: cout << s1 << std:: endl;
if (s1 == s2) { // 文字列 の 内容 で 比較 できる
std:: cout << "equal" << std:: endl;
} else {
std:: cout << "not equal" << std:: endl;
} // 文字列 の 長 さを length メンバ 関数 で 取得 できる
std::string::size_ type len = s1.length();
std::cout << len << std::endl;
if (s1.empty()) { // 空 か どう か
std::cout << "empty" << std::endl;
} else {
std::cout << "not empty" << std::endl;
}
s1 += s2; // +、+= で 連結 できる
std::cout << s1 << std::endl;
char c = s1[1]; // 1 番目 の 文字 を 読み取る
s1[1] = 'X'; // 1 番目 の 文字 を 書き換える
std::cout << s 1 << std::endl;
const char* s = s1. c_ str(); // 必要 なら const char* として 取り出せる
std::cout << s << std::endl;
}実行 結果: abc xyz abc equal 3 not empty abcabc aXcabc aXcabc
std:: basic_ string を 使う 魅力 として 大きい のは、 メモリ の 管理 が 自動 化 さ れる こと です。 文字列 操作 に つきまとう、 バッファ オーバーラン の 問題 が 解消 する の です。 たとえば、 = 演算子 や += 演算子 を 使っ て、 文字列 を 書き換え たり、 連結 し たり する と、 元 の 文字列 よりも 長く なる こと が あり ます が、 この とき 確保 済み の メモリ 領域 が 足り なく なる ので あれ ば、 自動的 に 新しい メモリ が 確保 さ れ ます。 もちろん、 最終 的 に メモリ を 解放 する 役目 は、 デストラクタ に 任さ れ て いる ので、 我々 は メモリ の 確保 と 解放 について 意識 する 必要 が あり ませ ん。
std::vector
std::vector は、 動的 な 配列 を 表現 する クラス テンプレート です。 使用 する には vector という 名前 の 標準 ヘッダ を インクルード し ます。 std::vector の テンプレート パラメータ には、 動的 な 配列 の 要素 の 型 を 指定 し ます。std::vector は、 途中 で 要素 数 を 増減 さ せる こと が でき ます。 std::basic_string と 同様、 メモリ の 確保 と 解放 は 完全 に 隠さ れ て い ます から、 使用者 は 気 に する 必要 が あり ませ ん。
#include < iostream >
#include < vector >
void print_ vector( const std:: vector < int >* vec) {
const std::vector< int >::size_type size = vec->size();
for (std::vector< int >::size_type i = 0; i < size; ++ i) {
std::cout << (*vec)[ i] << std::endl;
}
}
int main() {
std::vector < int > v1; // サイズ 0、 容量 0
std::vector < int > v2( 5); // サイズ 5、 容量 5( 以上)
std::vector < int > v3 = {0, 10, 20, 30}; // サイズ 4、 容量 4( 以上)
print_vector(& v1);
print_vector(& v2);
print_vector(& v3);
std::cout << "v1: " << "size = " << v1.size() << ", capacity = " << v1.capacity() << std::endl;
std::cout << "v2: " << "size = " << v2.size() << ", capacity = " << v2.capacity() << std::endl;
std::cout << "v3: " << "size = " << v3.size() << ", capacity = " << v3.capacity() << std::endl;
for (int i = 0; i < 5; ++ i) {
v1. push_back(i); // 末尾に追加。必要に応じて容量拡張
}
print_vector(& v1);
std::cout << "v1: " << "size = " << v1.size() << ", capacity = " << v1.capacity() << std:: endl; // size メンバ 関数 で サイズ を 取得
const std::vector< int >::size_type size = v2.size();
for (std::vector< int >::size_type i = 0; i < size; ++ i) {
v2[i] = 10; // []演算子で要素アクセス
}
print_vector(& v2);
std::cout << "v2: " << "size = " << v2.size() << ", capacity = " << v2.capacity() << std::endl;
if (v1.empty()) { // 空かどうか
std::cout << "empty" << std::endl;
} else {
std::cout << "not empty" << std::endl;
}実行結果: 0 0 0 0 0 0 10 20 30
v1: size = 0, capacity = 0 v2: size = 5, capacity = 5 v3: size = 4, capacity = 4
0 1 2 3 4
v1: size = 5, capacity = 6
10 10 10 10 10
v2: size = 5, capacity = 5 not empty
std::list
std::list は、 双方向 の 連結 リスト を 表現 する クラス テンプレート です。 使用 する には、 list という 名前 の 標準 ヘッダ を インクルード し ます。std::list の 使い方 は、 std:: vector に 似 て い ます が、 データ 構造 の 特性 上、 ランダムアクセス( 特定 の 要素 を ピン ポイント で アクセス する 方法) が でき ない ので、[] 演算子 が あり ませ ん。 要素 を 1つ ずつ 辿っ て いく シーケンシャル アクセス の 動作 が 基本 になり ます。 std::list の 理解 の ため に 重要 なのは、 イテレータ( 反復 子) という 概念 です。 イテレータ は、 各 要素 へ アクセス する 動作 を、 クラス に し て 表現 し た もの です が、 きちんと し た 理解 には オブジェクト 指向 の 考え方 が 求め られる ので 本書 では 割愛 し ます。 とは いえ、 単純 な 使い方 だけ なら 覚え て しまえ ば 十分 使え ます。
#include < iostream >
#include < list >
void print_list( const std::list< int >* lst)
{
const std::list< int >::const_iterator itEnd = lst->end();
for (std::list< int >::const_iterator it = lst->begin(); it != itEnd; ++ it) {
std::cout << *it << std::endl;
}
}
int main() {
std::list< int > lst1; // 空 の リスト
std::list< int > lst2(5); // 要素数 5 の リスト
std::list< int > lst3 = {0, 10, 20, 30}; // 要素数 4 の リスト
print_list(& lst1);
print_list(& lst2);
print_list(& lst3); // begin、 end でイテレータを取得 // イテレータはポインタのように操作できる
const std::list< int >::iterator itEnd = lst2.end();
for (std::list< int >::iterator it = lst2. begin(); it != itEnd; ++it) {
*it = 10;
}
print_list(& lst 2);
lst2.push_back(0); // 末尾 に 0 を 追加
lst2.push_front(9); // 先頭 に 9 を 追加
print_list(& lst2);
std::cout << "-----" << std::endl;
lst2.pop_back(); // 末尾 の 要素 を 取り除く
lst2.pop_front(); // 先頭 の 要素 を 取り除く
print_list(&lst2);
std::cout << "-----" << std::endl;
std::cout << "size = " << lst2.size() << std::endl; // size メンバ 関数 で 要素 数 を 取得
lst2.clear(); // すべて の 要素 を 取り除く
std::cout << "size = " << lst2.size() << std::endl;
if (lst2.empty()) { // 空かどうか
std::cout << "empty" << std::endl;
} else {
std::cout << "not empty" << std::endl;
}
}実行 結果: 0 0 0 0 0 0 10 20 30 10 10 10 10 10 9 10 10 10 10 10 0 —– 10 10 10 10 10 —–
size = 5 size = 0 empty
std::list の デフォルトコンストラクタ は、 連結 リスト を 空 の 状態 で 準備 し ます。 整数 を 1つ 指定 する コンス トラクタ では、 指定 し た 数 の 分 だけ 要素 が 含ま れ た 状態 で 生成 し ます。 各 要素 は デフォルトコンストラクタ を 使っ て 初期化 さ れ ます。{ 0, 10, 20, 30} の よう な かたち での 初期化 も 可能 です。 要素 の 追加 は push_ back メンバ 関数 や push_ front メンバ 関数 を 使用 し ます。 push_back は 末尾 に、 push_front は 先頭 に 要素 を 追加 し ます。 一方、 要素 を 取り除く には、 pop_back メンバ 関数 や pop_front メンバ 関数 を 使用 し ます。 pop_back は 末尾 から、 pop_front は 先頭 から 取り除き ます。 先頭 に対する 処理 が ある ところ が std::vector との 違い で、 これ は データ 構造 の 特性 の 差 から 来る 長所 の 1つ です( 配列 は、 後続 の 要素 を ずらす 必要 が ある ため 非 効率 だ から 提供 さ れ ない)。 また、 要素 を すべて まとめ て 取り除く には、 clear メンバ 関数 を 使う と 簡単 です。 いつも の よう に、 要素 数 は size メンバ 関数 で 取得 でき ます。 戻り 値 の 型 は、 std::list の メンバ として 定義 さ れ た size_type 型 です。 また、 empty メンバ 関数 を 使って、 空かどうかを判定できます。
さて、 問題 の イテレータ です が、 これ は begin メンバ 関数 や end メンバ 関数 を 使っ て 取得 でき ます。 前者 は 先頭 の 要素 へ アクセス できる イテレータ を、 後者 は 末尾 の 要素 の 1つ 後ろ を アクセス する イテレータ を 返し ます。「 末尾 の 要素 の 1つ 後ろ」 に ある 要素 に アクセス し たら、 当然 ながら 範囲 外 アクセス に なっ て しまい ます。 これ は 終 端 判定 に 使う ため に あり ます。 イテレータ は ポインタ と 同じ よう な 使い方 が できる よう に、 似せ て 作ら れ て い ます。 ++、–、+=、-=、+、- といった 演算子 を 適用 し て、 指し示す 対象 を 変更 でき ます し、* 演算子 を 使っ て 指し て いる 要素 へ アクセス でき ます。 ==、!= の よう な 比較 演算子 を 適用 する こと も でき ます。 イテレータ の 型 は、 std:: list の メンバ として 定義 さ れ て おり、 その 名前 は iterator あるいは const_ iterator です。 iterator は 指し示し て いる 先 の 要素 を 書き換える こと が でき、 const_ iterator では 出来 ませ ん。 これ は、 通常 の ポインタ と const 付き ポインタ の 関係 性 と 同じ こと です。 いつも の よう に、 可能 で あれ ば const を 付ける べき です ので、 const_ iterator が 使える とき は こちら を 使う よう に し ましょ う。
std::stack
std::stack は その 名 の 通り、 スタック を 表現 する クラス テンプレート です。 使用 する には、 stack という 名前 の 標準 ヘッダ を インクルード し ます。
#include < iostream >
#include < stack >
int main() {
std::stack< int > s; // 空 の スタック
for (int i = 0; i < 5; ++ i) {
s.push(i); // 要素 を 追加
} // 要素 数 は size メンバ 関数 で 取得 できる
const std::stack< int >::size_type size = s.size();
for (std::stack< int >::size_type i = 0; i < size; ++ i) { // top() で、 最 上段 の 要素 を 取得 できる( 取り出さ れ ない)
std::cout << s.top() << std::endl; // pop() で、 最 上段 の 要素 を 取り除く
s.pop();
} if (s. empty()) { // 空 か どう か
std::cout << "empty" << std::endl;
} else {
std::cout << "not empty" << std::endl;
}
}実行結果: 4 3 2 1 0 empty
std::stack に 要素 を 追加 する には push メンバ 関数 を 使用 し ます。 また、 要素 を 取り除く には pop メンバ 関数 を 使用 し ます。 スタック なので、 要素 は 必ず 最 上段 に 積ま れ、 最 上段 から 取り除か れ ます。 要素 数 は size メンバ 関数 で 取得 でき ます。 戻り 値 の 型 は、 std::stack の メンバ として 定義 さ れ た size_type 型 です。 また、 empty メンバ 関数 を 使っ て、 空 か どう かを 判定 でき ます。 top メンバ 関数 は、 最 上段 に ある 要素 を 返し ます が 取り除き は し ませ ん。 一方 で pop メンバ 関数 は、 要素 を 取り除き ます が 返し ては くれ ませ ん。 なお、 これら の 関数 は、 スタック が 空の とき に 呼び出す と、 未 定義 の 動作 に なっ て しまい ます。 空 に なっ て いる 可能性 が ある とき には、 size メンバ 関数 や empty メンバ 関数 などを 使っ て 確認 を 行っ てから、 呼び出す よう に し て 下さい。


コメント