ViVi Home > 技術文書
> さくさく理解する C/C++ コンソールアプリ入門 >
手を動かしてさくさく理解する 文字列クラス std::string 入門
std::string とは C++ で標準に使用できる便利な文字列クラスでござるぞ。
C/C++ ではダブルクォートで文字列リテラルを表し、通常配列に文字を格納し加工することもできる。
strlen() など文字列の状態を返したり、sprintf() などの文字列を作成する関数などが用意されている。
しかし、リテラル文字列は動的な操作に不向きで、柔軟性に欠ける。
そのため、C++では動的にサイズを変更可能な文字列クラス std::string が導入された。
通常文字列と同じように [] 演算子で値を参照・代入することはもちろん、サイズ情報等の取得やデータの挿入削除なども可能だぞ。
std::string は std::vector とよく似ている。size(), push_back() など、用意されているメンバ関数もほぼ同じで、
文字列特有のメンバ関数がいくつか用意されている。
vector との大きな違いは、文字列の最後にヌル文字('\0')が格納されている点だ。
※ 本稿では、初級者には難しいと思うので、どうしても必要な場合以外では「イテレータ」については言及しない。 イテレータを使用しなくても string は充分有用なクラスだと考えている。
本章では、string オブジェクトの宣言・初期化方法について説明する。
string オブジェクトを宣言するには、空stringを宣言する、サイズを指定して宣言する、サイズと全てのデータを指定して宣言する、 元データを指定して宣言する、他のオブジェクトを元にして宣言する、などの様にいくつもの方法がある。 本章ではそれらの方法を順次解説する。
ローカル変数またはグローバル変数として string オブジェクトを宣言すると、オブジェクトが生成され利用可能になる。 これらはスコープを外れると自動的に破棄される。
通常のクラスなので new で生成し、delete で破棄することも可能だ。
string は C++標準のライブラリであり、「#include <string>」を記述することで利用可能になる。
名前空間は「std」なので、使用の度に「std::」を前置するか、または「using namespace std;」を記述しておく。
#include <string> // ヘッダファイルインクルード int main() { std::string str; // ローカル変数として、str を生成 ..... }
#include <string> // ヘッダファイルインクルード using namespace std; // 名前空間指定 int main() { string str; // ローカル変数として、v を生成 ..... }
std::string オブジェクトを生成するには「std::string 文字列名;」 と記述する。
文字列名とは、宣言する文字列の名前のことだ。変数名と言ってもよい。
string はクラスなので、それを実体化したものはオブジェクトまたはインスタンスと呼ばれる。
下記は、string 型のオブジェクト str を宣言している例だ。
std::string str; // string 型のオブジェクト str の宣言
上記のコードのように、コンストラクタ引数が何も無い場合、空文字列が生成される。空文字列とは文字をひとつも持たない文字列のことである。
空文字列だけでなく、文字数・文字を指定して文字列を生成することも可能だ。
「std::string 文字列名(文字数, '文字');」と記述すると、指定された文字数のメモリを確保し、中身を指定された文字で初期化する。
std::string str(10, 'a'); // 文字数10、全ての要素を 'a' で初期化(すなわち "aaaaaaaaaa")
上記の方法では、文字が全て同じ文字列でないと初期化できない。
コンストラクタ引数に文字列リテラルを指定すると、その文字列で初期化することが出来る。
std::string str("abc"); // "abc" で初期化
もちろんポインタを使用してもよい。
char *data = "hoge"; std::string str(data); // "hoge" で初期化
「std::string 文字列名(char* first, char* last);」と記述すると、first から last が指す先までの文字列で初期化する。
厳密に言うと、last は最後の文字の次を指す。[first, last) の範囲を元に、文字列を初期化する。
通常文字列でデータを指定し、それを元に文字列を構築出来る。
char org_str[] = {'4', '6', '5'}; // 元データ std::string str(org_str, std::end(org_str)); // 元データから文字列を生成
文字列リテラルの一部から文字列を作ることもできるぞ。
char *org_str = "abc123xyz"; // 元文字列 std::string str(org_str + 3, org_str + 6); // 元文字列の一部から文字列(この場合は"123")を生成
コピーコンストラクタとは、同じ型のオブジェクトを渡され、それと同じ内容のオブジェクトを生成するコンストラクタのことである。
「std::string 文字列名(コピー元文字列名);」と記述する。
std::string org("123"); std::string x(org); // コピーコンストラクタ
上記のコードは org をコピーするので "123" という文字列をもつ x を生成する。
演習問題:解答例
[] 演算子(operator[](int)) を使って、通常の文字列と同じように、i 番目の文字の参照・代入が可能。
operator[](int) と聞くと身構える人がいるかもしれないが、「str[10]」の様に、普通の文字列の要素にアクセスする時と同じ記述だ。
恐れることは何も無い。
下記は、i 番目の文字の参照と代入例。普通の参照・代入とまったく同じでしょ。
std::string str("31415926535"); for (int i = 0; i < 10; ++i) std::cout << str[i] << " "; // str の i 番目の要素を表示
const int SZ = 10; // 文字数 std::string str(SZ, 'a'); // 10 文字の 'a' から成る str を生成 for(int i = 0; i < SZ; ++i) str[i] = 'b' + i; // 文字を 'b', 'c' ... に設定 std::cout << str << "\n";
演習問題:解答例
+演算子(operator+())により、文字列を結合することができる。
std::string str1("12"); std::string str2("xyz"); std::string str3 = str1 + str2; // + 演算子により str1 と str2 を結合 std::cout << str3 << "\n";
演習問題:
文字を末尾に追加するには push_back(文字); または += 演算子を使う。
std::string str; // 空の文字列を生成 str.push_back('Z'); // 末尾に 'Z' を追加 str += '0'; // 末尾に '0' を追加 std::cout << str << "\n";
push_back() は文字しか追加できないが、+= 演算子は文字列を追加することも出来るし、 コードが見やすいので、通常は += 演算子の方が使用される。
先頭に文字を挿入したい場合は、前節に出てきた + 演算子を使用する。
std::string str("xyz"); str = "12" + str; // str の先頭に "12" を挿入と同じ std::cout << str << "\n";
push_back(), += 演算子共に、処理時間は O(1) で高速。O(式) は処理時間を表す数学的な記法。「ビッグ・オー記法」と呼ばれる。 O(1) は配列要素数に依らず常に一定時間で処理出来るという意味で、高速なのだ。
任意の位置に文字を挿入したい場合は insert(iterator, 文字) を使う。
insert(iterator, 文字) は、第1引数に挿入する場所へのイテレータを、第2引数に挿入する文字を指定する。
イテレータとは抽象化されたポインタのこと。ちゃんとした説明は長くなるし、初級者には理解が大変なのでここでは省略する。
i 番目に挿入したい場合は「文字列名.begin() + i」と書くと覚えて欲しい。
std::string str("1234"); // "1234" という文字列 str.insert(v.begin() + 2, '-'); // [2] の位置に '-' を挿入 std::cout << str << "\n"; // 結果は "12-34" となる
insert() の処理時間は O(N) 。O(N) とは配列要素数に比例して処理時間を要するという意味。
文字数が100倍になると、処理時間も100倍になる。
データをずらす処理を行う必要があるので、その分処理時間を食うというわけだ。
それに対して push_back(), operator+=() は O(1) なので、データ数がいくら増えても常に一定時間で処理が終わる。
ただし、文字数が少ない(数10文字程度)場合であれば、string は十分高速なので、処理時間は問題にはならない。
演習問題:解答例
最後の文字を削除したい場合は pop_back() を用いる。
std::string str("12345"); str.pop_back(); // 末尾文字(この場合は '5')を削除 std::cout << str << "\n";
pop_back() も push_back() 同様、処理時間は O(1) と高速である。
空の string に対して、pop_back() を実行するとデバッグモード・リリースモード共に例外が発生する(VS2010、VS2013)。
次の章で説明する empty() または size() で string が空でないことを確認するようにした方がよい。
pop_back() は何故か void 型で、削除した値を返さない。 なので、最後の要素の値を取り出して削除したい場合は、後で説明する back() を使用する。
任意の位置の文字を削除したい場合は erase(iterator) を使用する。
std::string str("01234"); str.erase(v.begin() + 2); // 3番目の要素('2')を削除 std::cout << str << "\n";
erase() で途中の文字を削除すると、サイズが一つ減り、削除された要素以降の文字がひとつずつ前に移動される。
イテレータは前節で言及したように、削除する要素位置を示すもので、i 番目の要素は「文字列名.begin() + i」で指定する。 本稿ではイテレータに関する詳しい説明は行わない。
演習問題:解答例
ここまで、文字列オブジェクトの宣言、文字の参照・代入・追加・削除方法について説明した。
それらにより文字列クラスを使うことができるはずだ。
実は文字列クラスには上記以外にも便利な機能がメンバ関数としていくつも用意されている。
文字列が空かどうかをチェックしたり、文字数を調べたり、文字列置換することもできる。
これらは通常文字列に対する明らかなアドバンテージである。
「bool empty()」は文字列が空かどうかを判定する関数。
次に出てくる size() を使って、size() == 0 と判定するのと同等だ。
が、コンテナクラスによっては size() 計算よりも empty() の方が高速な場合がある。
なので、string などのコンテナクラスに対しては empty() を使うことが推奨されている。
「size_t size()」、「size_t length()」は、文字数を返す関数。これらは同じものである。
通常文字列の文字数は「length()」だが、vector などとの互換性のために「size()」があるのだと推測される。
※ size_t はサイズを表す型で、符号なし整数の組み込み型である。ちなみに、sizeof() も size_t 型を返す。
「size_t capacity()」は、文字列が確保しているメモリに入る文字数、すなわち現在のデータ領域容量を返す関数。
string は実際の文字数より若干大目にメモリを確保している。これは文字が増えた時、常にメモリを再確保するのではなく、
余裕を持たせておくことで、頻繁なメモリ確保とデータのコピーを避けるためだ
(参照:文字列クラス 演習問題の図)。
「front()」は先頭文字を返す関数。「文字列名[0]」と記述するのと同等。
「back()」は末尾文字を返す関数。「文字列名[文字列名.size() - 1]」と記述するのと同等。
front() はあまりありがたみが無いが、back() はタイプ数が大幅に減るし、分かりやすいので存在価値が高いぞ。
「substr(位置、サイズ)」は、文字列を切り出すメンバ関数。
std::string str("012345"); std::string sub = str.substr(2, 3); // [2] の位置から3文字切り出す std::cout << sub << "\n";
「c_str()」、「data()」は文字列データ先頭アドレスを返す関数。「&文字列名[0]」と記述するのと同等。
std::string は導入当初、数値との相互変換など機能が不足していた。
そこで、これらのメンバ関数を使って従来の C の文字列関数を呼ぶことで、その欠点を補ってきたというわけだ。
string は通常文字列と同じようにデータ領域が連続したアドレスだということが保証されている。
なので、文字列アドレスを他の関数に渡して処理することも可能だ。
「clear()」は文字列を空にする関数。
サイズを0にするだけ。メモリが解放されるわけではない。
メモリを解放したい場合は後で説明する shrink_to_fit() または swap() イディオムを使用する。
「resize(size_t sz)」は文字数を指定サイズに設定する関数。
sz が現在のキャパシティを超えていれば、メモリがアロケートされ,文字列がコピーされる。
サイズが増えた部分の値を指定したい場合は、「resize(size_t sz, 文字)」と、第2引数で文字を指定する。
サイズが減っても、その分のメモリが解放されるわけではない。
不要になったメモリを解放したい場合は後で説明する shrink_to_fit() または swap() イディオムを使用する。
「reserve(size_t sz)」はキャパシティを指定する関数。
文字列を大量に追加する場合、メモリのアロケートと文字列コピーが多く呼ばれる場合がある。
そうなるとパフォーマンスが低下する場合があるので、追加する文字の数が予めわかっていれば、reserve() でメモリを確保する方がパフォーマンス的に好ましい。
std::string v; v.reserve(10000); // 予め1万個分の領域を確保しておいた方が高速 for(int i = 0; i < 10000; ++i) v.push_back(i);
「swap(文字列名)」は引数で指定されたオブジェクトと内容を入れ替える関数。
std::string v, z; v, z に文字列を設定 v.swap(z); // v と z の内容を入れ替える
先に書いたように、clear() を行ってもメモリは解放されないので、C++11以前では以下のようなコードが使用されていた。
std::string str; str に文字列を設定 std::string().swap(str); // 空のテンポラリ文字列を生成し、中身を入れ替える
「shrink_to_fit()」は、C++11 で追加された関数で、キャパシティを現在のサイズの値にし、余分なメモリを解放する関数。
C++11以前では、shrink_to_fit() がなかったので以下のようなイディオムが使用されていた。
std::string str; str に文字列を設定 std::string(str).swap(str); // v をコピーしたテンポラリ文字列を生成し、中身を入れ替える
「find(検索文字列, 開始位置=0)」は指定された検索文字列を、指定位置から末尾に向かって検索し、
マッチした場合はその位置を返す(型は size_t)。
開始位置を省略した場合は、先頭から検索する。
検索文字列が無かった場合は size_t の最大値を返す( -1 に相当する値)。
std::string str("abcabc"); std::cout << (int)str.find("b") << "\n"; // マッチする場合 std::cout << (int)str.find("b", 3) << "\n"; // 3文字目から末尾に向かって検索 std::cout << (int)str.find("x") << "\n"; // マッチしない場合
find には上記以外にもいくつかのコール方法がある。詳しくは ここ を参照。
「rfind(検索文字列, 検索開始位置=文字列長)」は指定された検索文字列を、指定位置から先頭に向かって検索し、
マッチした場合はその位置を返す(型は size_t)。
開始位置を省略した場合は、末尾から検索する。
検索文字列が無かった場合は size_t の最大値を返す( -1 に相当する値)。
std::string str("abcabc"); std::cout << (int)str.find("b") << "\n"; // マッチする場合 std::cout << (int)str.find("b", 3) << "\n"; // 3文字目から先頭に向かって検索 std::cout << (int)str.find("x") << "\n"; // マッチしない場合
rfind には上記以外にもいくつかのコール方法がある。詳しくは ここ を参照。
「replace(位置, 置換文字数, 置換文字列)」は指定位置から指定文字数を置換文字列で置き換える。
下記は、"12345" の 1文字目から2文字を "xyz" に置き換える例。
std::string str("12345"); str.replace(1, 2, "xyz"); std::cout << str << "\n";
演習問題:解答例
通常、std::string はテンプレートライブラリになっており、ソースが公開されている。
ソースを探しだして、読んでみると勉強になるかもしれない。
ただし、かなりレベルが高いコードなので、ちゃんと理解するのはそれなりの能力と知識と経験が必要だ。
上級者でないと無理だ。ちょっと見て理解不能と思ったら、すぐに勇気ある撤退をしよう。
文字列クラス String を実装する演習問題も用意している。
これをやり遂げれば、string の中身がどうなっているか、ある程度想像がつくようになるので、ぜひ挑戦してみて欲しい。
std::string str0; std::cout << "'" << str0 << "'\n";
std::string str("xyzzz"); std::cout << "'" << str << "'\n";
std::string a100(100, 'a'); std::cout << "'" << a100 << "'\n";
char *ptr = "3.14159"; std::string str(ptr + 2, ptr + 5); std::cout << "'" << str << "'\n";
解答例:std::vector(要素数, 要素) との統一性・一貫性のためだと考えられる。
std::string str(100, ' '); for(int i = 0; i < 100; ++i) str[i] = rand() % 26 + 'a'; std::cout << str << "\n";
std::string str; for(int i = 0; i < 10; ++i) str += 'a' + i; std::cout << str << "\n";
std::string str("1234567890"); // "1234" という文字列 str.insert(v.begin() + 5, '*'); // [5] の位置に '*' を挿入 std::cout << str << "\n";
std::string str("1234x"); str.pop_back(); std::cout << str << "\n";
std::string str("012345"); str.erase(str.begin() + 3); // [3] の文字:'3' が消えるはず std::cout << str << "\n";
std::string str; cout << str.empty() << "\n"; cout << str.size() << "\n"; cout << str.capacity() << "\n";
std::string str("314"); cout << str.empty() << "\n"; cout << str.size() << "\n"; cout << str.capacity() << "\n";
std::string str("314"); cout << str.front() << "\n"; cout << str.back() << "\n";
std::string str("314"); int *data = str.data(); int *c_str = str.c_str();
std::string str("123"); cout << atoi(str.c_str()) << "\n";
std::string str("314"); str.clear(); cout << str.empty() << "\n"; cout << str.size() << "\n"; cout << str.capacity() << "\n"; cout << str << "\n";
std::string str("314"); str.resize(8); cout << str.empty() << "\n"; cout << str.size() << "\n"; cout << str.capacity() << "\n"; cout << str << "\n";
std::string str("314"); str.resize(8, 'a'); cout << str.empty() << "\n"; cout << str.size() << "\n"; cout << str.capacity() << "\n"; cout << str << "\n";
std::string str("314"); str.resize(1); cout << str.empty() << "\n"; cout << str.size() << "\n"; cout << str.capacity() << "\n"; cout << str << "\n";
std::string str("314"); str.reserve(8); cout << str.empty() << "\n"; cout << str.size() << "\n"; cout << str.capacity() << "\n"; cout << str << "\n";
std::string str("314"); str.reserve(1); cout << str.empty() << "\n"; cout << str.size() << "\n"; cout << str.capacity() << "\n"; cout << str << "\n";
std::string str("314"); str.resize(1); cout << str << "\n"; cout << str.empty() << "\n"; cout << str.size() << "\n"; cout << str.capacity() << "\n"; str.shrink_to_fit(); cout << str.empty() << "\n"; cout << str.size() << "\n"; cout << str.capacity() << "\n";
std::string str("314"); std::string z("xyzzz"); str.swap(z); std::cout << str << "\n"; std::cout << z << "\n";