コンテンツにスキップ

Week 2 (2021/10/14)

今日やること

  • 演算子
  • 制御
  • 配列
  • 関数
  • 宿題の提出方法

演算子

それでは演算子について見ていきましょう。演算子とは、+(足し算)のような、要素に何らかの処理を与える記号です。 算術演算子、代入演算子、インクリメントおよびデクリメント、関係演算子、論理演算子のそれぞれについて、以下に見ていきます。

算術演算子

算術演算子とは、足し算のように、二つの値に処理を施すものです。これは数学で使うものと同じ概念なので、わかりやすいと思います。

int a = 5;
int b = 2;
printf("a + b = %d\n", a + b); // 足し算。7
printf("a - b = %d\n", a - b); // 引き算。3
printf("a * b = %d\n", a * b); // 掛け算。10
printf("a / b = %d\n", a / b); // 割り算。2 (注意!)
printf("a %% b = %d\n", a % b); // 割ったあまり。1
【10/14 講義後に更新】%を表示するには%%とすべきでした。訂正しました。 上のように、足し算、引き算、掛け算、割り算、あまり、が定義されます。 注意として、整数同士の算術演算では、結果も整数になります。 そのため、割り算の結果が小数になる場合は、小数部分が切り捨てられてゼロとなります。 よって、5 / 2の結果は2.5であるべきですが、小数が切り捨てられて2になります。 これは四捨五入ではなく切り捨てであることに注意しましょう。

また、小数と整数の演算の結果は小数になります

printf("a * 0.1 = %f\n", a * 0.1);  // 整数 * 小数 = 小数。0.5

まとめると以下のようになります。注意しましょう。

printf("3 / 2 = %d\n", 3 / 2);         // 整数の割り算なので切り捨て。1
printf("3.0 / 2 = %f\n", 3.0 / 2);     // 小数 / 整数なので小数。 1.5
printf("3.0 / 2.0 = %f\n", 3.0 / 2.0); // 小数。 1.5
ところで、上の計算における3.0 / 2の「3.0」という数字の型は一体なんなのでしょうか? この「3.0」だけでは、これがdoubleかfloatかわからないですよね。 C言語ではこのように直接表記された小数はdoubleであると決められています。 このような直接表記された値をリテラルと言います。 もし明示的にfloatのリテラルを使いたければ、3.0f / 2などとします。 また、整数リテラルは、intで表現できる整数ならばintになります。

注意として、「代入式そのもの」も値を返します。それは以下のように、代入された値になります。 これはちょっと不思議かもしれませんが、覚えておいてください。

int a;
printf("%d\n", a = 2);  // 「代入式そのもの」は、代入された値を返す。ここでは2

ところで、コード中でスラッシュが二個書かれているものはコメントといいます。 コメントで指定された部分は、コンパイラが無視します。なので、コードの説明などを書くことができます。 日本語を使っても構いません。 コメントには以下の二つの形式があります。

// 一行の形式。ダブルスラッシュ二つの右側はコメントになります。

/*
複数行の形式。「スラッシュ + アスタリスク」で初めて、
「アスタリスク+スラッシュ」で終る部分までがコメントになります。
*/

代入演算子とインクリメン/デクリメント

次に、代入演算子を見ていきましょう。

int a = 0;
a = a + 2;  // aは2になる
a = a + 2;  // aは4になる
上のa = a + 2は、「aに2を足したものをaに代入する」という意味です。なので、もともとのaが0の場合は、 aは2になります。これをもう一度繰り返すと4になります。 これは数学のイコールとは全く違う動作なので、注意してください。 上記のように自分自身に操作を施す処理は、次のようにも書けます。
a += 2;   // a = a + 2と同じ意味
これを代入演算子と言います。これは、上で説明した全ての算術演算子でも実行できます。
a -= 3;   // a = a - 3と同じ意味

また、1の加算には、インクリメント(++)という特殊記法が用意されています。この記号は次のように二通りの書き方があります。

int a = 0;
a++;  // a += 1 と同じ。aは1になる。
++a;  // a += 1 と同じ。aは2になる。
ここで、++aa++の違いは何でしょうか?実は「++a」の結果を変数に代入すると、1を追加した後の値が返ります。 「a++」の場合は1が追加される前のものが返ります。

int x = 0, y = 0, a = 0, b = 0;
x = ++a;  // xは1になる
y = b++;  // yは0になる
printf("x=%d, y=%d, a=%d, b=%d\n", x, y, a, b); // x=1, y=0, a=1, b=1

加算と同様に減算のデクリメント(--)も定義されています。

--a; // a -= 1と同じ
a--; // a -= 1と同じ

関係演算子と論理演算子

二つの要素を比較する演算を関係演算子(比較演算子)と呼びます。

int a = 2, b = 3;
printf("a == b     %d\n", a == b);  // aとbは等しいか?  正しくない:0
printf("a != b     %d\n", a != b);  // aとbは異なるか?  正しい:1
printf("a < b      %d\n", a < b);   // aはbより小さいか? 正しい:1
printf("a <= b     %d\n", a <= b);  // aはbより小さいor等しいか? 正しい:1
a == bという表記は、abが等しければ1を返します。そうでなければ0を返します。 すなわち、数学における「\(=\)」です。 このように、二つの要素の関係に応じて1か0を返すものが関係演算子です。 a != bは、abが異なれば1を返し、等しければ0を返します。数学で言うところの「\(\ne\)」です。すなわち、==の反対の操作です。

