このページでは、C言語における「ヌル文字(NULL 文字)」について解説していきます。
このヌル文字は、C言語で文字列を扱う際に非常に重要な役割を果たす文字になります。このヌル文字について理解しておかないと、自身のプログラムで上手く文字列を扱うことができませんし、下手すると重大なバグが発生する可能性もあります。
C言語で文字列を利用するのであれば、ヌル文字については必ず理解しておいた方が良いと思いますので、是非このページで理解を深めていってください!
Contents
ヌル文字とは
まず、ヌル文字について簡単に内容をまとめておきます。
ヌル文字は制御文字の1つ
ヌル文字とは、'\0'
で表される「制御文字」になります。制御文字とは、表示するためのものではなくプログラムに制御を行わせるための文字となります。例えば改行文字 '\n'
は「制御文字」の1つであり、これはプログラムに改行という制御を行わせるための文字となります。
Windows 環境などの場合は '\0'
や '\n'
が '¥0'
や '¥n'
と表示される場合があります
ちなみに、文字の実体は単なる整数であり、各文字が区別して扱えるよう、各文字に対して整数が割り振られています。'\0'
に割り振られている整数は 0
となります。
例えば、下記のように '\0'
を整数として出力すれば出力結果は 0
となるはずです。
printf("null character = %d\n", '\0');
スポンサーリンク
ヌル文字の役割
そして、このヌル文字 '\0'
は、プログラムに文字列の終端を認識させるための文字となります。
C言語では、実は文字列型という型は存在しません。そのため、文字を扱う際によく利用される char
型の配列(データ列)を用い、これを擬似的に文字列として扱うようになっています。
ただし、文字列として扱うためには char
型の配列に単に文字を格納すれば良いというわけではなく、文字列の最後の文字の後ろ側にヌル文字を格納しておく必要があります(ヌル文字で終端する)。
例えば、下記は配列 str
に Hello
という文字列を格納する処理の一例になります。配列 str
から Hello
の文字を1文字ずつ格納し、最後にヌル文字 '\0'
で終端しています。
char str[32];
str[0] = 'H';
str[1] = 'e';
str[2] = 'l';
str[3] = 'l';
str[4] = 'o';
str[5] = '\0';
これだけ見ると '\0'
が余計なものに見えるかもしれませんが、この '\0'
の格納は配列 str
を文字列として扱うために必要な制御文字となります。このヌル文字 '\0'
が必要な理由について、ここから解説していきます。
ヌル文字の必要性
続いて、このヌル文字が必要である理由について考えていきたいと思います。
文字列を扱ってヌル文字の必要性を考える
今回は、文字列のコピーを実現しながらヌル文字の必要性を考えていきたいと思います。
例えば下記のような stringcpy
関数において、char
型の配列 src
に格納された文字列を char
型の配列 dst
にコピーする処理を実現することを考えてみましょう!
void stringcpy(char dst[], char src[]) {
/* srcの文字列をdstにコピー */
}
文字列のコピーにおいても、考え方は基本的には単なる配列の各要素のコピーと同様で、文字列を格納した配列の先頭の要素から文字列の最後の要素までを繰り返しコピーしてやれば良いです。コピーは、コピー先の要素に対してコピー元の要素を代入することで実現できます。
繰り返しコピーすれば良いので、下記のように for
文やwhile
文を用いてループを組み、そのループの内部の処理で要素のコピーを行えば良いことになります。
void stringcpy(char dst[], char src[]) {
int i;
for (i = 0; ????; i++) {
dst[i] = src[i];
}
}
for
文やwhile
文のどちらで実現しても良いのですが、いずれにしても問題になるのがループの継続条件(or 終了条件)になるでしょう(上記では継続条件を ????
と記載しています)。前述の通り、文字列をコピーする場合は文字列の先頭から文字列の最後までの文字をコピーすることになるのですが、この際のループの継続条件は何にすれば良いでしょうか?
ヌル文字とは を読んでいただいた方なら大体予想がついているのではないかと思います。文字列がヌル文字で終端されていることを前提とすれば、ループの継続条件は「src[i]
がヌル文字('\0'
)でないこと」とすることができます。つまり、src[i] != '\0'
が成立する間ループを継続し、その間 dst[i] = src[i]
によって文字のコピーを行なっていけば良いことになります。
つまり、先ほど紹介した stringcpy
における for
文の継続条件は下記のように src[i] != '\0'
となります。この繰り返し処理により、文字列のコピーが行えるようになったことになります(後述で解説しますが、この関数はまだ文字列のコピーとしては処理が不十分です。理由が気になる方は、ここで是非理由を考えてみてください)。
void stringcpy(char dst[], char src[]) {
int i;
for (i = 0; src[i] != '\0'; i++) {
dst[i] = src[i];
}
}
スポンサーリンク
ヌル文字で終端されていることは「前提」である
このように、プログラム内で文字列を扱う際、ヌル文字('\0'
)を文字列の終端として扱うことで、上記のようにループを継続する期間や処理を行う範囲等を適切に設定することができるようになります。
つまり、C言語では、文字列がヌル文字で終端されていることを前提とし、その前提を考慮に入れて処理を実現することで、文字列をうまく扱うことができるようになっています。
ですが、この文字列がヌル文字で終端されていることを前提とした処理が存在するため、この前提が満たされていないと文字列を扱う処理が破綻する可能性があるので注意してください。
例えば、上記の for
ループは src
に格納されている文字列がヌル文字で終端されていることを前提とした処理となっています。そのため、src
に格納されている文字列がヌル文字で終端されていない場合、上記の for
ループが延々と繰り返し行われてしまう可能性があります(メモリアクセス違反が発生してプログラムが異常終了する可能性もあります)。
実際、下記のように stringcpy
関数を実行した場合、私の場合はメモリアクセス違反が発生してプログラムが異常終了しました。
#include <stdio.h>
#include <string.h>
void stringcpy(char dst[], char src[]) {
int i;
for (i = 0; src[i] != '\0'; i++) {
dst[i] = src[i];
}
}
int main(void) {
char dst[32];
char src[32];
memset(dst, 0x77, 32);
memset(src, 0x77, 32);
src[0] = 'H';
src[1] = 'e';
src[2] = 'l';
src[3] = 'l';
src[4] = 'o';
stringcpy(dst, src);
printf("dst = %s\n", dst);
printf("src = %s\n", src);
return 0;
}
上記のソースコードにおいて、memset
関数では配列 dst
と配列 src
全体の各要素を0x77
という値で埋める処理を行なっています。 0x77
は文字として出力した場合は 'w'
と表示されるので、配列 dst
と配列 src
の全要素に 'w'
という文字が格納されることになります。
文字列を扱う際に、必ず memset
関数を実行する必要があるというわけではないので注意してください。この memset
関数はヌル文字の必要性を理解していただくことを目的にあえて実行しています。上記では 'w'
を格納するようにしていますが、ヌル文字以外であれば他の文字を格納した場合でも同様の動作になります(memset
関数を実行しなかった場合の stringcpy
の動作については後述で説明します)。
そして、その後に配列 src
に文字を格納して stringcpy
を実行しています。つまり、stringcpy
関数実行時の配列 src
の中身は下の図のようになります。
この場合、配列 src
に文字列の終端を表す '\0'
が存在しないため、配列 src
の要素を全てコピーし終わった後もコピーが続けて行われることになります。そして、その際に配列外のアドレスにアクセスすることになり、他の変数や配列の値が上書きされたり、メモリアクセス違反が発生してプログラムが異常終了したりします。
このような動作を防ぐためには、前提である「文字列がヌル文字で終端されている」を満たすように文字列を扱う必要があります。上記の例であれば、src[5] = '\0'
を行なってから stringcpy
を実行すれば、少なくとも文字列のコピーは正常に完了するはずです。
#include <stdio.h>
#include <string.h>
void stringcpy(char dst[], char src[]) {
int i;
for (i = 0; src[i] != '\0'; i++) {
dst[i] = src[i];
}
}
int main(void) {
char dst[32];
char src[32];
memset(dst, 0x77, 32);
memset(src, 0x77, 32);
src[0] = 'H';
src[1] = 'e';
src[2] = 'l';
src[3] = 'l';
src[4] = 'o';
src[5] = '\0';
stringcpy(dst, src);
printf("dst = %s\n", dst);
printf("src = %s\n", src);
return 0;
}
この場合、src[5]
にヌル文字が存在しますので、i
が 5
の際に src[i] != '\0'
が不成立となって for
ループが終了し、配列外へのアクセスを確実に防ぐことができます。
このように、文字列をヌル文字('\0
)で終端されていることを前提とすることで、文字列の終わりを認識することができるようになり、文字列を適切に扱うことができるようになります。
ヌル文字で終端されていることを前提とする関数が存在する
さて、先ほど紹介したソースコードのプログラムでは、ヌル文字を格納するようになったことで、文字列のコピー自体はヌル文字が見つかった時点で確実に終了するようになります。
ただし、このプログラムを実行して出力される文字列は下記のようになります。実行したタイミング等によって異なる結果が出力される可能性もありますが、dst
の出力で Hello
以降に w
が出力されてしまっているはずです。
dst = Hellowwwwwwwwwwwwwwwwwwwwwwwwwww src = Hello
この出力は printf
関数によって行われているため、つまりは printf
関数の出力がおかしいことになります。
なぜおかしくなってしまっているかというと、それは dst
がヌル文字で終端されていないからです。
stringcpy
関数では、src
の先頭からヌル文字の直前までのみが dst
にコピーされるようになっています。つまり、ヌル文字のコピーは行われていません。したがって、dst
がヌル文字で終端されていないことになります。
なので、dst
を文字列として正しく扱うためには、stringcpy
関数のループが終わった後に、dst
に対してヌル文字を格納する必要があります。
つまり、stringcpy
関数は下記のように変更する必要があります。これで文字列をコピーする関数としては出来上がりということになります。
void stringcpy(char dst[], char src[]) {
int i;
for (i = 0; src[i] != '\0'; i++) {
dst[i] = src[i];
}
dst[i] = '\0';
}
上記のように変更すれば、配列 dst
の文字列の最後にヌル文字が格納されるようになり、
printf
関数実行時には、src
からコピーされた文字列部分のみが出力されるようになります。
ここで重要なのは、ヌル文字での終端を忘れないようにしましょう!というのが一点で、もう一点はヌル文字で終端されていることを前提として動作するC言語標準ライブラリ関数が存在するという点になります。
その1つが上記で挙げた printf
関数になります。printf
関数で変換指定子に %s
を指定すれば引数で指定したアドレスの文字列を出力することは可能なのですが、あくまでもその文字列はヌル文字で終端されていることを前提としています。stringcpy
関数同様に、引数で指定したアドレスから順々にヌル文字が見つかるまで1文字ずつ出力が行われるようになっているのだと思います。
そのため、文字列がヌル文字で終端されていない場合、出力結果が意図しないものになってしまいます。
このように、文字列を扱う標準ライブラリ関数においても、ヌル文字で文字列が終端されていることが前提となっているものが多いです。特に関数名が str
から始まる関数のような、文字列を扱う標準ライブラリ関数に対して文字列を引数として入力する場合などは、の文字列を確実にヌル文字で終端しておくことが必要となります。
ここまでをまとめると、文字列を扱う処理や文字列を扱う関数は「文字列がヌル文字で終端されている」ことを前提としているものが多いです。逆に、その前提が崩れてしまうと処理や関数が正常に動作しなくなってしまうため、それを防ぐためにヌル文字での終端が必要となっています。
ヌル文字に関する注意点
ヌル文字の必要性については理解していただけたでしょうか?
次は、ヌル文字に関する注意点について説明していきたいと思います。
スポンサーリンク
ヌル文字での終端忘れに注意
1つ目の注意点は、ここまでの解説の中でも説明しているように、文字列を扱う際にはヌル文字での終端を忘れないようにする必要があるという点になります。
ヌル文字の必要性 でも説明したように、基本的に文字列を扱う処理や関数は、その文字列がヌル文字で終端されていることを前提としています。そのため、ヌル文字での終端を忘れてしまうと、それらの処理や関数が正常に動作しなくなってしまいます。
ヌル文字での終端忘れは見つけにくい
ただ、このヌル文字での終端忘れのバグは発見しにくいという問題があります。
この点を理解していただくために、次は下記のソースコードのプログラムの動作について考えていきたいと思います。
このソースコードは ヌル文字の必要性 で紹介したソースコードを少し変更したもので、src
がヌル文字で終端されていないというバグが存在しています。
#include <stdio.h>
#include <string.h>
void stringcpy(char dst[], char src[]) {
int i;
for (i = 0; src[i] != '\0'; i++) {
dst[i] = src[i];
}
dst[i] = '\0';
}
int main(void) {
char dst[32];
char src[32];
memset(dst, 0x77, 32);
memset(src, 0x77, 32);
src[0] = 'H';
src[1] = 'e';
src[2] = 'l';
src[3] = 'l';
src[4] = 'o';
stringcpy(dst, src);
printf("dst = %s\n", dst);
printf("src = %s\n", src);
return 0;
}
このプログラムの場合、一度実行すればプログラムのバグを確実に発見することができます。
実際に実行してみれば分かると思いますが、実行するとメモリアクセス違反等が発生して異常終了する、もしくは dst
の出力結果が異常であることが確認できると思います(Hellowwwww....
と Hello
の後ろに余計な w
が付いている)。
上記プログラムにおいて、一度実行すればプログラムのバグを確実に発見することが出来るようになっているのは memset
を実行していることが理由になります。memset
で配列の中身を全てヌル文字以外(今回の場合は 'w'
)で埋め尽くしているため、ヌル文字での終端を忘れていた場合、配列内にヌル文字が存在しないため確実にプログラムの結果が意図しないものになります。
では、memset
関数を実行しなかった場合、プログラムの動作はどうなるでしょうか?正常に文字列のコピーが完了するでしょうか?
この答えは「どう動作するか分からない」となります。その理由は “配列 src
が未初期化” & “未初期化の変数には不定値が格納されている(何の値が格納されているか分からない)” からになります。
つまり、memset
関数を実行しなかったとしても、たまたま配列 src
の全体にヌル文字以外が格納されている可能性があります。この場合、上記のソースコードのように memset
関数を実行した場合と同様の結果が得られ、バグが存在することを発見することができます。
ですが、逆に、偶然 src[5]
には '\0'
が格納されている可能性もあります。
この場合、stringcpy
関数では src[0]
〜 src[4]
へのコピーが終わった後にループを抜けて関数が終了し、プログラムの結果は正常なものになります。つまり、この場合、プログラムの動作確認を行なってもバグを発見できないことになります。ただし、次に実行した時は偶然 src[5]
にヌル文字以外が格納されている可能性もあり、この場合はプログラムで得られる結果が異常となります。
このように、ヌル文字での終端を忘れたとしても、それを動作確認時に発見しにくいというのが、ヌル文字の扱いの難しいところになると思います。とにかく文字列を扱う際は、文字列をヌル文字で終端することに細心の注意を払う必要があります。
自動的にヌル文字で終端される仕組みを利用する
ただ、それでも人間がプログラミングを行う以上、ミスでヌル文字での終端を忘れてしまうことはあり得ます。
このミスを防ぐためには、ヌル文字で自動的に終端される仕組みを利用することが必要になると思います。
ここまでの説明では、配列に直接文字を格納することで文字列を作成してきましたが、別の手段で文字列を作成することも可能です。
その1つの手段が文字列リテラルを利用することになると思います。文字列リテラルとは、ダブルクォーテーションマークで囲った文字列のことになります。
この文字列リテラルは文字列のデータとして扱うことができ、この文字列は自動的にヌル文字で終端されることになります。
ですので、例えば、今まで示したソースコードでは配列に直接 'H'
'e'
'l'
'l'
'o'
'\0'
という文字を1文字ずつ格納することで Hello
をいう文字列を扱えるようにしてきましたが、配列の初期化時に下記のように直接 "Hello"
という文字列リテラルを格納して文字列を生成することもできます。
char str[] = "Hello";
前述の通り、文字列リテラルは自動的にヌル文字で終端されているため、ヌル文字での終端忘れが発生しにくいです。
また、文字列を生成するC言語標準ライブラリ関数においても自動的にヌル文字で終端してくれるものが多く存在します。例えば文字列のコピーを行う strcpy
関数においては、コピー先の文字列は自動的にヌル文字で終端されることになります(strcpy
関数は第2引数で指定された文字列を第1引数のアドレスに格納する関数になります)。
したがって、下記のように strcpy
関数を実行した場合、dst
の配列に格納される文字列は自動的にヌル文字で終端されることになります。
char dst[32];
strcpy(dst, "Hello");
標準ライブラリ関数が生成する文字列がヌル文字で終端されるかどうかは関数の仕様として記載されているはずなので、関数の仕様はしっかり理解して利用するようにしましょう。例えば、私の環境では strcpy
関数の仕様として下記のように記載されており、dst
(コピー先の文字列が格納されるアドレス)の文字列がヌル文字で終端されることが確認できます。
The stpcpy() and strcpy() functions copy the string src to dst (including the terminating `\0' character.)
このように、自動的にヌル文字での終端が行われる文字列リテラルや標準ライブラリ関数を利用することによって、ヌル文字での終端忘れを防ぎやすくなると思います。
文字列を格納する配列のサイズに注意
2つ目の注意点が「文字列を格納する配列のサイズ」となります。
ここまで説明してきたように、文字列を扱う際には、文字列のヌル文字での終端が必要となります。つまり、文字列を格納する配列は、最低でも 扱いたい文字列の文字数 + 1
のサイズが必要となります。+ 1
は文字列の後ろにヌル文字を格納するために必要となります
例えば、Hello
という文字列を扱う際、配列のサイズとしては最低限6文字分が必要となります(Hello
の文字数である5に加えてヌル文字格納用の1文字)。
下記のソースコードのように、strcpy
関数を利用して "Test"
という文字列をサイズ 4
の char
型の配列 dst
にコピーした場合、dst
の配列外に対して '\0'
が格納されることになります。
char dst[4];
strcpy(dst, "Test");
printf("dst = %s\n", dst);
dst
の配列外に対して '\0'
が格納されることになるので、他の変数の値の上書きやメモリアクセス違反による異常終了などの致命的なバグとなります。
上記の例の場合は、配列 dst
のサイズは最低限 5
が必要であり、この場合は配列 dst
内にヌル文字を格納することができ、正常に strcpy
関数が動作することになります。
このように、ヌル文字を格納することを考慮せずに配列のサイズを設定してしまうと、致命的なバグが生まれる可能性があるので注意が必要です。
まとめ
このページでは、C言語における「ヌル文字」について解説しました!
ヌル文字は文字列の終わりを判断するための制御文字です。文字列がヌル文字で終端されていることを前提とすることで、文字列を適切に扱うことができます。ただし、この前提が崩れると、つまりはヌル文字での終端を忘れてしまうと、文字列を扱う処理や関数の動作が破綻してしまう可能性があるので注意してください。
正直、C言語で文字列を正しく扱うのは結構難しいと思います。その最大の理由は、ヌル文字での終端忘れが発生しやすいことと、ヌル文字を考慮して配列等のサイズを設定する必要があることの2点になると思います。特にこれらに注意しながら文字列を扱うようにしていきましょう!
オススメの参考書(PR)
C言語学習中だけど分からないことが多くて挫折しそう...という方には、下記の「スッキリわかるC言語入門」がオススメです!
まず学習を進める上で、参考書は2冊持っておくことをオススメします。この理由は下記の2つです。
- 参考書によって、解説の仕方は異なる
- 読み手によって、理解しやすい解説の仕方は異なる
ある人の説明聞いても理解できなかったけど、他の人からちょっと違った観点での説明を聞いて「あー、そういうことね!」って簡単に理解できた経験をお持ちの方も多いのではないでしょうか?
それと同じで、1冊の参考書を読んで理解できない事も、他の参考書とは異なる内容の解説を読むことで理解できる可能性があります。
なので、参考書は2冊持っておいた方が学習時に挫折しにくいというのが私の考えです。
特に上記の「スッキリわかるC言語入門」は、他の参考書とは違った切り口での解説が豊富で、他の参考書で理解できなかった内容に対して違った観点での解説を読むことができ、オススメです。題名の通り「なぜそうなるのか?」がスッキリ理解できるような解説内容にもなっており、C言語入門書としてもかなり分かりやすい参考書だと思います。
もちろんネット等でも色んな観点からの解説を読むことが出来ますので、分からない点は別の人・別の参考書の解説を読んで解決していきましょう!もちろん私のサイトも参考にしていただけると嬉しいです!
入門用のオススメ参考書は下記ページでも紹介していますので、こちらも是非参考にしていただければと思います。
https://daeudaeu.com/c_reference_book/