Cの文字列は単なる文字配列であることを理解しています。そこで、次のコードを試してみましたが、ガベージ出力やプログラムのクラッシュなど、奇妙な結果が得られました。
#include <stdio.h>
int main (void)
{
char str [5] = "hello";
puts(str);
}
なぜこれが機能しないのですか?
gcc -std=c17 -pedantic-errors -Wall -Wextra
できれいにコンパイルされます。
注:この投稿は、文字列を宣言するときにNULターミネータ用のスペースを割り当てることができないことから生じる問題の正規のFAQとして使用することを意図しています。
C文字列は、nullターミネータで終わる文字配列です
すべての文字にシンボルテーブル値があります。 nullターミネータは、シンボル値_0
_(ゼロ)です。文字列の終わりを示すために使用されます。文字列のサイズはどこにも保存されないため、これは必要です。
したがって、文字列用のスペースを割り当てるたびに、ヌルターミネータ文字用に十分なスペースを含める必要があります。あなたの例ではこれを行わず、_"hello"
_の5文字分のスペースのみを割り当てます。正しいコードは次のとおりです。
_char str[6] = "hello";
_
または同等に、5文字と1つのヌルターミネータの自己文書化コードを書くことができます。
_char str[5+1] = "hello";
_
文字列のメモリを実行時に動的に割り当てる場合は、nullターミネーター用のスペースも割り当てる必要があります。
_char input[n] = ... ;
...
char* str = malloc(strlen(input) + 1);
_
文字列の最後にnullターミネータを追加しない場合、文字列を予期するライブラリ関数は正しく機能せず、ガベージ出力やプログラムクラッシュなどの「未定義の動作」のバグが発生します。
Cでnullターミネーター文字を書き込む最も一般的な方法は、いわゆる「8進エスケープシーケンス」を使用することで、次のようになります:_'\0'
_。これは_0
_を書くことと100%同等ですが、_\
_はゼロが明示的にnullターミネーターであることを明示する自己文書化コードとして機能します。 if(str[i] == '\0')
などのコードは、特定の文字がnullターミネーターかどうかをチェックします。
Nullターミネーターという用語は、nullポインターやNULL
マクロとは関係がないことに注意してください。これは混乱する可能性があります-非常に似た名前ですが、非常に異なる意味です。これが、ヌルターミネーターが1つのLでNUL
と呼ばれることがあり、NULL
またはnullポインターと混同しないようにする理由です。詳細は this SO question への回答を参照してください。
コード内の_"hello"
_はstring literalと呼ばれます。これは読み取り専用の文字列と見なされます。 _""
_構文は、コンパイラが文字列リテラルの最後にnullターミネータを自動的に追加することを意味します。したがって、sizeof("hello")
を出力すると、ヌルターミネーターを含む配列のサイズが取得されるため、5ではなく6になります。
それはgccできれいにコンパイルします
実際、警告すらありません。これは、C言語の微妙な詳細/欠陥により、文字配列を、配列内のスペースとまったく同じ数の文字を含む文字列リテラルで初期化し、nullターミネーターを静かに破棄するためです(C17 6.7.9/15)。歴史的な理由から、この言語は意図的にこのように動作しています。詳細については、 文字列初期化の一貫性のないgcc診断 を参照してください。また、C++はここでは異なり、このトリック/欠陥を使用できないことに注意してください。
C標準から(7.1.1用語の定義)
1 文字列は、最初のnull文字で終了し、最初のnull文字を含む連続した文字のシーケンスです。マルチバイト文字列という用語は、文字列に含まれるマルチバイト文字に与えられる特別な処理を強調したり、混乱を避けるために使用されることがあります。ワイドストリング。文字列へのポインタは、その最初の(最下位のアドレス指定された)文字へのポインタです。文字列の長さはnull文字の前のバイト数であり、文字列の値は含まれている文字の値のシーケンスです。
この宣言では
char str [5] = "hello";
文字列リテラル"hello"
は次のような内部表現を持っています
{ 'h', 'e', 'l', 'l', 'o', '\0' }
したがって、終了ゼロを含めて6文字です。その要素は、5文字だけのスペースを予約する文字配列str
を初期化するために使用されます。
C標準(C++標準の反対)では、文字列リテラルの終了ゼロが初期化子として使用されていない場合に、このような文字配列の初期化が可能です。
ただし、その結果、文字配列str
には文字列が含まれていません。
配列に文字列を含める場合は、次のように記述できます
char str [6] = "hello";
あるいは単に
char str [] = "hello";
最後のケースでは、文字配列のサイズは、6に等しい文字列リテラルの初期化子の数から決定されます。
すべてのstringsは文字の配列と見なすことができます(Yes)、すべて文字配列は考慮されますstrings(No)。
- 何故なの?そして、なぜそれが重要なのですか?
文字列の長さが文字列の一部としてどこにも保存されないことを説明する他の回答と、文字列が定義されている標準への参照に加えて、裏側は「Cライブラリ関数は文字列をどのように処理するか」です。
文字配列は同じ文字を保持できますが、最後の文字の後にnul-terminating文字が続いていない限り、それは単なる文字の配列です。そのnul-terminating文字により、文字の配列を文字列と見なす(扱う)ことができます。
文字列を引数として期待するCのすべての関数は、文字のシーケンスがnul-terminatedであることを期待しています。 なぜ?
すべての文字列関数が機能する方法に関係しています。長さは配列、文字列関数の一部として含まれていないため、nul-characterになるまで配列を前方にスキャンします(例:'\0'
-と同等10進数0
)が見つかりました。 ASCIIテーブルと説明 を参照してください。 strcpy
、strchr
、strcspn
などを使用しているかどうかに関係なく、すべての文字列関数はnul-terminatingに依存していますその文字列の終わりがどこにあるかを定義するために存在する文字。
string.h
の2つの類似した関数を比較すると、nul-terminating文字の重要性が強調されます。例えば:
char *strcpy(char *dest, const char *src);
strcpy
関数は、src
からdest
にバイトをコピーするだけで、nul-terminating文字が見つかり、strcpy
文字のコピーを停止する場所。次に、同様の関数memcpy
を使用します。
void *memcpy(void *dest, const void *src, size_t n);
関数は同様の操作を実行しますが、src
パラメータを文字列と見なしたり、必要としたりしません。 nul-terminating文字に到達するまで、memcpy
は単にsrc
を前方にスキャンしてバイトをdest
にコピーすることができないため、 3番目のパラメーターとしてコピーする明示的なバイト数。この3番目のパラメーターは、memcpy
に同じサイズの情報を提供しますstrcpy
は、nul-terminating文字が見つかるまで順方向にスキャンするだけで導出できます。
(これは、関数にnul-terminated文字列を指定して失敗した場合、strcpy
(または文字列を期待する関数)で何が問題になっているのかを強調します-それ(= /// =)nul-characterを呼び出すまでUndefined Behaviorを呼び出すことで、メモリセグメントの残りの部分でうまく競合します。たまたまメモリのどこかで見つかる-または、セグメンテーション違反が発生する)
つまり、why関数はnul-terminated文字列を期待しているため、nul-terminated文字列となぜ重要なのか。