a < bは、数学の不等号と同じく、abより小さければ1を返し、そうでなければ0を返します。 a <= bは、数学における「\(\le\)」を意味します。すなわち、abより小さいか、あるいはabが等しければ1を返します。そうでなければ0を返します。

不等号が逆のバージョンである、a > b および a >= bも同様に定義されます。

このように、条件によって真 (0ではない)偽 (0)かを返すことを、真偽値を返すという風に言います。 c言語における「真」とは、「0ではない」という意味ですので覚えておいてください。 つまり、「3」や「5834」や「-25」も「真」です。

注意として、値の一致判定が含まれる==, !=, <=, >=は、小数に対しては使わないようにしましょう。 浮動小数は数を厳密に表現しているわけではないので、一致していると思っていても一致しない、ということがあるからです。

次に、論理演算子を見てみましょう。

int a = 10;
printf("%d\n", (3 < a) && (a <= 5));  // 「3 < a」かつ「a <= 5」かどうか。 違うので、0
printf("%d\n", (a == 10) || (a <= 100));  // 「a == 10」または「a == 100」かどうか。 正しいので、1
printf("%d\n", !(a == 10));  // 「a == 100」であるかどうか、の否定。a==10は真なので、その否定で、偽 (0)
&&および||は、その両サイドに二つの式を取ります。 &&は「かつ」を表します。論理学で言うところの「\(\cap\)」です。 二つの式の両方が真である場合に、1を返します。そうでなければ、0を返します。

||は「または」を表します。「\(\cup\)」です。二つの式のうち片方あるいは両方が真である場合に、1を返します。 ここで注意として、||の左側が真である場合は、その場で真を返します。右側を評価しません。 これは、右側が真だろうが偽だろうがどうせ真になるからです。よって、例えば右側に計算時間がかかる処理が 書いてあったとしても、その計算は発生しません。

!は、否定を表します。右側に要素をとります。その右側が真であれば偽を、偽であれば真を返します。

演算子の優先順位

演算子が複数ある場合に実行する順番は、規則により定められます。 例えば、*+より優先度が高いです。これは数学と同じです。

int a = 1 + 2 * 3; // 「2 * 3」を計算し、「1」を足す。7になる。
ここで、足し算の部分を先に計算してほしいときは、カッコをつけます。これも数学と同じです。
int a = (1 + 2) * 3; // 「1 + 2」を計算し、「3」を掛ける。9になる。
以下のようなものは注意が必要です。
int a = 2 == 3;  // 0になる
==のほうが=より優先順位が高いので、まず2 == 3が計算され、それは0になります。それがaに代入されるため、aは0になります。 これは、より明示的に以下のように書くこともできます。
int a = (2 == 3);  // 0になる

次に、優先度が同じ場について述べます。 優先度が同じ場合に、左から実行する(左結合)か、右から実行する(右結合)かは、演算子によって異なります。

int a = 3 + 2 - 1; // 左から実行: ((3 + 2) - 1)
printf("%d\n", a); // 4

int x, y, z;
x = y = z = 3;  // 右から実行:(x = (y = (z = 3)))
printf("%d %d %d\n", x, y, z);  // 3 3 3
ここで、+-の優先度は同じです。そして、この二つは「左から実行」のルールです。なので、 まず3 + 2が実行され5が出来ます。次に5 - 1が実行され4となります。 一方、=は「右から実行」のルールです。そのため、まずz = 3が実行されます。 ここで、「代入式そのもの」は、代入された値になることを思いだしましょう。なので、「z = 3」は3になります。 そして、それを用いて次はy = 3が実行されます。

演算子の優先度および実行順は、こちらを参考にしてください。 式が複雑になる場合は、カッコを使って整理することをオススメします。

やってみよう(30分)

適当なmain.cを作り、上記を写経して実行してみましょう。

クイズ

  1. 以下は1.5になりません。何故でしょうか?

    double a = 3 / 2;
    printf("3 / 2 = %f\n", a);
    

  2. 以下でifは何になるでしょうか?

    int i;
    float f;
    f = i = 23.5f;
    

  3. c言語では、a < b < c といった書き方は出来ますが、意図した結果になりません。例えば-20 < -10 < -2 は真になりそうですが、なりません。何故でしょうか?

  4. BMI指数を計算するプログラムを書いてみましょう。体重\(w\) kg、身長 \(h\) mの人のBMI指数は \(\frac{w}{h^2}\)で計算されます。 たとえば身長160cm、体重50kgの場合は\(50 / 1.6 / 1.6 \sim 19.5\)です。

  5. int year = 2020;がうるう年の場合を判定する条件式を考えてみましょう。 ここでうるう年とは、(1) 4で割り切れる (2) 100では割れない、の二つの条件を満たすものです。

    int is_leap = (year % 4 == 0 && year % 100 != 0);   // yearがうるう年のときは1になる
    
    しかし、実は例外として、yearが400で割り切れるときはやはりうるう年です。上の条件式をどう変更すればいよいでしょうか?

