C++編【標準ライブラリ】 第2章 string

この章の概要

この章の概要です。

string

C++標準ライブラリには、文字列を安全かつ便利に扱える string という機能があります。 これを使うには、string という名前の標準ヘッダをインクルードする必要があります

string の正体は、basic_string というクラステンプレート(【言語解説】第12章参照)に 対する typedef(C言語編第26章)です。 つまり、

typedef std::basic_string<char> string;

このように定義されています。

実際には、テンプレート引数は3つあるので、 typedef basic_string<char, char_traits<char>, allocator<char> > string; というのが正確な定義になります。

ワイド文字列用に、wstring も定義されていますが、これは、

typedef std::basic_string<wchar_t> wstring;

となっている訳です。 つまり、string にせよ、wstring にせよ、そこに含まれる機能はどちらも std::basic_string が提供しているものですから、 まったく同じ使い方ができます。 C言語のように、char型配列なら strcpy関数(⇒リファレンス)を、 wchar_t型配列なら wcscpy関数(⇒リファレンス)をといった使い分けも必要なくなります。

この章では、std::string を使って解説しますが、std::wstring であっても扱い方は変わりません。

C++11(UTF-8 のサポート)

C++11 の std::string は、UTF-8 をサポートします。

C++11 では、u8"あいうえお" のように、u8 というプリフィックスを付けることで、 UTF-8 の文字列リテラルであることが表現されます。 u8'あ' のような、UTF-8 の文字リテラルに対するプリフィックスは存在しません(【言語解説】第4章)。

u8"あいうえお" は char型の配列なので、そのまま std::string に渡すことができます。 これを使えば、UTF-8 を取り扱えます。

#include <iostream>
#include <string>

int main()
{
	std::string s(u8"あいうえお");

	std::cout << s << std::endl;
}

実行結果

あいうえお

VisualC++ 2013 は、u8プリフィックスに対応していませんが、 2015 では対応しています。 clang 3.7 では使用できます

C++11(UTF-16、UTF-32 のサポート)

C++11 では、u16stringu32string が追加されました。 これは、次のように定義されています。

typedef std::basic_string<char16_t> u16string;
typedef std::basic_string<char32_t> u32string;

char16_t型は UTF-16、char32_t型は UTF-32 を扱うための型で、 C++11 で新たに追加された標準の型です(【言語解説】第4章)。 u16string や u32string を使うことで、これらの型による文字列操作を、 従来の string や wstring と同じ方法で扱えるようになります。

VisualC++ 2013 では、これらの型が定義されていますが、正式に対応済みとはされておらず、 事実上、使用できない状態になっています(例えば、char16_t型の文字リテラルは u'a' のような構文で表現されるはずですが、この表記自体に対応できていません)。 2015 では対応されています。
clang 3.7 は対応しています

初期化

変数宣言時に、初期値を与えればその値で初期化されます。 初期値を与えなければ、空の文字列で初期化されます(不定な状態にはなりません)。

#include <iostream>
#include <string>

int main()
{
	std::string str1;           "" で初期化
	std::string str2 = "str2";  "str2" で初期化
	std::string str3("str3");   "str3" で初期化
	
	std::cout << str1 << "\n"
	          << str2 << "\n"
	          << str3 << std::endl;
}

実行結果


str2
str3

変数str2 では、C言語の頃からお馴染みの方法で初期値を与えていますが、変数str3 のように書くこともできます。 いずれも同じ意味になりますが、この後で取り上げるように、( ) を使うと複数のパラメータを渡すことができるため、 ( ) で統一する方が良いという考え方もあります。

#include <iostream>
#include <string>

int main()
{
	const char* const CSTR = "abcdefg";

	std::string str1(5, 'a');             // 5個の 'a' で初期化
	std::string str2(CSTR, 5);            // CSTR の先頭 5文字分で初期化
	std::string str3(str2);               // str2 と同じ内容で初期化(コピー)
	std::string str4(str2, 1);            // str2[1] 以降の文字列で初期化
	std::string str5(str2, 1, 3);         // str2[1] からの 3文字分で初期化
	std::string str6(&CSTR[3], &CSTR[6]); // CSTR[3]〜CSTR[5] の範囲の文字列で初期化
	
	std::cout << str1 << "\n"
	          << str2 << "\n"
	          << str3 << "\n"
	          << str4 << "\n"
	          << str5 << "\n"
	          << str6 << std::endl;
}

実行結果

aaaaa
abcde
abcde
bcde
bcd
def

str1 は、第2引数で指定した文字を、第1引数で指定した個数分並べた文字列で初期化します。
str2 は、第1引数に指定した文字列を、第2引数で指定した文字数分だけ先頭から切り出したもので初期化します。
str3 は、別の std::string を指定し、その内容と同じ結果になるように初期化します。
str4 は、第1引数に別の std::string を指定し、第2引数で指定した位置以降の部分文字列で初期化します。
str5 は、第1引数に別の std::string を指定し、第2引数で指定した位置から、第3引数で指定した文字数分だけを切り出して初期化します。
str6 は、第1引数に別の文字列の開始位置を指定し、第2引数に終了位置(この位置の手前までが対象)を指定し、その範囲内の文字列で初期化します。 この「範囲」という部分は、C++標準ライブラリでは一貫した法則・方法があります。 これについては、「イテレータ」の項で改めて取り上げます。

