CS50:Arrays

CS50 for Japanese(開始4日目。なお、カウントは取り組んでいる日数)

 

Week2 Arrays

前回からとても時間が空いた。本業の法律を数多く勉強していたら後回しになっていった。
金銭を支払って受講等をしていないことのいい点は、無理してやりたくない時に取り組む必要がないことである。

映像授業を受講した。映像は2時間24分59秒。

今回の授業では前回のC言語をより掘り下げていくことが目的として挙げられた。
絶対に間をあけるべきではなかったが、やってしまったものは仕方ない。
特に概念的な特徴であったりそういったものを学習することでより理解を深めるということだった。

 

先週は赤い文字とか黄色い文字(エラー)が出た場合のお話をしていたけれど、今日は無視して実行をしていた白い文字についてもお話していくというのが冒頭で共有された。

  • make は実際のところ、オプション付きでコンパイラであるclangを呼び出すプログラムにすぎません。ソースコードファイルhello.cコンパイルするには、clang hello.cコマンドを実行します。何も起きていないようですが。これはエラーがなかったということです。lsを実行すると、a.outファイルがディレクトリに表示されます。ファイル名はデフォルトのままなので、より具体的に指定するコマンドclang -o hello hello.cを実行できます。

この話をしたときに、clangの表示に関しては歴史的な経緯があって…という共有がなされ、そういった背景があることもあるのねと思った。
大事ではないがそういったこともあることを知ると人間が作った感じがして親しみがわいた。

あと、自動化されて自分たちから見えていなかったことを見て詳らかにしていくというのは、ええやんそんなことせんでも、と思いがちなのだが仕組みを知ることでより深く学習できるという点において優れていると考えた。

 

次に、以下のことが懇切丁寧に説明された。

また、それらの具体的な例と処理について結局はコンピューターの読めるバイナリコードにしていく必要があるのだけれど、それを手動でやっていくととんでもない時間がかかるため、それを自動化していた、それがmakeまたはclangというものであったということでまとめられた。

よって、コンパイルとは実際1つの小さなステップを指すのだが、プログラマコンパイルというと4つのステップを指すのであるとのことだった。

 

次に、バグを見つけて修正する「デバッグ」ということについて。

バグとはその昔、実際に虫が入って不具合を引き起こしていたということに由来するかどうかは確かではないが、意図しない動きやミス、問題のことを言うとのことで、
それを修正するプロセスのことを「デバッグ」というとのこと。

以下は実際に作業をしながら進めていったもの。正直、ついていくので精一杯だった。

  • buggy0.cを見てみましょう。
