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
%
を表示するには%%
とすべきでした。訂正しました。
上のように、足し算、引き算、掛け算、割り算、あまり、が定義されます。
注意として、整数同士の算術演算では、結果も整数になります。
そのため、割り算の結果が小数になる場合は、小数部分が切り捨てられてゼロとなります。
よって、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になる。
++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 == 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.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 = 2020;
がうるう年の場合を判定する条件式を考えてみましょう。 ここでうるう年とは、(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");
}
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");
やってみよう(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
多次元配列¶
配列を入れ子にすることで、多次元版の配列を考えることができます。以下は二次元配列の例です。
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アカウントを作り、かつそれを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_k
は2
を返します。val=4525
,k=4
のとき、count_k
は1
を返します。
ここでも、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;
}