覚えようとすると大変ですが、幾つか方法が存在することを知っておき、 状況に応じて、良いパターンが無いかを調べて使えばいいでしょう。

C++11(std::initializer_list版)

C++11 では、std::initializer_list(初期化リスト)が使えるようになりました。 これによって、次のような初期化が可能になります。

#include <iostream>
#include <string>

int main()
{
	std::string str = {'a', 'b', 'c'};
	
	std::cout << str << std::endl;
}

実行結果

abc

char型配列を初期化するときにこのような記述ができますが、それと同じ方法が使えます。

VisualC++ 2013/2015、clang 3.7 のいずれも対応しています

代入

std::string型同士での代入は、単に代入演算子を使うだけです。 strcpy関数(⇒リファレンス)の出番はありません。

#include <iostream>
#include <string>

int main()
{
	std::string str1("abcde");
	std::string str2;

	str2 = str1;
	std::cout << str2 << std::endl;
	
	str2 = "xyz";
	std::cout << str2 << std::endl;
	
	str2 = 'a';
	std::cout << str2 << std::endl;
}

実行結果

abcde
xyz
a

このように、代入元は std::string型でもいいですし、char型の配列、1つの文字でも構いません。

また、assign関数を使って、もう少し複雑な方法で代入する文字列を指定することもできます。

#include <iostream>
#include <string>

int main()
{
	std::string str1("abcde");
	std::string str2;

	str2.assign(str1);         // 他の std::string から代入
	std::cout << str2 << std::endl;

	str2.assign(str1, 1, 3);   // str1[1] から 3文字を代入
	std::cout << str2 << std::endl;

	str2.assign("xyz");        // const char*型から代入
	std::cout << str2 << std::endl;

	str2.assign("xyzxyz", 4);  // "xyzxyz" の先頭から 4文字を代入
	std::cout << str2 << std::endl;

	str2.assign(5, 'a');       // 5個の 'a' を代入
	std::cout << str2 << std::endl;

	str2.assign(&str1[0], &str1[3]);  // 指定範囲を代入
	std::cout << str2 << std::endl;
}

実行結果

abcde
bcd
xyz
xyzx
aaaaa
abc

詳細な解説はしないので、コメントと実行結果をご確認ください。 なお、最後の範囲指定タイプについては、「イテレータ」の項で、改めて考え方を取り上げます。

C++11(std::initializer_list版)

C++11 の =演算子および assign関数には、std::initializer_list型を引数に持ったものが追加されています。

#include <iostream>
#include <string>

int main()
{
	std::string str;
	
	str = {'a', 'b', 'c'};
	std::cout << str << std::endl;

	str.assign( {'x', 'y', 'z'} );
	std::cout << str << std::endl;
}

実行結果

abc
xyz

VisualC++ 2013/2015、clang 3.7 のいずれも対応しています

ところで、代入する文字列の長さを特に気にしていません。 char型配列を使う場合、配列サイズをオーバーすれば、当然問題が起こる訳ですが、std::string の場合はこの心配はありません。 なぜなら、std::string は必要に応じて、自動的にメモリを割り当ててくれるからです。 代入前よりも長い文字列を代入しても大丈夫なのです。
一方で、単なる代入のように見える箇所で、動的なメモリ割り当てが行われていることがあり得るので、 処理負荷を気にしないといけないこともあります。 この辺りの詳細は、「サイズと容量」の項で改めて取り上げます。


代入先が char型の配列の場合には、copy関数を使います

#include <iostream>
#include <string>

int main()
{
	std::string str1("abcde");
	char cstr[10];
	std::size_t len;

	len = str1.copy(cstr, sizeof(cstr) - 1);
	cstr[len] = '\0';
	std::cout << cstr << std::endl;

	len = str1.copy(cstr, sizeof(cstr) - 1, 3);
	cstr[len] = '\0';
	std::cout << cstr << std::endl;
}

実行結果

abcde
de

copy関数は、第1引数に代入先の配列を指定し、第2引数にコピーされる最大文字数を指定します。 第3引数はコピーを開始する位置を指定します。 第3引数に関しては省略でき、その場合は先頭からコピーが行われます。 また、戻り値として実際にコピーされた文字数が返されます。
面倒なことに、コピー先の文字列の末尾に '\0' は付加されません。 そのため、C言語の文字列形式として有効な形にするためには、戻り値を受け取って、末尾に '\0' を手動で付けてやる必要があります。
copy関数は、バッファオーバーフローの可能性を持っているため、コンパイラによっては警告を出します。


代入先が、単体の char型の場合には、添字アクセスを行えばいいです。

