見出し画像

コンピュータサイエンス概論 #4(4週目)

振り返り

前回

は、switchとかループに関しての週でした。ちなみに取り上げませんでしたが、Rustなんかだと、

loop {
  <statement>;
}

というループもあります。当然breakしないと無限ループになります。

Reading課題

この週は例外処理とか関数を取り上げます。下記を読んだ上で授業を受けるものとしてReadingの宿題が出ます。

下記pdfのChapter3

http://greenteapress.com/thinkcpp/thinkCScpp.pdf

Lecture 8/9

振り返り&補足

  • ループはネストできる

for (int x = 0; x < 10; ++x) {  // outer loop
  for (int y = 0; y < 10; ++y) {  // inner loop
    <statement>;
  }
}

outer loop1回につきinner loopが10回回るという話ですね。
基本的に構文は入れ子にできるものが多いですね。Pythonだと関数の中に関数書いたりもできます。まぁ、ラムダとかも関数と考えると関数どこにでも書けると言えなくもない。C++だとラムダ式の実体は関数オブジェクトとか深堀すると色々ありますね。あまりネストは深すぎると可読性が落ちるのでPythonやRubyのlintで複雑度として怒られることも。

  • loop内での変数の上書き

変数のスコープの話ですね。ただし、外側で定義された変数を内側で上書きしたら期待する動作にならない可能性もあるので気を付けないといけないやつですね。あとは、外でループに使う変数を定義する場合(Whileは常に、forも古い書き方や後でループ数使いたいときとか)に使い回したり外のループの条件に使う変数を変更するとおかしなことになったりする話です。

int i = 0;
for (; i < 10; ++i) {
  for (int j = 0; j < 10; ++j) {
     if (<expression>) {
       i += 3;
     }
  }
}

とか、あとは内側のループの終了時の処理で外側の条件を間違って変更したりとかコピペベースの初心者がやりがちなバグ。

文字列

文字列というのは簡単なようで複雑です。
C++ではクラスの文字列とC互換の配列の文字列があったり、文字コードでハマったり、罠がいっぱい。

入力

  • std::cin >> 変数

  • std::get_line(std::cin, 変数)

多くの言語で標準入力には複数の書き方がありますね。C++ではオペレータ(算術演算+-*やシフト演算<</>>など)を上書きできるので、cinの右シフトでは右側のオペランドに標準入力を代入します。std::cout << varと左シフトで渡すと標準出力のバッファに値を流し込んで表示させます。

エラー

  • syntax error

  • logic error

  • runtime error

シンタックスエラーは構文が正しくないのでコンパイルできないものです。C++などセミコロン(;)で文が終わる言語で漏れていたり、かっこが嚙み合わなかったり、コンピューターを機械語(CPUの話す言葉)に翻訳できない場合に起きます。
他に例としてはmain関数の漏れや、未定義の変数や関数の利用とかそれの原因となるタイポ、セミコロン(文の終了マーク)のつけ忘れや、文字列や文字に絡むコーテーション関連の漏れや不正な利用があります。

ロジックエラー(論理エラー)は処理が正しく書けていない場合です。一見正しく動いていますが、結果が異なるようないわゆるバグの埋め込まれたコードで、クラッシュはしないけど正しく動かない場合です。

ランタイムエラー(実行時エラー)はコンパイルは通るものの、実行時に想定外のエラーが発生してプログラムがクラッシュするようなものです。例えば、ゼロ除算(0で割る)であったり、再帰呼び出しをし過ぎた場合のスタックオーバーフロー辺りはパッと思いつきます。

エラーのデバッグ

  • シンタックスエラー

    • コンパイラのエラーメッセージを読む

    • エラーの解決法をググる

  • 論理/実行時エラー

    • std::coutで変数の値を出力してみる

    • コードをトレースする

    • 怪しい箇所をコメントアウトする

解決法ですが、シンタックスエラーで英語のエラーが出ると反射的に逃げてしまう人も多いですが、たいてい理由が書いてあるので読みましょう。特にエラーの発生した行数を注意深く観察して少しずつ修正して変化あるか見ると良いと思います。

