ひぐぺん工房トップへ おかげさまで29周年!

ひぐぺん工房(松浦健一郎・司ゆき) - HigPen Works
Follow @higpenworks
・今までの仕事 ・書籍 ・最近の業務 ・対応可能言語 ・お見積

『C言語[完全]入門』
Q&A

以下の回答で問題が解決しなかった場合には、 こちらから ご連絡ください。

訂正


2024/11/28
Q.改行の出力(Chapter18, p.644)
A.
plot.cについて、改行(\n)の出力が不適切な部分がありました。申し訳ありません。新しいダウンロードファイルでは、出力例(p.642)と同様の(紙面では一部改行を追加しています)出力に修正しましたので、ご利用ください。変更箇所は次の通りです。

変更前:"fill=\"white\" stroke=\"black\"/>", w+size*4, h+size*4);
変更後:"fill=\"white\" stroke=\"black\"/>\n", w+size*4, h+size*4);

変更前:fprintf(fp, "<title>(%.2f, %.2f)</title></circle>",
変更後:fprintf(fp, "<title>(%.2f, %.2f)</title></circle>\n",

変更前:"fill=rgb(%d,%d,%d) stroke=\"black\">\n",
変更後:"fill=rgb(%d,%d,%d) stroke=\"black\">",

変更前:fprintf(fp, "<title>(%.2f, %.2f)</title></rect>",
変更後:fprintf(fp, "<title>(%.2f, %.2f)</title></rect>\n",

補足


2024/01/15
Q.増刷や改訂の予定を知りたい
A.
増刷や改訂に関しては、出版社が方針を決定し、著者の私共は出版社から直前に連絡を受ける…という状況です。
※お問い合わせへの回答メールが戻ってきてしまうので、こちらで回答させていただきました。

2024/01/15
Q.未研磨・未改装の本を入手したい
A.
未研磨・未改装の本の入手方法については、申し訳ありません、私共には心当たりがありません(研磨・改装について、あまり意識したことがありませんでした)。
※お問い合わせへの回答メールが戻ってきてしまうので、こちらで回答させていただきました。

2024/02/03
Q.「n<(w-4)/2*(h-3)/2」と「m[(w-4)+(h-3)*w]='G'」の意味(Chapter19, p.662)
A.
dig.cに記述された、以下の式について補足します。
(1) n<(w-4)/2*(h-3)/2
(2) m[(w-4)+(h-3)*w]='G'

wは迷路の幅、hは迷路の高さです。p.658のように、迷路の周囲には文字+、各行の末尾には改行文字∖n、配列の末尾にはヌル文字∖0を配置してあります。例えば次のような迷路では、wは6、hは5です。説明のために、非常に小さな迷路の例にしました。以下で□は格子点を表します。
+++++∖n
+###+∖n
+#□#+∖n
+###+∖n
+++++∖n∖0

式(1)(2)に含まれる(w-4)は、wから「+を2個、#を1個、∖nを1個、で合計4個」を取り除いた文字数を計算しています。上記の例では、(w-4)は2です。これを2で割って「(w-4)/2」を計算すると1となり、幅方向の格子点の個数(上記の例では1個)が求まります。

式(1)(2)に含まれる(h-3)は、hから「+を2個、#を1個、で合計3個」を取り除いた文字数を計算しています。上記の例では、(h-3)は2です。これを2で割って「(h-3)/2」を計算すると1となり、高さ方向の格子点の個数(上記の例では1個)が求まります。

幅方向の格子点の個数と、高さ方向の格子点の個数を乗算すれば、全ての格子点の個数が求まります。したがって式(1)の「(w-4)/2*(h-3)/2」は、全格子点の個数を表します。式(1)の「n<(w-4)/2*(h-3)/2」は、処理済みの格子点の個数(n)が、全格子点の数に満たない限り、処理を続けるための条件式です。

式(2)の「(w-4)+(h-3)*w」は、右下端の格子点(ゴールの位置)に相当する、配列の添字を計算しています。右下端の格子点は、左から(w-4)桁目、上から(h-3)行目の位置にあります(いずれも0から数えます)。配列には1行あたりw個の文字があるので、「(h-3)*w」で(h-3)行目の左端の添字が計算できます。これに(w-4)を加算して「(w-4)+(h-3)*w」とすれば、右下端の格子点(ゴールの位置)の添字になります。

以下はwが8、hが7の例です。上記の方法で式(1)(2)を計算し、格子点の個数やゴールの位置が正しく求まることを確認してみてください。
+++++++∖n
+#####+∖n
+#□#□#+∖n
+#####+∖n
+#□#□#+∖n
+#####+∖n
+++++++∖n∖0

2024/04/09
Q.「path」か「*path」か(Chapter17, p.600)
A.
ext.cの「printf("%s/%s\n", path, entry->d_name)」における「path」は、「*path」ではなく、掲載されている「path」が正しいです。このプログラムの「path」と「*path」は、次のような意味です。
printf関数において、変換指定の%sに対する引数は文字列(文字を指すアドレス)です。そのため*path(文字コード)ではなく、path(文字を指すアドレス)を渡しています。

上記を理解するには、文字列・配列・アドレス・ポインタといった知識を連携させる必要があるので、少し難易度が高いです。本書における関連項目を以下に挙げますので、ご参照いただけましたら幸いです。

2024/05/09
Q.find_files関数の動作(Chapter17, p.602-604)
A.
find_files関数の動作について補足します。例えば、CSample/chapter17において「ext2 ..」を実行すると、find_files関数は以下のように動作します。説明のために、このfind_files関数の呼び出しを「0段目のfind_files関数」と呼びます。
  1. 「DIR* dir=opendir(path);」において、pathは".."なので、CSampleフォルダ以下にあるファイルとディレクトリの一覧を取得します。
  2. 取得する項目は、例えば以下の通りです(途中までを掲載しました)。これらの項目は「ext ..」(p.601)で確認できます。
    .
    ..
    chapter10
    chapter11
    chapter12

  3. 「while ((entry=readdir(dir))!=NULL) {」において、0番目の項目「.」を取得します。
  4. 「if (strcmp(".", entry->d_name)!=0 && strcmp("..", entry->d_name)!=0) {」は、entry->d_nameが「.」なので、不成立になります。
  5. 「while ((entry=readdir(dir))!=NULL) {」において、1番目の項目「..」を取得します。
  6. 「if (strcmp(".", entry->d_name)!=0 && strcmp("..", entry->d_name)!=0) {」は、entry->d_nameが「..」なので、不成立になります。
  7. 「while ((entry=readdir(dir))!=NULL) {」において、2番目の項目「chapter10」を取得します。
  8. 「if (strcmp(".", entry->d_name)!=0 && strcmp("..", entry->d_name)!=0) {」は、entry->d_nameが「chapter10」なので、成立します。
  9. 「sprintf(s, "%s/%s", path, entry->d_name);」において、sは「../chapter10」になります。
  10. 「find_files(s);」において、「../chapter10」を引数にして、find_filesを再帰呼び出しします。
再帰呼び出しされたfind_files関数は、以下のように動作します。このfind_files関数の呼び出しを「1段目のfind_files関数」と呼びます。
  1. 「DIR* dir=opendir(path);」において、pathは「../chapter10」なので、CSample/chapter10フォルダ以下にあるファイルとディレクトリの一覧を取得します。
  2. 取得する項目は、例えば以下の通りです(途中までを掲載しました)。これらの項目は「ext ../chapter10」で確認できます。
    .
    ..
    array.c
    array2.c
    array3.c

  3. 「while ((entry=readdir(dir))!=NULL) {」において、0番目の項目「.」を取得します。
  4. 「if (strcmp(".", entry->d_name)!=0 && strcmp("..", entry->d_name)!=0) {」は、entry->d_nameが「.」なので、不成立になります。
  5. 「while ((entry=readdir(dir))!=NULL) {」において、1番目の項目「..」を取得します。
  6. 「if (strcmp(".", entry->d_name)!=0 && strcmp("..", entry->d_name)!=0) {」は、entry->d_nameが「..」なので、不成立になります。
  7. 「while ((entry=readdir(dir))!=NULL) {」において、2番目の項目「array.c」を取得します。
  8. 「if (strcmp(".", entry->d_name)!=0 && strcmp("..", entry->d_name)!=0) {」は、entry->d_nameが「array.c」なので、成立します。
  9. 「sprintf(s, "%s/%s", path, entry->d_name);」において、sは「../chapter10/array.c」になります。
  10. 「find_files(s);」において、「../chapter10/array.c」を引数にして、find_filesを再帰呼び出しします。
さらに再帰呼び出しされたfind_files関数は、以下のように動作します。このfind_files関数の呼び出しを「2段目のfind_files関数」と呼びます。
  1. 「DIR* dir=opendir(path);」において、pathは「../chapter10/array.c」なので、ディレクトリとしては開けません。
  2. 「if (dir) {」は不成立になります。
  3. 「else {」以下の「puts(path)」において、「../chapter10/array.c」を出力します。
ここで「1段目のfind_files関数」に戻り、CSample/chapter10フォルダ以下にあるファイルとディレクトリの一覧について、処理を続けます。全て処理したら、「0段目のfind_files関数」に戻ります。

「0段目のfind_files関数」に戻ったら、CSample以下にあるファイルとディレクトリの一覧について、処理を続けます。chapter11などのフォルダについては、chapter10と同様に、再帰呼び出しを行います。

全て処理したら、main関数に戻ります。通常の関数呼び出しと同様に、戻り先は関数を呼び出した箇所です。このプログラムの場合は「find_files(argv[1]);」という箇所に戻ります。これでmain関数も終わりです。

chapter10やchapter11などのディレクトリに関しては、再帰呼び出しが一時的に深くなりますが、サブディレクトリを処理したら、最後は「0段目のfind_files関数」に戻ってきます。無制限に再帰呼び出しが深くなってしまうということは無いので、ご安心ください。

各段階のfind_files関数は、スタック上に別々の記憶領域を持っていることに注意してください。例えば、「0段目のfind_files関数」の処理中に「1段目のfind_files関数」を呼び出しますが、この間に「0段目のfind_files関数」の状態は保存されています。したがって「0段目のfind_files関数」に戻った後に、ファイルとディレクトリの一覧を、まだ処理していなかった箇所から引き続き処理できます。

p.604における「../chapter10/array」などの表示は、前述のように「else {」以下の「puts(path)」で行います。なお、p.604に掲載した「../chapter10/array」は、ディレクトリではなく、macOS/Linuxにおける実行ファイルです。

2024/07/18
Q.「*p++」の解釈(Chapter13, p.480)
A.
array5.cの「while (*p) printf("%d ", *p++);」について、「*p++」は「(*p)++」ではなく、「*(p++)」と解釈されます。「*(p++)」の動作は次の通りです。後置インクリメントでは、加算が後で行われる(加算の結果が後で反映される)ことがポイントです。

(1) 例えば、pに1000番地が入っているとします。
(2) 「p++」を処理します。++は後置インクリメントなので、「p++」は加算前の1000番地を返し、実際にpに1を加算するのは後で行います。
(3) 「*(p++)」を処理します。(2)より「p++」は1000番地なので、「*(p++)」は1000番地の値(int型)を取り出します。
(4) pに1を加算します。pはint*型なので、実際にはsizeof(int)、本書の環境では4が加算され、pは1004番地になります。

もし「(*p)++」と解釈すると、「p(int*型)に1を加算する」のではなく、「pが指している値(int型)に1を加算する」という動作になります。これはarray5.cとは異なる動作です。

通常は、優先順位が高い演算子を先に計算しますが、後置インクリメントの場合は加算を後で行う(加算の結果を後で反映する)ため、少し難解かもしれません。「*p++」の場合、優先順位は*よりも++の方が高いため「*(p++)」と解釈しますが、*は加算前のpの値を使い、++によるpの加算は後で行います。

2024/08/01
Q.EXT構造体に関するメモリの確保と解放(Chapter17, p.607-609)
A.
EXT構造体(ext_sub.h)に関する、メモリの確保と解放について補足します。

(1) ext_begin関数(ext_sub.c)の「ext_p=malloc(sizeof(EXT));」について、EXT構造体のextメンバのサイズは決まっているか?
決まっています。EXT構造体のextメンバはchar*(char型へのポインタ)型なので、extメンバのサイズは本書の環境では8バイト(64ビットのアドレスを格納するためのバイト数)です。

(2) extメンバのサイズは、他のlong型のメンバとサイズが異なるか?
本書の環境では、extメンバは8バイト、long型のメンバ(countとsize)は4バイト(Windows)または8バイト(macOS/Linux)です。

(3) extメンバと他のlong型のメンバの間に、パディングは入るか?
本書の環境では4バイトアライメントなので、extとcountの間にも、countとsizeの間にも、パディングは入りません。

(4) reallocが行われると、extメンバのサイズは変化するか?
変化しません。extメンバはchar*型なので、本書の環境では常に8バイトです。変化するのは、extメンバが指すメモリ(extメンバに格納されたアドレスが指すメモリ)のサイズです。

(5) ext_end関数(ext_sub.c)の「free(ext_p[i].ext);」は必要か?
必要です。「free(ext_p);」はEXT構造体のメモリを解放しますが、extメンバが指すメモリは解放しません。extメンバが指すメモリは「「free(ext_p[i].ext);」で解放します。

2024/11/27
Q.「const char* file」におけるconstの効用(Chapter18, p.628)
A.
load.hのload_points関数は、引数を「const char*」で受け取ります。このように引数を「char*」ではなく「const char*」とすることには、次のような効用があります。

(1) 受け取った文字列を関数内でうっかり変更することを防止する。
(2) 関数が受け取れる文字列の種類を広くする。

(1)については、p.204に記載した内容の発展です(本書では詳しく取り上げていません)。constをchar*のようなポインタ型に付けると、ポインタが指している先の値(今回の場合は文字列)の変更を禁止します。

(2)については、以下で解説します。load.hでは、(A)のようにconstを使ってload_points関数を書きました。一方、constを使わずに(B)のように書くこともできます。

(A) POINTS* load_points(const char* file);
(B) POINTS* load_points(char* file);

(A)と(B)は似た動きをしますが、受け取れる引数の種類が異なります。次のような、constである文字列sと、constではない文字列tを考えます。

const char s[]="test.csv";
char t[]="test.csv";

(A)はsもtも受け取れますが、(B)はsを受け取れず(gccの場合は警告が出ます)、tのみを受け取れます。つまり、(A)の方が受け取れる文字列の種類が広くなります。このように、受け取った文字列を関数内で変更する必要が無いときは、「char*」ではなく「const char*」で受け取っておくと、constである文字列もconstではない文字列も受け取れる、という利点が生じます。

標準ライブラリにおいて文字列を受け取る関数(例えばprintf関数など)は、受け取った文字列を変更しない場合は、「const char*」で受け取るように書かれています。load.hのload_points関数も、標準ライブラリと同様に「const char*」で文字列を受け取るように書きました。

2024/11/27
Q.isdigit関数の動作(Chapter18, p.628)
A.
標準ライブラリのisdigit関数は、引数で文字列ではなく、文字を受け取ります。そして、受け取った文字が数字ならば0以外を返し、数字でなければ0を返します。

load.cでは、「ファイルから読み込んだ行が数字から始まるかどうか」を調べるために、isdigit関数を使います。「行の全体が数字かどうか」は調べません。そこで、load.cの「if (isdigit(s[0])) n++;」のように、isdigit関数にs[0](文字列sの最初の文字)を渡すことで、文字列の最初の文字が数字かどうかを判定しています。

2024/11/27
Q.「POINTS* points=new_points(n);」の「POINTS*」は必要か(Chapter18, p.628)
A.
load.cの「POINTS* points=new_points(n);」における「POINTS*」は、変数pointsの型を指定するために必要です。このプログラムは、「型 変数名=式;」という変数の宣言と初期化の構文(p.167)に相当します。

「new_points関数の戻り値はPOINTS*型なので、pointsの型を自動的にPOINTS*型にしてくれてもいいのでは?」と思うかもしれません。このように、変数を初期化(または変数へ代入)する値の型に基づいて、変数の型を自動的に決める機能のことを、型推論と呼びます。

例えばC++やJavaなどには型推論の機能があります。一方、C言語には型推論の機能が無いので、変数を宣言する際には明示的に型を指定する必要があります。

なお「POINTS* points=new_points(n);」において、pointsの型としては「POINTS*」以外に「void*」(p.504)も候補になります(「POINTS*」が唯一の候補ではないことに注目してください)。つまり「void* points=new_points(n);」でも、この箇所についてはコンパイルできます。

2025/02/25←2024/11/28
Q.CSVの読み込みにおけるfgets関数とfscanf関数の動作(Chapter18, p.629)
A.
load.cの下記の処理における、fgets関数とfscanf関数の動作について説明します。

// 点の座標を読む
for (POINT* p=points->point; fgets(s, sizeof s, fp); ) {
  if (fscanf(fp, "%lf,%lf", &p->x, &p->y)!=2) continue;
  …
}

上記の処理において、fgets関数には次のような役割があります。

(A) ファイルの先頭行を読み飛ばす。
(B) ファイルの各行末の改行を読み飛ばす(より正確には「座標,座標」よりも後から改行までを読み飛ばす)。
(C) ファイルに座標以外の行が混じっていた場合に読み飛ばす。
(D) ファイルの末尾を検出してループを終了する。

例えば、次のようなCSVファイルを読み込む場合を考えます。

population,latitude(改行)
585097,24.47(改行)
778567,9.07(改行)

この場合、fgets関数とfscanf関数は次のように動作します。

(1) fgets関数が「population,latitude(改行)」を読み飛ばす。
(2) fscanf関数が「585097,24.47」を読み込む。
(3) fgets関数が「(改行)」を読み飛ばす。
(4) fscanf関数が「778567,9.07」を読み込む。
(5) fgets関数が「(改行)」を読み飛ばす。
(6) fscanf関数が座標を読み込もうとするが、読み込めない(continueを実行する)。
(7) fgets関数がファイルの末尾を検出してループを終了する。

上記の動作を理解するには、for文の動作(p.246)と、fgets関数の動作(p.330)をご確認ください。上記の処理では、for文の条件式でfgets関数を呼び出していることがポイントです。for文による毎回の繰り返しの最初に、fgets関数が呼び出されます。これはfscanf関数の呼び出しよりも前です。

実際にfgets関数が読み飛ばした文字列を表示すると、動作を理解する助けになるかもしれません。例えば以下のようにプログラムを改造してみてください。fgets関数が読み込んだ文字列(s)を表示します。

// 点の座標を読む
for (POINT* p=points->point; fgets(s, sizeof s, fp); ) {
  printf("fgets:%s", s); // この行を追加
  if (fscanf(fp, "%lf,%lf", &p->x, &p->y)!=2) continue;
  …
}

プログラムを実行すると、例えば以下のように表示されます。改行が出力される箇所については、以下では(改行)と示しました。

fgets:population,latitude(改行)
fgets:(改行)


前述の通り、fgets関数によって、先頭行の「population,latitude(改行)」や、各行末の「(改行)」が読み飛ばされていることが確認できます。


2024/11/28
Q.「iteration」とは(Chapter18, p.632)
A.
iteration(イテレーション)とは「繰り返し」のことです。コンピュータにおける繰り返し処理のことをiterationと呼びます。

2024/11/28
Q.文字列の連結(Chapter18, p.644)
A.
plot.cの下記の部分では、紙面の都合から、長い文字列を2個の文字列に分けた上で連結しています。文字列リテラルを並べて書くと、コンパイル時に1個の文字列へ連結されます(p.321)。

// 外枠を出力
fprintf(fp, "<rect width=%d height=%d "
  "fill=\"white\" stroke=\"black\"/>", w+size*4, h+size*4);

2024/11/28
Q.「/>」の意味(Chapter18, p.644)
A.
plot.cの下記の部分で出力している「/>」は、SVGの要素の末尾を表します。

"fill=\"white\" stroke=\"black\"/>"

SVGの要素は、次のような2種類の形式で書けます。

(1) 1個のタグで完結する「<要素名 … />」という形式。例えば「<rect …/>」。
(1) 2個のタグを使う「<要素名 … > … </要素名>」という形式。例えば「<circle …> … </circle>」。

2024/11/28
Q.座標の計算結果(Chapter18, p.644)
A.
正規化した点の座標(p.632)は、小数点以下2桁までを表示しています。そのため、表示された座標からplot.c(p.644)の方法「(int)(p->x*w)+size*2, h-(int)(p->y*h)+size*2」で円の座標を計算すると、実行結果(p.642)とは異なる場合があります。

本来の座標で計算すると、実行結果に一致します。print.c(p.630)の以下の部分を、例えば次のように書き換えると、本来の座標(により近い値。この例では小数点以下6桁まで)を表示できます。

書き換え前:printf("%15.2f %15.2f %10.2f %10.2f %10d\n",
書き換え後:printf("%15.2f %15.2f %f %f %10d\n",

例えば、(585097.00, 24.47)という点の座標は、正規化した本来の座標は(0.027160 0.594399)で、小数点以下第2位までを表示すると(0.03、0.59)です。本来の座標を使って円の座標を計算すると、

(int)(0.027160*1000)+10*2 = 47
1000-(int)(0.594399*1000)+10*2 = 426

となり、実行結果(p.642)の、

<circle cx=47 cy=426 r=10 fill=rgb(37,197,60) stroke="black">

に一致します。

正規化した点の座標の表示(p.632)については、紙面の都合で桁数を少なめに表示しましたが、本来の値との間には誤差が生じます。画面の広さに余裕がある場合は、より多くの桁数を表示するように改造すると、結果が分かりやすくなりそうです。

2024/12/11
Q.足跡を消すタイミング(Chapter19, p.667)
A.
solve.cにおいて、足跡を消すタイミングについて補足します。スタートから開始して、以下の位置まで進んだとします。
#######
#SOOOO#
### # #
# # # #
# # ###
#    G#
#######

「for (int i=0; i<4; i++) {」において、iが2のとき下に進みます。「int p1=p+u[i], p2=p1+u[i];」で計算したp1とp2の位置は、以下の1と2です。
#######
#SOOOO#
### #1#
# # #2#
# # ###
#    G#
#######

「m[p1]='o';」で1の位置に足跡を付けます。「if (m[p2]=='G') return 1;」は成立しないので、「m[p2]='o';」で2の位置に足跡を付けます。
#######
#SOOOO#
### #O#
# # #O#
# # ###
#    G#
#######

「if (solve(maze, p2)) return 1;」において、前述の2を新しい現在の位置として、solve関数を再帰呼び出しします。行き止まりなので、solve関数は0を返してきます。そこで「m[p1]=m[p2]=' ';」を実行し、前述の1と2の位置を空白で上書きし、足跡を消します。
#######
#SOOOO#
### # #
# # # #
# # ###
#    G#
#######


2024/12/11
Q.ゴールに到達したときの処理(Chapter19, p.667-669)
A.
solve.cでは、迷路を2歩進むたびに、solve関数を再帰呼び出しします。例えば、以下のような迷路を考えます。
#######
#S    #
### # #
# # # #
# # ###
#    G#
#######

最初はsolve_maze関数から、スタートの位置を現在の位置として、「solve(maze, 2+maze->width*2);」のようにsolve関数を呼び出します。これがsolve関数の1段階目の呼び出しです。以下では1と示します。
#######
#1    #
### # #
# # # #
# # ###
#    G#
#######

右に2歩進む際に、「if (solve(maze, p2)) return 1;」において、solve関数を再帰呼び出しします。これがsolve関数の2段階目の呼び出しです。以下では2と示します。Oは足跡です。2の位置にも足跡が書かれています。
#######
#1O2  #
### # #
# # # #
# # ###
#    G#
#######

以下はゴールの直前まで進んだ状態です。solve関数の呼び出しは4段階目です。
#######
#1O2  #
###O# #
# #3# #
# #O###
#  4 G#
#######

右に2歩進む際に、「m[p1]='o';」で1歩先に足跡を付けます。
#######
#1O2  #
###O# #
# #3# #
# #O###
#  4OG#
#######

実際には、1の位置はS、2・3・4の位置はOです。以下のように、ゴールまでの経路に足跡が書かれた状態になっています。
#######
#SOO  #
###O# #
# #O# #
# #O###
#  OOG#
#######

以後は再帰呼び出しから戻る処理です。「if (m[p2]=='G') return 1;」において、ゴールに到達したことが分かるので、1を返します。solve関数の4段階目の呼び出しから、3段階目の呼び出しに戻ります。
#######
#1O2  #
###O# #
# #3# #
# #O###
#  OOG#
#######

3段階目から4段階目を呼び出したのは「if (solve(maze, p2)) return 1;」の「solve(maze, p2)」だったので、ここに戻ってきます。solve関数(4段階目)の戻り値は1なのでif文が成立し、「return 1;」で1を返して、2段階目の呼び出しに戻ります。
#######
#1O2  #
###O# #
# #O# #
# #O###
#  OOG#
#######

同様に、2段階目から3段階目を呼び出した「if (solve(maze, p2)) return 1;」に戻ります。solve関数(3段階目)の戻り値は1なのでif文が成立し、「return 1;」で1を返して、1段階目の呼び出しに戻ります。
#######
#1OO  #
###O# #
# #O# #
# #O###
#  OOG#
#######

同様に、1段階目から2段階目を呼び出した「if (solve(maze, p2)) return 1;」に戻ります。solve関数(2段階目)の戻り値は1なのでif文が成立し、「return 1;」で1を返して、solve_maze関数に戻ります。
#######
#SOO  #
###O# #
# #O# #
# #O###
#  OOG#
#######

main関数(p.669)の「solve_maze(maze);」を実行すると、上記のようにゴールまでの経路(解答)が書き込まれた迷路が得られます。この迷路を「print_maze(maze);」で表示します。

関数の戻り値をどのような仕様にするのかは、プログラマが自由に決められます。solve関数の場合は、「ゴールに到達したら戻り値は1、到達していなかったら戻り値は0」という仕様にしましたが、他の仕様にすることも可能です。

main関数の場合は、正常終了の場合は0、異常終了の場合は0以外(例えばエラーコードなど)にする慣習があります。main関数の戻り値は、プログラムを起動した環境(コマンドプロンプトやターミナルなど)が、プログラムが正常に動いたかどうかを判別するために利用する場合があります。

2025/03/17
Q.座標の計算方法(Chapter18, p.644)
A.
円や正方形の座標を計算する、以下の式について補足します。
上記の式は、以下のように導出しました。
  1. 以下は点および中心の座標です。正規化してあるので、X座標とY座標の範囲は0.0~1.0です(p.625)。
    p->x, p->y
    c->x, c->y
  2. 図の幅(w)と高さ(h)に合わせて、座標を変換します。X座標の範囲を0~w、Y座標の範囲を0~hにするために、wおよびhを掛けます。
    p->x*w, p->y*h
    c->x*w, c->y*h
  3. 画面上の座標は整数なので、(int)でキャストして整数に変換します。
    (int)(p->x*w), (int)(p->y*h)
    (int)(c->x*w), (int)(c->y*h)
  4. 今回のプログラムでは、左下を原点(0, 0)として結果を図示したいと考えました。しかし、画面上の座標は左上が(0, 0)、右下が(w, h)です。そこで、hからY座標を引くことにより、Y座標を反転します。
    (int)(p->x*w), h-(int)(p->y*h)
    (int)(c->x*w), h-(int)(c->y*h)
  5. 円は中心の座標を、正方形は左上の座標を指定します。そこで、円については座標に半径のsizeを足します。
    (int)(p->x*w)+size, h-(int)(p->y*h)+size
    (int)(c->x*w), h-(int)(c->y*h)
  6. 円の幅と高さはsize*2、正方形の幅と高さはsize*3です。円の中心と正方形の中心を一致させるために、正方形の座標からsize/2を引きます。
    (int)(p->x*w)+size, h-(int)(p->y*h)+size
    (int)(c->x*w)-size/2, h-(int)(c->y*h)-size/2
  7. 今回のプログラムでは、図の周囲(枠の内側の上下左右)にsizeずつの余白を設けています。そこで、左と上の余白分として、sizeを足します。
    (int)(p->x*w)+size+size, h-(int)(p->y*h)+size+size
    (int)(c->x*w)-size/2+size, h-(int)(c->y*h)-size/2+size
  8. 式を整理すると、最終的な式が得られます。
    (int)(p->x*w)+size*2, h-(int)(p->y*h)+size*2
    (int)(c->x*w)+size/2, h-(int)(c->y*h)+size/2
プログラムを改造して、上記の各段階に示したように式を変更し、出力される図がどのように変化するのかを、実際に確かめてみてください。

最終更新 2025/03/17
トップページへ
©ひぐぺん工房 禁無断転載
最新刊『機械語がわかる図鑑』 
このサイトはリンクフリーです。
このサイトはChromeで動作検証しています。ブラウザにかかわらず表示に乱れがありましたらどうぞお知らせ下さい。メールを送る