#include <iostream>
#include <string>

int main()
{
	std::string str1("abcde");
	char c;

	c = str1[2];
	std::cout << c << std::endl;
}

実行結果

c

通常の [] を使った添字アクセスが行えるので、char型の配列のような感覚で使うことができます。

もう1つの方法として at関数があります。 引数は、[] に入れるものと同じで添字そのものですが、 at関数の場合は、範囲外アクセスを検出して out_of_range例外を投げるという違いがあります。 例外については、【言語解説】第32章を参照して下さい。

#include <iostream>
#include <string>
#include <stdexcept>

int main()
{
	std::string str1("abcde");
	char c;

	try {
		c = str1.at(5);
	}
	catch (std::out_of_range) {
		std::cerr << "範囲外アクセス" << std::endl;
		return 1;
	}
	std::cout << c << std::endl;
}

実行結果

範囲外アクセス

[]演算子を比べて安全性を高められる反面、[]演算子よりは低速になります。

連結

+=演算子を使えば、連結を簡単に行えます。 strcat関数(⇒リファレンス)の代わりになります。

#include <iostream>
#include <string>

int main()
{
	std::string str1("abcde");
	std::string str2("aaa");

	str2 += str1;  // "abcdeaaa"
	str2 += "xyz"; // "abcdeaaaxyz"

	std::cout << str2 << std::endl;
}

実行結果

abcdeaaaxyz

このように、C言語形式の文字列であっても簡単に連結できます。 なお、+=演算子を分解すれば、+演算子と =演算子ですから、

str1 = str1 + str2;

このように書いても同じ結果になります。 ただし一般的に、+=演算子のような複合代入演算子の方が効率的です。


また、append関数を使う方法もあります。 これは、代入における assign関数とよく似ています。

#include <iostream>
#include <string>

int main()
{
	std::string str1("aaa");
	std::string str2("abc");

	str1.append(str2);         // 他の std::string を連結
	std::cout << str2 << std::endl;

	str1.append(str2, 1, 3);   // str2[1] から 3文字を連結
	std::cout << str2 << std::endl;

	str1.append("xyz");        // const char*型を連結
	std::cout << str2 << std::endl;

	str1.append("xyzxyz", 4);  // "xyzxyz" の先頭から 4文字を連結
	std::cout << str2 << std::endl;

	str1.append(5, 'a');       // 5個の 'a' を連結
	std::cout << str2 << std::endl;

	str1.append(&str2[0], &str2[3]);  // 指定範囲を連結
	std::cout << str2 << std::endl;
}

実行結果

abcdeaaaxyz

詳細な解説はしないので、コメントと実行結果をご確認ください。 なお、最後の範囲指定タイプについては、「イテレータ」の項で、改めて考え方を取り上げます。

C++11(std::initializer_list版)

C++11 の +=演算子、append関数には、std::initializer_list型を引数に持ったものが追加されています。

#include <iostream>
#include <string>

int main()
{
	std::string str("abc");
	
	str += {'x', 'y', 'z'};
	std::cout << str << std::endl;

	str.append( {'X', 'Y', 'Z'} );
	std::cout << str << std::endl;
}

実行結果

abcxyz
abcxyzXYZ

VisualC++ 2013/2015、clang 3.7 のいずれも対応しています


最後にもう1つ、push_back関数という関数を使っても連結が行えます。

#include <iostream>
#include <string>

int main()
{
	std::string str1("abcde");

	str1.push_back('x');         // 1文字連結
	std::cout << str2 << std::endl;
}

実行結果

abcdex

この関数は、1文字だけを末尾に連結します。 他の使い方はできません。
他と比べて随分と限られた使い方のようですが、STL に含まれるほかの機能群にも同名の関数が用意されており、 まったく同じ使い方ができるように設計されています。 つまり、統一性を持たせるために用意されている関数と言えます。

挿入

文字列の途中に、他の文字列や文字を挿入することができます。 これは、char型配列とC標準ライブラリで行うには面倒な処理ですが、 std::string なら insert関数を使うだけで簡単に実現できます。

#include <iostream>
#include <string>

int main()
{
	std::string str1;
	std::string str2("ABCDEF");

	str1 = "abc";
	str1.insert(1, "x");       // str[1] の直後に "x" を挿入
	std::cout << str1 << std::endl;

	str1 = "abc";
	str1.insert(1, "xyz", 2);  // str[1] の直後に "xyz" の最初の 2文字を挿入
	std::cout << str1 << std::endl;

	str1 = "abc";
	str1.insert(1, 3, 'a');    // str[1] の直後に 'a' を 3個挿入
	std::cout << str1 << std::endl;

	str1 = "abc";
	str1.insert(1, str2);      // str[1] の直後に str2 を挿入
	std::cout << str1 << std::endl;

	str1 = "abc";
	str1.insert(1, str2, 2, 3); // str[1] の直後に str2[2] から 3文字分を挿入
	std::cout << str1 << std::endl;
}

