Cで宣言され、初期化されていない変数はどうなりますか?価値はありますか?


139

CIの場合:

int num;

に何かを割り当てる前にnumnum不確定の値はありますか?


4
ええと、それは宣言された変数ではなく、定義された変数ではありませんか?(それが私のC ++が輝いているとすみません...)
sbi

6
いいえ。変数を定義せずに宣言できます。extern int x;ただし、定義は常に宣言を意味します。これはC ++では当てはまりません。静的なクラスメンバー変数では、宣言はクラス定義(宣言ではなく)内にあり、定義はクラス定義の外になければならないため、宣言せずに定義できます。
bdonlan 2009年

ee.hawaii.edu/~tep/EE160/Book/chap14/subsection2.1.1.4.html 定義されているように見えるので、初期化する必要もあります。
2009年

回答:


188

静的変数(ファイルスコープと静的関数)はゼロに初期化されます。

int x; // zero
int y = 0; // also zero

void foo() {
    static int x; // also zero
}

非静的変数(ローカル変数)は不確定です。値を割り当てる前にそれらを読み取ると、未定義の動作が発生します。

void foo() {
    int x;
    printf("%d", x); // the compiler is free to crash here
}

実際には、最初は無意味な値が含まれる傾向があります。一部のコンパイラは、特定の固定値を入力してデバッガを確認するときに明らかになる場合があります-厳密に言えば、コンパイラはクラッシュから呼び出しまで自由に実行できますあなたの鼻の通路を介して悪魔

単に「未定義/任意の値」ではなく、未定義の動作である理由については、さまざまなタイプの表現に追加のフラグビットを持つCPUアーキテクチャがいくつかあります。最近の例はItaniumで、レジスタに「Not Thing」ビットがあります。もちろん、C標準の草案作成者は、いくつかの古いアーキテクチャを検討していました。

これらのフラグビットが設定された値を処理しようとすると、実際には失敗しないはずの操作(たとえば、整数の加算や別の変数への割り当て)でCPU例外が発生する可能性があります。また、変数を初期化せずにそのままにしておくと、コンパイラはこれらのフラグビットが設定されたランダムなガベージを取得する可能性があります。つまり、初期化されていない変数に触れると致命的になる可能性があります。


2
ああ、そうではありません。あなたがRでヶ月で、顧客の目の前にいないとき、あなたが幸運なら、彼らは、デバッグモードでは、あるかもしれない
マーティンベケット

8
何ですか?標準では静的な初期化が必要です。ISO / IEC 9899:1999を参照してください6.7.8#10
bdonlan

2
最初の例は私が知る限り問題ありません。なぜコンパイラが2番目のものでクラッシュする可能性があるのか​​については、あまり

6
@Stuart:「トラップ表現」と呼ばれるものがあります。これは基本的に有効な値を示さないビットパターンであり、実行時にハードウェア例外などを引き起こす可能性があります。すべてのビットパターンが有効な値であることが保証されている唯一のCタイプはcharです。他のすべてはトラップ表現を持つことができます。あるいは、初期化されていない変数へのアクセスはとにかくUBなので、準拠するコンパイラは単にチェックを行い、問題を通知することを決定するかもしれません。
Pavel Minaev 2009年

5
bdonianは正しいです。Cは常にかなり正確に指定されています。C89およびC99の前は、1970年代初頭にdmrの論文でこれらすべてが指定されていました。最も粗い組み込みシステムであっても、適切に処理するために必要なのは1つのmemset()だけなので、非準拠環境の言い訳はありません。私は私の回答で基準を引用しました。
DigitalRoss

57

静的またはグローバルの場合は0、ストレージクラスが自動の場合は不定

Cは常にオブジェクトの初期値について非常に具体的でした。グローバルまたはの場合static、それらはゼロになります。の場合auto、値は不確定です。

これはC89以前のコンパイラの場合であり、K&RおよびDMRの元のCレポートでそのように指定されていました。

これはC89の場合でした。セクション6.5.7初期化を参照してください。

自動保存期間を持つオブジェクトが明示的に初期化されていない場合、その値は不確定です。静的ストレージ期間を持つオブジェクトが明示的に初期化されない場合、算術型を持つすべてのメンバーに0が割り当てられ、ポインター型を持つすべてのメンバーにnullポインター定数が割り当てられたかのように、オブジェクトは暗黙的に初期化されます。

これはC99の場合でした。セクション6.7.8初期化を参照してください。

