記事ディレクトリ
序文
文字列は、C++ プログラマーが日常的に扱うものですが、他の基本的な文法と比較して、文字列には多くの "トラップ" があり、初心者を混乱させることがよくあります。C++シリーズの始まりとして、この章は著者の学習過程における文字列クラスのまとめです。
文字列をオブジェクトとして扱う多くの言語とは異なり、C の文字列はほとんど後付けのようなもので、実際には文字列データ型を十分に活用しておらず、単純なバイト配列のみを使用しています。C++ は、第一級のデータ型として文字列を提供し、そのための一連の便利なツールを設計しました。
この章では、最初に C スタイルの文字列を紹介し、次に C++ の文字列クラスとその「読み取り専用バージョン」string_view を紹介します。
C スタイルの文字列
C++ はより優れた文字列抽象化を提供しますが、C++ プログラミングでこれらのコードを使用することがまだ可能であるため、C から始める必要があります。
C 言語では、文字列は文字の配列として表され、文字列の最後の文字は null (/0) として指定され、文字列の終了位置をコンパイラに伝えます。したがって、プログラマーが最初に陥りがちな落とし穴は、/0 にストレージ スペースを割り当てるのを忘れることです (たとえば、"hello" は 5 文字のように見えますが、実際には 6 文字を格納する必要があります)。
C++ には、C 言語の文字列操作関数が含まれていますが、これらの関数は <cstring> ヘッダー ファイルで定義されていますが、通常、これらの関数はメモリ割り当てを直接操作しません。たとえば、strcpy() 関数には 2 つの文字列パラメーターがあり、適切に入力できるかどうかに関係なく、2 番目の文字列を最初の文字列にコピーします。
char* copyString(const char* str){
char* result{
new char[ strlen(str) + 1 ] }; // +1 for '/0'
strcpy(result , str);
return result;
}
strlen() 関数は文字列の実際の長さのみを返すことに注意してください。これは sizeof() 関数に対応します。sizeof() は、指定されたデータ型または変数のサイズを返します
char text1[] {
"abcdefg"};
size_t s1 {
sizeof(text1) }; //is 8
size_t s2 {
strlen(text1) }; //is 7
ただし、C スタイルの文字列が char* として格納されている場合、sizeof() 関数はポインターのサイズを返します。
const char* str {
"abcdefg"};
size_t s3 {
sizeof(str) }; //size of pointer
文字列リテラル
cout<<"hello"<<endl;
上記のコードでは、「hello」は変数ではなく値として表示されるため、文字列リテラルです。メモリの読み取り専用部分に格納されます。これは、コンパイラが同じ文字列リテラルを再利用する方法です。つまり、プログラムが「hello」を 1000 回使用しても、コンパイラはメモリ内に hello のインスタンスを 1 つだけ作成します。この手法はリテラル プーリングと呼ばれます
これはメモリの読み取り専用部分に格納されるため、C++ 標準では「n const char の配列」と定義されています。しかし、実際の使用では、const をサポートしないコードとの互換性を保つために、コンパイラは通常、const なしで char* に文字列リテラルを割り当てることを許可します。しかし、リテラルを変更しようとすると、未定義の動作が発生します。
char* ptr {
"hello" };
ptr[1] = 'E'; // Undefined behavior;
You can also use a stringliteral as a initial value of a character array char[]. この場合、コンパイラは適切なサイズの文字配列を作成し、文字列を配列に割り当てます。このとき、コンパイラはリテラルを読み取り専用領域に配置したり、リテラル プールの動作を設計したりしません。
char arr[] {
"hello" };
arr[1] = 'E'; //is OK
生の文字列リテラル
生の文字列は、エスケープ文字に頼らずに複数行のコードにまたがることができる文字列リテラルです。生の文字列は R"( ... )" の形式です。
const char* str1{
"hello "world"!"}; // Error!
const char* str2{
"hello \"world\"!"}; // is OK!
const char* str3{
R"(hello "world"!)" };// is OK!
const char* str4{
R"(hello
world!)"} // 换行
元の文字列はマークとして )" を使用しているため、文字)" には表示されませんが、現時点では、元の文字列は次をサポートしています。
R"d-char-sequence( )d-char-sequence"
d-char-sequence は、長さが 16 文字未満の区切り文字シーケンスであり、" と ( の間に新しい識別子として配置されます。同様に、この区切り文字シーケンスは元の文字列には表示されません。
const char* str {
R"-( "(Hello)" )-"}; // print "(Hello)"
C++ の文字列クラス
C++ は文字列の機能を大幅に改善しました. C++ では, 文字列は (basic_string テンプレート クラスのインスタンスとしての) クラスですが, C++ は多くの作業を行ったので, プログラマはほとんどの場合に単純な組み込み型として使用できます.使用されています。
1 つ目は演算子のオーバーロードです。string は演算子 + をオーバーロードして、文字列を連結します。
string a{
"12"};
string b{
"34"};
string c;
c = a + b; //c is "1234"
a += b; //a is "1234"
文字列比較:
C スタイルの文字列を == で比較することはできません。これは、文字列の内容ではなく、ポインターの値を比較するためです。C++ の文字列クラスでは、==、!=、< などをオーバーロードするだけでなく、実際の文字列文字を直接操作して比較することができます。
string a {
"12"};
string b {
"34"};
if(a == b){
/*...*/ };
文字列のサイズを比較するための compare() 関数も C++ で提供されており、より小さい、等しい、より大きい場合は -1、0、1 を返します。
string a {
"12"};
string b {
"34"};
auto result {
a.compare(b) };
if(result < 0) {
cout<<"less"<<endl; }
if(result > 0) {
cout<<"great"<<endl; }
if(result == 0) {
cout<<"equal"<<endl; }
メモリ処理
文字列を拡張する必要がある場合、文字列クラスはメモリ要件を自動的に処理します。これらの文字列オブジェクトはすべてスタック上の変数として作成され、文字列クラスは確かに多くの割り当てとサイズ変更を行いますが、文字列オブジェクトがスコープ外になると、文字列クラスのデストラクタがメモリをクリーンアップします。
string mystring {
"hello"};
mystring += ",there";
string myotherstring {
mystring };
if( mystring == myotherstring ){
myotherstring[0] = 'H';
}
cout<<myotherstring<<endl; // Hello,there
C スタイルの文字列との互換性
C++ は、C スタイルの文字列 const char ポインターを返す c_str 関数を提供しますが、string がメモリの再割り当てを実行するか、文字列オブジェクトが破棄されると、返された const ポインターは永久に無効になります。
C++14 以前の c_str のように const char* を返す data() メソッドもありますが、C++17 からは char* を返します。
コンパイラは通常、文字列リテラルを const char* として解釈します。文字列として解釈したい場合は、その後に s を追加できます
auto string1 {
"abcd" };
auto string2 {
"abcd"s };
これは、テンプレート実引数推定 (CTAD) を使用する場合に特に重要です。
vector names {
"john", "sam", "joe"}; // vector <const char*>
vector names2 {
"john"s, "sam"s, "joe"s}; // vector <string>
数値変換
高度な数値変換
文字列と数値の間の変換を実現するために std::string で定義された多くの補助関数があります。
string to_string(T val);
long double d {
3.14L};
string s {
to_string(d) }; //long double to string
上記の例に示すように、to_string 関数を使用して、値を文字列に変換します. T は、int、long、long long などの型にすることができます. これらはすべてメモリを操作し、新しい文字列オブジェクトを作成して返します.
文字列を数値に変換するには、次の関数を使用できます。
int stoi(const string& str, size_t *idx = 0, int base = 10);
long stol(const string& str, size_t *idx = 0, int base = 10);
unsigned long stoul(const string& str, size_t *idx = 0, int base = 10);
long long stoll(const string& str, size_t *idx = 0, int base = 10);
unsigned long long stoull(const string& str, size_t *idx = 0, int base = 10);
float stof(const string& str, size_t *idx = 0);
double stod(const string& str, size_t *idx = 0);
これらの関数は、変換される文字列を表す文字列型の値を受け取ります。idx は、変換されていない最初の文字のインデックスを指すポインターです。base は、変換ベースを表します。デフォルトは 10 です。base が 0 に設定されている場合、コンパイラはベースを指定された数の が自動的に推定されます。
- 0x で始まるものは 16 進数として解析されます
- 0 で始まる数値は 8 進数として解析されます
- それ以外の場合は、10 進数として解析されます
低レベルの数値変換
低レベルの数値変換関数は <charconv> ヘッダー ファイルで定義されており、高レベルの数値変換との違いは、メモリを操作したり、文字列クラスを使用したりせず、呼び出し元によって割り当てられたバッファーを使用することです。
これらの関数は主に「完全なラウンドトリップ」を実現するために存在するため、これにより、低レベルの数値変換が高レベルの数値変換よりも桁違いに高速になります。これには、数値のシーケンスを文字列に変換してから、結果の文字列を数値に戻す必要があり、結果のラウンドトリップはまったく同じです。
整数を文字列に変換するには、次を使用します。
to_chars_result to_chars(char* first, char* last,IntegerT value,int base = 10);
const size_t BufferSize{
50 };
string out(BufferSize, ' ');
auto result{
to_chars( out.data(), out.data()+out.size(),12345)};
逆に、文字列を数値に変換する場合:
from_chars_result from_chars(const char* first, const char* last, IntegerT& value, int base = 10);
from_chars_result from_chars(const char* first, const char* last, FloatT& value, chars_format format = chars_format::general);
string_view クラス
C++17 より前では、読み取り専用文字列を受け取るためのパラメーターの型を選択することはジレンマでした。プログラマーには 2 つのオプションがあり、それぞれに欠点があります。
const char* が使用されている場合、ユーザーは c_str() 関数を呼び出す必要があり、string によって提供される利便性がすべて失われます。
const string& を使用する場合、文字列リテラルを渡すときに、コンパイラはデフォルトで新しい一時文字列オブジェクトを作成して関数に渡します。これにより、オーバーヘッドが増加します。
最後の手段として、プログラマーは通常、同じ関数の 2 つのオーバーロード バージョンを作成してそれぞれ const char* と const string& を受け取りますが、C++17 で string_view が導入されるまで、これは洗練されたソリューションではありません。
string_view は基本的に const string& の置き換えですが、文字列をコピーしないのでオーバーヘッドはありません。string_view() がサポートするインターフェースは string() と非常によく似ています。唯一の例外は c_str() 関数がないことです。string_view() は remove_prefix(size_t) を提供して開始ポインターを指定された値だけ前方に移動します。 (size_t) 終了ポインターを後方に移動します。
string_view extractExtension(string_view filename){
// both const char* and string are OK!
return filename.substr()filename.rfind('.');
}
上記の例では、string_view はすべてのタイプの異なる文字列を受け取ることができます。また、string_view には文字列へのポインタと文字列の長さしか含まれていないため、通常は string_view を渡すときに値で渡します。ポインターと長さを使用して string_view を構築することもできます。
const char* raw {
/*...*/};
size_t length {
/*...*/};
auto res = extractExtension({
raw, length});
string_view と一時文字列
string_view を使用して文字列を暗黙的に作成することはできません. 次の例の handleExetension 関数は文字列値を受け取ります. 呼び出し 1 の extractExtension によって返される string_view は、文字列に暗黙的に変換することはできません. 呼び出し 2 および呼び出し 3 に示されているメソッドを使用して、明示的に文字列クラスをパパラッチするか、data() 関数を呼び出します
void handleExtension(const string& str){
/*...*/};
handleExtension(extractExtension("myfile.txt")); // Error!
handleExtension(string{
extractExtension("myfile.txt")}); // is OK!
handleExtension(extractExtension("myfile.txt").data()); // is OK!
上記の理由により、data() メソッドまたは append() を使用しない限り、string と string_view() を連結することはできません。
string str{
"hello"};
string_view sv{
"hello"};
auto result{
str + sv}; // Error!
auto result{
str + sv.data()}; // is OK!
string result{
str};
result.append(sv.data(),sv.size()); // is OK!
一時的な文字列を保持するために string_view を使用しないでください。
string s{
"hello"};
string_view sv {
s+"world"};
cout<<sv<<endl;
上記の例では、sv は一時文字列へのポインターを保存しますが、ポインターがアクセスされると、一時文字列は破棄されており (スコープを離れて)、このときのアクセスは未定義の状態です。
string_view リテラル
文字列は、sv を使用して string_view として解釈できます。
auto sv{
"mystring_view"sv};