実行結果

axbc
axybc
aaaabc
aABCDEFbc
aCDEbc

分かりづらくなるので、1回ずつ "abc" に初期化し直してから、挿入操作を行っています。 それぞれの使い方の意味は、コメントと実行結果を参照して下さい。

上記の使い方の他に、イテレータを使ったものが存在しています。 イテレータの考え方は「イテレータ」の項で、改めて考え方を取り上げますが、 使い方だけを挙げておくと次のようになります。

#include <iostream>
#include <string>

int main()
{
	std::string str1;
	std::string str2("ABCDEF");

	str1 = "abc";
	str1.insert(str1.begin(), str2.begin(), str2.end());  // str1 の先頭に str2 の全体を挿入
	std::cout << str1 << std::endl;

	str1 = "abc";
	str1.insert(str1.begin(), 'x');  // str1 の先頭に 'x' を挿入
	std::cout << str1 << std::endl;

	str1 = "abc";
	str1.insert(str1.begin(), 3, 'x');  // str1 の先頭に 'x' を 3個挿入
	std::cout << str1 << std::endl;
}

実行結果

ABCDEFabc
xabc
xxxabc
C++11(std::initializer_list版)

C++11 の insert関数には、std::initializer_list型を引数に持ったものが追加されています。

#include <iostream>
#include <string>

int main()
{
	std::string str("abc");
	
	str.insert(str.begin(), {'x', 'y', 'z'});  // str の先頭に 'x' 'y' 'z' を挿入
	std::cout << str << std::endl;
}

実行結果

xyzabc

VisualC++ 2013/2015、clang 3.7 のいずれも対応しています

サイズと容量

サイズと容量については、両者の区別を明確に理解しておく必要があります。

std::string において、サイズというのは、文字列の長さと同義です。 これは size関数、または length関数を使って取得できます。

#include <iostream>
#include <string>

int main()
{
	std::string str("abc");

	std::string::size_type size = str.size();
	std::cout << size << std::endl;

	std::string::size_type len = str.length();
	std::cout << len << std::endl;
}

実行結果

3
3

size関数と length関数は、名前が違うだけでまったく同じ結果になるので、どちらを使っても構いません。
また、どちらも戻り値の型は、std::string::size_type型です。 これは、符号無しの整数型の typedef になっています。 なお、可能性のある上限値を max_size関数で取得できます。

また、サイズが 0 かどうかを知りたい場合には、empty関数が使えます。

#include <iostream>
#include <string>

int main()
{
	std::string str1;
	std::string str2("a");

	std::cout << str1.empty() << std::endl;
	std::cout << str2.empty() << std::endl;
}

実行結果

1
0

空かどうかの判定には、以下の方法が考えられます。

  1. size関数の戻り値が 0 かどうか調べる。
  2. length関数の戻り値が 0 かどうか調べる。
  3. empty関数の戻り値が真かどうか調べる。
  4. str=="" のように空文字列と比較する。

1と2は、両者の関数の効果は完全に同じなので、効率面でも同じです。 3についても、1や2と効率は変わりません。 4については、効率面では劣ります。
結果として、1〜3のいずれかが良いのですが、empty関数を使うのが無難でしょう。

list(第10章)の場合、size関数よりも empty関数の方が効率が優れている可能性があります (C++11 では必ず同等です)。 そのため、空かどうかの判定には常に empty関数を使うと覚えておいた方が良いという訳です。

サイズに関係する関数として、resize関数があります。 この関数は、指定したサイズになるように強制的に保持している文字列を書き換えます。
現在のサイズよりも大きな値を指定すると、そのサイズになるまで空文字(または第2引数に指定した文字)を埋めます。 逆に、現在のサイズよりも小さい値を指定すると、余分な文字列をカットします。

#include <iostream>
#include <string>

int main()
{
	std::string str("abc");

	str.resize(5);
	std::cout << str << std::endl;

	str.resize(10, 'x');
	std::cout << str << std::endl;

	str.resize(1);
	std::cout << str << std::endl;
}

実行結果

abc  
abc  xxxxx
a


一方、「容量」の方は、実際に確保済みになっているメモリの量のことを指します。 実は、std::string は「サイズ」よりも多めにメモリを確保することがあります。 メモリを動的に確保することは、処理速度に大きな影響を与えるため、事前に少し多めに確保しておくことで、効率化を図っているためです。

現在の容量は、capacity関数で取得できます。

#include <iostream>
#include <string>

int main()
{
	std::string str("abc");

	std::string::size_type capa = str.capacity();
	std::cout << capa << std::endl;
}

実行結果

15

capacity関数の戻り値の型も、std::string::size_type型です。
実行結果にあるように、"abc" という 3文字のデータに対して、15 という値を返されています。 この 15 は、15Byte を表しています。

現在の容量を超えない限り、新たなメモリ割り当ては行われません。 実験してみましょう。

#include <iostream>
#include <string>

