このページでは、文字列内の「特定の文字のカウントの仕方」について解説していきます。
例えば文字列が "she sells sea shells by the sea shore."
である場合、文字 's'
の個数は 8
となりますし、文字 'e'
の個数は 7
となります。こんな感じで、文字列の中に存在する特定の文字の個数をカウントするやり方を解説していきます。
おそらく、特定の文字の個数をカウントするような標準ライブラリ関数はC言語には存在しません。
そのため、自力でプログラミングして特定の文字の個数をカウントしてやる必要があります。ですが、そんなに難しくはないですし、特定の文字の個数をカウントできるようになることで文字や文字列の扱いに慣れることもできると思います!
Contents
特定の文字をカウントする
まずは、特定の文字のカウントの仕方の考え方について解説していきます。
復習:C言語における文字列の扱い
念のためですが、最初にC言語における文字列について簡単に整理しておきたいと思います。
C言語において、文字列は「文字の配列」として扱われます。具体的には char
型の配列として扱われることが多いです。このページでも、文字列は char
型の配列として扱うものとして解説していきます。
さらに、C言語においては文字列の終端をヌル文字 '\0'
で表現します。つまり、配列の先頭の要素
〜 '\0' の手前の要素
までが1つの文字列として扱われます。
ですので、下の図の配列 str
で扱う文字列は Hello
ということになります。'\0'
以降も文字がありますが、前述の通り '\0'
は文字列の終端を表すものですので、それ以降は異なる文字列や無関係なデータとなります。
実際に、上記の配列 str
を自身で作成して printf("%s\n", str);
を実行した場合は、Hello
のみが出力されることになります。
#include <stdio.h>
int main(void) {
char str[12];
str[0] = 'H'; str[1] = 'e'; str[2] = 'l';
str[3] = 'l'; str[4] = 'o'; str[5] = '\0';
str[6] = 'W'; str[7] = 'o'; str[8] = 'r';
str[9] = 'l'; str[10] = 'd'; str[11] = '!';
printf("%s\n", str);
}
スポンサーリンク
特定の文字のカウントの仕方
で、今回はやりたいことは文字列の中の「特定の文字のカウント」です。
これは、文字列の中の「特定の文字と一致する文字」の個数を数えることで実現できます。
文字列は前述の通り配列で扱われますので、要は配列の各要素と特定の文字が一致するかどうかを判断し、その一致する個数を数え上げることで、特定の文字のカウントを実現することができます。
さらに、これは配列の先頭の要素から順に1つずつ特定の文字と一致するかどうかを繰り返し判断していくことで実現できます。
ただし、前述の通り文字列は '\0'
で終端されますので、上記の繰り返しは '\0' の手前の要素
までのみ行うように制御する必要があります。'\0'
より後ろ側もカウントすると、文字列以外の文字を余分にカウントすることになるためカウント結果がおかしくなってしまいます。この点にのみ、注意が必要です。
まとめると、文字列を扱う配列において、配列の先頭の要素
〜 '\0' の手前の要素
に対して下記を繰り返し実行することで、特定の文字のカウントを行うことができます。
要素
が特定の文字と一致するかどうかを判断- 一致する場合:文字の個数をカウントアップ
例えば、文字列を扱う配列を str
、個数をカウントしたい「特定の文字」が格納された変数を chr
、chr
の数をカウントする変数を count
とすれば、文字列 str
から特定の文字 chr
をカウントする処理は下記のような for
ループで実現することができます。for
ループが終了した時点の count
の値が、文字列 str
から特定の文字 chr
の数となります。
unsigned int count = 0;
for (unsigned int i = 0; str[i] != '\0'; i++) {
if (str[i] == chr) {
count++;
}
}
ポイントはやはり for
ループの継続条件が str[i] != '\0'
である点だと思います。これにより、str[i] == '\0'
が成立した際にループが終了するようになりますので、配列の先頭の要素
〜 '\0' の手前の要素
のみに対して特定の文字のカウントを行うことができるようになります。
ちなみに、今回は利用しませんが、strlen
関数を利用した場合、上記の for
ループと同様の繰り返し処理を次のように実現することもできます(strlen
関数を利用するためには string.h
をインクルードしておく必要があります)。
unsigned int count = 0;
for (unsigned int i = 0; i < strlen(str); i++) {
if (str[i] == chr) {
count++;
}
}
strlen
関数は、引数で指定された配列に対し 配列の先頭の要素
〜 '\0' の手前の要素
の個数を返却する関数ですので、上記のように書き換えても前述で紹介した処理と同等の処理を実現することができます。
文字列から特定の文字をカウントするプログラムの例
特定の文字のカウントについては、先程紹介した for
ループの処理で実現することができます。
続いては、この処理を利用し、文字列から特定の文字をカウントするプログラムの例として下記の3つを紹介していきたいと思います。
ソースコード内で定義された文字列に対するカウント
下記はソースコード内で定義された文字列(下記の場合は "she sells sea shells by the sea shore."
)に対して特定の文字(下記の場合は 's'
と 'e'
と ' '
と '.'
)のカウントを行うプログラム例になります。
文字のカウント自体は countChr
関数で実行しています。
#include <stdio.h>
unsigned int countChr(char str[], char chr) {
unsigned int count = 0;
for (unsigned int i = 0; str[i] != '\0'; i++) {
if (str[i] == chr) {
count++;
}
}
return count;
}
int main(void) {
char str[] = "she sells sea shells by the sea shore.";
char chr;
unsigned int count;
printf("in [%s]\n", str);
chr ='s';
count = countChr(str, chr);
printf("%c : %u\n", chr, count);
chr ='e';
count = countChr(str, chr);
printf("%c : %u\n", chr, count);
chr =' ';
count = countChr(str, chr);
printf("%c : %u\n", chr, count);
chr ='.';
count = countChr(str, chr);
printf("%c : %u\n", chr, count);
return 0;
}
countChr
関数の中の処理は 特定の文字のカウントの仕方 と同じですので説明は不要かなぁと思います。
main
関数では、文字列 str
と文字 chr
を引数として countChr
関数を実行し、その結果を printf
関数で出力しています。
文字列 str
は下記のように宣言しており、文字列の終端を表すヌル文字 '\0'
が存在しないように思えますが、実際には、データとしては最後の .
の後ろにヌル文字 '\0'
が存在することになります。
char str[] = "she sells sea shells by the sea shore.";
ですので、str
を第1引数に指定して countChr
関数を実行すれば、文字 '.'
の比較が終わった後に str[i] != '\0'
が不成立してループが終了することになり、"she sells sea shells by the sea shore."
の中のみから文字のカウントを行うことができます。
また、一応説明しておくと、C言語のソースコードにおいては文字列を "
で囲って表現します。それに対し、文字は '
で囲って表現します。なので、chr
への代入時は '
で囲って文字を指定するようにしています。
もちろん、"A"
のように1文字であっても、"
で囲った以上は文字列として扱われることになります。例えば char str[] = "A";
と変数宣言した場合は、str[0]
には文字 'A'
が格納され、str[1]
にはヌル文字 '\0'
が格納されることになります。
コマンドライン引数で指定された文字列に対するカウント
次は、コマンドライン引数で「文字列」と「文字」を指定できるようにし、指定された文字列の中から指定された文字をカウントする例を紹介します。
といっても、特定の文字をカウントする処理は先ほどの ソースコード内で定義された文字列に対するカウント と全く一緒です(countChr
関数は全く同じ)。
#include <stdio.h>
#include <string.h>
#define MAX_STR 255
unsigned int countChr(char str[], char chr) {
unsigned int count = 0;
for (unsigned int i = 0; str[i] != '\0'; i++) {
if (str[i] == chr) {
count++;
}
}
return count;
}
int main(int argc, char *argv[]) {
char str[MAX_STR + 1];
char chr;
unsigned int count;
if (argc != 3) {
printf("コマンドライン引数を2つ指定してください\n");
printf("プログラム名 <検索先の文字列> <特定の文字>\n");
printf("<検索先の文字列>に指定可能なのは%d文字までです\n", MAX_STR);
printf("例:./main.exe Hello l\n");
return 0;
}
strncpy(str, argv[1], MAX_STR);
chr = argv[2][0];
count = countChr(str, chr);
printf("in [%s]\n", str);
printf("%c : %u\n", chr, count);
return 0;
}
例えば、上記をコンパイル等を行なって生成される実行可能ファイルを main.exe
とすれば、下記の1行目のように引数を指定することで 検索先の文字列
と 特定の文字
をプログラム実行時に指定できるようになります(%
の部分は入力不要です。以降も同様です)。
% ./main.exe kakikukeko k in [kakikukeko] k : 5
ソースコード内で定義された文字列に対するカウント の時とは異なり、文字列や特定の文字を変更するたびにコンパイルを行う必要がないので便利だと思います。
また、上記のように引数を指定してプログラムを実行した場合、main
関数の引数 argv
の各要素は下記の文字列を指すことになります。
argv[0]
:"main.exe"
argv[1]
:"kakikukeko"
argv[2]
:"k"
そして、argv[1]
を str
にコピーし、argv[2]
の先頭の要素を chr
に格納して countChr(str, chr)
を実行することで、プログラム実行時に指定された引数に合わせた結果を得ることができるようになっています(argv[2]
は文字列であることに注意してください。先頭の文字のみを chr
に格納するために chr = argv[2][0]
を行うようにしています)。
こういったプログラム実行時にプログラム(main
関数)に渡す引数をコマンドライン引数と呼びます。コマンドライン引数については下記ページで解説していますので、詳しく知りたい方はコチラを参考にしていただければと思います。
また、少し話は逸れますが、コマンドライン引数における各引数はスペースで区切られます。そのため、引数に指定する文字列にスペースを含ませたい場合は、"
で囲む or その文字の前に \
を付ける必要があります。
% ./main.exe "she sells sea shells by the sea shore." s in [she sells sea shells by the sea shore.] s : 8
% ./main.exe she\ sells\ sea\ shells\ by\ the\ sea\ shore. s in [she sells sea shells by the sea shore.] s : 8
特に後者のような文字指定をエスケープシーケンスと呼びます。皆さんもよく使用する \n
もエスケープシーケンスの1つです。
エスケープシーケンスについては下記ページで解説していますので、詳しく知りたい方は読んで見てください。知っておくとC言語での文字の扱い時やターミナル等でのコマンド入力時にも役立ちます!
【C言語】エスケープシーケンスで特殊文字を扱う読み込んだファイル内の文字列に対するカウント
次は、読み込んだファイル内の文字列から特定の文字をカウントするプログラムを紹介します。
この場合、ファイル内の文字列が長すぎて一度に全ての文字列を読み込むのが困難であることがあり得ます(必要になる配列のサイズが大きくなりすぎる場合がある)。
そのため、ファイルから全ての文字列を一度に読み込んでカウントを行うのではなく、部分的にファイルから文字列を読み込み、読み込んだ文字列内のみに対して特定の文字の個数のカウントを行うようにしていきたいと思います。
一度にカウントできるのがファイル内の一部の文字列に対してのみになりますが、これをファイルの先頭から最後まで繰り返し、各カウント回数を足し合わせれば、ファイル内の文字列全体における特定の文字のカウントを実現することができます。
この考え方に基づいて作成した、読み込んだファイル内の文字列から特定の文字をカウントするプログラムのソースコード例は下記のようになります。
#include <stdio.h>
#define MAX_LINE 512
unsigned int countChr(char str[], char chr) {
unsigned int count = 0;
for (unsigned int i = 0; str[i] != '\0'; i++) {
if (str[i] == chr) {
count++;
}
}
return count;
}
int main(int argc, char *argv[]) {
FILE *stream;
char str[MAX_LINE];
char chr;
unsigned int count;
if (argc != 3) {
printf("コマンドライン引数を2つ指定してください\n");
printf("プログラム名 <ファイルパス> <特定の文字>\n");
printf("例:./main.exe test.txt l\n");
return 0;
}
chr = argv[2][0];
stream = fopen(argv[1], "r");
if (stream == NULL) {
printf("%sが開けません\n", argv[1]);
return 0;
}
count = 0;
while (fgets(str, MAX_LINE, stream) != NULL) {
count += countChr(str, chr);
}
printf("in [%s]\n", argv[1]);
printf("%c : %u\n", chr, count);
fclose(stream);
return 0;
}
コマンドライン引数で指定された文字列に対するカウント 同様に、コマンドライン引数を受け取って処理を行うプログラムになっています。ただし、コマンドライン引数の1つ目には読み込むテキストファイルのファイルパスを指定する必要があります。
main
関数では、その指定されたファイルパスのファイルを fopen
で開き、そのファイルから fgets
関数で1行ずつ文字列の読み込みを行なっています。そして、fgets
関数から読み込まれた文字列に対して countChr
関数を実行して特定の文字のカウント結果を返却値で受け取っています。
countChr
関数自体は ソースコード内で定義された文字列に対するカウント と コマンドライン引数で指定された文字列に対するカウント で紹介したものと全く同じですが、main
関数でその返却値自体を最終的な結果とするのではなく、fgets
関数を実行するたびにその結果を足し合わせていっているところがポイントになります。
fgets
関数を実行する while
ループは、fgets
関数で1行ずつファイルから文字列を読み込み、ファイルの最後まで読み込みが完了したらループを終了するようになっていますので、その中で countChr
関数の返却値を足し合わせていくことで、ファイル全体における特定の文字のカウントを実現しています。
ちなみに、ここで紹介した fgets
関数に関しては下記ページで紹介していますので、fgets
関数をご存知ない方、上記のソースコードで fgets
関数がどのように動作するのか分からないという方は是非読んで見てください。
fgets
関数を使いこなすことができれば、C言語でのテキストファイルの扱いもかなり楽になります。
各文字の文字数をカウントする
ここまでは、特定の文字のみをカウントする方法やプログラム等について解説を行ってきました。
次は、特定の文字だけでなく、文字列内の各文字の個数をカウントする方法について解説していきます。ただ、各文字といっても漢字などもカウントできるというわけではないので注意してください。カウントするのは1バイトで表現できる文字のみとなります。例えば半角英数字などの文字をカウントすることができます。
スポンサーリンク
文字コード
このような文字のカウントを実現する上でポイントになるのが「文字コード」になります。
コンピュータ内部では文字は数値として扱われています。その数値を画面等に表示する際に文字として表現しているだけです。
より正確には、コンピュータ内部で扱われるのは 0
と 1
の2つのデータのみです。これを複数桁の2進数として扱うことで様々な数値を扱うことができます
例えば、printf("%c", 'A')
を実行すれば当然 'A'
が出力されますが、printf("%d", 'A')
を実行すれば、おそらく 65
が出力されるのではないかと思います(このページでは 65
が出力されることを前提として解説していきます)。また、printf("%c", 65)
を実行すれば 'A'
を出力することができます。
printf("%c\n", 'A'); /* 出力結果:A */
printf("%d\n", 'A'); /* 出力結果:65 */
printf("%c\n", 65); /* 出力結果:A */
printf("%d\n", 65); /* 出力結果:65 */
つまり、'A'
と 65
の実体は同じものであり、コンピュータ内部では文字 'A'
は 65
として扱われます。そして、printf("%c", 65)
などで文字としてを出力する際には、その数値に対応する文字、この場合は 'A'
として画面に表示されることになります。
この文字に対応する数値を「文字コード」と呼びます。ここでは 'A'
を例に挙げて解説しましたが、全ての文字に対してこの文字コードが割り振られています。この文字コードは実際に printf
で確認することができます。
下記のコメント内で示したものは私の環境での出力結果になります。
printf("%d\n", 'A'); /* 出力結果:65 */
printf("%d\n", 'B'); /* 出力結果:66 */
printf("%d\n", 'Z'); /* 出力結果:90 */
printf("%d\n", 'a'); /* 出力結果:97 */
printf("%d\n", 'b'); /* 出力結果:98 */
printf("%d\n", '+'); /* 出力結果:43 */
ここで重要なのは、文字コード自体の数値ではなく、文字が文字コード、すなわち数値(整数)で扱われるという点です。数値で扱われるので、文字に対して演算も行うことができますし、文字を配列の添字にも利用することができます。
配列の添字に文字コードを指定してカウント
今回は、文字を配列の添字に利用可能であることを利用して、各文字の個数のカウントを実現していきます。
前述の通り、今回カウントするのは1バイトで表現できる文字、すなわち文字コードが 0
〜 255
である文字の個数となります。
各文字の個数をカウントする際の基本的な考え方は 特定の文字のカウントの仕方 で解説した内容とほとんど同じです。ただ 特定の文字のカウントの仕方 の場合は特定の1つの文字の個数のみをカウントしていましたが、今回は全ての文字の個数をカウントしていく必要があります。
これは、前述の通り配列を用いることで簡単に実現することができます。要素数が 256
の配列を用意すれば、その配列の 256
個の要素それぞれで文字コードが 0
〜 255
の文字の個数をカウントすることができます。
例えば、配列名が count
であれば、文字コードが 65
の個数を count[65]
でカウントしていくことになります。前述の通り、65
は文字 'A'
に割り振られた文字コードですので、count[65]
は文字 'A'
の個数をカウントする要素であると考えることができます。
また、文字はコンピュータ内部で数値(文字コード)として扱われるのですから、count['A']
のように配列の添字に文字を指定することもできます。count['A']
と count[65]
は同じ要素を示すことになります。
さらに、今回は与えられた文字列の中の各文字の個数をカウントしていくことになります。文字列は配列であり、その配列の各要素は文字です。そして、その文字は文字コードとしてコンピュータ内部で管理されています。
したがって、文字列の配列を str
とすれば、str[i]
は文字列の第 i
文字目の文字であり、count[str[i]]++
を実行すれば str[i]
の文字の個数のカウントアップを行うことになります。例えば str[i]
が 'A'
なのであれば、count[str[i]]++
により count['A']++
が実行されることになるため、'A'
の個数をカウントアップすることになります。
あとは、文字列の各要素 str[i]
に対し、i
を変化させながら繰り返し count[str[i]]++
を繰り返していくことで、文字列 str
内の各文字の個数をカウントすることができます。
前述の通り、今回カウントするのは文字コードが 0
〜 255
の文字の個数のみになります。ですが、半角英数字や半角記号には 0
〜 255
の範囲内の文字コードが割り当てられていますので、英文のテキストファイルなどであれば、そのテキスト内に現れる全ての文字の個数をカウントすることができるはずです。
文字列から各文字の個数をカウントするプログラムの例
上記の考え方に基づいて作成した、文字列内の各文字の個数をカウントするプログラムのソースコード例は下記のようになります。
#include <stdio.h>
#define MAX_LINE 512
#define CHR_NUM 256
void countAllChr(unsigned int count[], char *str) {
for (unsigned int i = 0; str[i] != '\0'; i++) {
count[(unsigned char)str[i]]++;
}
}
int main(int argc, char *argv[]) {
FILE *stream;
char str[MAX_LINE];
unsigned int count[CHR_NUM];
if (argc != 2) {
printf("コマンドライン引数を1つ指定してください\n");
printf("プログラム名 <ファイルパス>\n");
printf("例:./main.exe test.txt\n");
return 0;
}
stream = fopen(argv[1], "r");
if (stream == NULL) {
printf("%sが開けません\n", argv[1]);
return 0;
}
for (unsigned int i = 0; i < CHR_NUM; i++) {
count[i] = 0;
}
while (fgets(str, MAX_LINE, stream) != NULL) {
countAllChr(count, str);
}
printf("in [%s]\n", argv[1]);
for (unsigned int i = 0; i < CHR_NUM; i++) {
printf("%c : %u\n", i, count[i]);
}
fclose(stream);
return 0;
}
読み込んだファイル内の文字列に対するカウント で紹介したもの同様に、コマンドライン引数で指定された ファイルパス
のファイルを読み込み、そのファイル内の文字のカウントを行うプログラムになっています。
ただし、特定の文字のみだけなく、文字コードが 0
〜 255
の全ての文字の個数をカウントするようになっているため、コマンドライン引数では2つ目の引数(特定の文字
)を指定する必要がありません。
また、countAllChr
関数は文字コードが 0
〜 255
の全ての文字の個数をカウントする関数になります。
countAllChr
関数は返却値を持ちませんが、第1引数で受け取った配列に対して各文字のカウント結果を格納します。つまり、countAllChr
関数呼び出し側は、第1引数に指定した配列から各文字のカウント結果を取得することができます。ただし、countAllChr
関数の呼び出し側で第1引数に指定する配列を用意しておく必要があるので注意してください。
また、countAllChr
関数を実行する前に、第1引数で指定する配列 count
の各要素を 0
に初期化しておく必要がある点にも注意してください。ローカル変数の配列の各要素は変数宣言しただけだと不定値になります。
countAllChr
関数の動作について解説しておくと、この関数は前述の解説通り、文字列 str
の各要素 str[i]
の文字の個数を count[(unsigned char)str[i]]++
でカウントアップしているだけです。ただ、char
型だと 128
以上の値が負の値として扱われてしまうため、それを避けるために str[i]
を unsigned char
型にキャストして負の値にならないようにしています(文字コードとして良く使用されるアスキーコードには 128
以上が割り振られている文字が無いはずなので、おそらくキャストしなくても問題ないとは思います)。
この、count[(unsigned char)str[i]]
のように、配列の添字に配列の要素を指定することは結構多いので、この形の書き方の意味はしっかり理解しておくと良いと思います。
上記ソースコードをコンパイル等を行なって実行すれば、0
〜 255
の全ての文字コードに対し、その文字と個数のカウント結果が表示されることになります(結果は読み込んだファイルによって異なります)。単なる文字だけなく、改行などの制御文字も含まれるので注意してください。
〜略〜 A : 2 B : 3 C : 1 D : 1 E : 0 F : 2 G : 1 H : 1 I : 12 J : 1 K : 0 L : 1 M : 3 N : 3 O : 1 P : 0 Q : 0 R : 0 S : 1 T : 3 U : 1 V : 0 W : 0 X : 0 Y : 0 Z : 0 [ : 0 \ : 0 ] : 0 ^ : 0 _ : 0 ` : 0 a : 83 b : 17 c : 27 d : 59 e : 126 f : 23 g : 20 h : 63 i : 54 j : 1 k : 5 l : 25 m : 29 n : 72 o : 76 p : 10 q : 0 r : 67 s : 56 t : 86 u : 32 v : 9 w : 20 x : 0 y : 26 z : 3 〜略〜
上記ソースコードでは全ての文字の個数のカウント結果を表示していますが、配列 count
の要素に文字 or 文字コードを指定することで特定の文字のカウント結果のみを表示するようなことも可能です。
printf("s : %u\n", count['s']);
スポンサーリンク
まとめ
このページでは、文字列内の「特定の文字のカウントの仕方」について解説しました!
C言語の文字列は文字の配列ですので、各要素が特定の文字と一致するかどうかを調べていくことでカウントすることが可能です。ただ、文字列の最後はヌル文字 '\0'
で終端されますので、ヌル文字 '\0'
よりも後ろ側の文字をカウントしないように注意してください。
また、文字は文字コードで扱われるため、配列の添字に指定することが可能であり、これを利用すれば特定の文字だけでなく、文字列内に現れる各文字の個数のカウントも簡単に実現することができます。
ただし、配列の添字に指定可能なのは文字であって、文字列ではないので注意してください(単なる数値ではなくアドレスを配列の添字に指定することになってしまう)。
今回は文字のカウントを行いましたが、同様の考え方で配列内のデータの最頻値(モード)を求めることも可能です。これについては下記ページで解説していますので、興味があればぜひ読んでみてください!
【C言語】最頻値(モード)を求める方法オススメの参考書(PR)
C言語学習中だけど分からないことが多くて挫折しそう...という方には、下記の「スッキリわかるC言語入門」がオススメです!
まず学習を進める上で、参考書は2冊持っておくことをオススメします。この理由は下記の2つです。
- 参考書によって、解説の仕方は異なる
- 読み手によって、理解しやすい解説の仕方は異なる
ある人の説明聞いても理解できなかったけど、他の人からちょっと違った観点での説明を聞いて「あー、そういうことね!」って簡単に理解できた経験をお持ちの方も多いのではないでしょうか?
それと同じで、1冊の参考書を読んで理解できない事も、他の参考書とは異なる内容の解説を読むことで理解できる可能性があります。
なので、参考書は2冊持っておいた方が学習時に挫折しにくいというのが私の考えです。
特に上記の「スッキリわかるC言語入門」は、他の参考書とは違った切り口での解説が豊富で、他の参考書で理解できなかった内容に対して違った観点での解説を読むことができ、オススメです。題名の通り「なぜそうなるのか?」がスッキリ理解できるような解説内容にもなっており、C言語入門書としてもかなり分かりやすい参考書だと思います。
もちろんネット等でも色んな観点からの解説を読むことが出来ますので、分からない点は別の人・別の参考書の解説を読んで解決していきましょう!もちろん私のサイトも参考にしていただけると嬉しいです!
入門用のオススメ参考書は下記ページでも紹介していますので、こちらも是非参考にしていただければと思います。
https://daeudaeu.com/c_reference_book/