閉じる

ゼロからわかるC++のstd::string: 基本操作と安全な書き方

文字列はプログラムの随所で扱いますが、C言語の文字配列では安全性や可読性に課題がありました。

C++のstd::stringはそれを解決し、直感的で効率の良い操作を可能にします。

本記事では初心者がゼロから安全に文字列を扱えるように、std::stringの基本とベストプラクティスを、実行例とともに丁寧に解説します。

C++のstd::stringとは?安全で扱いやすい文字列

Cの文字配列との違いとメリット

基本概念の違い

Cの文字列はchar配列と終端のヌル文字'\0'で表現しますが、長さ管理や終端処理を自分で気にする必要があります

一方、C++のstd::stringは長さ情報を内部で保持し、動的に伸縮します。

  • C文字列: 実体はchar buf[N]'\0'終端。長さ計算はstrlenでO(n)。
  • std::string: 長さはO(1)で取得、メモリも自動管理。

安全性・可読性・保守性の観点でstd::stringを優先します。

例(C文字配列 vs std::string)

C++
#include <cstdio>      // Cの入出力
#include <cstring>     // strlen, strncpy
#include <string>
#include <iostream>

int main() {
    // Cの文字配列
    char nameC[8] = "Alice";
    // 危険: 末尾にヌル文字が入らずバッファオーバーランのリスク
    // strncpy(nameC, "AliceBob", sizeof(nameC)); // コメントアウト: 実行すると終端しない可能性
    std::printf("C string: %s (len=%zu)\n", nameC, std::strlen(nameC));

    // C++のstd::string
    std::string name = "Alice";
    name += "Bob"; // 自動的に拡張、終端の管理も自動
    std::cout << "std::string: " << name << " (len=" << name.size() << ")\n";
}
実行結果
C string: Alice (len=5)
std::string: AliceBob (len=8)

std::stringは終端やサイズの心配が不要で、読みやすく安全です。

どんな場面で差が出るか

  • 結合や挿入が頻繁: std::stringは表現が簡潔で安全。
  • 可変長入力: バッファ確保を気にせず受け取れる。
  • 例外安全: std::stringは失敗時に例外で通知されるため不正状態を避けやすい。

実務ではほぼ常にstd::stringを使います

自動メモリ管理でバッファオーバーランを防ぐ

バッファオーバーランとは

固定長配列に収まらないデータを書き込むと、隣接メモリを壊してしまいます。

動作未定義で、セキュリティ上の重大な欠陥になります。

std::stringは必要に応じて自動的に容量を拡張するため、この種の事故を大幅に減らせます。

例: 自動拡張の挙動

C++
#include <string>
#include <iostream>

int main() {
    std::string s = "hello";
    // 何度追加しても自動で再確保される
    for (int i = 0; i < 5; ++i) {
        s += " world";
        std::cout << "len=" << s.size() << ", capacity~=" << s.capacity() << '\n';
    }
}
実行結果
len=11, capacity~=15
len=17, capacity~=23
len=23, capacity~=31
len=29, capacity~=47
len=35, capacity~=47

capacityは内部の確保量で、必要に応じて増減します。

プログラマが手動で終端やサイズを管理する必要はありません

使い始めに必要なヘッダと型名 (ヘッダ: string, 型名: std::string)

必須ヘッダと名前空間

  • 使うヘッダ: #include <string>
  • 型名: std::string
  • 標準入出力と併用するなら#include <iostream>も必要です。
C++
#include <string>
#include <iostream>

int main() {
    std::string msg = "Hello, std::string!";
    std::cout << msg << '\n'; // '\n'は改行(フラッシュしない)
}
実行結果
Hello, std::string!

using namespace std;は名前衝突の原因になるため推奨しません

必要なときだけstd::を付けます。

std::stringの基本操作 (作成/代入/結合/長さ)

文字列の初期化と代入の基本

代表的な初期化方法

C++
#include <string>
#include <iostream>