int main()
{
	std::string str("abc");

	std::string::size_type capa = str.capacity();

	std::cout << "BEFORE" << "\n"
	          << "size: " << str.size() << "\n"
	          << "capa: " << str.capacity() << std::endl;

	// 容量に変化が出るまで、1文字の連結を繰り返す
	while (capa == str.capacity()) {
		str += 'z';
	}

	std::cout << "AFTER" << "\n"
	          << "size: " << str.size() << "\n"
	          << "capa: " << str.capacity() << std::endl;
}

実行結果

BEFORE
size: 3
capa: 15
AFTER
size: 16
capa: 31

実行結果を見ると、size が 16 になったところで容量が 31 に増加したようです。 元々の容量は 15 なので、これを超えたところで容量が増加したことが分かります。
このとき、容量がどれだけ増加するのかは、ライブラリの実装次第ですが、 大体、2倍ぐらいに増えていきます。
このような容量増加の瞬間には、次のことが行われています。

  1. 新しい容量分のメモリを新たに確保する。
  2. 以前のデータを、新たなメモリ領域へコピーする。
  3. 以前のデータがあったメモリ領域を解放する。

これはそれなりに負荷の大きな処理になるので、処理速度に影響します。 これを無視できないのであれば、1つの解決策として、reserve関数を利用できます。 reserve関数は、それを呼び出したタイミングで強制的にメモリ容量の拡張を行わせます。

#include <iostream>
#include <string>

int main()
{
	std::string str("abc");

	std::string::size_type capa = str.capacity();

	std::cout << "BEFORE" << "\n"
	          << "size: " << str.size() << "\n"
	          << "capa: " << str.capacity() << std::endl;

	str.reserve(100);

	std::cout << "AFTER" << "\n"
	          << "size: " << str.size() << "\n"
	          << "capa: " << str.capacity() << std::endl;
}

実行結果

BEFORE
size: 3
capa: 15
AFTER
size: 3
capa: 111

reserve関数には、新たな容量を指定します。 実行結果を見ると分かるように、ここで指定した値がそのまま使われる保証はありませんが、 もちろん、指定した値以上にはなります
事前に、どの程度のサイズになり得るのか分かっているのであれば、 reserve関数を使うことによって、メモリ確保に関する処理負荷を、reserve関数を呼び出した瞬間だけに限定できます。

reserve関数を使って容量を減らすこともできますが、これもやはり指定した値がそのまま使われる保証はありません。 特に、現在のサイズよりも小さくすることは理屈としておかしいのでできません。 なお、引数無しで reserve関数を呼びだすと、それは現在のサイズにまで容量を縮小するという意味の要求になります

vector(第8章)にも reserve関数が存在しますが、 そちらは容量を減らすことには使えません。

また、reserve関数によって容量を縮小する以外の場面で、勝手に容量が減ることはありません。 例えば、clear関数を呼んで、保持している文字列を空にしたとしても、それはサイズが 0 になるだけであって、 容量には影響を与えません。

C++11(容量が減る場面)

C++11 では、shrink_to_fit関数が追加されており、これを使ったときにも容量が縮小します。

比較

文字列同士の比較は、==、!=、<、<=、>、>= という通常の比較演算子によって行えます。 <、<=、>、>= に関しては、文字コード順の比較を行います。
比較対象は、std::string だけでなく、C言語形式の文字列でも構いません。

また、compare関数を使うこともできます。 この関数は、strcmp関数(⇒リファレンス)と同様に、 一致していれば 0、そうでなければ先頭の文字から順に調べ、最初に違いが見つかったときに両者の文字の値を比べ、 自身の方が引数側よりも小さければ 0未満、大きければ 0より大きい値をそれぞれ返します。

#include <iostream>
#include <string>

int main()
{
	std::string str("abcde");

	std::cout << str.compare("edcba") << std::endl;
	std::cout << str.compare("abcde") << std::endl;
	std::cout << str.compare("aaaaa") << std::endl;
	
	std::cout << str.compare("EDCBA") << std::endl;
	std::cout << str.compare("ABCDE") << std::endl;
	std::cout << str.compare("AAAAA") << std::endl;
}

実行結果

-1
0
1
1
1
1

文字コードの種類にもよりますが、ASCIIコード順では、大文字のアルファベットの方が、小文字のアルファベットよりも小さい値になるので、 このサンプルプログラムの後半の3つはすべて、0 より大きい値を返します。

検索

特定の文字や、文字列を検索するための関数も用意されています。 これは6種類あります。

find 指定した文字や文字列が、最初に出現する位置を返す。
rfind 指定した文字や文字列が、最後に出現する位置を返す。
find_first_of 指定した文字や文字列の一部である文字が、最初に出現する位置を返す。
find_last_of 指定した文字や文字列の一部である文字が、最後に出現する位置を返す。
find_first_not_of 指定した文字や文字列の一部ではない文字が、最初に出現する位置を返す。
find_last_not_of 指定した文字や文字列の一部ではない文字が、最後に出現する位置を返す。
#include <iostream>
#include <string>

