Week 2 (2024/10/10)¶
今日やること
- 演算子
- 制御
- 配列
- 関数
- 宿題の提出方法
はじめに¶
演算子¶
それでは演算子について見ていきましょう。演算子とは、+
(足し算)のような、要素に何らかの処理を与える記号です。
算術演算子、代入演算子、インクリメントおよびデクリメント、関係演算子、論理演算子のそれぞれについて、以下に見ていきます。
算術演算子¶
算術演算子とは、足し算のように、二つの値に処理を施すものです。これは数学で使うものと同じ概念なので、わかりやすいと思います。
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 「%%」は「%」を表示せよという意味
5 / 2
の結果は2.5
であるべきですが、小数が切り捨てられて2
になります。
これは四捨五入ではなく切り捨てであることに注意しましょう。
コラム
printf
中で%
を表示するためには%%
としなければいけないというルールは、\n
などのエスケープ文字とは違い、printfに直接書いたときだけ発動するという特殊なルールのようですので、注意してください。次の例のように、不思議な挙動になります。(week3で文字列を勉強したあとに戻ってきてもらえれば、不思議さがわかります)
printf("abc\n"); // abcと表示出力
char s1[] = "abc\n";
printf("%s", s1); // 同様に、abcと表示
printf("%%\n"); // %と表示
char s2[] = "%%\n";
printf("%s", s2); // %%と表示(!)
また、小数と整数の演算の結果は小数になります。
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になる。
++a
とa++
の違いは何でしょうか?実は「++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
という表記は、a
とb
が等しければ1を返します。そうでなければ0を返します。
すなわち、数学における「\(=\)」です。
このように、二つの要素の関係に応じて1か0を返すものが関係演算子です。
a != b
は、a
とb
が異なれば1を返し、等しければ0を返します。数学で言うところの「\(\ne\)」です。すなわち、==
の反対の操作です。
a < b
は、数学の不等号と同じく、a
がb
より小さければ1を返し、そうでなければ0を返します。
a <= b
は、数学における「\(\le\)」を意味します。すなわち、a
がb
より小さいか、あるいはa
とb
が等しければ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 == 10」であるかどうか、の否定。a==10は真なので、その否定で、偽 (0)
&&
および||
は、その両サイドに二つの式を取ります。
&&
は「かつ」を表します。論理学で言うところの「\(\cap\)」です。
二つの式の両方が真である場合に、1を返します。そうでなければ、0を返します。
||
は「または」を表します。「\(\cup\)」です。二つの式のうち片方あるいは両方が真である場合に、1を返します。
ここで注意として、||
の左側が真である場合は、その場で真を返します。右側を評価しません
(これを短絡評価(short-circuit evaluation)と言います)。
これは、右側が真だろうが偽だろうがどうせ真になるからです。よって、例えば右側に計算時間がかかる処理が書いてあったとしても、その計算は発生しません。
また、&&
でも同様に左側が偽の場合短絡評価が行われます。
!
は、否定を表します。右側に要素をとります。その右側が真であれば偽を、偽であれば真を返します。
演算子の優先順位¶
演算子が複数ある場合に実行する順番は、規則により定められます。
例えば、*
は+
より優先度が高いです。これは数学と同じです。
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.5になりません。何故でしょうか?
double a = 3 / 2; printf("3 / 2 = %f\n", a);
-
以下で
i
とf
は何になるでしょうか?int i; float f; f = i = 23.5f;
-
c言語では、
a < b < c
といった書き方は出来ますが、意図した結果になりません。例えば-20 < -10 < -2
は真になりそうですが、なりません。何故でしょうか? -
BMI指数を計算するプログラムを書いてみましょう。体重\(w\) kg、身長 \(h\) mの人のBMI指数は \(\frac{w}{h^2}\)で計算されます。 たとえば身長160cm、体重50kgの場合は\(50 / 1.6 / 1.6 \sim 19.5\)です。
-
int year = 2022;
がうるう年の場合を判定する条件式を考えてみましょう。 ここでうるう年とは、(1) 4で割り切れる (2) 100では割れない、の二つの条件を満たすものです。しかし、実は例外として、yearが400で割り切れるときはやはりうるう年です。上の条件式をどう変更すればいよいでしょうか?int is_leap = (year % 4 == 0 && year % 100 != 0); // yearがうるう年のときは1になる
答え
- 右辺の
3 / 2
は整数の割り算なので、その結果は整数の1
になります。それを小数のa
に代入するため、a
は1.0になります。 i
は23,f
も23になります(23.5ではありません) ここで、=
は右結合なので、式は次の順番で処理されます:f = (i = 23.5f)
そのため、まず 小数をint
型変数に代入することで切り捨てが起こります。そして、代入式は代入された値がその式の値にもなるため、f = 23
となります。<
は左結合なので、式は次の順番で処理されます:(-20 < -10) < -2
. ここで、-20 < -10
は真なので、1
になります。よって、1 < -2
が評価されます。これは偽なので、最終的に0が返ります。- 例えば以下のようになります。
double w = 50, h = 1.6; printf("Your BMI is %f\n", w / h / h);
- ここで最後の
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");
{
および}
)を使うと、文をまとめることができます。これをブロックと呼びます。
{
// 複数の文をまとめてブロックにする
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");
}
{
)は改行してもしなくても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, 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
が立っている)ならば、val1
とval2
のうち小さいほうをresult
に代入する、という意味になりますね。
for¶
次にfor文について見ていきましょう。forは繰り返しを表します。
int i;
for (i = 0; i < 5; ++i) {
printf("%d\n", i);
}
i
は繰り返しをコントロールするカウンタ変数などと呼ばれます。
for文は次のような構造になっています。式の間はセミコロンである点に注意してください。
for (式1; 式2; 式3)
文
- 式1を実行(これは
i
のようなカウンタの変数の初期化を行うことが多いです) - 式2を実行(これはループを続ける条件です)
- 式2が真の場合
- 文を実行(ここが処理の本体です)
- 式3を実行(ループの更新です。カウンタを増加させることが多いです)
- 「式2を実行」に戻る
- 式2が偽の場合
- 終了してfor文を抜ける
- 式2が真の場合
さて、上で述べた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 (式)
文
ちなみに、for文はwhile文で書き直すことができます。
for (式1; 式2; 式3)
文
式1;
while (式2) {
文
式3;
}
また、do-while構文というものも存在します。
int j = 0;
do {
printf("%d\n", j);
++j;
} while (j < 5);
ちなみに、forやwhileを使うと、永遠にループし続ける無限ループを作ることもできます。これは、プログラムの外部から何らかの入力が入ってくるまで 待っているときなどに使います。覚えておきましょう。
// whileを使った無限ループ
while (1) {
sleep(1); // 1秒間何もしないという関数
printf("waiting\n");
}
// forを使った無限ループ
for (;;) {
sleep(1);
printf("waiting\n");
}
コラム
macでsleep
関数を使う場合には#include <unistd.h>
を先頭に書く必要があります。
breakとcontinue¶
ループの途中で、条件に応じてループの脱出を制御するために、breakとcontinueを使います。
for (int n = 0; n < 5; ++n) {
if (n == 3) {
break;
}
printf("%d\n", n);
}
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);
}
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: 文
}
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");
コラム
for文でi
などをインクリメントする場合、++i
とi++
のどちらが良いでしょうか?
Google c++ coding standardでは前置(++i
)にするほうが良いとありますので、松井は癖で全て前置にしています。これは、i
がイテレータというものである場合、i++
とすると「インクリメントする前の値」のコピーを作ってしまう可能性があるからです。本講義で扱う範囲では、++i
でもi++
でもどちらでも同じです。
やってみよう(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
クイズ
-
以下は、ifのブロック、elseのブロック、どちらに進むでしょうか?そして、それは何故ですか?
if (3) { printf("Here?\n"); } else { printf("or here?\n"); }
-
ループを使って、100未満の「2の冪」を表示するコードを書いてみましょう。ここで出力は数字の間にコンマ・スペースを挟み、最後に1回だけ改行するようにしましょう。つまり、
このようにするにはどうすればいいでしょうか?(最後の一回の改行を忘れないでください) これをfor文、while文両方で書いてみましょう。$ gcc main.c $ ./a.out 1, 2, 4, 8, 16, 32, 64, $
答え
- ifのほうに進む。
if(式)
に対し、式が真であればそのブロックに進む。c言語では真とは「0ではない値」のことなので、3であってもそれは真になる。 - 色々な書き方があります。for文の場合の例:
while文の場合の例:
for (int n = 1; n < 100; n *= 2) { printf("%d, ", n); } printf("\n");
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
【10/12追記】 また、配列は、通常の変数と同じく、宣言しただけでは初期化されていない(その要素が何なのかは未定義)という点に注意しましょう。 次の例を見てみましょう。
// ダメ
int a[3]; // 初期化していないので要素は未定義
a[0]++; // 未定義
// OK
int b[3] = {0, 0, 0};
b[0]++; // b[0]は0から1になる
int a[3]
と宣言しただけでは、a[0]
の値は未定義だからです。未定義の値をインクリメントしても、未定義な値になります。
二番目の例のように、明示的に初期化を行うことで、初めてb[0]
に対して意味のある処理を行うことが出来ます。
多次元配列¶
配列を入れ子にすることで、多次元版の配列を考えることができます。以下は二次元配列の例です。
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};
これらを実行してみましょう。
クイズ
- 以下のコードは何が出力されるでしょうか?そして、それは何故ですか?
float a[3] = {0.5, 0.3, 0.8}; printf("%f\n", a[3]);
答え
- 未定義の値が出力されます。理由は添え字ミスです。要素が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のまま
}
b
がplus_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;
}
f
をmain
より下に宣言・定義しています。そして、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
を作り、上記を写経して実行してみましょう。
クイズ
- 二つの
float
を受け取り、その平均を返す関数average
を書いてみましょう。 int a
とint b
を受け取り、\(a^b\) を返す関数power
を書いてみましょう。
答え
- 例えば、
float average(float a, float b) { return (a + b) / 2.0; }
- 例えば以下。
int power(int a, int b) { int ret = 1; for (int i = 0; i < b; ++i) { ret *= a; } return ret; }
宿題¶
それではここで、GitHub Classroomを用いた宿題提出の方法を紹介します。 今日いるみなさんはすでに全員GitHubアカウントを作り、かつそれをUTOLから松井まで提出しているはずです。もしまだなら松井まで個別に連絡してください。 宿題の提出方法について、宿題の提出方法 に従ってください。
それでは今週の宿題です。
- 締切は次の授業の前日の深夜23:59までです(今回の場合、2024/10/16, 23:59)
- 宿題リンクをslackで配付します。このリンクは公開しないでください。
week2_1¶
まずは練習です。宿題リポジトリを開くと、main.c
の中身が以下のようになっています。
#include <stdio.h>
int main() {
printf("Hello World!!\n")
return 0
}
$ gcc main.c
$ ./a.out
Hello World!
week2_2¶
10~59歳までの男女の年齢データを受け取り、それを可視化するプログラムを作りましょう。「男23歳」「男33歳」「女35歳」「男38歳」「女45歳」といったデータがきたときに、これをまとめて次のように表示します。
50 - 59:
40 - 49:#
30 - 39:#**
20 - 29:*
10 - 19:
宿題リポジトリの中のmain.c
を編集することで、これを実現してください。
main.c
をコンパイルした後、次のように./a.out
の後ろに「半角スペースと半角数字」をたくさん並べて書いて実行するとします。これにより、プログラムにデータを入力します。
$ ./a.out 230 330 351 380 451
230
といった数字のうち、1の位については0
であれば男性、1
であれば女性を表します。そして、100および10の位で年齢を表します。よって、ここでは「男23歳」「男33歳」「女35歳」「男38歳」「女45歳」という意味になります。
すると、ソースコード中のint N
には人数がセットされます。すなわち上記ではN=5
となります。そして、intの配列であるint arr[N]
には、入力データがセットされます。上記の例では、arr[0]=230
、arr[1]=330
、arr[2]=351
、arr[3]=380
、arr[4]=451
となります。
これらの処理は、// ==== ここから ====
から// ==== ここまでは無視 ====
の部分に書いてある処理によって実行されます。
この部分は編集しないでください。
さて、それではやるべきことを説明します。プログラムは、次のような挙動をするようにしてください。出力フォーマットは、半角スペースの有無を含め厳密に一致させてください。関数は自由に追加していいです。
例1:
$ ./a.out
50 - 59:
40 - 49:
30 - 39:
20 - 29:
10 - 19:
例2:
$ ./a.out 220
50 - 59:
40 - 49:
30 - 39:
20 - 29:*
10 - 19:
例3:
$ ./a.out 491 140
50 - 59:
40 - 49:#
30 - 39:
20 - 29:
10 - 19:*
例4:
$ ./a.out 500 561 321
50 - 59:#*
40 - 49:
30 - 39:#
20 - 29:
10 - 19:
例5:
$ ./a.out 230 330 351 380 451
50 - 59:
40 - 49:#
30 - 39:#**
20 - 29:*
10 - 19:
上記の5つの例を自動採点で検証します。合っているように見えても、自動採点がエラーになる場合は何かを間違えています。自動採点がダメなときは得点がつかないので注意してください。難しければ、出来る範囲でやってみてください。
採点の本番は別の数値を用いて行うので、ズルをしてもダメです。(本番採点では、特殊な・いじわるなコーナーケースを出すことはありません)
注意:math.hなど数学関数は使わずに、1から書いてみてください。特に、「手元のターミナルでは動くけど自動採点はパスしない」という状況のときは、「バグがあるけど偶然手元ではうまくいってしまっている」という可能性があります。
答えの例¶
初回にしてはちょっと難しかったからもしれません。今回の宿題に限らずの話ですが、難しい問題を解くときは常に以下の3点を考えるといいです。
- 問題を分割する
- 複雑な問題を複雑なまま考えてもうまく行きません。小さな問題に分割して、それぞれを考えれば良いです。例えば今回の場合は「入力情報を整えてまとめる」と「表示する」は別の処理なので、独立に考えることが出来ます。
- 簡単な例から考える
- 問題が難しい場合は、一番簡単な例に注目するといいです。それが出来てから難しい例にすすみましょう。今回の場合はいきなり「例2」も対応しようとするのではなく、まずは「例1」を解けるようにしましょう。
- 常に可視化する
- 複雑なものをそのまま考えることは大変なので、どんな情報でも常に可視化するようにしましょう。今回の例でいうと、もし情報を配列に保存していてその配列の処理で躓いたなら、「配列の中身を表示する」という可視化関数を書き、それを使って常に情報を表示できるようにしましょう。将来的にはデバッガというものを用いて配列の中身を眺めたりするようになります。
また、講義で習った範囲で解ける問題を出しますので、もし講義で習っていないことを用いようとしている場合(例えば今回の課題でポインタを使おうとする)は、一度立ち止まってもっと簡単に解けないか考えてみるといいでしょう(採点自体は、自動採点をクリアしていればなんでも大丈夫です。)
答え
week2_1
#include <stdio.h>
#include <stdlib.h>
// 関数は自由に追加していいです。
int main(int argc, char *argv[]) {
// ==== ここから ====
int N = argc - 1;
int arr[N];
for (int i = 0; i < N; ++i) {
arr[i] = atoi(argv[i + 1]);
}
// ==== ここまでは無視 ====
// ==== ここから下に記入 =====
// 人数をカウントするための配列を準備。ゼロで初期化を忘れない。
int man[5] = {0}; // man[0]: 10-19, man[1]: 20-29, ..., man[4]: 50-59
int woman[5] = {0}; // woman[0]: 10-19, woman[1]: 20-29, ..., woman[4]: 50-59
// 一人一人について、適切な配列位置に1を追加して人数カウント
for (int i = 0; i < N; ++i) {
// 性別は1の桁で判別できるので、10で割った余りを見ればよい
int gender = arr[i] % 10; // gender=0ならば男性。gender=1ならば女性
// 年代は100で割った値で判別できる
int gen = arr[i] / 100; // 23歳ならばgen=2。57歳ならばgen=5
if (gender == 0) {
man[gen - 1]++; // 20代の場合はgen=2で、その際man[1]をインクリメントするため、gen - 1を指定
} else if (gender == 1) {
woman[gen - 1]++;
}
}
// 表示
for (int i = 0; i < 5; ++i) { // 5周決め打ちのフォームを使用。
printf("%d - %d:", 10 * (5 - i), 10 * (5 - i) + 9); // (5-i)の表記で順番を逆にできる
// 人数カウント配列の要素のぶんだけ単純に#および*をプリント
for (int n = 0; n < woman[5 - i - 1]; ++n) { // genと配列インデックスは1ずれだったので、ここは5-i-1になる
printf("#");
}
for (int n = 0; n < man[5 - i - 1]; ++n) {
printf("*");
}
printf("\n"); // 改行は最後に1回
}
}