int main() {
    std::string a;                 // 空文字列
    std::string b = "abc";         // 文字列リテラルから
    std::string c("xyz");          // コンストラクタ
    std::string d(5, 'A');         // 'A'を5回「AAAAA」
    std::string e = b;             // コピー
    std::string f = b + c;         // 連結で初期化

    std::cout << a << "|" << b << "|" << c << "|" << d << "|" << e << "|" << f << '\n';
}
実行結果
|abc|xyz|AAAAA|abc|abcxyz

状況に応じて最も意図が明確な初期化を選ぶと読みやすくなります。

代入

C++
#include <string>
#include <iostream>

int main() {
    std::string s = "hello";
    s = "world";        // 代入
    s.assign(3, '!');   // "!!!"に置き換え
    std::cout << s << '\n';
}
実行結果
!!!

長さを調べる (size, length)

同じ意味の2つのメンバ関数

size()length()は同じ値を返します。

慣習的にsize()の使用が多いです。

C++
#include <string>
#include <iostream>

int main() {
    std::string s = "こんにちは";
    std::cout << "size=" << s.size() << ", length=" << s.length() << '\n';
}
実行結果
size=5, length=5

sizeはバイト数でなく「文字数ではない」場合がある点に注意(UTF-8ではマルチバイトを含むため)。

本記事ではバイト列としての操作に限定します。

文字や別の文字列を追加する (+=, append, push_back)

代表的な追加方法

C++
#include <string>
#include <iostream>

int main() {
    std::string s = "C++";
    s += " and ";                  // 文字列追加
    s.push_back('S');              // 1文字追加
    s.append("tring");             // 末尾に追加
    std::cout << s << '\n';
}
実行結果
C++ and String

頻繁な連結ではreserve()で事前確保すると効率的です。

C++
#include <string>
#include <iostream>

int main() {
    std::string s;
    s.reserve(64); // 64バイト分の容量を予約(必要に応じて増える)
    s += "header:";
    s.append(50, '*'); // '*'を50回
    std::cout << s << " (len=" << s.size() << ")\n";
}
実行結果
header:************************************************** (len=57)

空文字かを判定する (empty)

空かどうかのチェック

C++
#include <string>
#include <iostream>

int main() {
    std::string s;
    if (s.empty()) {
        std::cout << "empty\n";
    }
    s = "x";
    if (!s.empty()) {
        std::cout << "not empty\n";
    }
}
実行結果
empty
not empty

空判定はsize() == 0よりempty()を使うと意図が明確です。

中身を消す (clear)

使い方と注意

C++
#include <string>
#include <iostream>

int main() {
    std::string s = "data";
    s.clear();                    // 中身を空にする
    std::cout << "[" << s << "] size=" << s.size() << '\n';
}
実行結果
[] size=0

clear()は内容を消しますが容量(capacity)は必ずしも減りません

容量も減らしたい場合はstd::string().swap(s)などの手法があります。

安全なアクセスと加工のしかた

範囲外アクセスを防ぐ (at)

operator[]との違い

operator[]は境界チェックを行いません。

at()は範囲外の場合std::out_of_rangeを送出します。

初心者は基本的にat()を使うと安全です。

C++
#include <string>
#include <iostream>

int main() {
    std::string s = "abc";
    try {
        std::cout << s.at(1) << '\n';  // 'b'
        std::cout << s.at(5) << '\n';  // 例外
    } catch (const std::out_of_range& e) {
        std::cout << "out_of_range: " << e.what() << '\n';
    }

    // 参考: [] は未定義動作の可能性があるので注意
    // char ch = s[5]; // <mark style="background-color:rgba(0, 0, 0, 0);color:#cf2e2e" class="has-inline-color"><strong>範囲外アクセス</strong></mark>
}
実行結果
b
out_of_range: basic_string::at: __n (which is 5) >= this->size() (which is 3)

文字を読む基本 (範囲for)

範囲forで1文字ずつ読む

C++
#include <string>
#include <iostream>

int main() {
    std::string s = "Hello";
    for (char ch : s) {            // 読み取り専用
        std::cout << ch << ' ';
    }
    std::cout << '\n';

    for (char& ch : s) {           // 参照で受け取れば書き換え可能
        ch = std::toupper(static_cast<unsigned char>(ch));
    }
    std::cout << s << '\n';
}
実行結果
H e l l o 
HELLO

