C++ Learning

第3回 std::string を使う

C の文字列は char*<string.h>。連結は strcat、比較は strcmp、コピーは strcpy、バッファ確保と '\0' 終端を自分で管理──と苦行が続きます。C++ の std::string はそれらを 1 つの型で全部面倒を見てくれます。本回では 対比コードSSO 可視化インタラクティブな操作デモで、C++ の文字列の扱いやすさを体感します。

このページで押さえること
✅ 最低限ここだけ覚える
  • #include <string> で使えるようになる
  • 連結は s1 + s2、比較は s1 == s2
  • 長さは s.size()、空判定は s.empty()
  • '\0' とバッファサイズを意識しなくていい
⭐ 余裕があれば読む
  • SSO(Small String Optimization)の仕組み
  • c_str()char* を取り出す
  • substr / find / replace の典型パターン
  • C++17 の std::string_view へのつなぎ

1. std::string とは ― まず触ってみる

std::string は、長さを自動で管理してくれる文字列型です。まずこれだけ覚えてください。

C の文字列は「末尾に '\0' を付けた char の配列」でした。長さを知るには先頭から '\0' まで数える(strlen)、連結にはコピー先の空きを自分で用意する(strcat)、とプログラマの仕事が多いのが悩みでした。

std::string は、文字列の長さ・記憶場所・終端を全部内部で管理してくれます。使う側は「文字列を入れる変数」として、int とほぼ同じ感覚で扱えます。

