コンテンツへスキップ
Topos Uranos

要約

テキストはプログラムでもっとも頻繁に現れるデータである。名前、メッセージ、ファイルの各行、ユーザーのコマンドがそうだ。この回は、テキストを C++ にとっての本来の姿、すなわち固有の操作をもつ文字の列として扱う。まず std::string を紹介する。これはテキスト用の char のベクターのように、作成し、連結し、比較し、走査するものである。次にその検索と抽出の操作(find、substr、starts_with、contains)と、find が何も見つからなかったことを伝える値 std::string::npos を扱う。さらに、テキストをコピーせずに参照するビュー std::string_view と、それが依存する条件、つまり参照先のテキストが存在し続けることを扱う。最後に、他人のコードを読むための C 文字列と、C++ が単語を比較し並べる順序を扱う。

学習目標

  1. std::string の操作(size、+、find、substr、starts_with、contains)を使ってテキストの問題を解く。
  2. find の結果を解釈して std::string::npos と比べ、それを普通の数として扱うと何が起こるかを予測する。
  3. 関数でテキストを std::string_view として受け取り、ビューが参照先の文字列より長く生きてはならない理由を例で説明する。
  4. C 文字列を使うコードを読み、std::string との違い、すなわちヌル終端、固定のサイズ、コンパイルできない二つのリテラルの和を説明する。

文字列 std::string

作成、連結、走査

レッスン 文字・真偽値・型変換・定数 は、文字 char を、表によって文字に変換される小さな数として紹介した。テキストは文字の並びである。そうであれば、テキストは std::vector<char> に格納でき、レッスン ベクター が列について教えたことはテキストにも当てはまるはずである。しかし、テキストには任意の列には求めない操作が求められる。別のテキストをつなげる、中から単語を探す、一部分を取り出す、一度に書き出す、といった操作である。ここから専用の型、文字列 std::string が必要になる。ヘッダ <string> にあるこの型は、テキスト用の char のベクターである。

C++ のプログラム
#include <format>#include <print>#include <string>​int main(){    const std::string first{"Ada"};    std::string greeting{"Hola, " + first};    greeting += '.';    std::println("{}({} 文字)", greeting, greeting.size());    std::println("最初「{}」、最後「{}」", greeting[0], greeting.back());    int spaces{0};    for (const char c : greeting) {        if (c == ' ') {            ++spaces;        }    }    std::println("空白:{}", spaces);    const std::string code{std::format("{}-{:03}", "MAT", 7)};    std::println("{} {}", code, code == "MAT-007");}
出力
Hola, Ada.(10 文字)最初「H」、最後「.」空白:1MAT-007 true

7 行目はリテラルから文字列を作る。8 行目はそれをスペイン語のあいさつ「Hola, 」(「こんにちは、」)と連結する。文字列とリテラルのあいだの演算子 + は、両方の文字を順に並べた新しい文字列を作る。9 行目は += で末尾に文字を一つ加える。この演算子は文字列やリテラルも受け付ける。残りはベクターについてすでに知っていることである。size() は文字を数え(10。コンマ、空白、ピリオドも文字だからである)、greeting[0] が最初の文字、back() が最後の文字であり、13 行目から 17 行目のループは、レッスン ループ の範囲を使う形で文字列を一文字ずつ走査する。ベクターと同じく、文字列は push_back、pop_back、empty も使え、GCC 15 のライブラリは、最適化しないときは operator[] で添字を検査する(最適化すると、この検査は消える)。

最後の 2 行は、レッスン 関数による設計 が予告したことを果たす。<format> にある std::format は、std::println と同じプレースホルダと同じ書式指定を受け付ける({:03} は 3 桁になるまでゼロで埋める)が、結果を書き出す代わりに文字列として返す。あとで保存し、比較し、関数に渡すテキストを組み立てる方法がこれである。そして == は二つの文字列を内容で比べる。サイズが同じで、同じ文字が同じ順序で並んでいれば等しい。したがって code == "MAT-007" は true である。左のテキストは部分ごとに組み立てられ、右のテキストはリテラルであるにもかかわらずである。

比較

文字列は == と != のほかに <、<=、>、>= も使え、これらは辞書式順序、つまり辞書の順で比べる。二つの文字列を並行してたどり、最初に異なる位置で小さい文字のほうが小さいと決まる。差がないまま一方が先に尽きれば、短いほうが小さい。こうして、スペイン語の単語「casa」は「cosa」より小さい。2 番目の位置で 'a' < 'o' だからである。また「mar」は「marea」より小さい。前者が後者の接頭部だからである。これらの演算子は文字列を比べる。引用符で囲んだ二つのリテラルのあいだでは別のものを比べてしまう。リテラルは std::string ではなく配列だからであり、このことは C 文字列の節で見る。どの文字が「小さい」かを決めるのはアルファベットではなくその文字コードである。そこから何が導かれるかは、この回の最後の節で示す。