その他のエラーではログを埋め込んで何が起こってるか確認するのが基本ですね。面倒ですが簡単に見つからないのであればデバッグ機能で1つずつ処理を実行してどこでおかしくなったのかを確認するのも有効です。また、それでも厳しい場合、丸ごとコメントアウトして、動くか、から開始して少しずつコメントアウトを外して原因を特定したり、という事もよくやります。

コードはかなりの割合が例外処理に割かれたりもしますが、初心者が最初に身に付けたいのはどこがおかしいか"自分で"探る方法ですね。メンターとか同僚に聞くにしても原因の特定までできていれば解決方法の議論が容易です。中級以上はググっても解決法が見つからず公式リファレンスとかを読んだら解決することも結構ありますね。

Lecture 10

関数の回

分割

  • 問題をより小さな部分タスクに分解する

  • 手続き的分解を行う

    • レシピや清掃のような

  • インクレメンタルプログラミング

アルゴリズムとかでもそうですが、プログラムを書く場合、エンジニアリングの観点では大きな問題を取り扱う場合、小さな部分問題に分割します。システム開発でも、目的を達成するために機能単位に分割してアサインしたりすると思います。目的を小さく分割することで共通部品としても使えることになります。

関数

  • 関数とは?

    • 処理やサブルーチンを行うコードのブロック

  • 定義済みの関数

    • 標準ライブラリ

  • 関数の目的(3R)

    • Reduce(問題の縮小)

    • Reuse(コードの再利用)

    • Readability(コードの可読性)

関数とは同じ目的を持った処理の塊ですね。基本的には"関数名の示す目的のみをする処理の塊"です。ついでに何かやっちゃおうとか初学者はやりがちですが、基本的に単一目的を厳守しましょう。

また、3Rって聞いたことなかったんですが、より問題を小さく簡単にして、同じコードを繰り返し書くのでなく関数呼び出しで同じコードを使えるようにして、かつ、関数名が処理を示せばコードが読みやすくなる、という話ですね。基本的に英語で書きます。日本語名かっこ悪いです。わかれば良いのだけど、標準ライブラリや構文のキーワード(for/while)とか自体が英語をベースなので整合性を取っておいた方が良いでしょう。keisan()内でmin()呼んでたりしたら片言の人みたいです。

定義済み関数

  • sqrt()

  • pow()

  • abs()

  • rand()

引数省略していますが、sqrtは平方根、powは累乗、absは絶対値を返す関数ですね。randはランダムな数値を取得します。一般的に有用な処理は既に定義されていることが多いです。確認するときは標準ライブラリのリファレンスマニュアルを見ることが多いです。C++の数値系だと<cmath>のライブラリを確認したり。

手続き的分解

  • 関数定義

int main() {}

関数の中身空ですが、最初のintは関数の戻り値の型です。関数が評価(実行)されると戻り値に置き換わります。

int i = func(n, m);

とあって、func(n, m)の結果としての戻りが4なら

int i = 4;

に置き換わると考えてもらうとわかりやすいかも。
その際に型が合ってる必要があります。コンパイラは整合性を確認する必要があります。この場合は代入ですが、他のケースもあって、基本的に計算結果をどれだけのメモリのサイズで保持してどう解釈するか、そういった意味でも関数の戻りの型を明示するのは意味があると思います。コンパイル言語は整合性を予めチェックしてくれますが、Pythonとかインタープリタ言語は実行時に一致しないとエラーを吐いたりするので、型ヒントを使いたがる人もいますね。

mainは関数名ですね。ちょっとmain関数は特殊でプログラムのエントリーポイントになる関数の指定です。が、基本的には関数に、関数がメモリに置かれている住所(アドレス)に付けられた別名(エイリアス)のようなものです。そう考えると変数のように関数ポインタが使えるのにも納得できるかと。アドレスの場所は変わるし、それ覚えるの大変なので別名を付けちゃおう的な。

{}はスコープと言いますが、関数がどこからどこまでかを示します。言語によって関数のスコープの示し方は違ったりします。Pythonはインデントだったり。

ユーザー定義の関数(mainは必要不可欠)としては

void func_name() {}

mainの部分を好きな名前に変える感じですね。関数が目的としていることを関数名にするのが普通です。voidというのは戻り値のない関数で、実行するだけで何にも置き換わらないので代入の右辺には使えません。例えばprintXX()とか出力するだけの関数とかに使われたり、ここではまだクラスの話はでてきていないので知っている人以外聞き流して良いですが、クラス内部の変数に対する処理とか。

  • 関数呼び出し