自動保存期間を持つオブジェクトが明示的に初期化されていない場合、その値は不確定です。静的ストレージ期間を持つオブジェクトが明示的に初期化されていない
場合、次のようになります。—ポインター型がある場合、オブジェクトはnullポインターに初期化されます。
—算術型の場合、(正または符号なし)ゼロに初期化されます。
—集合体の場合、すべてのメンバーはこれらのルールに従って(再帰的に)初期化されます。
—ユニオンの場合、最初の名前付きメンバーはこれらのルールに従って(再帰的に)初期化されます。

正確に不確定とはどういう意味かについて、私はC89について確信が持てないとC99は言います

3.17.2

特定の値またはトラップ表現の不定値

しかし、実際の標準では、どのような基準に関係なく、実際には各スタックページはゼロから始まりますが、プログラムがautoストレージクラスの値を調べると、プログラムが最後にスタックアドレスを使用したときに、自分のプログラムが残したものをすべて参照します。多数のauto配列を割り当てると、最終的にはゼロからきれいに始まることがわかります。

あなたは疑問に思うかもしれませんが、なぜこのようになっているのですか?別のSO回答がその質問を扱います。https//stackoverflow.com/a/2091505/140740を参照してください。


3
通常、不定(慣れている?)は何でもできることを意味します。ゼロにすることも、そこにあった値にすることも、プログラムをクラッシュさせることも、コンピューターにCDスロットからブルーベリーのパンケーキを作り出すこともできます。保証は一切ありません。それは惑星の破壊を引き起こすかもしれません。少なくとも仕様については...実際にそのようなことをするコンパイラを作った人は誰でもB-)にひどく眉をひそめるでしょう
Brian Postow 09/10/20

C11 N1570ドラフトでは、定義はindeterminate value3.19.2にあります。
user3528438 2016

静的変数にどのような値を設定するかは、常にコンパイラまたはOSに依存するようですか?たとえば、誰かが自分のOSまたはコンパイラを作成し、静的の初期値も不確定としてデフォルトで設定している場合、それは可能ですか?
Aditya Singh

1
@ AdityaSingh、OS はコンパイラーをより簡単にすることができますが、世界の既存のCコードのカタログを実行するのは最終的にコンパイラーの主な責任であり、標準を満たすことは副次的な責任です。別の方法で実行することは確かに可能ですが、なぜでしょうか。また、OSは実際にはセキュリティ上の理由から最初にページをゼロにする必要があるため、静的データを不確定にするのは難しい作業です。(自動変数は、あなた自身のプログラムが通常以前の時点でそれらのスタックアドレスを使用していたため、表面的には予測不可能です。)
DigitalRoss

@BrianPostowいいえ、それは正しくありません。stackoverflow.com/a/40674888/584518を参照してください。不確定な値を使用すると、未定義の動作ではなく、不特定の動作が発生します。トラップ表現の場合を除きます。
ランディン

12

変数の保存期間によって異なります。静的ストレージ期間を持つ変数は、常に暗黙的にゼロで初期化されます。

自動(ローカル)変数については、初期化されていない変数の不定です。不確定な値は、とりわけ、その変数で「見る」ことができる「値」が予測不可能であるだけでなく、安定していることさえ保証されないことを意味ます。たとえば、実際には(つまり、UBを1秒間無視する)このコード

int num;
int a = num;
int b = num;

は変数aを保証せずb、同じ値を受け取ることになります。興味深いことに、これは一部の理論的な概念ではありません。これは、最適化の結果として実際に容​​易に発生します。

したがって、一般に、「メモリにあるゴミで初期化される」という一般的な答えは、リモートでさえ正しくありません。初期化されていない変数の動作は、ガベージで初期化された変数の動作とは異なります。


私が理解できない(まあ、私は非常によくでき D:これはちょうど分後DigitalRossからのものよりもはるかに少ないupvotesを持っている理由)
アンティHaapala

7

Ubuntu 15.10、カーネル4.2.0、x86-64、GCC 5.2.1の例

十分な標準があります。実装を見てみましょう:-)

ローカル変数

標準:未定義の動作。

実装:プログラムはスタックスペースを割り当て、そのアドレスには何も移動しないので、以前そこにあったものが使用されます。

#include <stdio.h>
int main() {
    int i;
    printf("%d\n", i);
}

コンパイル:

gcc -O0 -std=c99 a.c

出力:

0

と逆コンパイルします:

objdump -dr a.out

に:

