【C言語】
初期化されていない自動変数が大量にある
スパゲティコード
(Uninitialized Variable)

warning: ‘ptr’ is used uninitialized

強い警告:初期化されていない自動変数を使用した
[-Wuninitialized]


■この記事の概要

この記事ではC言語で未初期化変数が発生しやすいスパゲティコードに焦点を当て、if文やfor文、switch文などにおける典型的な未初期化エラーと防止策を解説。

安全なコード設計のポイントも紹介します。


■初期化してない自動変数はゴミ

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

初期化していない自動変数(ローカル変数)の値はゴミです。

お客様へのバグの報告はいつも大変な作業ですが、
特にゴミの問題はプログラムに詳しくないお客様にも
問題である事が明確なので言い訳ができません。
C言語プログラマはこの問題を出さないように
常に気を付けましょう。


■初期化されていないポインタは何処を指す?

int main(void){  
    int     *ptr;
    *ptr = 0;//★NGこれはポインタの初期化ではない
}

初期化していない自動変数のポインタの値もゴミですので
メモリの何処を指しているのかわかりません。
何処を指しているかわからない
ポインタの指す先に
書き込みをすると多くの場合
Segmentation fault (コアダンプ )
を起こします。


warning: ‘x’ may be used uninitialized in this function

弱い警告:初期化されていない自動変数を使用したかも
[-Wmaybe-uninitialized]

■スパゲティコード

この警告が出るのは
gccコンパイラが変数の初期化をどこで行っているか
ソースコードを追いきれないからです。
gccコンパイラも追いきれないソースコードなので
普通のプログラマもソースコードを追うのは一苦労です。
このようなソースコードを
スパゲティコード(こんがらがったコード)と言います。


■if文の設定経路に漏れがある時

#include <stdio.h>
char    *gomi(int x,int y){
    char *ret ;
    if(x){
        if(y){
            ret = "真真";
        } else{
            ret = "真偽";
        }
    } else {
        if(y){
            ret = "偽真";
        }
    }
    return  ret ;
}
int main(void){
    printf("%s\n",gomi(1,1));
    printf("%s\n",gomi(1,0));
    printf("%s\n",gomi(0,1));
    printf("%s\n",gomi(0,0));
}

変数設定経路に漏れがあります。
xとyが共に0の時
retは設定されず未初期化のまま。


■for文で最大値変数が0の時

#include    <stdio.h>
#include    <string.h>

int gomi(char *str,int x)  {
    int  match;//max == 0の考慮漏れ
    int max = strlen(str);
    for(int i = 0;i<max;i++) {
        if(str[i] == x) {
            match = i ;
            break;
        } else {
            match = -1;
        }
    }
    return  match;
}
int main(void){
    char    ary[] = "jugemujyugemu";
    printf("%d\n",gomi(ary,'X'));
    printf("%d\n",gomi(ary,'g'));
    printf("%d\n",gomi("",'g'));
}

for文第2式の最大値が変数の時、
最大値が0かもしれない。
ループが回らないとmatchは未初期化のまま。


■for文でif文が一度も成立しない時

#include    <stdio.h>
int gomi(char *str,int x)  {
    int match;
    for(int i = 0;str[i] != '\0';i++) {
        //必ず一度は成立すると思い込むバグ
        if(str[i] == x) {
            match = i ;
            break;
        }
    }
    return  match; ;
}
int main(void){
    char    ary[] = "jugemujyugemu";
    printf("%d\n",gomi(ary,'j'));
    printf("%d\n",gomi(ary,'u'));
    printf("%d\n",gomi(ary,'g'));
    printf("%d\n",gomi(ary,'e'));
    printf("%d\n",gomi(ary,'M'));//?
}

for文内のif文が一度も成立しないと
matchは未初期化のまま。


■else if の最後にelse が無い時

#include <stdio.h>
char *gomi(int  x){
    char *ret;
    if(x > 0) {
        ret = "プラス";
    } else if(x < 0)  {
        ret = "マイナス";
    }//else 
        //多分岐通過時retはゴミ
    return  ret;
}
int main(void){
    printf("%s\n",gomi(1));
    printf("%s\n",gomi(-1));
    printf("%s\n",gomi(0));
}

else if 多分岐の最後に
else処理を記述して下さい。
x==0の時retは未初期化のまま。


■switch 最後にdefault処理が無い時

#include <stdio.h>
typedef enum{g,c,p} gcp_t;
char *gomi(gcp_t    x){
    char *ret;
    switch(x) {
    case  g:  ret = "グー" ;    break;
    case  c:  ret = "チョキ";   break;
    case  p:  ret = "パー";     break;
    default:
        //何もしない
    } 
    return  ret; 
}
int main(void){
    printf("%s\n",gomi(g));
    printf("%s\n",gomi(c));
    printf("%s\n",gomi(p));
    printf("%s\n",gomi(-1));
}

異常な入力で、
defaultを通過したとき
retは未初期化のままです。

設計上有り得ないと
コンパイラの警告を無視しないで、
エラー処理を追加しましょう。


■関数を跨いだ未初期化変数は解析が困難

#include <stdio.h>
void f1(int *p) {
    printf("%p\n",p);
    *p = 0 ;//初期化している
}
void f2(int *p) {
    printf("%p\n",p);
    //初期化していない
}
int main(void) {
    int     x,y;
    
    f1(&x) ; 
    f2(&y) ;//この行だけでは問題の有無を判断できない
    
    printf("%d\n",x);
    printf("%d\n",y);
}


gcc等の最近のコンパイラは未初期化を検出してくれますが、
完全に検出してくれるわけではありません。

関数を跨いだりファイルを跨いだりした
未初期化の検出は非常に困難で未検出となる場合があります。


この例ではgccは警告してくれません。

高額な静的解析ツールのカタログでは未初期化変数問題の検出可能をうたう物も多いですが、
実際のプロジェクトのソースコードを解析すると
タイムアウトとか、メモリ不足とかで
解析中断してしまうものが多いようです。


■推奨する対策

#include <stdio.h>
int  f1(void) {
    return 0 ;
}
int f2(void) {
    return ;//返却値を忘れてもコンパイラが警告を出してくれる
}
int main(void) {
    int x = f1() ; 
    printf("%d\n",x);

    int y = f2() ;
    printf("%d\n",y);
}

未初期化を防ぐ方法として以下のスタイルがあります。

1:変数宣言、値設定、値参照の行間を可能な限り縮める。
  C99が使用できるなら変数参照の直前で
  変数宣言と初期化を同時に行う。

2:関数のアドレス渡しによる値設定ではなく戻り値を使う
  func(&x) ; ではなくて
  x = func() ;とする。


参考:

CWE-457: Use of Uninitialized Variable

EXP33-C. 初期化されていないメモリからの読み込みを行わない

https://www.jpcert.or.jp/sc-rules/c-exp33-c.html

出力用引数よりも戻り値を使用してください。

https://ttsuki.github.io/styleguide/cppguide.ja.html#Output_Parameters

C-FAQ1.30: 自動の寿命を持つ変数の中身は、明示的に初期化しない限りゴミである

http://www.kouno.jp/home/c_faq/c1.html#30

■アンケート

23
あなたの初期化のスタイルを教えてください