✏️ 下のフィールドに文字を打つと、長さが自動で計算されます(size()
std::string s = " ";

まずは 4 行だけ

上でやっていることを C++ コードで書くとこうです。これだけ書ければ、std::string を「使う側」としては合格です。

first_string.cppC++ 最小例
#include <iostream> #include <string> int main() { std::string s = "Hello"; // 作る s = s + ", World!"; // 連結(自動で伸びる) std::cout << s << "\n"; // そのまま出せる }
ここまでで覚えること(3 つだけ):
  • std::string 名前 = "文字列"; で作る(#include <string> が必要)
  • 連結は +、比較は ==、長さは 名前.size()
  • strlen / strcat / strcpy / '\0' は全部忘れて OK

s.size(). って何? ― メソッド呼び出しを一言で

上のコードで msg.size() のような「変数名 . 関数名()」という書き方が出てきます。C では見慣れない構文のはずなので、先にここだけ押さえておきます。

意味はシンプルで、「msg に対して size という機能を実行してね」と頼んでいるだけです。C と並べると、書く向きが変わっただけだと分かります。

C のスタイル関数(対象)
strlen(s); // 関数に対象を渡す strcat(s, "!"); strcpy(d, s);
C++ のスタイル対象.機能()
s.size(); // 対象に機能を頼む s.append("!"); d = s;

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。

次のセクションから「C ではこう書く」との対比が出ます。 C の strcatstrcmp のコードも出てきますが、細部は読み飛ばしても構いません。「C ではこんなに手間だった」という雰囲気だけ掴めば十分です。

2. C の文字列処理との比較

たった「"Hello, " + name を出力したい」だけでも、C では考えることが多すぎます。

greet.c C
#include <stdio.h> #include <string.h> int main(void) { char name[64] = "Alice"; char msg[128]; // 毎回バッファサイズを自分で決める strcpy(msg, "Hello, "); strcat(msg, name); strcat(msg, "!"); // 比較は strcmp (== では中身を比べられない) if (strcmp(name, "Alice") == 0) puts(msg); // 長さは strlen printf("len=%zu\n", strlen(msg)); return 0; }
バッファサイズを間違うと バッファオーバーフロー
greet.cpp C++
#include <iostream> #include <string> int main() { std::string name = "Alice"; // + で連結、自動で伸びる std::string msg = "Hello, " + name + "!"; // 比較は == / != / < でそのまま if (name == "Alice") std::cout << msg << "\n"; // 長さは size() (length() も同じ) std::cout << "len=" << msg.size() << "\n"; }
バッファ管理不要/型安全/読みやすい
差分の本質: C では「メモリをどこに・どれだけ確保するか」を人間が決めます。std::string長さに応じて自動で確保/拡張/解放してくれるので、文字列の長さを気にせずに書けます。

3. std::string の基本操作(ライブデモ)

下のフィールドを書き換えると、各操作の結果がリアルタイムに変わります。C++ の std::string で何ができるかを手で触って確かめてください。

▶ std::string インタラクティブ操作

上のデモでやっていることをコードで書くとこうなります。

basic_ops.cppC++
#include <iostream> #include <string> int main() { std::string s1 = "Hello"; std::string s2 = "World"; // 連結 std::string joined = s1 + " " + s2; // "Hello World" // 比較 (辞書順) bool eq = (s1 == s2); // false bool lt = (s1 < s2); // true (H < W) // 長さ・空判定 auto n = s1.size(); // 5 bool e = s1.empty(); // false // 末尾に追加 (+=) s1 += "!"; // "Hello!" // 先頭/末尾の文字 char first = s1.front(); // 'H' char last = s1.back(); // '!' }
basic_ops.c (相当)C
#include <stdio.h> #include <string.h> int main(void) { char s1[64] = "Hello"; char s2[64] = "World"; char joined[128]; // 連結: 3 ステップ strcpy(joined, s1); strcat(joined, " "); strcat(joined, s2); // 比較: 0 == 等しい という直感に反する戻り値 int eq = strcmp(s1, s2) == 0; int lt = strcmp(s1, s2) < 0; // 長さ size_t n = strlen(s1); // 末尾追加: 長さに気を付けてバッファに余裕が要る strcat(s1, "!"); return 0; }

4. メソッド一覧 ― よく使う 10 個

行を選ぶと、その操作がデモの結果にも反映されます。C の同じ処理を書くと何行かかるか対比しながら眺めてください。

C++ メソッドC 相当意味
s.size() / s.length()strlen(s)文字数(バイト数)を返す
s.empty()s[0] == '\0'空文字列なら true
s1 + s2strcpy + strcat連結した新しい string を返す
s1 == s2strcmp(s1,s2)==0中身が等しければ true
s1 < s2strcmp(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::npossize_t の最大値)が返ります。if (s.find("x") != std::string::npos) { ... } がイディオムです。C の strstr はヌルポインタを返すのと対照的。

5. SSO(Small String Optimization)の可視化

std::string は短い文字列ならスタック上のオブジェクト内部に直接格納し、長くなったらヒープに確保します。これを SSO と呼びます。下のデモで、文字数を変えて切り替わる瞬間を見てください(閾値は実装依存ですが、libc++/libstdc++ は通常 15〜22 文字前後)。

▶ SSO スライダ
5 文字
std::string オブジェクト スタック
ヒープ領域 動的
短いのでスタック内に直接格納(SSO 有効)
SSO の嬉しさ: ログ出力のような短い文字列を大量に扱う場面で、new/malloc を呼ばずに済むので圧倒的に速い。C の固定サイズ char[16] + 動的確保のハイブリッド戦略を、型の内部で勝手にやってくれています。
閾値は実装依存: libstdc++ は 15 文字、libc++ は 22 文字 (64-bit) が典型値。規格で保証される値ではないので、「○○文字以下なら速い」に依存するコードは書かないこと。
余裕があれば読む ― ここから先は応用
最低限はここまで(C との比較/基本操作/メソッド/SSO)。残りは C API との連携や罠なので、急ぐなら次章に進んで OK。

6. C の char* と相互変換

既存の C ライブラリや OS の API は const char* を要求します。std::string からは c_str() で取り出せます。逆に C の文字列リテラルからは暗黙的に std::string に変換されます。

C ライブラリへ渡すstd::string → char*
#include <string> #include <cstdio> std::string path = "data.txt"; FILE* fp = fopen(path.c_str(), "r"); // c_str() は ヌル終端された const char* を返す
C 文字列を受け取るchar* → std::string
#include <string> const char* name = "Bob"; std::string s = name; // 暗黙変換でコピー // 部分文字列から作る std::string t(name, 2); // "Bo"
c_str() の寿命に注意: 返されたポインタは std::string 本体が存続する間だけ有効です。foo(make_string().c_str()) のように一時オブジェクトから取り出したポインタを後で使うと、そのときには文字列が破棄されています(dangling pointer)。これは C++ での文字列バグの定番です。

C++17: std::string_view への橋渡し

読むだけなら std::string_view が軽量です。std::string / char* / 文字列リテラルのどれからでも作れ、コピーしないのでコストゼロ。引数の型を std::string_view にすると、呼び出し側はどの形式の文字列でも渡せます。

本サイトの方針: 読み取り専用の関数引数は std::string_view を基本にします(第 16 回で詳しく扱います)。書き換える場合のみ std::string& を使います。

7. よくある罠と実戦 Tips

❌ 罠 1: string + int はできない

NGコンパイルエラー
std::string s = "n=" + 42; // ← "n=" はポインタなので 42 を足すと // ポインタ演算になり、意味不明な文字列に
OK推奨
std::string s = "n=" + std::to_string(42); // または std::string s2 = std::string("n=") + "42";

❌ 罠 2: ループ内で s += ch を書くときは reserve

文字列の末尾に 1 文字ずつ足すとき、内部バッファが満杯になるたびに再確保されます。最終サイズが分かっているなら reserve で先に確保しましょう。

遅い再確保多発
std::string s; for (int i = 0; i < 100000; ++i) s += 'x';
速い推奨
std::string s; s.reserve(100000); for (int i = 0; i < 100000; ++i) s += 'x';

❌ 罠 3: find の戻り値を -1 と比較しない

NG符号なし比較
if (s.find("x") == -1) { ... } // size_t vs int で警告、意図通り動かない
OK推奨
if (s.find("x") == std::string::npos) { ... // 見つからなかった }

✅ Tip: 数値⇄文字列

number → stringC++11~
std::string s = std::to_string(3.14); // "3.140000" // 書式付きは C++20 の std::format // std::format("{:.2f}", 3.14) -> "3.14"
string → numberC++11~
int i = std::stoi("42"); double d = std::stod("3.14"); // 失敗時は例外 std::invalid_argument
まとめ: std::string値型です。関数に渡すとコピーが発生する点だけ意識しておけば、日常使いはほぼ int と同じ感覚で扱えます。コピーを避けたい場面は、関数引数に const std::string&std::string_view を使う。
広告スペース

確認クイズ

std::string の理解度を 4 問で確認しましょう。

Q1. std::string を使うために必要なヘッダは?

<string.h>
<string>
<cstring>
<strings>
C++ の標準ヘッダには .h が付きません。<string>std::string を提供します。<cstring> は C の <string.h>strlen/strcpy など)を C++ から使うためのヘッダです。

Q2. 2 つの std::string の中身が等しいかを調べるには?

s1 と s2 は比較できない
s1 == s2 で OK
strcmp(s1, s2) == 0
&s1 == &s2
std::string== 演算子がオーバーロードされているので、中身の比較がそのまま書けます。C の strcmp は「等しければ 0」という独特な戻り値で直感に反するのが廃れた理由のひとつ。

Q3. s.find("abc") が見つからなかったときの戻り値は?

-1
0
std::string::npos
NULL
find の戻り値は符号なし整数 size_t。見つからないことを表す特別な値 std::string::npos(実体は size_t の最大値)が返ります。-1 と比較すると暗黙変換で警告や意図しない挙動になります。

Q4. SSO(Small String Optimization)の説明として正しいものは?

コンパイラが関数呼び出しをインライン展開する最適化
短い文字列をヒープではなく string 本体のバッファに直接格納する最適化
文字列リテラルを共有して重複を減らす最適化
文字列の長さを計算済みにして strlen を不要にする最適化
SSO は、std::string オブジェクト自体が内部に固定サイズのバッファ(14〜22 文字程度)を持ち、短い文字列はそこに格納することでヒープ確保を避ける実装上の最適化です。長くなったときだけヒープに切り替わります。
この記事をシェア