#include <stdio.h>
  int main(void) {     // Print 10 hashes     for (int i = 0; i <= 10; i++)     {         printf("#\n");     } }
  • 10個の#を印刷したいのですが、11個あるようです (プログラムはエラーなしでコンパイルされているので、ロジックにエラーが発生しています) 。何が問題なのかわからない場合は、一時的に別のprintf を追加します。
#include <stdio.h>
  int main(void) {     for (int i = 0; i <= 10; i++)     {         printf("i is now %i\n", i);         printf("#\n");     } }
  • これで、iが0から開始して10まで続いていることがわかります。forループを10で停止するため、<= 10ではなく< 10とします。
  • CS50 IDEには、プログラムのデバッグに役立つ別のツールdebug50があります。これはスタッフが作成したツールで、gdbと呼ばれる標準ツール上に構築されています。これらのデバッガはどちらも、プログラムをステップバイステップで実行し、プログラムの実行中に変数やその他の情報を確認できるプログラムです。
  • debug50 ./buggy0を実行すると、プログラムを変更したので再コンパイルするように指示されます。次に、デバッガがプログラムを一時停止するコードの行にブレークポイントまたはインジケータを追加するように指示します。
    • ターミナルで上下キーを使うことで、過去のコマンドを再入力せずに利用できます。
  • コードの6行目の左側をクリックすると、赤い円が表示されます。
  • そして、debug50 ./buggy0をもう一度実行すると、右側にデバッガ・パネルが開きます。
  • 作成した変数iローカル変数 (Local Variables) セクションの下にあり、値が0であることがわかります。
  • ブレークポイントは6行目でプログラムを一時停止し、その行を黄色で強調表示しています。続行するには、デバッガパネルにいくつかのコントロールがありますが、青い三角形は、別のブレークポイントまたはプログラムの最後に到達するまでプログラムを続行します。右側にあるカーブした矢印 「Step Over」 は、行を 「Step Over (またいで)」 して行を実行し、直後にプログラムを再び一時停止します。
  • そこで、カーブした矢印を使用して次の行を実行し、その後の変化を確認します。printfの行に戻り、カーブした矢印をもう一度押すと、ターミナルウィンドウに#が1つ表示されます。矢印をもう一度クリックすると、iの値が1に変わります。矢印をクリックし続けると、プログラムが1行ずつ実行されます。
  • デバッガを終了するには、control + Cを押して実行中のプログラムを停止します。
  • buggy1.c: 別の例としてbuggy1.cを見てみましょう。
#include <cs50.h>
#include <stdio.h>
  // Prototype int get_negative_int(void);   int main(void) {     // Get negative integer from user     int i = get_negative_int();     printf("%i\n", i); }   int get_negative_int(void) {     int n;     do     {         n = get_int("Negative Integer: ");     }     while (n < 0);     return n; }
  • 別の関数get_negative_intを実装し、ユーザから負の整数を取得しました。main関数の前にプロトタイプを記述する必要があり、そうすればコードがコンパイルされます。
  • しかし、プログラムを実行すると、負の整数を指定した後も負の整数を指定するように要求され続けます。行10 int i = get_negative_int();ブレークポイントを設定します。これが最初の興味深いコード行だからです。debug50 ./buggy1を実行し、デバッグパネルの 「コールスタック」 セクションでmain関数にいることを確認します (「コールスタック」 とは、その時点でプログラム内で呼び出され、まだから返されていないすべての関数を指します。これまでは、main関数のみが呼び出されていました) 。
  • 下向きの矢印 「Step Into」 をクリックすると、デバッガがその行で呼び出されている関数get_negative_intの中に (into) 移動します。コールスタックが関数の名前で更新され、変数nが値0で更新されています。
  • 「Step Over」をもう一度クリックすると、n-1に更新されています。これは実際に入力した値です。
  • もう一度 「Step Over」 をクリックすると、プログラムがループ内に戻っているのがわかります。whileループはまだ実行中であるため、whileループがチェックする条件はtrueである必要があります。< 0は負の整数を入力すると真であるため、>= 0に変更してバグを修正する必要があります。
  • debug50の使い方を学ぶために少しの時間を投資することで、将来多くの時間を節約することができます。

あと、ちょっとした冗談なのか、初心者はコードについて考えるときはゴム製のアヒルに話しかけろ、聞いてくれる人間がいるのはものすごく幸運だから感謝しろ、とのことだった。
また、自分が論理的だと思っていることを口に出して話してみると非論理的だと気付けることもあるので、ゴム製のアヒルに話しかけることは有効、という話もあった。

 

5分の休憩の後に、メモリの話があった。
正直よくわかっていなかった部分について説明してくれたので理解がクリアになった。
具体的には以下が明確な理解ができた部分である。

  • コンピュータの内部にはRAM(ランダム・アクセス・メモリ)と呼ばれるチップがあります。RAMは、プログラムが実行されている間のコードや、プログラムが開いている間のファイルなど、短期間使用するためのデータを格納します。プログラムやファイルをハードドライブ (またはSSD、ソリッドステートドライブ) に保存して長期保存することもありますが、RAMの方がはるかに高速なので使用します。ただし、RAMは揮発性であるため、データを保存するために電力が必要です。
  • RAMに格納されたバイトは、グリッドにあるかのように考えることができます。
  • 実際には、チップあたり数百万または数十億バイトです。
    • 各バイトはチップ上での位置 (最初のバイト、2番目のバイトなど) に対応します。
  • Cでは、char型の変数を作成すると、その変数は1バイトのサイズになり、RAM上のこれらのボックスの1つに物理的に格納されます。4バイトの整数は、これらのボックスの4つを占有します。

いつも使っているPCやスマホにこういったものが備え付けられ、動作していることに少し感動した。

次に、試験の平均点を算出するコードを書いてみようというところからC言語の配列について学んだ。
C言語においては、連続して格納された値のリストのことを配列と呼ぶらしい。

具体的にやっていたことは以下だが、展開すること、増えること、応用することを念頭に置いて考えることが大変重要になってくるように思う。

  • 3つの数の平均を取りたいとします。
#include <stdio.h>
  int main(void) {     int score1 = 72;     int score2 = 73;     int score3 = 33;       printf("Average: %f\n", (score1 + score2 + score3) / 3.0); }
  • 3ではなく3.0で除算するので、結果もfloatになります。
    • プログラムをコンパイルして実行すると、平均値が表示されます。
  • プログラムの実行中、3つのint変数がメモリに格納されます。
  • 各intは4バイトを表す4つのボックスを持ち、各バイトは8ビットの0、1で構成され、電気部品によって格納されます。
  • メモリ内では、変数を次々に連続して格納し、ループを使用してより簡単にアクセスできることがわかります。Cでは、連続して格納された値のリストを配列と呼びます。
  • 上のプログラムでは、int scores[3];によって3つの整数の配列を宣言できます。
  • また、scores[0] = 72として配列内の変数を割り当てて使用することもできます。大カッコを使用して、配列内の 「0番目」 の位置にインデックスを付けて、その位置に移動します。配列のインデックスは0から始まります。つまり、最初の値のインデックスは0、2番目の値のインデックスは1というようになります。
  • 配列を使用するようにプログラムを更新します。
#include <cs50.h>
#include <stdio.h>
  int main(void) {     int scores[3];     scores[0] = get_int("Score: ");     scores[1] = get_int("Score: ");     scores[2] = get_int("Score: ");       // Print average     printf("Average: %f\n", (scores[0] + scores[1] + scores[2]) / 3.0); }
  • ここで、ユーザに3つの値を要求し、前と同じように平均を出力しようと思いますが、配列に格納された値を使用します。
  • 配列内の項目は、その位置に基づいて設定およびアクセスすることができ、その位置は変数の値にもまたなるため、ループを使用できます。
#include <cs50.h>
#include <stdio.h>
  int main(void) {     int scores[3];     for (int i = 0; i < 3; i++)     {       scores[i] = get_int("Score: ");     }       // Print average     printf("Average: %f\n", (scores[0] + scores[1] + scores[2]) / 3.0); }
  • ハードコーディング、つまり各要素を3回手動で指定する代わりに、forループとiを配列内の各要素のインデックスとして使用します。
  • そして、配列の長さを表す値3を2つの異なる場所で繰り返しました。プログラムでは定数 (固定値の変数) を使用できます。
#include <cs50.h>
#include <stdio.h>
  const int TOTAL = 3;   int main(void) {     int scores[TOTAL];     for (int i = 0; i < TOTAL; i++)     {       scores[i] = get_int("Score: ");     }       printf("Average: %f\n", (scores[0] + scores[1] + scores[2]) / TOTAL); }
  • constキーワードを使用して、TOTALの値がプログラムによって変更されてはならないことをコンパイラに伝えることができます。そして慣例により、変数の宣言をmain関数の外に置き、その名前を大文字にします。これはコンパイラには必要ありませんが、私たちには、この変数が定数であり、最初から見やすいようにします。
    • しかし、正確に3つの値がなければ、現在の平均は間違っている数値です。
  • 平均を計算する関数を追加します。
float average(int length, int array[]) {     int sum = 0;     for (int i = 0; i < length; i++)     {         sum += array[i];     }     return sum / (float) length; }
  • lengthとintの配列 (任意のサイズ) を渡し、ヘルパー関数内で別のループを使用して値を合計して変数sumにします。(float) を使用してlengthをfloatにキャストするので、この2つを分割した結果もfloatになります。
    • main関数では、printf("Average: %f\n", average(TOTAL, scores);を使用して新しいaverage関数を呼び出すことができます。 main内の変数の名前は、のみが渡されるため、averageが呼び出すものと一致させる必要はありません。
    • 配列の長さをaverage関数に渡しているため、average関数はいくつ値があるか知ることができます。

 

正直、今回もついていくのが精一杯で、発展であるとかそういったことにまったく考えが及ばなかった。
実際にC言語を用いて何かをするということがないだけになかなか理解が深まらない部分もあるのかもしれない。

 

本講義のNoteは以下。

https://cs50.jp/x/2021/week2/notes/