答え
  1. 右辺の3 / 2は整数の割り算なので、その結果は整数の1になります。それを小数のaに代入するため、aは1.0になります。
  2. iは23,fも23になります(23.5ではありません) ここで、=は右結合なので、式は次の順番で処理されます:f = (i = 23.5f) そのため、まず 小数をint型変数に代入することで切り捨てが起こります。そして、代入式は代入された値がその式の値にもなるため、f = 23となります。
  3. <は左結合なので、式は次の順番で処理されます:(-20 < -10) < -2. ここで、-20 < -10 は真なので、1になります。よって、 1 < -2が評価されます。これは偽なので、最終的に0が返ります。
  4. 例えば以下のようになります。
    double w = 50, h = 1.6;
    printf("Your BMI is %f\n", w / h / h);
    
  5. int is_leap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
    
    ここで最後のyear % 400 == 0のカッコは無くてもOKです。

制御

それでは次に制御文について学んでいきましょう。制御を理解することで、処理を分岐させたり場合分けしたり出来ます。

まず用語を定義しましょう。 x = 0や、i++や、printf("xxx")などはと呼ばれます。下記のように、その末尾にセミコロンを置くと、それはになります。

// 「式;」を「文」と呼ぶ
x = 0;
i++;
printf("xxx");
Cでは、セミコロンは文の終了を表します。 カッコ({および})を使うと、文をまとめることができます。これをブロックと呼びます。
{
    // 複数の文をまとめてブロックにする
    a = 3;
    b += 2;
    int c = 123;  // ブロック内で宣言した変数はブロックの内側でしか使えない
}
// カッコの最後にはセミコロンが無いことに注意
// ブロックを抜けたあとではcにはアクセスできない
ブロックは文法的には単一の文と同等に扱われます。またブロックの終わりを示す右カッコの後ろにはセミコロンはつけません。 ブロックの内側で作った変数は、ブロックの外側からは触ることが出来ません。

if

それではまずはif文を見ていきましょう。if文は分岐を表します。

int n = 10;
if (n % 2 == 0)
{
    printf("n is even\n");
}
else
{
    printf("n is odd\n");
}
上の中で、if (式)の「式」の部分に条件を書きます。この条件が真であれば直後のブロックにすすみます。もし偽であれば、その次のelseのブロックに進みます。 これにより、条件によって処理を分岐することができます。 この例では、「nを2で割ったあまりが0に等しいか」どうかを条件とします。もしそうであれば、nは偶数です。 elseのブロックで何もしない場合は、elseのブロックは省略することもできます。 また、if文のように新たにブロックが発生するときは、インデント(字下げ)を行うとよいです。こうすることで可読性が上がります。

また、上記は次のようにも書けます

