はじめに
想定読者:C++の基礎文法を理解している初~中級者の方
C++を書いている人なら、未定義動作(Undefined Behavior。以下UBと表記)という言葉を聞いたことがあるでしょう。
UBの典型例としては、配列の範囲外アクセスやヌルポインタへのアクセスなどが挙げられますね。
UBは厄介なことに、コンパイルは通るしランタイムエラーも出ていない、だけど挙動が変...という不可解な現象が起きることがあり、原因特定に地味に苦労します。
本記事では、うっかり起きがちなUBを3つ紹介します。
皆さまのコードから少しでも未定義動作が減るきっかけになればうれしいです。
また、間違いや意見などありましたらコメントお願いします。
UB例1. 文字列と整数を+で結合
#include <iostream>
using namespace std;
int main() {
cout << "a" + 1; // これはセーフ(ただし意図に反した動作ではある)
cout << "a" + 2; // UB
return 0;
}
文字列と整数を+
演算子で結合したら暗黙的に文字列変換されるかと思いきや、そうではなくポインタ演算になります。
JavaやJavascriptなどでは正常動作するので、同じようにC++でもいけるだろう...と思うと思わぬ結果を招くことになります。
System.out.println("a" + 2); // OK
console.log("a" + 2); // OK
C++において、"a"
はconst char*
(文字列リテラル)です。
"a" + 1
という演算は、ポインタ演算により"a"
の2文字目(終端の\0
)を指します。よって出力されるのは空文字であり、こちらは特に問題ありません。
ただし、"a" + 2
のような場合は、ポインタ演算の結果が終端を超える(配列の範囲外となる)ためUBとなります。
正しいコード
cout << "a" + to_string(2);
整数をto_string
で囲んであげましょう。これで安心ですね。
UB例2. 配列に文字列終端\0
が入らないケース
char name[4] = {'A', 'B', 'C'};
cout << name; // セーフ(これはUBになると記載していましたが、コメントにてご指摘頂きセーフに修正しました。)
char name[3] = {'A', 'B', 'C'}; // 危険(使い方次第でUBが起きるかも)
上記のname[4]
の行のように、配列を初期化する際の要素数が配列の要素数より短い場合、空きの部分は0初期化されるため、問題ありません。(コメントにてご指摘いただきました。)
危険なのが、上記の例でname[4]
をname[3]
とした場合です。
これは配列の最後に終端文字が入らないため、使い方次第ではUBになる恐れがあります。
要素数には注意しましょう。
char name[] = "ABC"
のように要素数を省略する記法を使うと上記のようなうっかりは防げます。
または、string
という終端を気にしなくていい型を使うという方法もありますね。
UB例3. 未初期化変数の使用
int cnt;
for (int i = 0; i < 10; i++) {
cnt++; // UB
}
初歩的ながら、やらかしがちです。
競技プログラミングの話ですが、時間を気にしてババッとコードを書いていたら見事にこれをやらかし、レーティングを溶かしたのは良い思い出です。
正しいコード
int cnt = 0;
for (int i = 0; i < 10; i++) {
cnt++;
}
初期化は大事。
以上、油断すると起きるUB例の紹介でした。
知らなかった!という方は、ぜひ覚えておいてください。
知ってた!という方は、改めてうっかりしないよう意識していきましょう。