書き換える場合は参照(char&)で受け取ることに注意します。

部分文字列を取り出す (substr)

substrの使い方

substr(pos, count)で部分文字列を取り出します。

範囲外のposは例外です。

C++
#include <string>
#include <iostream>

int main() {
    std::string s = "path/to/file.txt";
    std::string dir  = s.substr(0, 8);           // "path/to/"
    std::string file = s.substr(8);              // "file.txt"
    std::string ext  = s.substr(s.find_last_of('.') + 1); // "txt"
    std::cout << dir << "|" << file << "|" << ext << '\n';
}
実行結果
path/to/|file.txt|txt

文字列を検索する (find)

find系の基本

findは見つからないとstd::string::nposを返します。

必ずnposチェックをする習慣を付けましょう。

C++
#include <string>
#include <iostream>

int main() {
    std::string s = "abracadabra";
    auto pos1 = s.find("bra");        // 最初の"bra"
    auto pos2 = s.rfind('a');         // 最後の'a'
    if (pos1 != std::string::npos) {
        std::cout << "find: " << pos1 << '\n';
    }
    if (pos2 != std::string::npos) {
        std::cout << "rfind: " << pos2 << '\n';
    }
}
実行結果
find: 1
rfind: 10

見つからない条件を先に考えるとバグを防げます。

初心者向けの安全ルールとベストプラクティス

関数にはconst参照で渡す

なぜconst参照か

オブジェクトのコピーを避け、変更不可であることを明示できます。

読み取り専用の引数はconst std::string&で受けるのが基本です。

C++
#include <string>
#include <iostream>

// 読み取り専用: コピーしない(高速)
void print_title(const std::string& title) {
    std::cout << "Title: " << title << '\n';
}

// 値渡しは不要なコピーが発生することがある
void print_copy(std::string s) {
    std::cout << "Copy: " << s << '\n';
}

int main() {
    std::string t = "Guide";
    print_title(t);
    print_copy(t);
}
実行結果
Title: Guide
Copy: Guide

大きな文字列を値渡しすると性能が低下します

変更したい場合のみ非常に明確な意図で値渡しを検討します。

c_strの使いどころと注意点

いつ使うか

CのAPI(例: printf、OSの関数)に渡すときにc_str()が必要です。

C++
#include <string>
#include <cstdio>

int main() {
    std::string s = "hello";
    std::printf("%s %d\n", s.c_str(), 42); // C関数に渡す
}
実行結果
hello 42

ポインタの寿命に注意

s.c_str()で得たポインタはsの内容を変更したりsが破棄されると無効になります。

C++
#include <string>
#include <iostream>

int main() {
    std::string s = "abc";
    const char* p = s.c_str(); // pはsの内部バッファを指す
    s += "def";                // ここで再確保が起きるかも(= pは無効に)
    // std::cout << p << '\n'; // <mark style="background-color:rgba(0, 0, 0, 0);color:#cf2e2e" class="has-inline-color"><strong>未定義動作の可能性</strong></mark>
    std::cout << s.c_str() << '\n'; // 必要な都度取り直す
}
実行結果
abcdef

必要なときに毎回c_str()を呼ぶようにします。

不要なコピーを避ける基本

受け渡しで避ける

  • 引数はconst std::string&
  • 戻り値は値返しでOK(RVO/NRVOで最適化される)
  • 保持するときにムーブを活用可能
C++
#include <string>
#include <vector>
#include <iostream>

std::string make_message() {
    std::string s = "hello";
    s += " world";
    return s; // RVOでコピー省略されることが多い
}

int main() {
    std::vector<std::string> v;
    std::string msg = make_message();
    v.push_back(msg);                 // コピー
    v.push_back(std::move(msg));      // ムーブ(高速、msgは未規定状態だが空になることが多い)
    std::cout << "v[0]=" << v[0] << ", v[1]=" << v[1] << '\n';
}
実行結果
v[0]=hello world, v[1]=hello world

