C/C++の配列は要注意で以前挙げたことがあるが、C言語やC++においては、(独自ライブラリーを使っていない通常配列で、なおかつ一般的な環境においては)配列外参照をしても問題ないように動いてしまうという問題を持っていることは簡単に説明した ((C++においてはstd::vectorやstd::arrayなどにおけるat()関数を使うなどをした場合はその限りではない)) 。ここでは、実際の例を挙げてみたい。
問題のコード例
さて、以下のコードがあったとする。
[code lang=”c”]#include <stdio.h>
#include <stdlib.h>
struct foo {
int bar[5];
int baz;
};
int main(void){
struct foo *qux = (struct foo *)calloc(1, sizeof(struct foo));
for (int i = 0; i <= 5; i++) {
qux->bar[i] = i;
}
for (int i = 0; i < 5; i++) {
printf("The value of qux->bar[%d]: %d\n", i, qux->bar[i]);
}
printf("The value of qux->baz: %d\n", qux->baz);
free(qux);
return 0;
}[/code]
上記のコードがあった場合、動作としては以下のいずれになるだろうか?前提条件として、AddressSanitizerなど特殊なツールを使っていない状況での動作を想定する。
選択肢
- コンパイル時にエラーが発生する
- 実行時に異常終了する。
- 最後まで動作する。
答え
3. 最後まで動作する
出力例
[text]The value of qux->bar[0]: 0
The value of qux->bar[1]: 1
The value of qux->bar[2]: 2
The value of qux->bar[3]: 3
The value of qux->bar[4]: 4
The value of qux->baz: 5[/text]
解説
今回の場合、構造体fooの中にbarというint型が5個格納された配列とbazというint型の変数が定義されている。
まず、一つ目のfor文でiが0以上から5以下までの間、barにiの値を代入するという処理を行っているが、C言語の配列は0スタートであるため、iが5の時、実際にはbarの配列の外を参照していることになる。
この際に、境界チェックが働いて異常終了することを期待する場合があるが、実際にはそうならず、barの配列の次の変数であるbazに代入してしまうという処理を行ってしまう。
そのため、上記の出力例のような結果になったというわけである。
これに関連する問題点と対策
このコードのように、配列外に参照した場合でも、C言語やC++における通常の配列を使用した場合に、このように配列外にアクセスしたときでも通常は異常終了することがなく、あたかも正常に動いてしまうように見えてしまうという問題がある。
この場合、システムが大規模になるほど意図せぬところで動作異常が発生するなど猛威を振るうことになる。これは、配列外参照をしたとしてクラッシュするよりも原因究明の難しいバグを引き起こすことになり、それだけ対処に苦慮しなければならなくなってしまう。
そのため、C++であればstd::vectorやstd::arrayなどを使い、参照時はat()関数を使う、C言語でも何らかの形で安全に使えるようにするように努めるのが望ましいのではないかと考えている。
最後に
今回はC言語における配列外参照の実例とその問題点を挙げてみた。C言語やC++はかなりシステムよりな制御を自由に行える反面、このように問題のあるコードが動いてしまうという危険性もあるため ((下手をすればシステムを破壊しかねないほどのものもある)) 、気をつけて書くようにしたいところである。
ウェブマスター。本ブログでITを中心にいろいろな情報や意見などを提供しています。主にスマートフォン向けアプリやウェブアプリの開発を携わっています。ご用の方はコメントかコンタクトフォームにて。