初めに
C++におけるvector<string>型変数の柔軟な扱い方について学習した記事です。
結論から言うと、vector<string>型変数:hogeはhoge[i][j]というように2次元配列として扱えます。
hoge[i]に収められている文字列を、hoge[i][j]で1文字ずつchar型として取り出せます。
ご指摘あればコメントでお願いいたします。
2023/12/23追記
コメントで今回扱ったような、「配列の配列」をジャグ配列と呼ぶことをご教示いただきました。
明解な概念を教えてくださり感謝いたします!
char Array[3][5] ← 「2次元配列」というとこういうやつで,
string Strs[3] みたいなのは,各stringを「charの配列」と考えるならば「配列の配列」なのであり,そういうのを「ジャグ配列 (jagged array)」と呼ぶ様子.
(後者の側は,個々の配列:ここではstring のサイズが異なり得る)
以下はC#でのジャグ配列についての具体例。↓
int[][] jarray = new int[2][];
jarray[0] = new int[] { 1, 2 };
jarray[1] = new int[] { 3, 4, 5 };
追記ここまで
きっかけ
C++で競技プログラミングサイトAtCoderの過去問
「ABC323 B - Round-Robin Tournament」
のユーザ解説を見ていて引っかかる点があった。
18行目のこの部分。
c[i][j]
この変数cは、9行目に宣言されている通りvector<string>型のvector。
さらっと流しそうになったのだが、2次元配列として扱っていることに躓く。
なぜ躓いたか
そもそも1次元ベクトルとして宣言しているので、c[i]としてアクセスするのは理解できる。
しかしどう見てもc[i][j]というのは2次元配列であり、しかもエラーにならず想定通りの挙動をしている。
想定というのはこの場合、i行目のj番目の文字列、-oxのうちのどれかにアクセスできているということ。
なぜこのような挙動ができるのか?素直に理解できず躓いてしまった。
ネットで調査してみても、明解なものが得られず。
(あくまで今回の疑問についてです。このような有用な知識を残してくださった方々に多謝)
「C++ vector string」などで検索。
C++のvectorまとめ
https://qiita.com/ysuzuki19/items/df872d91c9c89cc31aee
C++ で文字列の配列を作成する
https://www.delftstack.com/ja/howto/cpp/string-array-in-cpp/
競プロ等におけるC++の標準入力
https://qiita.com/zenrshon/items/cd19361842ae887afe8a
【C++】2次元配列の入力&出力方法
https://note.com/imsakasa/n/n0fcf0fd88744
std::vecotrを2次元配列として扱う
https://kaworu.jpn.org/cpp/std::vecotr%E3%82%92%EF%BC%92%E6%AC%A1%E5%85%83%E9%85%8D%E5%88%97%E3%81%A8%E3%81%97%E3%81%A6%E6%89%B1%E3%81%86
ChatGPTに頼る
結局、ChatGPT(3.5)に質問することに。ただ誤答もありえるため、裏付けをとるように心がける。
質問してみたところ、以下回答が得られた。
C++のstd::vectorやstd::stringなどの標準ライブラリコンテナは、operator[]をオーバーロードして、要素にアクセスするためのインターフェースを提供しています。
例えば、vector型の変数vがある場合、v[i]でi番目の要素にアクセスできます。これは、内部的にoperator[]がオーバーロードされており、対応する要素への参照(またはポインタ)を返すようになっています。
同様に、string型もoperator[]をオーバーロードしており、文字列中の各文字へアクセスできます。string型は実際にはchar型の配列として実装されているため、「abc」という文字列では0番目が'a'、1番目が'b'というようなアクセスが可能です。
この仕組みを利用することで、「c[i]」と書くことでi番目の文字列(c内部ではstring型)へアクセスし、「c[i][j]」と書くことでi番目の文字列中でj番目の文字(char型)へアクセスすることができます。
つまり、「c[i][j]」は「c[i]」から返されるstring型オブジェクトに対してさらに「[j]」を適用したものです。最初に「c[i]」が呼ばれて該当するstringオブジェクトへアクセスし、「[j]」が呼ばれてその中から該当するchar型要素へアクセスします。
したがって、二次元的な配列やベクター操作を必要とする場合でも簡単かつ直感的な記述方法です。
それらしい回答が得られた。個人的に重要だった部分を抜き出すと、
- [](インデックス演算子)をオーバーロードし、vectorでも利用できるようにしている
- vectorと宣言した場合、c[i]でそれぞれのstring型変数にアクセス
- c[i][j]で、指定したstring型の文字列を1文字ずつchar型として扱い、文字ごとにアクセス可能
ということだった。
そのため、c[i][j]とすることで文字列を2次元配列のようにアクセスすることが可能、ということになる。
裏付けも探してみたが、こちらの記事が参考になった。
i番目文字の参照(charに変換)、代入
[] 演算子(operator) を使って、通常の文字列と同じように、i 番目の文字の参照(char に変換)・代入が可能。
operator と聞くと身構える人がいるかもしれないが、「str[10]」の様に、普通の文字列の要素にアクセスする時と同じ記述だ。 恐れることは何も無い。
下記は、i 番目の文字の参照(char に変換)と代入例。普通の参照(char に変換)・代入とまったく同じでしょ。
std::string str("31415926535");
for (int i = 0; i < 10; ++i)
std::cout << str[i] << " "; // str の i 番目の要素を表示
const int SZ = 10; // 文字数
std::string str(SZ, 'a'); // 10 文字の 'a' から成る str を生成
for(int i = 0; i < SZ; ++i)
str[i] = 'b' + i; // 文字を 'b', 'c' ... に設定
std::cout << str << "\n";
vectorとして扱った記事ではないが、string型変数に配列としてアクセス→char型変数として取り出せることについて記述されていた。
結論
vectorはインデックス演算子[]がオーバーロードされているため、vector[]とすれば各行の要素にアクセスできる。
また、string型変数に[]を付与すれば、char型として1文字ずつアクセスできる。
これらを合わせてvector<string>型として宣言された変数は、変数名[][]というように2次元配列として扱うことができる。
振り返り
できれば公式ドキュメントを参照するのが一番良いのだろうが、そこまで理解できる読解力を持っていなかった。。
強いエンジニアになりたいと願った一件だった。
また、内容が充実している初心者・初級者向けC++の教本を読んだ方がいいかもしれないと痛感した。