関数定義に従って定義した名前で処理を呼び出します。

func_name();

呼び出しの際には型を書く必要はありません(代入なら変数前に書くけど)。関数名からコンピュータが関数のある場所(アドレス)がわかれば実行できます。

 #include  <iostream>

using namespace std;

int main() {
  cout << "2021年" << endl;
  cout << "春" << endl;
  cout << "夏" << endl;
  cout << "秋" << endl;
  cout << "冬" << endl;
  cout << "2022年" << endl;
  cout << "春" << endl;
  cout << "夏" << endl;
  cout << "秋" << endl;
  cout << "冬" << endl;
}

例えばこんなコードがあったとして、春夏秋冬の出力部分が繰り返されているので、関数でまとめられます。

 #include  <iostream>

using namespace std;

void print_season();

int main() {
  cout << "2021年" << endl;
  print_season();
  cout << "2022年" << endl;
  print_season();
}

void print_season() {
  cout << "春" << endl;
  cout << "夏" << endl;
  cout << "秋" << endl;
  cout << "冬" << endl;
}

一個目の void print_season(); はプロトタイプ宣言と呼ばれて、C++では関数が定義されていないと呼び出せなかったりするのでこれを消すとコンパイルエラーになります。プロトタイプ宣言するか、使う関数は先に定義する感じになります。

一般化

関数をさらに詳細化してしまうと逆にわかりにくくなったりします。

 #include  <iostream>

using namespace std;

void print_season();
void print_spring();
void print_summer();
void print_fall();
void print_winter();

int main() {
  cout << "2021年" << endl;
  print_season();
  cout << "2022年" << endl;
  print_season();
}

void print_season() {
  print_spring();
  print_summer();
  print_fall();
  print_winter();
}

void print_spring() {
  cout << "春" << endl;
}

void print_summer() {
  cout << "夏" << endl;
}

void print_fall() {
  cout << "秋" << endl;
}

void print_winter() {
  cout << "冬" << endl;
}

粒度は適切に決めないと逆に可読性が落ちるという例ですね。

スコープ

  • プログラムの部品で、宣言が有効になる範囲を示す

  • ローカル変数

    • 関数内で宣言されて関数内でだけ使える変数

  • 変数のローカライズ(局所化)

    • 最小のスコープで宣言して利用する

C++だと{}で囲まれた範囲が基本的にスコープですね。

void func() { // 1. start
  int total = 0;
  for (int i = 0; i < 10; ++i) {  // 2. start
    int twice = i * 2;
    total += twice;
  }  // 2. end
  std::cout << total << std::endl;
  {  // 3. start
    int tmp = total % 2;
    if (tmp) {  // 4. start
      std::cout << "total is odd" << std::endl;
    }  // 4. end
  }  // 3. end
}  // 1. end
    

totalは1で定義されているので{が噛み合う最後の}(1. end)まで使えます。
例えばtwiceは2の中で宣言されているので2. end以降では使えません(未定義エラーになる)。3のスコープは変数を局所化するために使っています。その後のコードがないので説明の為だけに囲んでますが。

あまりいいコードではないですが。

課題

前回の続きで、実装部分です。

  1. 前週のゲームを実装してください
    - 入力を促す箇所はわかりやすい説明が必要です
    - グローバル変数を使ってはいけません
    - ユーザーの誤った入力に対しても処理を適切に行ってください
      例えば整数の入力が期待される場合の小数の入力
    - 少なくとも2つの関数を定義して利用してください
    - ファイルのヘッダ等、適切なコメントをスタイルガイドに従って付けて下さい(例は省略)

  2. [Extra credit] 型の異なる入力もクラッシュしないように処理してください

  3. [Extra credit] ディーラー側をプレイするゲームを作ってみてください

あとがき

少しずつ実践的な話になりつつはありますが、まだ基礎の基礎ですね。
とはいえ基礎の基礎を見直すのは大事なことでもあります。見直して考えると見えていなかったものが見えてきたりもします。

こうすれば動く、だけで覚えてた人がなんでこう書くの?を理解するヒントになれば幸いです。コンピューターサイエンスはHowだけでなくWhyの理解も大事です。


この記事が気に入ったらサポートをしてみませんか?