文字列はプログラムの随所で扱いますが、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)
#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は必要に応じて自動的に容量を拡張するため、この種の事故を大幅に減らせます。
例: 自動拡張の挙動
#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>
も必要です。
#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の基本操作 (作成/代入/結合/長さ)
文字列の初期化と代入の基本
代表的な初期化方法
#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
状況に応じて最も意図が明確な初期化を選ぶと読みやすくなります。
代入
#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()
の使用が多いです。
#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)
代表的な追加方法
#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()
で事前確保すると効率的です。
#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)
空かどうかのチェック
#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)
使い方と注意
#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()
を使うと安全です。
#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文字ずつ読む
#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
は例外です。
#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
チェックをする習慣を付けましょう。
#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&
で受けるのが基本です。
#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()
が必要です。
#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
が破棄されると無効になります。
#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で最適化される)
- 保持するときにムーブを活用可能
#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'
を終端と解釈するため、途中のヌルで切れてしまう点に注意します。
#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, data | C APIへ受け渡し | 変更で無効化に注意 |
まずは安全性の高い関数(at, empty, find+nposチェック)を習慣化すると、初学者のうちは特に安心です。
まとめ
std::stringは安全で扱いやすいC++標準の文字列型で、長さや終端の管理を自動化し、バッファオーバーランの危険を大幅に減らします。
基本操作として初期化、代入、連結、長さ取得、空判定、消去を押さえ、アクセスはat()
で安全に行う意識を持つと良いです。
加工や検索ではsubstr
とfind
、互換性が必要な場面ではc_str()
を用います。
引数はconst std::string&
で受け、不要なコピーを避けることで性能も確保できます。
最後にヌル文字と空文字の違いを理解し、CのAPIと併用する際の落とし穴(途中の’\0’で途切れる)に注意してください。
これらのポイントを身に付ければ、文字列処理は強力かつ安全に行えるようになります。