読み取り専用は参照、所有権の移動はムーブを使うと性能が安定します。

ヌル文字と空文字の違い

概念整理

  • ヌル文字'\0': 文字コード0。C文字列の終端に使う特別な文字。
  • 空文字: 長さ0の文字列。std::string()""

std::stringは内部に'\0'を含むことが可能で、長さはsize()で管理されます。

CのAPIは'\0'を終端と解釈するため、途中のヌルで切れてしまう点に注意します。

C++
#include <string>
#include <iostream>
#include <cstdio>

int main() {
    std::string s = std::string("A
#include <string>
#include <iostream>
#include <cstdio>
int main() {
std::string s = std::string("A\0B", 3); // 'A', '\0', 'B'
std::cout << "size=" << s.size() << '\n';
// C++側の出力(バイナリを含むので安全ではないが例示)
std::cout << "C++ cout -> " << s << '\n';
// C側の出力は'\0'で途切れる
std::printf("C printf -> %s\n", s.c_str());
}
B", 3); // 'A', '
#include <string>
#include <iostream>
#include <cstdio>
int main() {
std::string s = std::string("A\0B", 3); // 'A', '\0', 'B'
std::cout << "size=" << s.size() << '\n';
// C++側の出力(バイナリを含むので安全ではないが例示)
std::cout << "C++ cout -> " << s << '\n';
// C側の出力は'\0'で途切れる
std::printf("C printf -> %s\n", s.c_str());
}
', 'B' std::cout << "size=" << s.size() << '\n'; // C++側の出力(バイナリを含むので安全ではないが例示) std::cout << "C++ cout -> " << s << '\n'; // C側の出力は'
#include <string>
#include <iostream>
#include <cstdio>
int main() {
std::string s = std::string("A\0B", 3); // 'A', '\0', 'B'
std::cout << "size=" << s.size() << '\n';
// C++側の出力(バイナリを含むので安全ではないが例示)
std::cout << "C++ cout -> " << s << '\n';
// C側の出力は'\0'で途切れる
std::printf("C printf -> %s\n", s.c_str());
}
'で途切れる std::printf("C printf -> %s\n", s.c_str()); }
実行結果
size=3
C++ cout -> A
C printf -> A

バイナリデータを扱うときはCのAPIに直接渡さない、または長さを一緒に扱えるAPIを使います。

よく使うメンバ関数の早見表

分類メンバ目的安全性/注意点
生成/代入constructor, assign文字列の生成・置き換え明確な意図で選ぶ
取得size/length長さ取得(O(1))マルチバイトは別問題
追加+=, append, push_back末尾に追加reserveで効率化
判定empty空文字か判定size比較より可読
消去clear中身を消す容量は残る
アクセスoperator[], at文字取得/設定atは境界チェックあり
加工substr, replace部分抽出や置換範囲外は例外
検索find, rfind, find_first_of位置検索nposチェック必須
互換c_str, dataC APIへ受け渡し変更で無効化に注意

まずは安全性の高い関数(at, empty, find+nposチェック)を習慣化すると、初学者のうちは特に安心です。

まとめ

std::stringは安全で扱いやすいC++標準の文字列型で、長さや終端の管理を自動化し、バッファオーバーランの危険を大幅に減らします。

基本操作として初期化、代入、連結、長さ取得、空判定、消去を押さえ、アクセスはat()で安全に行う意識を持つと良いです。

加工や検索ではsubstrfind、互換性が必要な場面ではc_str()を用います。

引数はconst std::string&で受け、不要なコピーを避けることで性能も確保できます。

最後にヌル文字と空文字の違いを理解し、CのAPIと併用する際の落とし穴(途中の’\0’で途切れる)に注意してください。

これらのポイントを身に付ければ、文字列処理は強力かつ安全に行えるようになります。

この記事を書いた人
エーテリア編集部
エーテリア編集部

C++をこれから学ぶ方に向けて、基礎的な文法や標準ライブラリの使い方を紹介します。モダンな書き方も初心者に合わせてやさしく説明しています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!