このページでは、”グローバル変数をあまり使わない方が良い理由” について解説していきたいと思います。
C言語では、関数外での変数宣言によりグローバル変数を作成することができます。
int x = 0; /* グローバル変数 */
int funcA(void) {
}
void funcB(void) {
}
このグローバル変数は、どの関数からも直接参照することが出来るため、グローバル変数を利用することでコーディングを楽に行うことが出来ます(引数でその変数の受け渡しをする必要がない)。
int x = 0; /* グローバル変数 */
int funcA(void) {
x = 100; /* グローバル変数の参照 */
}
void funcB(void) {
printf("%d\n", x); /* グローバル変数の参照 */
}
ただし、このグローバル変数は基本的には “あまり使用しない方が良い” とされています。この点もご存知の方が多いのではないかと思います。
その一方で、グローバル変数を使用しない方が良いという “理由” にピンときていない方も多いのではないかと思います。
確かに…
使わない方が良いって言われるけど、その理由を明確には答えられないなぁ…
そういう人多いと思うよ!
私も社会人になるまではグローバル変数使いまくってたよ…
グローバル変数をあまり使用しない方が良い理由は、「大規模なプログラム」や「複数の処理が並列動作するようなプログラム」の開発を行った時に直に実感できるものだと思います。
逆にそういったプログラムを開発する機会が少ない方は(C言語入門者や個人で勉強用にプログラミングしている方など)、グローバル変数をあまり使用しない方が良い理由を感じ取りにくいです。
このページでは、そういった方々にもグローバル変数のデメリットが分かるよう、分かりやすい例を用いて “グローバル変数をあまり使わないほうが良い理由” について解説していきたいと思います。
Contents
グローバル変数を使わない方が良い理由
では、グローバル変数をあまり使わない方が良い理由について解説していきたいと思います。
グローバル変数を使うと関数間で依存が発生する
グローバル変数をあまり使用しない方が良い理由の根本は、私は “関数間で依存が発生する” 点だと思っています。
この点が実感できるように、極端だけど分かりやすい例を用いて解説していきたいと思います。
ローカル変数のみを使用する場合は他の関数に依存しない
まずは下記の関数に注目したいと思います。プログラミングでは 0
での割り算はご法度ですね!0
で割り算をしてしまうと例外が発生し、プログラムが異常終了してしまいます。
そのため下記では、割り算実行前に除数の num2
が 0
であるかどうかを判断し、0
である場合は割り算を行わないようにしています。
int calc(int num1, int num2) {
int ans = -1;
if (num2 != 0) {
ans = num1 / num2;
}
return ans;
}
では、ここで質問です。上記の calc
関数において、0
での割り算が行われる可能性はあるでしょうか?(メモリ破壊などは発生していない状態とします。)
ないでしょ!
num2
が 0
の場合は if
文が不成立して割り算は行われないよね!
見ての通り、0
での割り算が行われる可能性は無いですね!(もしかしたら、メモリ破壊などが起こって、スタックメモリがぐちゃぐちゃになっているような状態だと可能性はあるかもしれませんが、基本的には可能性は無いと考えられます。)
なぜ上記の calc
関数では 0
での割り算が行われる可能性がないと言い切れるのかというと、それは関数内で使用している変数が全てローカル変数、つまり、関数内からしか変更できない変数だからです。要は “他の関数から変更できない変数” しか使用していない関数だからです。
num2
に関しても当然他の関数からは変更されないので、num2
が 0
でないと判断した上で num2
で割り算を行えば、絶対に num2
が 0
でない状態で割り算を行うことができると断言することができます。
つまり、ローカル変数のみを使用している関数の場合、他の関数の動作に依存しません。なので、その関数単体で正常に動作するかどうかが判断できます(関数で行われる処理のみから判断できる)。
グローバル変数を使用する場合は他の関数に依存する
では、続いて下記に注目したいと思います。今度は割り算に用いる値 num1
と num2
を引数ではなく、グローバル変数としています。
int num1 = 1;
int num2 = 1;
int calc(void) {
int ans = -1;
if (num2 != 0) {
ans = num1 / num2;
}
return ans;
}
では、ここで再度質問です。上記の calc
関数において、0
での割り算が行われる可能性はあるでしょうか?
いや、一緒でしょ?
num2
が 0
の場合は割り算が行われないんだから、0
での割り算が行われる可能性は無い!!
実は、0
で割り算が行われる可能性があるんだ…
そこがグローバル変数の怖いところだね…
おそらく、上記の calc
関数においても、0
での除算が行われる可能性は無いと考えた人が多いのではないでしょうか?もしかしたら深読みして可能性はあると予想してくださった方もおられるかもしれません。
上記の calc
関数では、0
での除算が行われる可能性があります。
どんな時に 0
での除算が起こりうるかというと、calc
関数と並列に num2
を変更する関数が実行されるような場合です。
ちょっと難しいプログラムになりますが、具体的には下記のソースコードをコンパイルして実行すれば、簡単に 0
除算での例外を発生させることが可能です(pthread
を使っているのでおそらく Windows ではコンパイルに失敗すると思います。が、Windows でも他の関数を利用して同様の処理を実現することは可能です)。
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <pthread.h>
int num1 = 1;
int num2 = 1;
int calc(void) {
int ans = -1;
if (num2 != 0) {
/* グローバル変数の値を取得 */
ans = num1 / num2;
}
return ans;
}
void *set(void *arg) {
while (1) {
/* グローバル変数の値を変更 */
num1 = rand() % 10;
num2 = rand() % 10;
}
return NULL;
}
void *get(void *arg) {
while (1) {
int ans = calc();
}
return NULL;
}
int main(void) {
pthread_t thread1, thread2;
/* 乱数の種の初期化 */
srand((unsigned int)time(NULL));
/* スレッドを生成 */
pthread_create(&thread1, NULL, set, NULL);
pthread_create(&thread2, NULL, get, NULL);
/* スレッドの終了待ち */
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
return 0;
}
pthread_create
や pthread_join
など、見慣れない関数かもしれませんが、これらはマルチスレッドプログラミングを行うための関数になります。
これらを利用することで、関数同士を並列に実行させる(同時に実行させる)ようなことが可能です。詳しくは下記ページで解説していますので、詳しく知りたい方はこのページの後にでも読んでみてください。
入門者向け!C言語でのマルチスレッドをわかりやすく解説ここでは、上記のプログラムの動作を要点だけ簡単に解説します。
まず、関数 get
と関数 set
は並列に動作します。
また、関数 set
では num2
を 0
〜 9
のいずれかの値に変更する処理が行われます。さらに関数 get
からは、関数 calc
が実行され、num2
が 0
でない場合は num1
に対して num2
での割り算が行われます。
関数 get
と関数 set
は並列に動作しますので、タイミングによっては下記のような動作になります。
- 関数
set
によりnum2
が0
以外の値に変更される - 関数
calc
によりif (num2 != 0)
の判断が行われ、この判断が成立する - 関数
set
によりnum2
が0
に変更される - 関数
calc
によりnum1 / num2
が実行される
要は、if (num2 != 0)
の直後に num2
が変更されてしまう可能性があると、if (num2 != 0)
が成立したからといって、num1 / num2
の時点で num2
が 0
であるということは保証されないことになります。
ですので、num2
をグローバル変数とした場合、0
での割り算が行われないと断言することができません。
この例のように、グローバル変数を使用している関数の動作は、同じグローバル変数を使用している関数の動作に影響を受けます。
つまり、グローバル変数を使用している関数の場合、その関数の動作は他の関数の動作に依存することになります。ですので、その関数単体で正常に動作するかどうかの判断をすることはできません。
その使用しているグローバル変数が、どの関数からどのように変更されるか・どのように使用されるかを考慮して関数が正常に動作するかを判断する必要があります。
さらには、上記のようなマルチスレッドを用いて関数同士を並列動作させるような場合、グローバル変数の値はさまざまなタイミングで変更される可能性があることになります(極論すればいつでも変更される可能性がある)。
ですので、どのタイミングで変更されるのかも考慮して、関数が正常に動作するかを判断する必要があります(ローカル変数のみを使用している場合、他の関数からローカル変数を変更される可能性がないので、”関数単体” の動作を確認するために他の関数の実行タイミングを考慮する必要はありません)。
ローカル変数のみを使用する関数においても、引数にグローバル変数や malloc
で確保したメモリのアドレスを指定するような場合は、実は他の関数との依存が発生します
この場合は、グローバル変数を直接参照しているときと同様の点に注意が必要になります
上記では、グローバル変数が直接悪影響をもたらす例として分かりやすいかなぁと思って並列動作の例を示しましたが、関数間で依存が発生するのは並列動作の有無に関わりません。
そして、グローバル変数を利用しない方が良い理由の根本は、グローバル変数を利用することで発生する関数間での依存になります。
ここからは、この関数間での依存によって、どのような悪影響があるのかについて解説していきたいと思います。
スポンサーリンク
グローバル変数を使うとコードが読みにくい
グローバル変数を使用する関数は、その関数で行われる処理が他の関数の動作に依存してしまうため、ソースコードが読みにくい・関数やプログラムの動作を追いにくいというデメリットがあります。
例えば先ほどの並列動作するプログラムでいうと、同じグローバル変数を参照する関数 calc
と関数 set
の処理内容(グローバル変数 num1
と num2
がどのようにして扱われるか)や、これらの関数が同時に動作する可能性があるかのあたりを考慮して、関数 calc
および関数 set
が正常に動作するかを判断しなければなりません。
このプログラムの場合は、”set
が num2
を 0
にする可能性がある& calc
が num2
で割り算する& set
と calc
が同時に動作する可能性がある” ので、set
と calc
のどちらかの関数 or 両方の関数を修正する必要があることになります。
つまり、関数がどのように動作をするのかをソースコードから読み取るためには、その関数だけでなく、同じグローバル変数を使用している関数の処理も一緒に確認する必要があります。いろんな関数の処理を一緒に確認する必要があるので、ソースコードは読みにくいですし、その関数やプログラム全体の動作を追いにくいです。
今回の例の場合は、グローバル変数 num1
と num2
を参照している関数が calc
と set
の2つのみなので、まだソースコードは読みやすい方です。
もちろん同じグローバル変数を参照する関数が増えれば増えるほど、ソースコードは読みにくくなってしまいます。
また、他のファイルの関数から同じグローバル変数が参照されている場合、ファイルを跨いでソースコードを確認する必要があり、その分ソースコードは読みにくくなります(グローバル変数は extern
宣言を利用すればどのファイルの関数からも参照可能)。
さらには、並列動作を行うような場合、グローバル変数の値を変更・取得する関数がどのタイミングで実行されるかも考慮しながらソースコードを読む必要があるので、さらにソースコードは読みにくくなります。
グローバル変数を使うとバグの原因が分かりにくい
また、関数の動作がおかしいような場合、グローバル変数を使用していると、その動作がおかしい原因を突き止めるのが難しくなります。
なぜなら、グローバル変数を使用している関数の動作は、同じグローバル変数を使用している関数の動作に依存するからです。
つまり、関数の動作がおかしいのは、その関数自体の処理が原因ではなく、同じグローバル変数を使用している関数の処理が原因である可能性があります。
ですので、動作がおかしい関数だけでなく、その関数と同じグローバル変数を使用している関数すべての処理を確認し、どの関数が原因でバグが発生しているかを突き止める必要があります。
さらには並列動作を行うような場合、同じグローバル変数がどのタイミングで実行されるのかも考慮してバグの原因を突き止める必要があるので、さらにバグの原因を突き止めるのが難しくなります。
グローバル変数を使うとバグりやすい
また、グローバル変数を使用している場合、バグが発生しやすいです。
グローバル変数に値を格納する関数が変更されると、同じグローバル変数を利用している関数の全てがその変更の影響を受けることになります。
要は、グローバル変数を使用していると関数間に依存が発生するので、関数単体の変更の影響が、依存先すべての関数に波及することになります。
もちろん、他の関数に悪影響を及ぼさないように変更すれば問題ないですが、これを実現するためには同じグローバル変数を利用している関数すべての動作をしっかり理解した上で変更する必要があります。
で、これを理解せずに or 考慮せずに変更してしまうと、他の関数でバグが発生してしまう可能性があります。
また、並列動作を行うような場合、同じグローバル変数に対して同時に値の変更・取得が行われるような場合は、排他制御という制御が必要になります。この排他制御により、同じグローバル変数への参照が同時に行われるのを防ぐことができるのですが、この排他制御を忘れるとバグになります。
さらに、排他制御を行う箇所が多いと、場合によってはデッドロックが発生し、プログラムが途中で停止するようなこともあり得ますので、排他制御が新たなバグを生み出す可能性もあります。
この排他制御については下記ページで詳しく解説していますので、詳細を知りたい方はこのページの後にでも下記ページを読んでみてください(マルチスレッドを知らない方は、まずマルチスレッドについて理解してから読んだほうが良いと思います)。
【C言語】排他制御について解説【Mutex】スポンサーリンク
グローバル変数を使うと流用がしにくい
また、グローバル変数を使用した関数は他の関数と依存することになるので、関数単位での流用がしにくいです。
プログラムを作成するときに、他のプログラムや Web 上で公開されている関数をコピペして使用することって結構多いのではないでしょうか?要は、こういったコピペ(流用)が、グローバル変数を使用しているとやりにくくなります。
グローバル変数を使用した関数を他のプログラムに流用したいような場合は、その関数だけでなく、その関数と依存する関数(同じグローバル変数を使用している関数)も一緒に流用したり、そのグローバル変数を適切に使用する処理を流用先のプログラムに別途追加する必要があります。
もちろんローカル変数のみを使用している関数の場合も、構造体を利用していたり他の関数を呼び出したりしていると、それらの構造体の定義や他の関数も合わせて流用する必要があります。
が、グローバル変数を利用している場合は、それらにプラスして、グローバル変数によって依存する処理も合わせて流用 or 流用先への追加が必要になり、その分関数の流用がしにくくなります。
グローバル変数は絶対に使ってはいけないのか?
ここまで、グローバル変数を使用しない方が良い理由について解説してきました。
その感覚重要だよ!
ソースコード書くときは便利だけど、書いた後のことを考えるとむしろ不便な場合が多いんだ
いや、そんなことはないよ!
グローバル変数は使わないことじゃなくて、特性を理解した上で適切に使うことが重要なんだ!
では、グローバル変数は絶対に使ってはいけないのでしょうか?
そんなことはありません。私も社会人になって、実際に製品に搭載されるようなC言語プログラムのソースコードをたくさん見てきましたが、グローバル変数は結構使われています。
ただし、単にソースコードを書くのが楽になるという理由でグローバル変数を使用することは無いと思います。ここまで解説してきた “グローバル変数を使用しないほうが良い理由” を踏まえた上で、それでもグローバル変数を使用した方が良いと判断できるような場合に、グローバル変数を使用します。
グローバル変数の使用例
では、どのような場合にグローバル変数を使用することがあるのか?について例を用いて紹介していきたいと思います。
あえて関数間で依存関係を持たせるために使用する
グローバル変数を使用すると関数間に依存関係が発生することが問題であると述べてきましたが、逆にその依存関係を発生させるためにグローバル変数を使用するようなこともあります。
例えば、下記では processA
で行う処理と processB
で行う処理を交互に実行するために、グローバル変数 flag
を使用している例になります。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
int flag = 0;
void *processA(void *arg) {
while (1) {
/* processBによりflagが0にセットされるまで待ち */
while (flag != 0) {
sleep(1);
}
/* 処理A(ここではprintfだけ)*/
printf("処理A\n");
/* processBの処理を再開 */
flag = 1;
}
return NULL;
}
void *processB(void *arg) {
while (1) {
/* processBによりflagが0以外にセットされるまで待ち */
while (flag == 0) {
sleep(1);
}
/* 処理B(ここではprintfだけ)*/
printf("処理B\n");
/* processAの処理を再開 */
flag = 0;
}
return NULL;
}
int main(void) {
pthread_t thread1, thread2;
/* スレッドを生成 */
pthread_create(&thread1, NULL, processA, NULL);
pthread_create(&thread2, NULL, processB, NULL);
/* スレッドの終了待ち */
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
return 0;
}
簡単に上記について解説しておくと、まず processA
関数と processB
関数は同時に並列に実行されます。ですので、グローバル変数の flag
を利用しなかった場合、処理A
と 処理B
という文字列が交互に表示されるとは限りません。
ただし、上記ではグローバル変数 flag
の値によって待ちを行うようにしており、printf
を実行する部分の処理は、一方の関数からしか同時に動作されないようになっています(flag
が 0
の時は processB
は sleep
のループから抜け出せない、flag
が 1
の時は processA
は sleep
のループから抜け出せない)。
また printf
を実行後は、他方の関数のみの動作が進むように flag
を設定しているので、これにより他方側の printf
が実行されます。そして、これを繰り返すことで、各関数で交互に printf
が実行されるようになっています。
要は、グローバル変数を利用して関数間で依存関係を作ることで、関数間での処理の実行タイミングを制御していることになります。こんな感じで、わざとグローバル変数を利用して関数間で依存関係を作ることもあります。
上記のようにグローバル変数の利用により printf
を交互に実行するようなことは可能ですが、メッセージキューやイベントフラグなどを使って同期を取るほうが本当は良いです
バグを減らす&モジュール間の責務分けのために使用する
また、グローバル変数を使用しないということは、基本的に関数間でのデータのやりとりはすべて引数と戻り値でやり取りする必要があることになります。つまり、関数内で使用するデータを全て、関数呼び出し側が保持していなければならないことになります。
そうなると、関数間でやりとりするデータが多くなるので、逆にバグが発生する可能性が高くなることもあり得ます。さらに、本当は呼び出し側の関数が知る必要のない情報(むしろ知ってほしくないような情報)まで知る必要が出てきます。
例えばですが、下記のような擬似的に乱数(乱数というよりてきとうな値ですが…)を生成して返却する関数があったとします。この関数は呼び出すたびに、毎回異なる値を返却するように、グローバル変数 i
をインクリメントしながら処理を行うようにしています。
int i = 0;
int my_rand(void) {
int ret = i * 10233423 / 52342 % 1024;
i++;
return ret;
}
上記はグローバル変数を利用している例になりますが、もしグローバル変数を使用しない場合はどのような関数になるでしょうか?
まず変数 i
を利用しているので、i
の情報を引数として渡してもらう必要があります。
int my_rand(int i) {
int ret = i * 10233423 / 52342 % 1024;
return ret;
}
さらに、変数 i
が毎回同じだと、毎回同じ値を返却することになるので、毎回異なる値を呼び出し側に引数として指定してもらう必要があります。つまり、この関数を呼び出す関数は全て、現在の i
の値を知っておく必要があります。
これって関数呼び出し側からするとかなり面倒ですよね…。
さらに、別に乱数を生成するための変数 i
の情報は知る必要がないですし、関数呼び出し側のモジュールが乱数を生成する責務を持つモジュールでなければ、この情報を知っているのはむしろおかしいです。
なので、上記のような場合は、”バグを減らす” & “モジュールの責務分けを明確にする” という意味で、グローバル変数を利用した方が自然と考えられます。
上記の場合は i
を static
なローカル変数にすることでも代用可能ですが、もしこの i
を他の関数からも同様に使用されるような場合は、グローバル変数にする必要があります。
読み取り専用の変数を参照しやすくするために使用する
あとは、今後変更する可能性がない&読み取り専用の配列をグローバル変数宣言し、いろんな関数から参照するようなケースもあります。
例えば、下記のような 0
〜 9
の添字で指定された数値を英語に変換するような配列は、今後も変更することがありませんし(0
や 9
の英語表記が今後変わる可能性はほぼないですよね?)、読み取り専用で使用するのであればバグを誘発するようなこともありません。また、参照されるタイミングを考慮する必要もありません(読み取り専用なので、どのタイミングでも同じ値が格納されている)。
const char number[10][16] = {
"ZERO", "ONE", "TWO", "THREE", "FOUR",
"FIVE","SIX", "SEVEN", "EIGHT", "NINE"
};
こういった配列の場合、各関数間で引数により配列を引き回すよりも、グローバル変数として参照できた方が、バグの発生を抑えることができることの方が多いです。
ただし、うっかりミスなどでプログラム実行中に変更されないよう、読み取り専用として扱う変数は上記のように const
指定しておいた方が良いです。
また、開発途中で配列の中身を変更してしまうと、参照している関数に影響が出てしまうので、変更する可能性が低いものをグローバル変数にした方が良いです(変更する必要があるのであれば、参照している関数の開発者に変更しても問題ないかをしっかり確認する必要があります)。
スポンサーリンク
重要なのはグローバル変数の特性を理解した上で使用すること
こんな感じで、グローバル変数は絶対に使用してはいけないものではありません。重要なのは、グローバル変数の特性・デメリットを理解した上で、使用するかどうかを判断することです。
上記のような場合は、”グローバル変数を使用しない方がバグが増える”・”グローバル変数のデメリットを受けにくい” 等の理由から、グローバル変数を使用することを判断した例になります。
このような判断を行うには、グローバル変数のデメリットや特性を理解しておく必要がありますので、このページで説明したような内容については覚えておいた方が良いと思います!
安全に利用するために修飾子を活用する
ただ、グローバル変数を利用する場合でも、極力その変数のスコープは絞った方が良いです。また、特にグローバル変数への変更はバグを誘発しやすいので、変更する必要がないのであれば変更できないようにした方が良いです。
そのためには、グローバル変数を使用する際には、例えば下記のようなことを心がけると良いと思います。
- 他のファイルの関数からは使用できないように
static
指定する - 本当に参照が必要なファイルのみから
extern
宣言を行うようにする - 変更する可能性がないのであれば
const
指定を行う
これらの static
や extern
、const
については下記のページで詳細を解説していますので、詳しく知りたい方はぜひ読んでみてください。
特に static
と const
の修飾子は、プログラムを安全に開発していくための仕組みにもなりますし、今回紹介したグローバル変数を利用する際のデメリットを最小限に抑えることもできますので、知っておくと良いと思います!
まとめ
このページでは、”グローバル変数をあまり使用しない方が良い理由” について解説しました!
グローバル変数を使用すると、”関数間での依存関係” が生まれてしまいます。
さらにそれにより下記のようなデメリットが発生することがあります。
- ソースコードが読みにくくなる
- バグが発生しやすくなる
- バグの原因が分かりにくくなる
- 関数単体の流用がしにくくなる
分かりやすくいうと、グローバル変数はソースコードを書く時は便利ですが、ソースコードを書いた後は不便です…。
特に並列動作を行うようなプログラムの場合は、上記のデメリットが顕著になります。
なので、並列動作を行うようなプログラミングを行うようになると、自然とグローバル変数は使用したくないと思うようになると思いますよ…。私もそうで、並列動作(マルチスレッド)を利用するまではグローバル変数大好きでしたが、利用するようになって嫌いになりました…。
皆さんも現場でプログラミングや設計を行うようになると、おそらくグローバル変数の厄介さを身をもって体験することができるのではないかなぁと思います。
ただ、そういった体験がないと、グローバル変数の特性を理解せずについつい使用してしまうことも多いと思います。ですので、ぜひこのページの内容を頭の片隅に置いて、本当にグローバル変数を使用したほうが良いのかどうかを考えながらプログラミングに取り組んでみてください!
オススメの参考書(PR)
C言語学習中だけど分からないことが多くて挫折しそう...という方には、下記の「スッキリわかるC言語入門」がオススメです!
まず学習を進める上で、参考書は2冊持っておくことをオススメします。この理由は下記の2つです。
- 参考書によって、解説の仕方は異なる
- 読み手によって、理解しやすい解説の仕方は異なる
ある人の説明聞いても理解できなかったけど、他の人からちょっと違った観点での説明を聞いて「あー、そういうことね!」って簡単に理解できた経験をお持ちの方も多いのではないでしょうか?
それと同じで、1冊の参考書を読んで理解できない事も、他の参考書とは異なる内容の解説を読むことで理解できる可能性があります。
なので、参考書は2冊持っておいた方が学習時に挫折しにくいというのが私の考えです。
特に上記の「スッキリわかるC言語入門」は、他の参考書とは違った切り口での解説が豊富で、他の参考書で理解できなかった内容に対して違った観点での解説を読むことができ、オススメです。題名の通り「なぜそうなるのか?」がスッキリ理解できるような解説内容にもなっており、C言語入門書としてもかなり分かりやすい参考書だと思います。
もちろんネット等でも色んな観点からの解説を読むことが出来ますので、分からない点は別の人・別の参考書の解説を読んで解決していきましょう!もちろん私のサイトも参考にしていただけると嬉しいです!
入門用のオススメ参考書は下記ページでも紹介していますので、こちらも是非参考にしていただければと思います。
https://daeudaeu.com/c_reference_book/