0000000000400536 <main>:
  400536:       55                      push   %rbp
  400537:       48 89 e5                mov    %rsp,%rbp
  40053a:       48 83 ec 10             sub    $0x10,%rsp
  40053e:       8b 45 fc                mov    -0x4(%rbp),%eax
  400541:       89 c6                   mov    %eax,%esi
  400543:       bf e4 05 40 00          mov    $0x4005e4,%edi
  400548:       b8 00 00 00 00          mov    $0x0,%eax
  40054d:       e8 be fe ff ff          callq  400410 <printf@plt>
  400552:       b8 00 00 00 00          mov    $0x0,%eax
  400557:       c9                      leaveq
  400558:       c3                      retq

x86-64呼び出し規約に関する知識から:

  • %rdiは最初のprintf引数なので、"%d\n"アドレスの文字列0x4005e4

  • %rsiは2番目のprintf引数なので、iです。

    これ-0x4(%rbp)は、最初の4バイトのローカル変数であるから取得されます。

    この時点rbpで、スタックの最初のページはカーネルによって割り当てられているので、その値を理解するには、カーネルコードを調べて、それが何に設定されているかを調べます。

    TODOカーネルは、プロセスが停止したときに他のプロセスに再利用する前に、メモリを何かに設定しますか?そうでない場合、新しいプロセスは他の終了したプログラムのメモリを読み取ることができ、データをリークします。参照:初期化されていない値はセキュリティリスクですか?

次に、独自のスタック変更を試して、次のような楽しいものを書くこともできます。

#include <assert.h>

int f() {
    int i = 13;
    return i;
}

int g() {
    int i;
    return i;
}

int main() {
    f();
    assert(g() == 13);
}

のローカル変数 -O3

実装分析:<値が最適化されました>はgdbで何を意味しますか?

グローバル変数

標準:0

実装:.bssセクション。

#include <stdio.h>
int i;
int main() {
    printf("%d\n", i);
}

gcc -00 -std=c99 a.c

コンパイル:

0000000000400536 <main>:
  400536:       55                      push   %rbp
  400537:       48 89 e5                mov    %rsp,%rbp
  40053a:       8b 05 04 0b 20 00       mov    0x200b04(%rip),%eax        # 601044 <i>
  400540:       89 c6                   mov    %eax,%esi
  400542:       bf e4 05 40 00          mov    $0x4005e4,%edi
  400547:       b8 00 00 00 00          mov    $0x0,%eax
  40054c:       e8 bf fe ff ff          callq  400410 <printf@plt>
  400551:       b8 00 00 00 00          mov    $0x0,%eax
  400556:       5d                      pop    %rbp
  400557:       c3                      retq
  400558:       0f 1f 84 00 00 00 00    nopl   0x0(%rax,%rax,1)
  40055f:       00

# 601044 <i>それiはアドレスに0x601044あり、そして:

readelf -SW a.out

含む:

[25] .bss              NOBITS          0000000000601040 001040 000008 00  WA  0   0  4

これは0x601044.bssセクションの真ん中にあり、0x6010408バイト長で始まります。

ELF標準はその後、名前のセクションがあることを保証し.bss、完全にゼロの充填されています。

.bssこのセクションは、プログラムのメモリイメージに寄与する初期化されていないデータを保持します。定義により、システムはプログラムの実行が開始されるとデータをゼロで初期化します。セクションタイプで示されるように、セクションはファイルスペースを占有しませんSHT_NOBITS

さらに、タイプSHT_NOBITSは効率的で、実行可能ファイルのスペースを占有しません。

sh_sizeこのメンバーは、セクションのサイズをバイト単位で示します。SHT_NOBITSセクションタイプがでない限り、セクションsh_size はファイルのバイトを占有します。タイプのセクションはSHT_NOBITSゼロ以外のサイズである可能性がありますが、ファイル内のスペースを占有していません。

次に、プログラムを開始したときにメモリにプログラムをロードするときに、そのメモリ領域をゼロにするのはLinuxカーネルの責任です。


4

場合によります。その定義がグローバル(関数の外)である場合num、ゼロに初期化されます。ローカル(関数内)の場合、その値は不確定です。理論的には、値を読み取ろうとしても未定義の動作があります-Cは値に寄与しないビットの可能性を許容しますが、変数の読み取りから定義された結果を得るために特定の方法で設定する必要があります。


1

コンピュータの記憶容量は有限であるため、自動変数は通常、以前に他の任意の目的で使用された記憶要素(レジスタまたはRAM)に保持されます。値が割り当てられる前にそのような変数が使用された場合、そのストレージは以前に保持していたものをすべて保持する可能性があるため、変数の内容は予測できません。