int main()
{
	std::string str("abcddabc");

	std::cout << str.find("abc") << std::endl;
	std::cout << str.rfind("abc") << std::endl;
	std::cout << str.find_first_of("abc") << std::endl;
	std::cout << str.find_last_of("abc") << std::endl;
	std::cout << str.find_first_not_of("abc") << std::endl;
	std::cout << str.find_last_not_of("abc") << std::endl;
}

実行結果

0
5
0
7
3
4

これらの検索系関数はどれも、何かを発見できた場合は、その位置(添字)を返します。 もし発見できなかったときは、std::string::npos という値を返します。 std::string::npos は、std::string::size_type型(これは符号無し整数)の -1 で定義されています。

先ほどのサンプルプログラムのような使い方なら問題はありませんが、 戻り値を一旦変数に受け取る場合には、必ず std::string::size_type型で受け取って下さい。 面倒と思って int型などで受け取るような横着をすると、std::string::npos との比較がうまく行われない可能性があり、バグの原因になります。

#include <iostream>
#include <string>

int main()
{
	std::string str("abcddabc");

	std::string::size_type index = str.find("xyz");
	if (index == std::string::npos) {
		std::cout << "見つかりません" << std::endl;
	}
	else {
		std::cout << index << std::endl;
	}
}

実行結果

見つかりません


さて、find系の 6つの関数は、それぞれに更にパターンが存在します。 ここでは find関数についてだけ取り上げますが、他の関数でも同じパターンがあると考えてください。

#include <iostream>
#include <string>

int main()
{
	std::string str("abcddabc");
	std::string search("abc");

	std::cout << str.find(search) << std::endl;        // "abc" を探す
	std::cout << str.find(search, 3) << std::endl;     // str[3] 以降の "abc" を探す
	std::cout << str.find("abc") << std::endl;         // "abc" を探す
	std::cout << str.find("abc", 3) << std::endl;      // str[3] 以降の "abc" を探す
	std::cout << str.find("abcde", 3, 3) << std::endl; // str[3] 以降の "abc" を探す
	std::cout << str.find('d') << std::endl;           // 'd' を探す
	std::cout << str.find('d', 4) << std::endl;        // str[4] 以降の 'd' を探す
}

実行結果

0
5
0
5
5
3
4

置換

文字列の置換には、replace関数を使います。

#include <iostream>
#include <string>

int main()
{
	std::string str;
	std::string rep("xyz");

	str = "abcabc";
	str.replace(1, 4, rep);  // str[1] から 4文字分を std::string で置換
	std::cout << str << std::endl;

	str = "abcabc";
	str.replace(str.begin() + 1, str.begin() + 4, rep);  // str[1]〜str[4] を std::string で置換
	std::cout << str << std::endl;

	str = "abcabc";
	str.replace(1, 4, rep, 2, 1);  // str[1] から 4文字分を rep[2] からの 1文字分で置換
	std::cout << str << std::endl;

	str = "abcabc";
	str.replace(1, 4, "xyz");  // str[1] から 4文字分を const char* で置換
	std::cout << str << std::endl;

	str = "abcabc";
	str.replace(str.begin() + 1, str.begin() + 4, "xyz");  // str[1]〜str[4] を const char* で置換
	std::cout << str << std::endl;

	str = "abcabc";
	str.replace(1, 4, "xyz", 2);  // str[1] から 4文字分を "xyz" の 2文字分で置換
	std::cout << str << std::endl;

	str = "abcabc";
	str.replace(str.begin() + 1, str.begin() + 4, "xyz", 2);  // str[1]〜str[4] を 4文字分を "xyz" の 2文字分で置換
	std::cout << str << std::endl;

	str = "abcabc";
	str.replace(1, 4, 3, 'a');  // str[1] から 4文字分を 3個の 'a' で置換
	std::cout << str << std::endl;

	str = "abcabc";
	str.replace(str.begin() + 1, str.begin() + 4, 3, 'a');  // str[1]〜str[4] を 4文字分を 3個の 'a' で置換
	std::cout << str << std::endl;
}

実行結果

axyzc
axyzbc
azc
axyzc
axyzbc
axyc
axybc
aaaac
aaaabc
C++11(std::initializer_list版)

C++11 では、std::initializer_list(初期化リスト)が使えるようになりました。

#include <iostream>
#include <string>

int main()
{
	std::string str("abcabc");
	
	str.replace(str.begin() + 1, str.begin() + 4, {'x', 'y', 'z'});
	std::cout << str << std::endl;
}

実行結果

axyzbc

VisualC++ 2013/2015、clang 3.7 のいずれも対応しています

削除

空文字列の状態に戻すには、clear関数を使います。

#include <iostream>
#include <string>

int main()
{
	std::string str1("abcde");

	str1.clear();
	std::cout << str1 << std::endl;
}

実行結果


あるいは、erase関数を使っても同じことができます。

#include <iostream>
#include <string>

