次のコードは、2 行目でセグ フォールトを受け取ります。
char *str = "string";
str[0] = 'z'; // could be also written as *str = 'z'
printf("%s\n", str);
これは完全にうまく機能しますが:
char str[] = "string";
str[0] = 'z';
printf("%s\n", str);
MSVC および GCC でテスト済み。
次のコードは、2 行目でセグ フォールトを受け取ります。
char *str = "string";
str[0] = 'z'; // could be also written as *str = 'z'
printf("%s\n", str);
これは完全にうまく機能しますが:
char str[] = "string";
str[0] = 'z';
printf("%s\n", str);
MSVC および GCC でテスト済み。
C FAQ、質問 1.32を参照してください。
Q : これらの初期化の違いは何ですか?
char a[] = "string literal";
char *p = "string literal";
に新しい値を割り当てようとすると、プログラムがクラッシュしp[i]
ます。A : 文字列リテラル (C ソースの二重引用符で囲まれた文字列の正式な用語) は、2 つのわずかに異なる方法で使用できます。
- の宣言のように、char の配列の初期化子として、その配列内の
char a[]
文字の初期値 (および必要に応じてそのサイズ) を指定します。- それ以外の場所では、名前のない文字の静的配列に変わります。この名前のない配列は読み取り専用メモリに格納される可能性があるため、必ずしも変更することはできません。式のコンテキストでは、配列は通常どおり (セクション 6 を参照) すぐにポインターに変換されるため、2 番目の宣言は p を初期化し、名前のない配列の最初の要素を指すようにします。
一部のコンパイラには、文字列リテラルを書き込み可能にするかどうかを制御するスイッチがあり (古いコードをコンパイルするため)、一部のコンパイラには、文字列リテラルを形式的に const char の配列として扱うようにするオプションがあります (より適切なエラー キャッチのため)。
通常、文字列リテラルは、プログラムの実行時に読み取り専用メモリに格納されます。これは、文字列定数を誤って変更するのを防ぐためです。最初の例で"string"
は、読み取り専用メモリに格納され*str
、最初の文字を指しています。最初の文字を に変更しようとすると、segfault が発生します'z'
。
2 番目の例では、文字列"string"
はコンパイラによって読み取り専用のホームから配列にコピーされます。str[]
その後、最初の文字の変更が許可されます。これは、それぞれのアドレスを印刷することで確認できます。
printf("%p", str);
また、2 番目の例で のサイズをstr
出力すると、コンパイラが 7 バイトを割り当てたことがわかります。
printf("%d", sizeof(str));
これらの答えのほとんどは正しいですが、もう少し明確にするために...
人々が言及している「読み取り専用メモリ」は、ASM 用語のテキスト セグメントです。命令がロードされるメモリ内の同じ場所です。セキュリティなどの明らかな理由から、これは読み取り専用です。文字列に初期化された char* を作成すると、文字列データはテキスト セグメントにコンパイルされ、プログラムはテキスト セグメントを指すようにポインターを初期化します。変更しようとすると、カブーン。セグメンテーション。
配列として書き込まれると、コンパイラは代わりに初期化された文字列データをデータ セグメントに配置します。これは、グローバル変数などが存在するのと同じ場所です。データセグメントには命令がないため、このメモリは変更可能です。今度は、コンパイラが文字配列 (まだ単なる char* です) を初期化するときに、実行時に安全に変更できるテキスト セグメントではなく、データ セグメントを指しています。
文字列への書き込み時にセグメンテーション違反が発生するのはなぜですか?
C99 N1256 ドラフト
文字列リテラルには、次の 2 つの異なる用途があります。
初期化char[]
:
char c[] = "abc";
これは「より魔法」であり、6.7.8/14「初期化」で説明されています。
文字型の配列は、文字列リテラルで初期化することができ、オプションで中括弧で囲みます。文字列リテラルの連続する文字 (空きがある場合、または配列のサイズが不明な場合は、終端の null 文字を含む) は、配列の要素を初期化します。
したがって、これは次のショートカットです。
char c[] = {'a', 'b', 'c', '\0'};
他の通常の配列と同様に、c
変更できます。
それ以外の場合: 以下を生成します。
だからあなたが書くとき:
char *c = "abc";
これは次のようになります。
/* __unnamed is magic because modifying it gives UB. */
static char __unnamed[] = "abc";
char *c = __unnamed;
char[]
from からto への暗黙的char *
なキャストに注意してください。これは常に有効です。
次に、 を変更すると、 UB であるc[0]
も変更されます。__unnamed
これは、6.4.5「文字列リテラル」で文書化されています。
5 変換フェーズ 7 では、値ゼロのバイトまたはコードが、文字列リテラルまたはリテラルから生じる各マルチバイト文字シーケンスに追加されます。次に、マルチバイト文字シーケンスを使用して、シーケンスを格納するのに十分な長さの静的ストレージ期間の配列を初期化します。文字列リテラルの場合、配列要素は char 型を持ち、マルチバイト文字シーケンスの個々のバイトで初期化されます [...]
6 これらの配列の要素が適切な値を持っている場合、これらの配列が異なるかどうかは指定されていません。プログラムがそのような配列を変更しようとした場合、動作は未定義です。
6.7.8/32「初期化」は直接的な例を示しています:
例 8: 宣言
char s[] = "abc", t[3] = "abc";
「プレーンな」char 配列オブジェクト
s
を定義し、t
その要素は文字列リテラルで初期化されます。この宣言は、
char s[] = { 'a', 'b', 'c', '\0' }, t[] = { 'a', 'b', 'c' };
配列の内容は変更可能です。一方、宣言は
char *p = "abc";
「char へのポインタ」型で定義
p
し、要素が文字列リテラルで初期化される長さ 4 の「char の配列」型のオブジェクトを指すように初期化します。を使用p
して配列の内容を変更しようとした場合の動作は未定義です。
GCC 4.8 x86-64 ELF 実装
プログラム:
#include <stdio.h>
int main(void) {
char *s = "abc";
printf("%s\n", s);
return 0;
}
コンパイルと逆コンパイル:
gcc -ggdb -std=c99 -c main.c
objdump -Sr main.o
出力には次が含まれます。
char *s = "abc";
8: 48 c7 45 f8 00 00 00 movq $0x0,-0x8(%rbp)
f: 00
c: R_X86_64_32S .rodata
結論: GCC はchar*
それを.rodata
ではなくセクションに格納し.text
ます。
に対して同じことをするとchar[]
:
char s[] = "abc";
私達は手に入れました:
17: c7 45 f0 61 62 63 00 movl $0x636261,-0x10(%rbp)
そのため、スタックに格納されます (相対的に%rbp
)。
ただし、デフォルトのリンカ スクリプトは、実行権限はあるが書き込み権限がない同じセグメントに.rodata
andを置くことに注意してください。.text
これは、次の方法で確認できます。
readelf -l a.out
を含む:
Section to Segment mapping:
Segment Sections...
02 .text .rodata
最初のコードで、"string" は文字列定数です。文字列定数は読み取り専用メモリに配置されることが多いため、決して変更しないでください。「str」は、定数を変更するために使用されるポインターです。
2 番目のコードでは、「string」は配列初期化子であり、
char str[7] = { 's', 't', 'r', 'i', 'n', 'g', '\0' };
「str」はスタック上に配置された配列で、自由に変更できます。
"whatever"
最初の例のコンテキストでの型はconst char *
(const 以外の char* に割り当てた場合でも) であるため、書き込みを試みてはならないことを意味します。
コンパイラは、文字列をメモリの読み取り専用部分に配置することでこれを強制しているため、文字列に書き込むと segfault が生成されます。
char *str = "string";
上記のセットは、プログラムのバイナリ イメージにハードコードされstr
ているリテラル値を指すように設定さ"string"
れています。これは、メモリ内で読み取り専用としてフラグが立てられている可能性があります。
そのため、アプリケーションの読み取り専用コードに書き込もstr[0]=
うとしています。これはおそらくコンパイラに依存していると思います。
このエラーまたは問題を理解するには、最初にポインターと配列の違いを知っておく必要があるため、ここではまずそれらの違いについて説明します
char strarray[] = "hello";
メモリ配列では、連続したメモリ セルに格納され、[h][e][l][l][o][\0] =>[]
1 文字バイト サイズのメモリ セルとして格納されます。この連続したメモリ セルは、ここで strarray という名前でアクセスできます。したがって、ここでは、文字列配列strarray
自体に初期化された文字列のすべての文字が含まれています。この場合"hello"
、インデックス値で各文字にアクセスすることにより、メモリの内容を簡単に変更できます
`strarray[0]='m'` it access character at index 0 which is 'h'in strarray
その値が に変更された'm'
ため、strarray の値が に変更されました"mello"
。
ここで注意すべき点の 1 つは、文字列配列の内容を文字ごとに変更することで変更できますが、他の文字列を直接初期化することはできないということstrarray="new string"
です。
ポインタがメモリ内のメモリ位置を指していることは誰もが知っているように、初期化されていないポインタはランダムなメモリ位置を指しているため、初期化後は特定のメモリ位置を指しています
char *ptr = "hello";
ここで、ポインター ptr は"hello"
、読み取り専用メモリ (ROM) に格納されている定数文字列である文字列に初期化されるため"hello"
、ROM に格納されているため変更できません。
ptrはスタックセクションに格納され、定数文字列を指しています"hello"
したがって、読み取り専用メモリにアクセスできないため、ptr[0]='m' は無効です。
ただし、ptrは単なるポインターであるため、他の文字列値に直接初期化できるため、そのデータ型の変数の任意のメモリアドレスを指すことができます
ptr="new string"; is valid
char *str = "string";
コンパイラが実行可能ファイルの変更不可能な部分に配置する文字列リテラルへのポインターを割り当てます。
char str[] = "string";
変更可能なローカル配列を割り当てて初期化します
@matli がリンクしている C FAQ で言及されていますが、ここではまだ誰も言及していません。明確にするために、文字列リテラル (ソース内の二重引用符で囲まれた文字列) が文字配列を初期化する以外の場所で使用されている場合(つまり: @正しく動作するマークの 2 番目の例)、その文字列はコンパイラによって特別な静的文字列テーブルに格納されます。これは、本質的に匿名のグローバル静的変数 (もちろん読み取り専用) を作成することに似ています (変数 "name を持たない) 」)。読み取り専用部分は重要な部分であり、@Mark の最初のコード例が segfault になる理由です。
の
char *str = "string";
line はポインターを定義し、それをリテラル文字列にポイントします。リテラル文字列は書き込み可能ではないため、次のようにします。
str[0] = 'z';
セグフォルトが発生します。一部のプラットフォームでは、リテラルが書き込み可能なメモリにある可能性があるため、segfault は表示されませんが、無効なコード (未定義の動作が発生する) です。
この線:
char str[] = "string";
文字の配列を割り当て、リテラル文字列をその配列にコピーします。これは完全に書き込み可能であるため、その後の更新は問題ありません。
「string」のような文字列リテラルは、実行可能ファイルのアドレス空間に読み取り専用データとして割り当てられている可能性があります (コンパイラを与えるか取るか)。触ろうとすると、水着エリアにいることにびっくりし、セグ フォールトで知らせてくれます。
最初の例では、その const データへのポインターを取得しています。2 番目の例では、7 文字の配列を const データのコピーで初期化しています。
// create a string constant like this - will be read only
char *str_p;
str_p = "String constant";
// create an array of characters like this
char *arr_p;
char arr[] = "String in an array";
arr_p = &arr[0];
// now we try to change a character in the array first, this will work
*arr_p = 'E';
// lets try to change the first character of the string contant
*str_p = 'G'; // this will result in a segmentation fault. Comment it out to work.
/*-----------------------------------------------------------------------------
* String constants can't be modified. A segmentation fault is the result,
* because most operating systems will not allow a write
* operation on read only memory.
*-----------------------------------------------------------------------------*/
//print both strings to see if they have changed
printf("%s\n", str_p); //print the string without a variable
printf("%s\n", arr_p); //print the string, which is in an array.
そもそもstr
は を指すポインタです"string"
。コンパイラは、書き込みはできないが読み取りのみが可能なメモリ内の場所に文字列リテラルを配置できます。( を に割り当てているため、これは実際には警告をトリガーするはずでしconst char *
たchar *
。警告を無効にしましたか、それとも単に無視しましたか?)
2 番目に、完全にアクセスできるメモリである配列を作成し、 で初期化してい"string"
ます。(文字用に 6 つ、終端の '\0' 用に 1 つ) を作成し、char[7]
それを好きなように処理します。
1 つ目は、変更できない定数文字列です。2 番目は、値が初期化された配列であるため、変更できます。
アクセスできないメモリにアクセスしようとすると、セグメンテーション違反が発生します。
char *str
変更不可能な文字列へのポインタです(segfaultを取得する理由)。
一方char str[]
、配列であり、変更可能です..