C++の文字列1
stringクラス
C言語で文字列を扱うにはchar型配列を使用していました。
C++では文字列をより便利、かつ安全に扱える方法が提供されています。
#include <iostream>
#include <string>
int main()
{
const char str1[] = "ABC"; //C言語的な文字列
std::string str2 = "DEF"; //stringクラスによる文字列
std::cout << str1 << std::endl;
std::cout << str2 << std::endl;
std::cin.get();
}
std::string
というのが文字列を扱うクラスです。
stringクラスはstd
名前空間に属します。
クラスについては今はstd::cout
などと同様の「何か便利な機能」という認識で構いません。
stringクラスを使用するにはコード先頭に#include <string>
を記述します。
<string.h>
ではないので注意してください。
string.hはC言語の文字列操作系関数を使用するためのもので、別物です。
stringクラスはchar型の時のように配列を作る必要はなく、そのまま宣言しただけで文字列を扱えるようになります。
char型の配列による文字列よりも直感的で、とても手軽に扱えます。
初期化と代入
#include <iostream>
#include <string>
int main()
{
std::string str1 = "ABCDE";
//空の文字列を宣言
std::string str2;
std::cout << str2 << std::endl;
//str2にstr1をコピー
str2 = str1;
std::cout << str2 << std::endl;
//str2に長めの文字列をコピー
str2 = "abcdefghijklmn";
std::cout << str2 << std::endl;
std::cin.get();
}
ABCDE abcdefghijklmn
stringクラスの変数は初期化せずに宣言しても問題ありません。
その場合は自動的に空の文字列で初期化されますので、そのまま出力してもエラーにはなりません。
stringクラス変数同士は=
で代入するだけで文字列がコピーできます。
char型配列のようにstrcpy
関数などを使用する必要はありません。
文字列リテラルの代入も同様に可能です。
これは自動的にstringクラス文字列への変換が行われます。
変数に現在保存している文字数以上の文字列を代入しても問題ありません。
stringクラスは、文字列配列のように配列のサイズを気にする必要はなく、自動的に必要なサイズをメモリ上に確保してくれます。
そのため、バッファオーバーランは起こりません。
C言語ではちょっとクセのあった文字列ですが、C++ではint型などと同じくらいに手軽に扱えます。
初期化
初期化には以下の方法を用いることもできます。
これ以降の関数等の説明では同じ関数名で複数の使い方ができるものが多数登場します。
(→関数のオーバーロード)
全てを解説していては大変なので、使用頻度が比較的高そうなものだけをピックアップしています。
#include <iostream>
#include <string>
int main()
{
using namespace std;
string str1 = "ABCDEFG";
//str1で初期化
string str2(str1);
cout << str2 << endl;
//ABCDEFG
//str1の先頭から2文字以降で初期化
string str3(str1, 2);
cout << str3 << endl;
//CDEFG
//str1の2文字目から3文字で初期化
string str4(str1, 2, 3);
cout << str4 << endl;
//CDE
//str1の2文字目から4文字目手前までで初期化(範囲指定)
string str5(&str1[2], &str1[4]);
cout << str5 << endl;
//CD
//5個のxで初期化
string str6(5, 'x');
cout << str6 << endl;
//xxxxx
cin.get();
}
丸括弧を使用するのはクラスの機能のひとつであるコンストラクタというものを呼び出しています。
これはクラスの項で詳しく説明するので、こういう方法もあるということを知っておくだけで構いません。
なお、先頭文字は0文字目とカウントされることに注意してください。
これはchar型配列のときと同じです。
assign関数
代入にはassign
関数を使用する方法もあります。
#include <iostream>
#include <string>
int main()
{
using namespace std;
string str1 = "ABCDEFG";
string str2;
//str1を代入
str2.assign(str1);
cout << str2 << endl;
//ABCDEFG
//str1の先頭から2文字以降を代入
str2.assign(str1, 2);
cout << str2 << endl;
//CDEFG
//str1の2文字目から3文字代入
str2.assign(str1, 2, 3);
cout << str2 << endl;
//CDE
//str1の2文字目から4文字目手前までを代入(範囲指定)
str2.assign(&str1[2], &str1[4]);
cout << str2 << endl;
//CD
//5個のxを代入
str2.assign(5, 'x');
cout << str2 << endl;
//xxxxx
cin.get();
}
できることは初期化の時とほぼ同じです。
戻り値は代入された文字列への参照です。
n文字目にアクセス
char型配列のように、角括弧[]
(添え字演算子)で文字列の特定の文字にアクセスできます。
#include <iostream>
#include <string>
int main()
{
using namespace std;
string str = "ABCDE";
//「C」を表示
cout << str[2] << endl;
str[2] = 'x';
//「ABxDE」を表示
cout << str << endl;
//範囲外アクセス
//未定義動作なのでクラッシュするかもしれない
cout << str[10] << endl;
cin.get();
}
値の取り出しと値の書き換えができるのもchar型配列と同じです。
範囲外の要素にアクセスできないのも同じです。
ただしコンパイルエラーにはならず、プログラムが落ちない可能性もありますが、メモリ上の予期しない位置の読み書きが行われることに違いはありませんから、絶対に避けるべきです。
(char型配列でも同じです)
at関数
任意の位置の文字の読み書きにはat
関数を使用する方法もあります。
#include <iostream>
#include <string>
int main()
{
using namespace std;
string str = "ABCDE";
//不定、クラッシュするかもしれない
cout << str[10] << endl;
//例外を発生させる
cout << str.at(10) << endl;
cin.get();
}
角括弧[]
を使った場合と基本的に同じですが、範囲外の要素にアクセスしようとしたときの挙動が異なります。
角括弧の場合の動作は不定(どうなるかわからない)ですが、at
関数は例外を発生させます。
例外についてはまだ説明していませんが、例外は適切に処理することでプログラムを停止せずに問題に対処することができます。
(ただし若干速度が劣る可能性があります)
上記コードは例外処理をしていないので、プログラムは停止します。
文字列の結合
#include <iostream>
#include <string>
int main()
{
using namespace std;
string str1 = "ABC";
string str2 = "DEF";
string str3 = str1 + str2;
str3 += "GHI";
cout << str3 << endl;
//ABCDEFGHI
cin.get();
}
文字列の結合は結合したいstring型変数同士を+
演算子で結ぶだけです。
文字列リテラルともそのまま結合できます。
append関数
文字列の結合にはappend
関数を使用することもできます。
初期化および使い方はassign関数の時とほぼ同じです。
#include <iostream>
#include <string>
int main()
{
using namespace std;
string str1 = "ABCDEFG";
string str2 = "abc";
//str1を結合
str2.append(str1);
cout << str2 << endl;
//abcABCDEFG
str2 = "abc";
//str1の先頭から2文字以降を結合
str2.append(str1, 2);
cout << str2 << endl;
//abcCDEFG
str2 = "abc";
//str1の2文字目から3文字結合
str2.append(str1, 2, 3);
cout << str2 << endl;
//abcCDE
str2 = "abc";
//str1の2文字目から4文字目手前までを結合(範囲指定)
str2.append(&str1[2], &str1[4]);
cout << str2 << endl;
//abcCD
str2 = "abc";
//5個のxを結合
str2.append(5, 'x');
cout << str2 << endl;
//abcxxxxx
cin.get();
}
戻り値は結合された文字列への参照です。
文字列の長さの取得
#include <iostream>
#include <string>
int main()
{
using namespace std;
string str1 = "ABC";
string str2 = "あいう";
size_t len1 = str1.length();
size_t len2 = str2.size();
//3
cout << len1 << endl;
//6
cout << len2 << endl;
cin.get();
}
文字列の長さはlength
関数またはsize
関数で取得できます。
両者に違いはないようです。
返される値は「文字列を格納する配列の要素数」です。
(ただしchar型配列のように終端のNULL文字は含みません)
マルチバイト文字の場合はバイト数と同じですが、ワイド文字(wstring)の場合は基本的に一要素に一文字を格納できるため、文字数が返されます。
ただしワイド文字でも二要素が必要な文字も存在するので、正確な文字数を表すとは限りません。
なお、stringクラスの文字列は末尾にNULL文字がありません。
(Visual C++の場合。これはコンパイラによるらしいです)
そのため、NULL文字を文字列の終了と判定するコードを組むと期待通りの動作にならないかもしれません。
確保メモリのサイズ
stringクラスはメモリ上に文字列を保存するわけですが、stringクラスが「保存している文字列のサイズ」と「確保しているメモリのサイズ」は同じではありません。
stringクラスはあらかじめいくらかのサイズのメモリを確保し、そこに文字列を保存します。
確保しているメモリのサイズ以上のサイズの文字列を格納しようとしたとき、確保メモリを拡張します。
具体的なサイズはコンパイラ依存です。
現在確保しているメモリのサイズ(要素数)はcapacity
関数で取得できます。
#include <iostream>
#include <string>
int main()
{
string s;
cout << s.capacity() << endl;
s = "abcdefghijklmno"; //15文字
cout << s.capacity() << endl;
s = "abcdefghijklmnop"; //16文字
cout << s.capacity() << endl;
}
15 15 31
具体的なサイズはコンパイラ依存です。
上記の例ではあらかじめ15文字分のサイズが確保されており、これを超える場合にメモリが再確保されることになります。
メモリの確保と縮小
メモリの確保はそれなりにコストのかかる処理で、何度も再確保を繰り返すとパフォーマンスに影響がでる可能性があります。
必要なサイズがあらかじめわかっている場合は、そのサイズのメモリを最初に確保しておくことで再確保の回数を減らせるので、パフォーマンスが改善する可能性があります。
メモリの確保はreserve
関数を使用します。
stringクラスが確保するメモリは、自動的に拡張されることはあっても自動的に縮小されることはありません。
メモリを節約するために確保メモリを縮小する場合はshrink_to_fit
関数を使用します。
この関数はC++11以降で使用可能です。
#include <iostream>
#include <string>
//string文字列のcapacityを出力する関数
void showStringCapacity(const std::string& s)
{
std::cout << "capacity: " << s.capacity();
std::cout << ", \"" << s << "\"" << std::endl;
}
int main()
{
std::string s;
showStringCapacity(s);
//26文字が確保可能なサイズのメモリを確保
s.reserve(26);
showStringCapacity(s);
for (size_t n = 0; n < 26; n++) {
s += 'a' + n;
}
showStringCapacity(s);
s = "";
showStringCapacity(s);
//メモリ領域を縮小
s.shrink_to_fit();
showStringCapacity(s);
std::cin.get();
}
capacity: 15, "" capacity: 31, "" capacity: 31, "abcdefghijklmnopqrstuvwxyz" capacity: 31, "" capacity: 15, ""
reserve
関数は、指定の要素数が格納できるメモリサイズが確保されます。
指定の要素数「以上」であればよく、具体的なサイズは実装依存です。
現在確保しているメモリサイズ以下となる値を指定した場合、C++17までは縮小される可能性がありましたが、C++20からはこの関数でメモリの縮小は行われないように動作が変更されています。
shrink_to_fit
関数は、現在確保している文字列のサイズ(size
関数が返す値)が確保可能なサイズにメモリが縮小されます。
これも具体的なサイズは実装依存です。
ただしコンパイラによってはメモリの縮小は行われない可能性もあります。
上記のshowStringCapacity
関数は、引数を参照で渡しています。
ポインタや参照で渡さない場合は実引数をコピーしたものが関数に渡されることになりますが、コピーされるのは文字列のみであり、メモリ容量(capacity)はコピーされません。
文字列の比較
#include <iostream>
#include <string>
int main()
{
using namespace std;
string str1 = "ABCDEFG";
string str2 = "ABCDEFG";
string str3 = "aBCDEFG";
if (str1 == str2)
cout << "str1とstr2は同じ" << endl;
else
cout << "str1とstr2は違う" << endl;
if (str1 == str3)
cout << "str1とstr3は同じ" << endl;
else
cout << "str1とstr3は違う" << endl;
if (str1 == "ABCDEFG")
cout << "str1と'ABCDEFG'は同じ" << endl;
else
cout << "str1と'ABCDEFG'は違う" << endl;
cin.get();
}
str1とstr2は同じ str1とstr3は違う str1と'ABCDEFG'は同じ
文字列の比較は==
や!=
で行えます。
<
や<=
などの不等号による比較も可能です。
これは文字の順番(AとBならAの方が小さい)となります。
名前を昇順で並び替える場合などに使用します。
compare関数
文字列の比較はcompare
関数を使用することもできます。
#include <iostream>
#include <string>
int main()
{
std::string str1 = "ABCDEFG";
std::string str2 = "ABCDEFG";
std::string str3 = "aBCDEFG";
std::cout << str1.compare(str2) << std::endl;
std::cout << str1.compare(str3) << std::endl;
std::cout << str1.compare("ABCDEFG") << std::endl;
std::cin.get();
}
文字列同士が同じならば「0」を返します。
str1の方が小さければ-1以下を、str1の方が大きければ1以上を返します。
(int型)
空文字列かどうかを調べる
文字列が空か否かを調べるにはempty
関数を使用します。
#include <iostream>
#include <string>
int main()
{
std::string str1;
std::string str2 = "";
std::string str3 = "ABC";
std::cout << str1.empty() << std::endl;
std::cout << str2.empty() << std::endl;
std::cout << str3.empty() << std::endl;
//1
//1
//0
std::cin.get();
}
文字列が空の場合にtrue
、空ではない場合にfalse
を返します。
その他、文字列の長さを取得して0か否かで判定するという方法もあります。
ただ、専用の方法が用意されている場合は素直にそれを使用した方が効率面で優れている場合が多いです。
stringクラスにはまだ機能はありますが、ページが長くなりすぎるので分割します。