int main()
{
	std::string str1("abcde");

	str1.erase();
	std::cout << str1 << std::endl;
}

実行結果


いずれにせよ、これらの方法でサイズを減らすことができますが、容量は減らないことに注意して下さい。 (「サイズと容量」の項を参照)。

部分文字列

substr関数を使うと、文字列を部分的に切り出すことができます。

#include <iostream>
#include <string>

int main()
{
	std::string str("abcabc");
	
	std::cout << str.substr() << std::endl;     // そのまま返される
	std::cout << str.substr(2) << std::endl;    // 2文字目以降が返される
	std::cout << str.substr(2, 3) << std::endl; // 2文字目から 最大 3文字分が返される
}

実行結果

abcabc
cabc
cab

3つ目の使い方に関して、もし文字列の末尾に行き着いてしまった場合、末尾までの文字列が返されます。 第2引数の意味としては「最大文字数」ですから、それに満たなくても大丈夫です。

C言語形式との相互変換

char*型や const char*型のようなC言語形式の文字列と、std::string との相互変換はどうなっているでしょうか。 これができないと、いくら std::string が便利であっても、用途が限定されてしまいます。

まず、「初期化」の項で見たように、std::string を初期化する際に、 C言語形式の文字列が指定できます。

#include <iostream>
#include <string>

int main()
{
	const char* s1 = "abcde";
	char* s2 = "abcde";
	const char s3[] = "abcde";

	std::string str1(s1);
	std::string str2(s2);
	std::string str3(s3);

	std::cout << str1 << "\n"
	          << str2 << "\n"
	          << str3 << std::endl;
}

実行結果

abcde
abcde
abcde

また、「代入」「連結」の項で見たように、代入や連結の処理時にも、 C言語形式の文字列を指定できるので、この方向の変換に困ることはないはずです。


逆方向はどうでしょう。 例えば、

std::string str("abcde");
const char* s = str;

このようには書けません。これはコンパイルエラーになってしまいます。 ルールとして、std::string をC言語形式の文字列へ変換するには、明示的に関数を呼ぶ必要があります。

#include <iostream>
#include <string>

int main()
{
	std::string str("abcde");

	const char* s = str.c_str();
	std::cout << s << std::endl;
}

実行結果

abcde

c_str関数は、std::string が現在保持している文字列を const char*型の形で返します。 このとき、末尾には '\0' が付加された状態になっています。 また、std::string が空文字列を保持している場合は、"\0" が返されます

c_str関数は、文字列のコピーを取っている訳ではないので、std::string側の文字列に変化が生じた時点で、 戻り値で返されたポインタは無効になります

#include <iostream>
#include <string>

int main()
{
	std::string str("abcde");

	const char* s = str.c_str();
	std::cout << s << std::endl;

	str += "x";  // str の保持している文字列が変化

	// c_str() で取得したポインタは無効になったので、
	// 以下は保証できない。
//	std::cout << s << std::endl;
}

ちゃんとコピーを取りたければ、copy関数を使うようにして下さい。 ただしこちらは、末尾の '\0' が付かないことに注意して下さい。


data関数というものもあります。 この関数は、std::string が保持している文字列を const char*型の形で返しますが、末尾に '\0' は付きません。 また、std::string が空文字列の場合は、NULL が返されます
ただし、この挙動は C++98/03 の時点での話になります。 C++11 では挙動が大きく変わっているので注意が必要です

なお、data関数も、std::string 側に変化が起こると、返されたポインタは無効になります。

C++11(data関数の挙動)

C++11 の data関数は、c_str関数と同じになりました。 つまり、末尾に '\0' は付きますし、空文字列に対しては "\0" が返されます。

VisualC++ 2013/2015、clang 3.7 のいずれも、data関数と c_str関数は同じ結果になるように実装されているので、 C++11 の仕様に従っています。

イテレータ

イテレータ(反復子)は、データ構造に含まれる各要素に対する繰り返し操作を抽象化する仕組みです。 つまり、データ構造の種類を問わず、同じ方法で要素を操作できます。
イテレータ自体が STL の中では大きなテーマなので、詳細は別の章で改めて取り上げます(第14章)が、 ここでは、std::string に限った話として説明します。

先ほどは、繰り返し操作の抽象化と書きましたが、実際のところイテレータは、C言語のポインタに近い感覚の機能になっています。 つまり、イテレータは、データ構造に含まれる要素の1つを指し示すものです。 std::string のイテレータであれば、文字列の中に1つの文字を参照します。

それでは、実例を見てみましょう。 次のプログラムは、std::string に含まれる各文字を1文字ずつ、改行を挟みながら出力しています。

#include <iostream>
#include <string>

int main()
{
	std::string str("abcde");

	for (std::string::iterator it = str.begin();
	     it != str.end();
	     ++it
	) {
		std::cout << *it << std::endl;
	}
}

実行結果

a
b
c
d
e