// カッコで改行しないパターン
if (n % 2 == 0) {
    printf("n is even\n");
} else {
    printf("n is odd\n");
}
これはK&Rで使われている表記スタイルです。カッコ({)は改行してもしなくてもOKです。 このスタイルでも最初のスタイルでもどちらをとっても構いませんが、プログラム全体を通して統一したスタイルにすると良いです。

さらに、ブロックの中身が一文の場合は、以下のようにかっこを取り去ることもできます(ブロックは単一の文と文法的には同じなので)

// カッコを取り去ったパターン
if (n % 2 == 0)
    printf("n is even\n");
else
    printf("n is odd\n");
さらに、次のように改行を省略することもできます。
if (n % 2 == 0) printf("n is even\n");
else printf("n is odd\n");
ごく短い記述などではこのようにワンラインで書いたほうがわかりやすい 場合もあるのですが、可読性が下がりバグの元になる可能性のほうが高いです。 例えif文の内側が一行でも常にカッコをつけることをオススメします。 下で出てくる、「ネストしたif」の場合に、特に間違えやすくなります。

複数の分岐を行いたいときは、以下のようにif, elseをつなげて書きます。

double score = 0.7;

if (score < 0.3) {
    printf("Your grade is C\n");
} else if (score < 0.8) {
    printf("Your grade is B\n");
} else {
    printf("Your grade is A\n");
}
ここでscoreを色々変えてみると、結果が3通りに分岐することがわかります。 このように、最後のelseは、「それ以外の場合」という意味になります。

if, elseは入れ子にすることもできます。

int result;
int flag = 1;
int val1 = 10;
int val2 = 2;
if (flag == 0) {
    result = -1;
} else {
    if (val1 < val2) {
        result = val1;
    } else {
        result = val2;
    }
} 
上では、もしflagがたっていない(flag == 0)ならば処理を強制終了しresultに-1を代入します。 もしそうではない(flagが立っている)ならば、val1val2のうち小さいほうをresultに代入する、という意味になりますね。

for

次にfor文について見ていきましょう。forは繰り返しを表します。

int i;
for (i = 0; i < 5; ++i) {
    printf("%d\n", i);
}
上のコードを実行すると、0, 1, 2, 3, 4の数を表示するはずです。ここでiは繰り返しをコントロールするカウンタ変数などと呼ばれます。 for文は次のような構造になっています。式の間はセミコロンである点に注意してください。
for (式1; 式2; 式3)
    
これは次のような順番で実行されます。

  • 式1を実行(これはiのようなカウンタの変数の初期化を行うことが多いです)
  • 式2を実行(これはループを続ける条件です)
    • 式2が真の場合
      • を実行(ここが処理の本体です)
      • 式3を実行(ループの更新です。カウンタを増加させることが多いです)
      • 式2を実行」に戻る
    • 式2が偽の場合
      • 終了してfor文を抜ける

さて、上で述べたfor文は、次のようにも書けます

for (int i = 0; i < 5; ++i) {
    printf("%d\n", i);
}
ここでは、カウンタ変数であるint iをfor文の内側の初期化の位置で宣言しています。 この表記により、int iはfor文の内側でのみ有効になります。 iはforの外側では使わないため、こちらのほうがより安全なコードであると言えます。 この表記はC99というC言語の規格(バージョン)から使えるようになりました。Cの規格について本講義では立ち入らないのですが、 ようは最近のコンパイラであれば使える機能だと思ってください。 今後、本講義ではC99の機能を説明なしに使います。 ちなみに、K&R本や、古い記述では、int iはコードの最初に宣言しろ、 などと書かれていることがあるのですが、カウンタ変数はforの内側でのみ有効にするほうが安全でオススメです。

カウンタ変数や各種条件はなんでもいいので、上記の0, 1, 2, 3, 4を出力するコードは以下のようにも書けます

// ループを続ける条件を「<」から「<=」にする
for (int i = 0; i <= 4; ++i) {
    printf("%d\n", i);
}
// カウンタを1から始め、出力時に-1する
for (int i = 1; i < 6; ++i) {
    printf("%d\n", i - 1);
}
// カウンタを大きい数字から始め、減らしていく
for (int i = 5; 0 < i; --i) {
    printf("%d\n", 5 - i);
}
ですが、最初に述べた、「初期化はi=0」、「ループ条件をi < N」、「ループ更新はi++」が一番基本となるN回繰り返しのフォームですので、 わからくなったらこの基本フォームに立ち返るとよいと思います。

while

次にwhileについて勉強します。以下は、0, 1, 2, 3, 4を出力するものです。

int i = 0;
while (i < 5) {
    printf("%d\n", i);
    ++i;
}
whileの制御手順は次のようになります。
while ()
    
まずが計算されます。が真であれば、を実行します。そして、の計算に戻ります。

ちなみに、for文はwhile文で書き直すことができます。

for (式1; 式2; 式3)
    
上記のfor文は、下記のwhile文と同じです。
式1;
while (式2) {
    
    式3;
}
ループを構成するとき、多くの場合for文でもwhile文でも書けます。 while文は、繰り返し回数がわからない状況などで有効です。繰り返し回数がはっきりわかる場合は、for文を使うほうがわかりやすい場面が多いと思います。

また、do-while構文というものも存在します。

int j = 0;
do {
    printf("%d\n", j);
    ++j;
} while (j < 5);
これは、最初に必ずdoのブロックを一回実行し、そのあとwhileにてループ条件を指示するというものです。do-whileはあまり見ることは無いかもしれません。

ちなみに、forやwhileを使うと、永遠にループし続ける無限ループを作ることもできます。これは、プログラムの外部から何らかの入力が入ってくるまで 待っているときなどに使います。覚えておきましょう。

// whileを使った無限ループ
while (1) {
    sleep(1);  // 1秒間何もしないという関数
    printf("waiting\n");
}

// forを使った無限ループ
for (;;) {
    sleep(1);
    printf("waiting\n");
}
上記のコードを実行すると、実行が終わらなくなります。そのときは強制終了のコマンドであるCtrl+C (ctrlキーを押しながらCを押す) をターミナルに入力して強制終了させましょう。 ちなみにこの強制終了コマンドは非常によく使うので、必ず覚えておきましょう。

breakとcontinue

ループの途中で、条件に応じてループの脱出を制御するために、breakとcontinueを使います。

for (int n = 0; n < 5; ++n) {
    if (n == 3) {
        break;
    }
    printf("%d\n", n);
}
この出力は0, 1, 2になります。このfor文はもともとnが0, 1, 2, 3, 4となるループです。 ここで、n == 3のときはbreakを行います。これにより、ループをその場で終了してfor文を抜けます。 よって、3と4は出力されません。このように、ループ中のif文の中でbreakを行うと、ループを強制終了します。

一方で、continueを使うと一度だけループをスキップすることもできます。上のコード中で breakをcontinueに変えたものが下になります。

for (int n = 0; n < 5; ++n) {
    if (n == 3) {
        continue;
    }
    printf("%d\n", n);
}
この出力は0, 1, 2, 4になります。ループ中でcontinueに辿り着くと、「その一周分だけ」スキップして、 ループ処理を継続します。ここでは、n == 3のときにcontinueに辿り着くため、そこでその一周を終了します。 よって、printfは呼ばれません。次の一周ではn == 4となり、通常通りprintfが呼ばれます。

上記二種類のループ制御は重要ですので、覚えておいてください。

switch

ある値について、様々な分岐を行いたいときはswitchを使います。

int school_year = 2;  // 大学2年生
switch (school_year)
{
    case 1:
    case 2:
        printf("You are studying at Komaba\n");
        break;
    case 3:
    case 4:
        printf("You are studying at Hongo\n");
        break;
    default:
        printf("Error\n");
        break;
}
ここでは、school_yearの値が1か2のときは駒場だと出力し、3か4のときは本郷だと出力します。それ以外の値がきたときはエラーです。 switch文は少々ややこしいのですが、下記のような構造になっています
switch () {
    case 定数式: 
    case 定数式: 
    default: 
}
各caseは定数式で名前がつけられています。式が定数式に一致するかどうか検証し、それに応じて分岐します。 ここで、あるcaseに進み文が実行されたとき、そこで終わりなのではなく、その次のcase式に自動的に 進みます。上の例でいうと、case1に辿り着いたあとは、その直後の式が無いため、次のcase2に移り、printfに辿り着きます。 明示的にswitch文を脱出するときはbreak;を入力します。 また、どのcaseにも一致しなかった場合は、defaultに移動します。defaultは無くてもいいです。

switchはややこしいうえに、下記のようにif文で書き直せます。なので、自分で書く場合は多くの場合if文にするほうがバグが減ると思います。 一方で、人の書いたswitch文は読める必要があります。

if (school_year == 1 || school_year == 2) {
    printf("You are studying at Komaba\n");
} else if (school_year == 3 || school_year == 4) {
    printf("You are studying at Hongo\n");
} else {
    printf("Error\n");
}

goto

制御の最後に、gotoがあります。gotoは以下のように、コード中の好きな場所にラベルを張り、そこに強制的に移動できる機能です。

for (int n = 0; n < 5; ++n) {
    if (n == 3) {
        goto hoge;
    }
    printf("%d\n", n);
}
hoge:
printf("done\n");
この結果は0, 1, 2, doneになります。 gotoは強制的にジャンプするためバグの温床になります。使ってはいけません。K&Rが書かれた時点で既に使うなと言われています。

やってみよう(30分)

  • 適当なmain.cを作り、上記を写経して実行してみましょう。
  • if (式)では、式が真のときにブロックを実行します。よって、int flag=1としたときに、
    if (flag != 0) { ... }
    if (flag) { ... } 
    
    は同じ意味になります。写経したコードの一部を変更して確かめてみましょう。これは、「フラグが立っているときは実行する」という意味になります。 一方で、「フラグが立っていないときに実行する」という意味の、下記の二通りの表記もまた、同じです。
    if (flag == 0) { ... }
    if (!flag) { ... }
    
    このように短くかけることを覚えておいてください。実際にどちらの表記を使うかどうかは、場面によります。 上記のようにコードから明らかな場合は、短いほうが良いでしょう。複雑な場合は、可読性が高いほうにするとよいでしょう。
  • 二重ループは以下のように書けます。これを書いてみて、どうしてこのような挙動になるか、考えてみましょう。
    for (int i = 0; i < 3; ++i) {
        for (int j = 0; j < 4; ++j) {
            printf("i: %d, j: %d\n", i, j);
        }
    }
    
    結果:
    i: 0, j: 0
    i: 0, j: 1
    i: 0, j: 2
    i: 0, j: 3
    i: 1, j: 0
    i: 1, j: 1
    i: 1, j: 2
    i: 1, j: 3
    i: 2, j: 0
    i: 2, j: 1
    i: 2, j: 2
    i: 2, j: 3
    

クイズ

  1. 以下は、ifのブロック、elseのブロック、どちらに進むでしょうか?そして、それは何故ですか?

    if (3) {
        printf("Here?\n");
    } else {
        printf("or here?\n");
    }
    

  2. ループを使って、100未満の「2の冪」を表示するコードを書いてみましょう。ここで出力は数字の間にコンマ・スペースを挟み、最後に1回だけ改行するようにしましょう。つまり、

    $ gcc main.c
    $ ./a.out
    1, 2, 4, 8, 16, 32, 64, 
    $ 
    
    このようにするにはどうすればいいでしょうか?(最後の一回の改行を忘れないでください) これをfor文、while文両方で書いてみましょう。

答え
  1. ifのほうに進む。if(式)に対し、式が真であればそのブロックに進む。c言語では真とは「0ではない値」のことなので、3であってもそれは真になる。
  2. 色々な書き方があります。for文の場合の例:
    for (int n = 1; n < 100; n *= 2) {
        printf("%d, ", n);
    }
    printf("\n");
    
    while文の場合の例:
    int n = 1;
    while (n < 100) {
        printf("%d, ", n);
        n *= 2;
    }
    printf("\n");
    

配列

次に配列について勉強しましょう。配列とは、変数を複数個並べたものです。

配列の基本

配列を作り、値を代入し、表示する例は次になります。

int a[3];  // int型が3つ並んだ配列

// 値の代入
a[0] = 12;  // 配列の一つ目の要素に12を代入。インデックス(添え字)は0スタートであることに注意
a[1] = 3;
a[2] = 5;   // 最後。要素が3つのとき、a[2]が最後であることに注意

for (int n = 0; n < 3; ++n) {
    printf("a[%d] is %d\n", n, a[n]);     // 配列の中身の表示
}
この結果は以下のようになります。
a[0] is 12
a[1] is 3
a[2] is 5
配列は、上記のように型 変数名[要素数]という形式で作ります。 その要素にアクセスするには、変数名[インデックス]という記述を行います。 これを使って、値を代入することも、参照することもできます。 c言語やpythonでは一つ目の要素のインデックスは0であることを覚えておきましょう。 最後の要素は変数名[要素数-1]でアクセスするので注意しましょう。 ちなみに、FortranやMATLABやJuliaといった言語ではインデックスは1から始まります。

また、配列の初期化には次のような特別な表記も用意されています。

// 上の代入と同じ結果になる。最後のセミコロンを忘れずに。
int a[3] = {12, 3, 5};

// さらに省略。右辺から要素数が決定されるので、左辺で陽に「3」と書かなくてもいい。
int a[] = {12, 3, 5};

ここでメモリ上に配列がどうレイアウトされるかを見てみましょう。 配列を作ると、メモリ上で連続して要素が確保されます。その状況は下図のようになります。

ここでは、week1で習った通り、各要素にはアドレスが存在することも思い出しておきましょう。 intは一つ4バイトなので、ここではa[0]の先頭アドレスとa[1]の先頭アドレスは必ず4つ増加したものになります。 以下のコードで、そのことを直接確認できます。

for (int n = 0; n < 3; ++n) {
    printf("The address of a[%d] is %p\n", n, &(a[n]));  // &a[n] とも書けます
}
この結果は例えば以下のようになります。
The address of a[0] is 0x7ffffc7579ec
The address of a[1] is 0x7ffffc7579f0
The address of a[2] is 0x7ffffc7579f4

多次元配列

配列を入れ子にすることで、多次元版の配列を考えることができます。以下は二次元配列の例です。

int mat[2][3] = {
    {32,  5, 76},
    { 1, 12,  8}
};

for(int y = 0; y < 2; ++y) {
    for (int x = 0; x < 3; ++x) {
        printf("%d, ", mat[y][x]);
    }
    printf("\n");
}
二次元の配列を作るときは、mat[2][3]のように、変数名[縦方向要素数][横方向要素数]とします。 ここでは縦が2,横が3の行列\(M \in \mathbb{R}^{2\times3}\)を作ったようなものだと考えてください。 その要素は、mat[縦インデクス][横インデクス]の形でアクセスできます。すなわち、
printf("a[0][0] : %d\n", mat[0][0]);  // a[0][0] : 32
printf("a[1][2] : %d\n", mat[1][2]);  // a[1][2] : 8
です。

二次元配列のメンタルモデルとメモリ上の配置は以下になります。 頭の中では、二次元の行列のようなものだと考えてください。 実際には、メモリ上で、行ごとに値を並べていっています。 すなわち、多次元配列の一番後ろの軸が、実際にメモリ上で連続する軸になります。

やってみよう(20分)

  • 適当なmain.cを作り、上記を写経して実行してみましょう。
  • 三次元配列も同様に定義できます。int a[2][3][4] などとして作ってみましょう。
  • 配列の初期化の表記で、要素数以下を指定すると残りは0で埋められます。つまり、int a[5] = {1, 1}とするとint a[5] = {1, 1, 0, 0, 0};は同じ結果になります。 これを利用して、次のように書くと配列を0で初期化出来たりもします。int a[5] = {0}; これらを実行してみましょう。

クイズ

  1. 以下のコードは何が出力されるでしょうか?そして、それは何故ですか?
    float a[3] = {0.5, 0.3, 0.8};
    printf("%f\n", a[3]);
    
答え
  1. 未定義の値が出力されます。理由は添え字ミスです。要素が3つの配列のときは、最後の要素はa[2]です。なので、a[3]は定義されていません。 ちなみに、皆さんの環境次第では、このコードの実行結果はエラーにならず何か滅茶苦茶な値が表示されたかもしれません。 実は、a[3]とすることで、配列が宣言されているメモリ領域の次のもう一個分を無理やり呼んでいることになるのです。 そのため、滅茶苦茶な値になります。一方で、このように間違っていてもエラーが出ない場合はバグの元になります。なので、配列の要素数チェックはしっかりしておきましょう。

関数

さて、次は関数について学んでいきましょう。

関数の基本

これまではmain() { ... }に全ての処理を書いてきました。 実際にプログラムを書くときは、よく使う部分をまとめて関数として分離します。まずは例を見てみましょう。 以下をfunc.cとしましょう。

#include <stdio.h>

double absolute(double src) {
    double dst;
    if (0 < src) {
        dst = src;
    } else {
        dst = -src;
    }
    return dst;
}

int main () {
    double v = -0.4;
    double result = absolute(v);

    printf("absolute value of %f is %f\n", v, result);  
    printf("absolute value of %f is %f\n", 0.5, absolute(0.5));
}
この結果は以下になります
absolute value of -0.400000 is 0.400000
absolute value of 0.500000 is 0.500000
ここでは、小数を受け取りその絶対値を返す、absoluteという関数を定義しました。 関数は、何らかの情報を入力として受け取り、何らかの情報を出力します。以下のような構造になっています。
返り値の型 関数名(引数1の型 引数1の変数名, 引数2の型 引数2の変数名, ...) {
    関数本体の処理
    return 返り値;
}
ここでは、absolute関数はsrcというdouble型の引数(パラメータ)を入力として受け取ります。 その後関数内部で何らかの処理を行い、double型のdstという戻り値(返り値)を返します。 この出力の型は、変数名の左のdoubleで指定されます。 ここでは、srcが負の値の場合は正にしていることがわかります。 その結果を、return dst という部分で、出力しています。 この例では引数は1つですが、複数あっても大丈夫です。戻り値は必ず1つです。

mainの中を見ると、関数がどのように呼ばれているかわかると思います。 引数としてvを与え、結果をresultに代入しています。 注意として、関数宣言で使った引数名(src)と、本文中で引数に与える変数(v)の名前は違ってもOKです。 二つ目のprintfにあるように、値を直接代入しても大丈夫です。

このように、プログラム中で何度も使ったり再利用できるパーツがあるときは、それを関数という形で分離することができます。 これにより、処理を分割して整備することができます。

それでは別の例も見てみましょう。

// 引数が複数ある例
double cube(double w, double h, double d) {
    return w * h * d;  // このように、戻り値は明示的に変数にせずに、直接計算結果を返してもよい。
}

// 出力が無い例。その場合、voidと指定します
void display(int a) {
    printf("The input is %d\n", a);
    return; // 何も返さない。だが、ここで関数を抜ける。
    printf("after return\n");  // ここには到達しない
}

// 入力も出力もない例
void hello(void) {  // void hello() だけでもよい
    printf("hello!\n");
    // 最後のreturnは省略してもいい
}

関数の引数は値渡し

注意として、関数に引数を与えるということは、値をコピーするということです。 これを値渡しと言います。 よって、それによって関数の呼び出し元の値が変わることはありません。

void plus_one(int a) {
    a++;
}

int main () {
    int b = 10;
    plus_one(b);
    printf("%d\n", b); // 10のまま
}
上のコードでは、bplus_one関数に渡されます。 そして、その中で値がインクリメントされます。 しかし、関数に渡されるのはbそのものではなくbのコピーです。 bのコピーがaです。なので、aにどのような変更を加えても、 元のbの値は変わりません。

変数のスコープ

次に、変数のスコープについて学びましょう。変数を作ったとき、その変数にアクセスできる範囲をスコープと言います。 今日の「制御」で習った通り、ブロックの内側で作った変数には外側からアクセスすることはできません。 これは関数に対しても同じです。 関数の最初の例で出てきたabsolute関数の内側のdouble dstは、main関数からアクセスすることは出来ません。 同様に、main関数内で作った変数は、引数に与えない限りは他の関数からアクセスすることはできません。 このように、関数の内側がスコープである変数をローカル変数と言います。

一方で、プログラム全体からアクセス可能な変数を作ることもできます。これをグローバル変数 と言います。

#include <stdio.h>

int a = 0; // グローバル変数

void f() {
    ++a;
}

int main() {
    printf("%d\n", a); // 0
    f();
    printf("%d\n", a); // 1  fによりaが更新される
}
グローバル変数 int a は、上記のように、main関数の外側で宣言します。 最初のprintfのように、aにはmain関数の内側からアクセスすることができます。 また、f関数のように、普通の関数の内側からアクセスすることもできます。

注意として、関数内であらためてグローバル変数と同じ名前のローカル変数を作った場合はそちらが優先されます

#include <stdio.h>

int a = 0; // グローバル変数

void f() {
    int a = 3;  // グローバル変数と同じ名前のローカル変数
    ++a;
}

int main() {
    printf("%d\n", a); // 0
    f();
    printf("%d\n", a); // 0  fによりaが更新されない
}

コラム

コードを写経する際に、同じ変数名が出てくると写経しづらいと思います。

int main() {
    // 写経1
    int a = 123;
    // ここからaに関する処理。。。

    // 写経2
    int a = 345;   // これはダメ!同じ変数名で二個変数は作れない
    // ここからaに関する処理。。。
}
このとき、ブロックを明示的に作ると変数名が衝突しないので、書きやすいと思います。
int main() {
    {
        // 写経1
        int a = 123;
        // ここにaに関する処理。。。
    }

    {
        // 写経2
        int a = 345;   // OK!
        // ここにaに関する処理。。。
    }
}
これは、最初のブロックで作ったaのスコープはそのブロックの内側のみなので、 ブロックを抜けるときに消えるからです。

プロトタイプ宣言

次はプロトタイプ宣言について学びましょう。次の例を見てみましょう。

#include <stdio.h>

int main() {
    f(10, 0.5);  // ダメ。まだ宣言されていない関数を呼び出している
}

float f(int a, float b) {
    printf("%d\n", a);
    return b * 10;
}
上のコードでは、関数fmainより下に宣言・定義しています。そして、mainの内側でfを呼んでいます。 これはエラーになります。 C言語では、上から下に順番に処理が進みます。よって、main中でfを呼んだ時点ではまだfが 作られていません。そのため、エラーとなります。

これを解決するために、「fという関数を宣言します。その定義(本体)は別のところに書かれます」と 述べることをプロトタイプ宣言と言い、以下のように書きます。

#include <stdio.h>

// プロトタイプ宣言
float f(int, float);  // float f(int a, float b) でもいい

int main() {
    f(10, 0.5);
}

// 関数の定義
float f(int a, float b) {
    printf("%d\n", a);
    return b * 10;
}
ここでは、関数の本体ではなく、名前と使い引数の型だけを最初に宣言します。 これにより、コンパイラが、「fという関数が後から定義されるらしい。よってmain内でfを使ってもいい」 と判断してくれるため、上記は期待した動作になります。 注意として、この宣言の段階では明示的に変数名を書かなくても大丈夫です。すなわち、int aと書いてもいいですし、int だけでもいいです。

ところで、なぜこんなめんどくさいことを覚える必要があるのでしょうか? ソフト1の範囲では全てのコードを一つのファイルに書きますが、実際のエンジニアリング作業では、 複数のファイルを用意して、それらを一緒にコンパイルして大きなプログラムを作ります。 その際に、上記のような手続きの知識が必要になります。 これについてはソフト2で学びます。

メイン関数

また、実は、本文を書いているmainも、関数です。これはプログラムを実行したときに呼ばれる特別な関数です。 main関数を省略せずに書くと以下のようになります。

int main(int argc, char *argv[]) {
    // 本体
    return 0;
}
これが基本パターンです。引数については次回以降に説明します。とりあえず省略していても大丈夫です。 本来は最後にreturn 0を着けます。0を返すということは、正常終了したということを意味します。 この型がintなので、一番最初にもintを付けます。

やってみよう(30分)

適当なmain.cを作り、上記を写経して実行してみましょう。

クイズ

  1. 二つのfloatを受け取り、その平均を返す関数averageを書いてみましょう。
  2. int aint bを受け取り、\(a^b\) を返す関数powerを書いてみましょう。
答え
  1. 例えば、
    float average(float a, float b) {
        return (a + b) / 2.0;
    }
    
  2. 例えば以下。
    int power(int a, int b) {
        int ret = 1;
        for (int i = 0; i < b; ++i) {
            ret *= a;
        }
        return ret;
    }
    

宿題

それではここで、GitHub Classroomを用いた宿題提出の方法を紹介します。 今日いるみなさんはすでに全員GitHubアカウントを作り、かつそれをITC-LMSから松井まで提出しているはずです。もしまだなら松井まで個別に連絡してください。 宿題の提出方法について、宿題の提出方法 に従ってください。

それでは今週の宿題です。

  • 締切は次の授業の前日の深夜23:59までです(今回の場合、2021/10/20, 23:59)
  • 宿題リンクをslackで配付します。このリンクは公開しないでください。

week2_1

まずは練習です。宿題リポジトリを開くと、main.cの中身が以下のようになっています。

#include <stdio.h>

int main() {
    printf("hell world\n");
    return 0;
}
これを編集し、実行時の出力が以下のようになるようにしてください。
$ gcc main.c 
$ ./a.out
Hello World!

week2_2

0以上9以下の整数が複数個与えられたとき、そのヒストグラムを作りましょう。 main.cをコンパイルした後、次のように./a.outの後ろに数字を書いて実行するとします。

$ ./a.out 3 3 8
すると、変数int Nおよび配列int vals[N]に関して、次のように値がセットされます。

  • N: 整数の個数。上の場合N = 3
  • vals[N]: 各整数の値。上の場合vals[0] = 3, vals[1] = 3, vals[2] = 8

これは、// ==== ここから ====から// ==== ここまでは無視 ====の部分に書いてある処理によって実行されます。 この部分は編集しないでください。

さて、上記のようにvalsに値が入っているときに、「各整数の出現回数を*の個数で表現したヒストグラム」を作り、表示してください。フォーマットは下記のような形です。

例1:

$ ./a.out 3 3 8
のとき
0: 
1: 
2: 
3: **
4: 
5: 
6: 
7: 
8: *
9: 

例2:

$ ./a.out 6 2 4 2 2 3 5 6 7 9 2 2 2 5 6 9
のとき
0: 
1: 
2: ******
3: *
4: *
5: **
6: ***
7: *
8: 
9: **

week2_3

適当な整数valと、0以上9以下の整数kが与えられるとします。 このとき、valの各桁に現れる数字のうち、kが出現する回数を数えて返す関数int count_k(val, k)を書いてください。

例えば、

  • val=4525, k=5のとき、count_k2を返します。
  • val=4525, k=4のとき、count_k1を返します。

ここでも、main.cをコンパイルしたあと、./a.out 4525 5のように入力すると、valに4525、kに5が入るようになっています。

答えの例

答え

week2_1

#include <stdio.h>

int main() {
    printf("Hello World!\n");
    return 0;
}

week2_2

#include <stdio.h>
#include <stdlib.h>


// 関数は自由に追加していいです。

int main(int argc, char *argv[]) {
    // ==== ここから ====
    if (argc < 2) {
        printf("Error. Put arguments. For example: './a.out 3 4 6 6'\n");
        return 0;
    }
    int N = argc - 1;
    int vals[N];

    for (int n = 0; n < argc; ++n) {
        if (n == 0) {
            continue;
        }
        vals[n - 1] = atoi(argv[n]);
    }
    // ==== ここまでは無視 ====



    // ==== ここから下に記入 =====

    int hist[10] = {0}

    for (int n = 0; n < N; ++n) {
        hist[vals[n]]++;
    }

    for (int i = 0; i < 10; ++i) {
        printf("%d: ", i);

        for (int j = 0; j < hist[i]; ++j) {
            printf("*");
        }
        printf("\n");
    }


    return 0;
}

week2_3

#include <stdio.h>
#include <stdlib.h>


// この関数を編集
int count_k(int val, int k) {
    // このコーナーケース処理は無くてもいいです
    if (val == 0 && k == 0) {
        return 1;
    }

    // このコーナーケース処理は無くてもいいです
    if (val < 0) {
        val = -val;
    }

    int count = 0;
    while (val > 0) {
        if (val % 10 == k) {
            ++count;
        }
        val /= 10;
    }
    return count;
}

int main(int argc, char *argv[]) {
    // ==== ここから ====
    if (argc != 3) {
        printf("Error. Put two arguments. For example: './a.out 7662384 6'\n");
        return 0;
    }
    int val = atoi(argv[1]);
    int k = atoi(argv[2]);
    // ==== ここまでは無視 ====

    printf("%d\n", count_k(val, k));

    return 0;
}