追加のしわとして、多くのコンパイラは、関連する型よりも大きい変数をレジスタに保持する場合があります。コンパイラーは、変数に書き込まれて読み戻される値が切り捨てられたり、適切なサイズに符号拡張されたりすることを保証する必要がありますが、多くのコンパイラーは、変数が書き込まれるときにそのような切り捨てを実行し、変数が読み込まれる前に実行されます。このようなコンパイラでは、次のようになります。

uint16_t hey(uint32_t x, uint32_t mode)
{ uint16_t q; 
  if (mode==1) q=2; 
  if (mode==3) q=4; 
  return q; }

 uint32_t wow(uint32_t mode) {
   return hey(1234567, mode);
 }

wow()値1234567をそれぞれレジスタ0と1 に格納し、を呼び出すことになるでしょうfoo()。以来xの機能をレジスタ0にその戻り値を置くことになっているので、「foo」という内で必要とされていない、と、コンパイラは0に登録割り振ることができますqmodeが1または3の場合、レジスタ0にはそれぞれ2または4がロードされますが、それ以外の値の場合、その値が範囲内になくても、関数はレジスタ0にあったもの(つまり、値1234567)を返すことがあります。 uint16_tの。

初期化されていない変数がドメイン外の値を保持しないようにするための追加の作業をコンパイラーに要求しないようにし、不確定な動作を過度に詳細に指定する必要がないようにするため、標準では、初期化されていない自動変数の使用は未定義の動作であるとしています。場合によっては、この結果は、値がその型の範囲外であることよりもさらに驚くかもしれません。たとえば、次の場合:

void moo(int mode)
{
  if (mode < 5)
    launch_nukes();
  hey(0, mode);      
}

コンパイラはmoo()、3より大きいモードで呼び出すと必ずプログラムが未定義の動作を呼び出すため、コンパイラはmode、通常4そのような場合の核の打ち上げ。標準も現代のコンパイラ哲学も、「hey」からの戻り値が無視されるという事実を気にしないことに注意してください。それを返そうとする行為は、コンパイラに無制限のライセンスを与えて任意のコードを生成します。


0

基本的な答えは、はい、未定義です。

このために奇妙な動作が見られる場合、それが宣言されている場所に依存している可能性があります。スタックの関数内にある場合、関数が呼び出されるたびに内容が異なる可能性が高くなります。静的スコープまたはモジュールスコープの場合は未定義ですが、変更されません。


0

ストレージクラスが静的またはグローバルの場合、読み込み中に、変数に何らかの値が最初に割り当てられない限り、BSSは変数またはメモリの場所(ML)を0に初期化します。ローカルの初期化されていない変数の場合、トラップ表現はメモリ位置に割り当てられます。したがって、重要な情報を含むレジスタがコンパイラによって上書きされると、プログラムがクラッシュする可能性があります。

ただし、コンパイラによっては、このような問題を回避するメカニズムを備えている場合があります。

char以外のデータ型の未定義の値を表すビットパターンを持つトラップ表現があることに気づいたとき、私はnec v850シリーズで作業していました。初期化されていない文字を受け取ったとき、トラップ表現のためにデフォルト値がゼロになりました。これは、necv850esを使用するany1に役立ちます。


unsigned charを使用するときにトラップ表現を取得する場合、システムは準拠していません。トラップ表現を含めることは明示的に許可されていません、C17 6.2.6.1/5。
ランディン

-2

numの値は、メインメモリ(RAM)からの不要な値になります。作成直後に変数を初期化する方が良いでしょう。


-4

私が行った限り、それはほとんどコンパイラに依存していますが、一般的にほとんどの場合、値はコンパイラによって0と事前に想定されています。
TCが0として値を与えている間、VC ++の場合にガベージ値を取得しました。以下のように出力します

int i;
printf('%d',i);

たとえば、確定的な値を取得する場合0、コンパイラはおそらくその値を取得するために追加の手順を実行します(とにかく変数を初期化するコードを追加することによって)。一部のコンパイラは「デバッグ」コンパイルを行うときにこれを行いますが0、これらの値を選択することは、コード内の障害を隠すため、悪い考えです(より適切なことは、非常にありそうもない数などを保証するため0xBAADF00Dです)。ほとんどのコンパイラは、メモリを占有するガベージを変数の値として残していると思います(つまり、一般的にとして組み立てられません0)。
16
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.