イテレータは、std::string::iterator型です。 もちろんこれは、std::string に対するイテレータであって、他のデータ構造に対するイテレータは別の型が存在します。
begin関数を呼び出すと、その std::string の先頭の要素を参照するイテレータを取得できます。 一方、end関数は、最後の要素の1つ後ろを参照するイテレータを返します。

begin関数で取得した先頭要素を参照するイテレータは、変数it に格納されています。 it は、for文が1周するごとにインクリメントされていますが、これによって、イテレータit は、「次の要素」を参照するようになります。 for文の繰り返し条件は、イテレータit が最後の要素の1つ後ろのイテレータと一致していない間となっていますが、 これはつまり、文字列の末尾まで到達したら終わりという意味です。 このように使うために、end関数が返すイテレータは、最後の要素の "1つ後ろ" となっている訳です。 また、*演算子によって、イテレータが参照している先の要素をアクセスできます。

ちなみに、++演算子の部分ですが、後置インクリメントではなく、前置インクリメントにした方が処理効率が上がります。

同じことを、C言語のような作りに置き換えてみると、ポインタの仕組みと同じであることが分かります。

#include <iostream>
#include <string>

#define SIZE_OF_ARRAY(array)	(sizeof(array)/sizeof(array[0]))

int main()
{
	char str[] = "abcde";

	for (char* p = str;
	     p != &str[SIZE_OF_ARRAY(str)];
		 ++p
	) {
		std::cout << *p << std::endl;
	}
}

実行結果

a
b
c
d
e

++演算子や *演算子の使い方は、イテレータとポインタとで完全に一致しています。 ちなみに、--演算子で1つ前の要素に戻ることができる点も同様です。 また、ポインタを使って「p + 3」のような記述ができるように、イテレータでも「it + 3」のような記述が可能です。


STL のイテレータは、範囲を示す際にも利用されます。 同じデータ構造内にある要素を指すイテレータを2つ使うことによって、「ここからここまで」という範囲を表現するという訳です。

#include <iostream>
#include <string>

void PrintRange(std::string::iterator begin, std::string::iterator end);

int main()
{
	std::string str("abcde");

	PrintRange(str.begin(), str.begin() + 3);
}

void PrintRange(std::string::iterator begin, std::string::iterator end)
{
	for (std::string::iterator it = begin;
	     it != end;
	     ++it
	) {
		std::cout << *it << std::endl;
	}
}

実行結果

a
b
c

PrintRange関数にイテレータを2つ指定すると、その範囲内にある各要素を出力します。

C++11(begin関数、end関数)

C++11 には、std::begin関数std::end関数が追加されています。 効果としては同じですが、特定の型に依存しないという利点があります。 C++11 ならば、std::begin、std::end を使うように統一するのが良いでしょう。

#include <iostream>
#include <string>

int main()
{
	std::string str("abcde");

	for (std::string::iterator it = std::begin(str);
	     it != std::end(str);
	     ++it
	) {
		std::cout << *it << std::endl;
	}
}

実行結果

a
b
c
d
e

なお、VisualC++ 2013/2015、clang 3.7 のいずれでも使用できます


練習問題

問題@ std::string型の変数を 'X' という1文字で初期化するには、どうすればいいですか?

問題A あなたの使っているコンパイラにおいて、空の std::string はどれだけのサイズと容量を持つか調べてみて下さい。

問題B 文字列の途中に '\0' を挿入することによって、std::string のサイズは変化するでしょうか?

問題C std::string を渡すと、その中に含まれる小文字のアルファベットを大文字化するような関数を作成して下さい。 (ちなみに、std::string にそのような機能はありません)。

問題D 問題Cの関数を、イテレータによる範囲指定に対応させて下さい。


解答ページはこちら

参考リンク

更新履歴

'2016/10/15 clang の対応バージョンを 3.7 に更新。

'2016/1/23 C++11 では、u8'a' のような、文字リテラルに対する u8プリフィックスは存在しないので、文章を修正。 また、例示として ASCII の範疇ではあまり意味が無いので、日本語の文字を使うように修正。
リンク先が間違っていたのを修正。

'2015/12/27 SIZE_OF_ARRAYマクロの定義を修正。

'2015/10/12 clang の対応バージョンを 3.4 に更新。

'2015/9/5 VisualC++ 2012 の対応終了。

'2015/8/18 VisualC++ 2010 の対応終了。

'2015/8/15 VisualC++ 2015 に対応。

'2014/10/18 clang 3.2 に対応。

'2014/2/1 VisualC++ 2013 に対応。

'2014/1/22 compare関数の仕様についての記述を修正。また、サンプルプログラムに使用例を追加。

'2014/1/14 VisualC++2008 の対応終了。

'2014/1/13 clang 3.0 に対応。

'2014/1/5 「C++11(UTF-8 のサポート)」の項を追加。

'2013/10/20 初期化に関する誤った記述を訂正。

'2013/7/13 新規作成。



前の章へ

次の章へ

C++編のトップページへ

Programming Place Plus のトップページへ