はじめに
本記事はC言語におけるNULLガードという概念を勉強するにあたり、以下を調べてまとめた記事です。NULLガードとは、NULLを関数に渡した際のセグメンテーション違反を未然に防ぐ機構です。筆者の認識について過不足や誤りがありましたら是非コメントにてお知らせ頂けますと幸いです。
- NULLとは何か
- ヌルポインタとヌル文字の違いは何か
- NULLガードとは何か
- コード比較:なぜNULLガードが必要なのか
- WIP:NULLガードはどの関数が持つべきか(書籍でバリデーションの考え方を読んでインプット出来たら書きます。)
NULLとはなにか
参考:他多数
以下のように定義されています。
// 標準のヘッダーファイルの基本定義
#define NULL (void*)0
// 以下の場合もあるようです
#define NULL 0
つまりNULLとは、void型のポインタ変数であり、その中身は空っぽであることを意味しています。"NULL"と記載したときには「NULLポインタ」と表現することが正しいです。
「何のオブジェクトも指していないことが保証されたポインタ」です。コメントありがとうございます!
NULLポインタとヌル文字の違いは何か
C言語において文字列型を扱う際、文字列の終端を意味するヌル文字を意識することは一般的です。例えば文字列型の終端まで反復したい場合は以下のような制御文を記載します。
char *str;
str = "Hello, world!";
/* パターン1 */
while (*str)
/* パターン2 */
while (*str != '\0')
/* パターン3 */
while (*str != 0)
/* パターン4 : #warningが発生する */
while (*str != NULL)
// ※制御文の中身は以下が入るとする.
// str++;
- パターン1は論理式が省略されていますが、strはchar型のポインタ変数で、その間接参照先が何も無い=ヌル文字になった場合、制御文の判定式が偽を返すようになります。
- パターン2はパターン1を明示的に記載したものです。
- パターン3はパターン1, 2をASCIIコードで表現したケースです。
- パターン4はstrの間接参照先が "NULLポインタ" になった場合、制御文の判定式が偽を返すという風に読めますがコンパイル
できません。なぜならNULLはポインタ型の変数であり、strの間接参照されたint型変数と比較することは出来ないためです。できます。warningが発生するだけでした。
※パターン2と3について :
パターン1と完全に等価であると認識していますが、処理系によって異なるか、あるいはC言語の開発の常識的にどれを選択するべきかは調査スコープから外しました。もしご存知の方がいらっしゃいましたらコメントにてお知らせください。
参考:
NULLガード(NULLチェック)とは何か
ポインタ型変数を扱う際、その変数の間接参照先が有効であるかを判定し、無効な場合はその後の処理を実行しないNULLガードあるいはNULLチェックと呼ばれる制御機構があります。
目的はセグメンテーション違反を防ぐことです。詳しくは次項にて実際に説明します。
参考:
コード比較:なぜNULLガードが必要なのか
NULLガードをしていない場合
#include <stdio.h>
void test_while_loops(char *str) {
printf("Input string: %s\n", str);
// パターン1: 論理式省略
printf("Pattern 1:\n");
while (*str) {
printf("%c ", *str);
str++;
}
printf("\n");
// パターン2: ヌル文字終端判定
printf("Pattern 2:\n");
while (*str != '\0') {
printf("%c ", *str);
str++;
}
printf("\n");
// パターン3: ASCII コード0の終端判定
printf("Pattern 3:\n");
while (*str != 0) {
printf("%c ", *str);
str++;
}
printf("\n");
// パターン4: NULLとの比較
printf("Pattern 4:\n");
while (*str != NULL) {
printf("%c ", *str);
str++;
}
printf("\n");
}
#include "cmp_null.c"
void test_while_loops(char *str);
int main() {
// 通常のケース
char *str = "Hello, world!";
test_while_loops(str);
// ヌル文字のケース
char *str = "";
test_while_loops(str);
// NULLポインタのケース
char *str = NULL;
test_while_loops(str);
return 0;
}
こちらより誰でも実行できます。引数にNULLを代入した際にNULLガード機構を持たないため、セグメンテーション違反(不正なメモリアクセス)が発生します。
NULLガード機構を入れた場合
#include <stdio.h>
void test_while_loops(char *str) {
// NULLガード
if (str == NULL) {
printf("NULL pointer\n");
return;
}
int i = 0;
printf("Input string: %s\n", str);
// パターン1: 論理式省略
printf("Pattern 1:\n");
while (str[i] != '\0') {
printf("%c ", str[i]);
i++;
}
printf("\n");
// パターン2: ヌル文字終端判定
printf("Pattern 2:\n");
i = 0;
while (str[i] != '\0') {
printf("%c ", str[i]);
i++;
}
printf("\n");
// パターン3: ASCII コード0の終端判定
printf("Pattern 3:\n");
i = 0;
while (str[i] != 0) {
printf("%c ", str[i]);
i++;
}
printf("\n");
// パターン4: NULLとの比較
printf("Pattern 4:\n");
while (*str != NULL) {
printf("%c ", *str);
str++;
}
printf("\n");
}
#include "safe_cmp_null.c"
void test_while_loops(char *str);
int main() {
// 通常のケース
char *str = "Hello, world!";
test_while_loops(str);
// ヌル文字のケース
char *str = "";
test_while_loops(str);
// NULLポインタのケース
char *str = NULL;
test_while_loops(str);
return 0;
}
今回はNULLガード機構があるため、NULLを代入しても関数が途中で終了し不正なメモリアクセスを未然に防いでプログラムを実行することができました。
WIP:NULLガードはどの関数が持つべきか
Vladimir Khorikov (著), 須田智之 (翻訳), 『単体テストの考え方/使い方』 (2022年, マイナビ出版)
NULLガードをもつべき関数とそうでない関数の判断基準を学びたいことを相談した際、こちらを友人からおすすめされ、勉強中です。他にも良い書籍をご存知の方はぜひコメントにておすすめいただきたいです。
終わりに
NULLガードのまとめについては以上になります。意図しないセグメンテーション違反によるプログラムの途中終了をNULLガードによって防げるように実装していきたいと思います。閲覧いただきありがとうございました。