C の文字列は char* と <string.h>。連結は strcat、比較は strcmp、コピーは strcpy、バッファ確保と '\0' 終端を自分で管理──と苦行が続きます。C++ の std::string はそれらを 1 つの型で全部面倒を見てくれます。本回では 対比コードと SSO 可視化と インタラクティブな操作デモで、C++ の文字列の扱いやすさを体感します。
#include <string> で使えるようになるs1 + s2、比較は s1 == s2s.size()、空判定は s.empty()'\0' とバッファサイズを意識しなくていいc_str() で char* を取り出すsubstr / find / replace の典型パターンstd::string_view へのつなぎstd::string は、長さを自動で管理してくれる文字列型です。まずこれだけ覚えてください。
C の文字列は「末尾に '\0' を付けた char の配列」でした。長さを知るには先頭から '\0' まで数える(strlen)、連結にはコピー先の空きを自分で用意する(strcat)、とプログラマの仕事が多いのが悩みでした。
std::string は、文字列の長さ・記憶場所・終端を全部内部で管理してくれます。使う側は「文字列を入れる変数」として、int とほぼ同じ感覚で扱えます。
size())上でやっていることを C++ コードで書くとこうです。これだけ書ければ、std::string を「使う側」としては合格です。
std::string 名前 = "文字列"; で作る(#include <string> が必要)+、比較は ==、長さは 名前.size()strlen / strcat / strcpy / '\0' は全部忘れて OKs.size() の . って何? ― メソッド呼び出しを一言で上のコードで msg.size() のような「変数名 . 関数名()」という書き方が出てきます。C では見慣れない構文のはずなので、先にここだけ押さえておきます。
意味はシンプルで、「msg に対して size という機能を実行してね」と頼んでいるだけです。C と並べると、書く向きが変わっただけだと分かります。
C では「文字列データ」と「それを操作する関数(strlen など)」は完全に別物で、関数に対象を引数で渡していました。C++ の std::string はこの 2 つを 1 つの型にまとめて持っているのが大きな違いです。文字列自身に「長さ教えて」(s.size())「後ろに足して」(s.append("!"))と頼むイメージになります。
この「型に属していて、. で呼び出せる関数」のことを C++ では メソッド(正式にはメンバ関数)と呼びます。このあとのセクションでも s.empty() / s.find(...) / s.substr(...) のように . で機能を呼ぶ書き方がたくさん出てきますが、すべて同じ仕組みです。
strlen(s) と s.size() はやっていることは同じ。. は「この変数にくっついている機能を呼ぶ」記号だと思って読み進めて OK です。
Q. C の文字列リテラル "hello" とは違うの?
→ 違います。"hello" は「C 風の文字列リテラル(const char[6])」です。std::string s = "hello"; と書くと、その C 風文字列をコピーして std::string を作ります。
Q. 日本語も入る?
→ 入ります。ただし内部はバイト列なので、size() は「バイト数」を返します。「文字数」を数えたい場合は UTF-8 デコードが必要で、これは別回(付録)で扱います。英数字だけなら size() = 文字数で問題ありません。
Q. std::string を関数の引数にするときは?
→ 読むだけなら const std::string&、書き換えるなら std::string& がおすすめ。詳しくは第 8 回(参照)で扱います。今は int と同じく値渡しで書いて OK。
strcat や strcmp のコードも出てきますが、細部は読み飛ばしても構いません。「C ではこんなに手間だった」という雰囲気だけ掴めば十分です。
たった「"Hello, " + name を出力したい」だけでも、C では考えることが多すぎます。
std::string は長さに応じて自動で確保/拡張/解放してくれるので、文字列の長さを気にせずに書けます。
下のフィールドを書き換えると、各操作の結果がリアルタイムに変わります。C++ の std::string で何ができるかを手で触って確かめてください。
上のデモでやっていることをコードで書くとこうなります。
行を選ぶと、その操作がデモの結果にも反映されます。C の同じ処理を書くと何行かかるか対比しながら眺めてください。
| C++ メソッド | C 相当 | 意味 |
|---|---|---|
s.size() / s.length() | strlen(s) | 文字数(バイト数)を返す |
s.empty() | s[0] == '\0' | 空文字列なら true |
s1 + s2 | strcpy + strcat | 連結した新しい string を返す |
s1 == s2 | strcmp(s1,s2)==0 | 中身が等しければ true |
s1 < s2 | strcmp(s1,s2)<0 | 辞書順で小さい |
s.substr(pos, n) | strncpy + '\0' 付与 | 部分文字列を取り出す |
s.find("ab") | strstr(s,"ab") | 見つからなければ std::string::npos |
s.replace(pos,n,t) | 自力で切り貼り | pos から n 文字を t に置き換え |
s.erase(pos,n) | 自力で詰める | pos から n 文字を削除 |
s.c_str() | その変数自体 | C API に渡せる const char* |
find の戻り値: 見つからないときは 0 ではなく std::string::npos(size_t の最大値)が返ります。if (s.find("x") != std::string::npos) { ... } がイディオムです。C の strstr はヌルポインタを返すのと対照的。
std::string は短い文字列ならスタック上のオブジェクト内部に直接格納し、長くなったらヒープに確保します。これを SSO と呼びます。下のデモで、文字数を変えて切り替わる瞬間を見てください(閾値は実装依存ですが、libc++/libstdc++ は通常 15〜22 文字前後)。
new/malloc を呼ばずに済むので圧倒的に速い。C の固定サイズ char[16] + 動的確保のハイブリッド戦略を、型の内部で勝手にやってくれています。
char* と相互変換既存の C ライブラリや OS の API は const char* を要求します。std::string からは c_str() で取り出せます。逆に C の文字列リテラルからは暗黙的に std::string に変換されます。
c_str() の寿命に注意: 返されたポインタは std::string 本体が存続する間だけ有効です。foo(make_string().c_str()) のように一時オブジェクトから取り出したポインタを後で使うと、そのときには文字列が破棄されています(dangling pointer)。これは C++ での文字列バグの定番です。
std::string_view への橋渡し読むだけなら std::string_view が軽量です。std::string / char* / 文字列リテラルのどれからでも作れ、コピーしないのでコストゼロ。引数の型を std::string_view にすると、呼び出し側はどの形式の文字列でも渡せます。
std::string_view を基本にします(第 16 回で詳しく扱います)。書き換える場合のみ std::string& を使います。
string + int はできないs += ch を書くときは reserve を文字列の末尾に 1 文字ずつ足すとき、内部バッファが満杯になるたびに再確保されます。最終サイズが分かっているなら reserve で先に確保しましょう。
find の戻り値を -1 と比較しないstd::string は値型です。関数に渡すとコピーが発生する点だけ意識しておけば、日常使いはほぼ int と同じ感覚で扱えます。コピーを避けたい場面は、関数引数に const std::string& か std::string_view を使う。
std::string の理解度を 4 問で確認しましょう。
std::string を使うために必要なヘッダは?.h が付きません。<string> が std::string を提供します。<cstring> は C の <string.h>(strlen/strcpy など)を C++ から使うためのヘッダです。std::string の中身が等しいかを調べるには?std::string は == 演算子がオーバーロードされているので、中身の比較がそのまま書けます。C の strcmp は「等しければ 0」という独特な戻り値で直感に反するのが廃れた理由のひとつ。s.find("abc") が見つからなかったときの戻り値は?find の戻り値は符号なし整数 size_t。見つからないことを表す特別な値 std::string::npos(実体は size_t の最大値)が返ります。-1 と比較すると暗黙変換で警告や意図しない挙動になります。std::string オブジェクト自体が内部に固定サイズのバッファ(14〜22 文字程度)を持ち、短い文字列はそこに格納することでヒープ確保を避ける実装上の最適化です。長くなったときだけヒープに切り替わります。