C 言語の復習
このページの内容はすでに学んだ内容のはずですが、念のために復習しましょう。
1. 制御構文
1.1 if 文
if 文の構文:
if (式) 文1
- 式がなりたてば(非0なら)文1実行
if (式) 文1 else 文2
- 式がなりたてば(非0なら)文1実行。さもなくば、文2実行。
- 文1/2のところには、文もしくはブロックがきてよい。if 文や for 文だって文。
if (x > 0) {
printf("%d is strictly positive.\n", x);
} else {
printf("%d is not strictly positive.\n", x);
}
if 文のポイント:
- 式には、等式や不等式もかける。求まるのは、真偽値。
a > b
,a >= b
,a < b
,a <= b
,a == b
,a != b
等価判定には == をもちいることに注意- 論理演算(真偽値のAND, OR, NOT など)は、 例えば
式&&式
,式||式
,!式
で表す。
- 整数値を返すような式も記述可能
- 実は、等式や不等式が、 0 もしくは 非0 を返す演算として機能している
1.2 複合文(ブロック)
構文:
- { 宣言0 宣言1 .. 文0 文1 … }
- 宣言により変数を確保し、その後、文を順に実行
- 文の代わりに使用可能。if 文や for 文などと併用されることが多い。
別に、単独で出現しても構わない。 - 宣言した変数は、ブロック内でのみ利用可能なので、そういう用途にもどうぞ。
例:
if (x < y) {
int swap = x; /* 変数 swap は、ブロック内でのみ利用可能 */
x = y;
y = swap;
}
1.3 条件演算子
条件分岐を用いた式(not 文)を作りたいときに便利
構文:
conditionExpr? thenExpr : elseExpr
- この式の値:まず条件式(
conditionExpr
)を評価し、真(=非0)ならthenExpr
を評価してその値、偽(=0)ならelseExpr
を評価してその値を返す
- この式の値:まず条件式(
例:
int max = (x > y) ? x: y;
注意:
条件演算子は時々便利なんですが、間違いやすいです。 通常の if 文で同じ結果を得られるので、条件演算子を避けてもいいです。
1.4 switch 文
複数への分岐を一挙に記述するのに便利
文法:
switch(式) {
case 定数1: /* 式==定数1の場合、ここに制御が移る */
文の並び
case 定数2: /* 式==定数2の場合、ここに制御が移る */
文…
case …: … /* 同様 */
default: /* 上記の case 文が該当しなかった場合、ここに移る */
文…
}
例:
int score = 88;
printf("score %d: ", score);
switch(score/10) {
case 10:
case 9:
case 8:
printf("優\n");
break;
case 7:
printf("良\n");
break;
case 6:
printf("可\n");
break;
default:
printf("不可\n");
}
- break 文 (
break;
)で、switch文から抜けることができる。というより、break 文がないと、次の case 以降まで実行してしまう。気を付けましょう! default
はなくてもいいけど、コンパイルオプションによって警告が出る可能性があります。- 文には、ブロックを含め、どんな文がきてもよい。
1.5 do 文、while 文
do 文:
do 文 while (式);
- まず文を実行し、そのあと評価した式の値が 真(=非0)であるなら、再度、このdo文を実行。
- 文のところが、ブロックになっていることも多い。
- なんなら、文が繰り返し文を含んでいても構わない。
while 文:
while (式) 文;
- 式を評価して真(=非0)であるなら文を実行し、再度、このwhile 文を実行。
- 同じく、文のところがブロックのことが多い
注意:
- 当然だが、いつまで経っても式の値が真なら、無限ループになる。
- ループの中身は一つの文だけの場合は、
{
}
を入れなくていいのですが、避けるべきです。もしあとで命令を追加した場合、{
}
を忘れるとコンパイル出来るけど、実行がは思うように行かない!
1.6 for 文
文法:
for (初期化; 条件式; ステップ操作) 文
- 文のところにブロックがくることが基準
意味:
- 初期化後、条件式が成り立つ間は、文とステップ操作を繰り返す。
- 以下の while文を用いたコード片と等価
初期化;
while(条件式) {
文
ステップ操作;
}
ありがちな例:
{
int i;
for(i = 0; i < 10; i++) {
printf("%d\n", i); /* i を 0 (含む)から 10(含まない)まで、10回printf実行 */
}
}
注:
- C で 10回分の処理を行う場合、このように 0 以上(含む)から 10未満(含まない)のように記述することが多いです。最初を 0 番めと数えるやり方は、慣れると、結構分かりやすいので、馴染んでおきましょう。
1.7 break, continue 文
break 文:
- switch, do, while, for 文から抜ける
- これらの文が入れ子状になっている(ネストしている)場合は、break を含む、一番内側のものが、抜ける対象になる。
continue 文:
- do, while, for 文で、現在実行中の繰り返しステップを終了し、繰り返し条件判定に進む
例
for(i = 0; i < 10; i++) {
if(i==4) {
continue;
}
if(i*i > 50) {
break;
}
printf("%d ", i);
} /* 出力: 0 1 2 3 5 6 7 */
1.8 多重ループ
ループが入れ子状になっています。 2重でも3重でも、可能です。
1.9 その他
初期化
変数の宣言時に初期化まで行うことができる。
例: int x = 100;
注意:下記のケースに気を付けましょう。初期化しないと、変数が何かの値を持っている。しかし、値は不定です。実行して見ると多くの場合は 0
かもしれませんが、必ずしもそういうわけではない。
int a;
// ...
a = 42; // これは大丈夫
int a;
// ...
int b = a + 42; // 変数 b の値は不定!
複合代入
+=
, -=
などの「演算子」と「=」をつなげた代入演算
- 記法: 変数 += 式;
- 意味は、式を評価した値を変数に加える(ここは、演算によって異なる)
例:
x -= 10 * 2; // この文は次の
x = x - (10 * 2); // 文と等価
増加演算(increment)・減分演算(decrement)
整数型の変数を対象に、その値を一つ増やす/減らす演算子で、式を作る。
例:
x++
やx--
x
をひとつ増やす/減らす。式の評価値は増減前のx
。
++x
や--x
x
をひとつ増やす/減らすのは同じ。式の評価値は増減後のx
。
2. 関数
関数とは、「一連の作業をひとつにまとめたもので、他から呼べるようにしたもの」です。
- 関数が終わったら、呼出側(caller)に戻る
- 関数の中で関数を実行することも可能。
- 入れ子状の呼び出しも可能
文法:
- 返り値の型 関数名(パラメータ情報) { 宣言の並び 文の並び }
例:
#include <stdio.h>
int max(int x, int y) {
printf("max(%d, %d) is called\n", x,y);
if (x<y) {
return y;
}
return x;
}
int main(int argc, char* argv[]) {
int a = 10;
int b = max(a*3, a+4);
printf("result: %d\n", b);
return 0;
}
主に、以下の3種類が皆さんに馴染みかと。
printf()
やscanf()
といったライブラリ提供の関数- システムやライブラリで定義されていて、皆さんのプログラムから呼び出す
- main 関数
- プログラマが定義し、開始時にシステムが起動する関数
- プログラマが定義した関数
- 自分でつくって、main 関数や他の関数から呼びましょう
2.1 基本
- 値渡し (call by value): 引数の評価値をもらってきて、変数に格納するだけ。
- 呼出側(caller)と呼ばれた側(callee)は、別スコープ
- 呼出側の変数にアクセスしたい場合は、ポインタをもらいましょう。
max が呼ばれた時の Call Stack の状態
2.1.1 返り値
- 関数は、計算結果として値を返すことができ、返す値の型を定義しておかないといけない。
- return (式); で式を評価した値を、呼び出し側に返す。()は、省略可能。
- 値を返さない場合:
- 返り値型宣言は、void と記載
- return は引数をとらず、return; とかかけばよい。
- main の返り値:正常終了の際は 0 を返すというのがルール。
2.1.2 Parameters (仮引数)
- 宣言: 型と名前の対を列挙 (, でつなぐ)
- 変数宣言同様、パラメータ用の領域が確保される。
- 関数を呼び出す側では、argument (実引数)として、引数パラメータの型にあった式を記述する。式を評価した値が、parameter に格納されることになる。
2.2 再帰呼び出し
難しく考えるのは止めましょう。再帰関数なんて、必要がなければ使わなければいいんです。
ただ、アルゴリズムを表現するのに再帰が便利なことは多いです。 で、そんなとき、再帰関数があれば、素直に表現できてハッピーってな訳です。
- 例: 木構造の合計の量は、「左の部分木」の重さと「右の部分木」の重さと、自分の重さを足したもの。
sumup(node) = sumup(node->left)+sumup(node->right)+ node->weight;
例 Fibonaci Sequence
無駄な計算が結構ありますが、再帰呼び出し関数の例として分かりやすいです。
再帰関数の多くの実装は下記のパターンです:
- 何かの if 文で「それ以上再帰的に呼び出さない」判断すること
- 再帰呼び出しもう必要ない場合は、単に何もしない、あるいはバリューを返すだけ
- 再帰呼び出しまだ必要な場合は、再帰呼び出しして、貰った結果を組み合わせて return する
int fibonacci(int n) {
if (n <= 2) {
return 1;
} else {
int previous = fibonacci(n-1); // 再帰呼び出し
int twoBefore = fibonacci(n-2); // 再帰呼び出し、2つ目
return previous + twoBefore;
}
}
3. 変数・配列
3.1 変数と式と値
変数: 値を格納する箱
- 値:
1
とか10
とか - 式:
i
とかi+10
とかmax(i*3, i+3)
とか10
とか - 式を評価して値を求める。で、変数に格納する。
3.2 配列
配列: 同じ型のデータを格納するための箱が並んだもの
3.2.1 宣言**
- 例:
int a[4];
- int 用の領域を 4 個分、連続して確保
- 要素数は定数でなくてはいけない。
3.2.2 配列へのアクセス
a[4]
の各要素へのアクセス:a[0], ..., a[3]
- 値の読み込み
a[i]
: 配列 a の(整数) i 番要素に格納された値を取得する式。- 当然、他の式の要素にも支えるので、
a[i] * 3
とかb[a[i+1]]
などの式も可能
- 代入
- 例:
a[i] = a[i] * 2;
- 配列 a の i 番要素に
a[i] * 2
の結果を格納。つまり、2倍にする。 - 式の評価順:まず、右辺の値を計算し、左辺を計算する。
- 配列 a の i 番要素に
- 例:
3.2.3 初期化
- 例:
int a[5] = {0, 1, 2, 3, 4 };
a[5]
の5
は省略可能。
3.2 多次元配列
多次元配列: 例えば、3x4 の行列(Matrix)を作りたい、といった用途に使用。 同じ型&要素数の配列を要素にした配列。
3.2.1 宣言
- 例:
int M[3][4];
int[4]
, つまり 要素数 4 の int 配列」を3個確保する- つまり、
M[0]
やM[1]
が要素数4のint 配列というわけ。 - だから、要素のならびは
M[0][0], M[0][1], M[0][2], M[0][3], M[1][0], ..., M[2][2], M[2][3]
の順。
3.2.2 配列へのアクセス
- 読み込み:
M[i][j]
- 代入:
M[i][j] = 式
多次元配列への典型的アクセス例:
for(i = 0; i < 3; i++) {
for(j = 0; j < 4; j++) {
r += M[i][j];
}
}
3.2.3 初期化例
int M[2][3] = {
{ 1, 2, 3 },
{ 4, 5, 6 },
};
- 最初の要素数は、省略可能
- 2番め以降の要素数は、要素の型(要素数3の int 配列)情報に相当するので、省略不可。
4. ポインタ
4.1 概要
変数や配列が、値をいれる箱だということは、いままでも述べてきました。
で、C 言語では、箱に対し、その場所=メモリアドレスを介して、アクセスできるようになっています。
C 言語では、そのようなアドレスの事をポインタ(pointer)型として扱います。 箱の型に応じて、 int 型へのポインタや double型へのポインタという風にポインタの型も分かれます。
4.2 変数へのポインタ値の取得
-
例えば、 int x という変数へのポインタは以下で取得できます。
&x
- & はアドレス演算子(address operator)と呼ばれます。int へのポインタ型の値が取得されます。
- printf の表示指定には %pを使いましょう。
4.3 ポインタ型変数の宣言
-
ポインタ値を格納したければ、ポインタ型の変数を宣言しましょう。以下で、int へのポインタ型である 変数 xp が宣言できます。「ポインタ=アドレス」を格納する箱です。
int * xp;
-
変数に、「ポインタ値」を代入します。
xp = &x;
4.4 参照先にアクセス(dereference)
- ポインタが指し示す変数への読み書きが可能。指し示すことを参照(reference),参照をたぐることをdereferenceと言います。
- 参照先にアクセスできるので、int へのポインタ型とdoubleへのポインタ型は、別物として扱う必要があります。
-
参照先変数の値取得(例)
*xp
-
参照先への書き込み(例)
*xp = 3;
4.5 各種ポインタ
何も指さないポインタ
- NULL という定数で、表記
- stdio.h などの中で宣言されている。
- 結構よく使います。
ポインタ変数を指すポインタ
- ポインタを使いたいのは、int 変数や double 変数に限らないわけで、ポインタ変数へのポインタなども可能です。
4.6 ポインタと関数
ポインタなんて、なにに使うんだって話ですが、実は、scanf とかで使ってますよね。
`scanf("%d", &input);`
- 呼び出し側(caller)は、
input
という変数のポインタを取得して(&input
)、 - 「
scanf
さん、変数へのポインタ渡すから、整数値書き込んでよ」っと呼び出す。 - 呼び出された側(callee)である
scanf
は、ポインタ(&input
)の参照先(input
相当)に書き込む - 呼び出し側(caller) は、無事に値ゲット
同じようなポインタを複数準備しておけば、caller に複数の値を返すことができるので、よく利用されます。(別解として、返り値として構造体を返すという手もあります。)
例:swap
- ポインタ理解でよく出てくるサンプル
swap1
は値の交換に失敗。swap2
は、値の交換に成功。- 練習問題: debugger で、
xp
のポインタ値、&a
の値を比較しましょう。*xp
への書き込み時のa
の値変化を確認しましょう。
void swap1(int x, int y) {// swap できてないよ
int tmp = x;
x = y;
y = tmp;
}
void swap2(int* xp, int* yp) { // 正しい swap だよ。
int tmp = *xp;
*xp = *yp;
*yp = tmp;
}
int main(void) {
int a = 3;
int b = 4;
swap1(a,b);
swap2(&a,&b);
return 0;
}
4.7 ポインタと配列
ポインタと配列は別物
-
そりゃそうです。箱がたくさんあるのと、箱を指すためのポインタがひとつあるのは、全然別です。
int a[4];
// 箱が 4 個int * xp;
// ポインタ用の箱が一つ
配列の要素に対して、ポインタをとることもできる
-
配列の要素は変数見たいなものですから、&でポインタがとれます
xp = &a[1];
// 1つ目の要素のアドレスをとって、int へのポインタとして扱う。
4.8 ポインタは整数値の加減算が可能 (pointer arithmetic)
- (xp+2) や (xp-1) は、xp
を配列要素へのポインタとして見た場合の、2つ後や1つ前の要素へのポインタを意味する。
- 実際には、値は sizeof(要素型)分増減する。
- 別に、ポインタ+ポインタとかする訳じゃない。
xp = &a[1]; // xp は 1番要素を指す
*xp = 31; // 1番要素へ値 31を書き込み
xp++; // xp は 2番要素を指す
*xp = 32; // 2番要素へ値 32を書き込み
4.9 配列名は、配列の先頭要素のポインタ値として利用可能
- 式中に、a とだけ書くと、先頭要素への「 int へのポインタ」値となる。
- ポインタ変数ができるわけじゃない。代入とかできないし。
*a
とa[0]
が等価になり、*(a+3)
とa[3]
が等価になる。
4.10 関数のパラメータ宣言に配列型を書いても、パラメータとみなされる
- パラメータ宣言に配列を書いても、要素型へのポインタとしてみなす
int foo(int a[3]) { ... }
// int foo(int * a) { ... } と等価
- argument (実引数)に、配列名を書いても、配列(の先頭要素)へのポインタが渡されるだけ。
- 結果、呼び出し側(caller)が準備した配列そのものにアクセスされる。ある意味、便利。(ちなみに、構造体の場合は、コピーが作られて、パラメータに格納される。’)
void fillZero(int array[], int from, int to) {
int i;
for(i = from; i < to; i++)
array[i] = 0;
}
int main(void) {
int a[] = { 0, 1, 2, 3, 4, 5 };
fillZero(a, 1, 3); /* 1 から 3(含まず)まで、0で埋める */
/* 実行後の a の中身を printf もしくは、debugger で確認のこと */
return 0;
}
5. 構造体
5.1 概要
構造体というのは、複数の変数を内部に格納したデータ構造です。 構造体を表す変数を確保すると、内部の変数用の領域をまとめて確保します。
各変数のことをフィールドとも呼びます。
struct record {
int i;
int a[4];
};
struct record r; // 構造体変数(構造体を一つ確保)
5.2 構造体へのアクセス
- フィールドから値を取得するなら、例えば
r.i
で OK. -
フィールドへの代入も
r.i = 10;
とかで OK. - 実は、構造体から構造体への代入もできます。
struct record r1, r2
があれば、r1=r2;
で全フィールドがコピーされます。 - 関数呼出の際、構造体自身を引数にすると、コピーが作成され呼び出された関数(callee)に渡されます。
- 関数の返り値に構造体を使うこともできて、この場合も結果のコピーが返されます。
- 関数呼出の際に、構造体のコピーを嫌う人は、ポインタを使うことが多いです。
- よく使うので略記法も導入されています。
- ‘struct record * rp’ に対して、
(*rp).i
と書く代わりに、rp->i
でOK.
5.3 typedef
struct とかポインタを使っていると、型表現がややこしくなってきます。 ということで、struct には名前をつけることをおすすめします。
typedef struct record {
int i;
int a[4];
} record_t;
これで、record_t
で struct record
という型を表すことができます。普通に使っている int
, float
, double
等と等しい。関数のパラメーターや変数の宣言時に便利です。
// typedef せずに
struct record myRecord;
// typedef した場合
record_t myRecord;
6. 各種変数&メモリ領域
6.1 変数の種別
- 1: 関数や関数内のブロック(複合文)中で宣言された変数:局所変数(local variable)
- 変数の見える範囲(有効範囲、scope)は、宣言後ブロック終了まで
- static 修飾がない場合は、宣言時に領域確保され、ブロックが抜けると解放される (確保/解放に時間は要さない)。つまり、関数呼び出しごとに作成されている。このように、プログラムの流れにしたがって、自動で確保/解放が行われるので、自動変数(automatic variable)とも呼ばれる。本来は static の代わりに auto 修飾可能だが、普通は省略する。
- 自動変数の場合、初期化/代入されない限り、その値は不定
- 2: 関数外で宣言された変数:大域変数(global variable)
- 変数の見える範囲(有効範囲、scope)は、宣言後ブロック終了まで(詳しい話は後述)。
- プログラム開始時に、領域が一つ分だけ確保される。初期化がない場合は、0初期化が行われる。(static な記憶領域)
- 3: static 修飾された局所変数
- scope については、局所変数
- 但し、領域確保は、プログラム開始時に一つ分だけ確保される。つまり、関数が何度も呼ばれても、共通の領域(及び格納された値)を使うことになる。
6.2 メモリ領域
プログラム実行中のメモリ領域は、以下の3種類の領域に分けれて管理されます。
- 局所変数 (local 変数, 正確には local auto 変数)
- 関数内で宣言された変数。関数の引数も、局所変数の一種と理解してください。
- 関数呼出ごとに、局所変数用の領域が確保されていく。
- 内部構造的にも、スタックというデータ構造を用いて実現されている
- Call Stack: 関数の呼出関係を表すスタック
- Stack Frame: 各「関数呼出し」相当。局所変数はここに作成される。
- 関数呼出状況に応じて、スタックが伸びたり縮んだりします。
- ヒープ領域
- malloc などで確保することができる領域。自分で確保&開放を管理できる。
- 大域変数 (static 変数)
- プログラム実行に対して、変数を一つ確保するだけ。
6.3 動的メモリ確保
- ヒープ領域から必要なデータサイズを切り出してきて、変数領域として利用。
- 主な手順は
- サイズを調べて(sizeof)
- メモリを確保して(malloc)
- もらったアドレスを対応するポインタ型などにキャスト
- 利用
- メモリを解放(free)
record_t * rp2 = (record_tp)malloc(sizeof(record_t)); // 例
rp2->a = 42; // 仮に、record_tの中に int a; あったとして
free(rp2); // 忘れずに!
6.4 スコープに関する注意点
局所変数
- 有効範囲は、宣言した場所から、ブロックの終了まで
大域変数
- 宣言した場所から、ファイルの最後まで。
- extern 宣言:
extern
修飾付きで変数を宣言すると、変数定義=領域確保は行われず、宣言だけが行われる。 C は、複数のファイルからなるプログラムを記述できる。複数ファイルで、おなじ大域変数にアクセスしたければ、一ヶ所を除いて extern 宣言で対応する必要。- ただし、実際のソフトウエア開発では、大域変数は最低限に
複数変数のスコープ重複について
- 同じ変数が、大域変数やブロックで利用されていても構わない。
- 一番内側のものが有効になる。
- とはいえ、変数名の重複は、可読性からいって、ある程度避けた方がいい。
例:
int a; // variable not visible inside myFunction
void myFunction(){
float a = 0.0; // local variable "shadows" another
}
ややこしくなるので、避けてください!
ちなみに、gcc
のコンパイラーが -Wshadow
のオプションを使うと、発見した場合警告してくれます!
関数のスコープとプロトタイプ宣言
- 大域変数同様、関数定義もしくはプロトタイプ宣言した場所からファイルの最後までが有効。
- プロトタイプ宣言というのは、こんな関数があるよという宣言。関数の中身は定義しない。
例:
int max2(int, int);
ヘッダファイルの include
- ヘッダファイル:変数の
extern
宣言や、関数のプロトタイプ宣言などをまとめて記述したもの - 用途:他のプログラムから include して使う
- 例: stdio.h により、標準 I/O (standard I/O)の宣言群を取り込む。
#include <stdio.h>
7. コンパイラーオプション
C 言語で許されることが多いです。ディフォルトで、gcc
コンパイラーはあまり文句を言わずにコンパイルしてくれます。
もう少し厳しいコンパイルオプションを利用すると、多くのミスやバグを防げるので是非使ってください。そして経験を重ねると C 言語や、他のプログラミング言語でも上手になります。
おすすめは:
-Wall
ほとんど全ての忠告を有効にする-Wextra
-Wall でまだ有効でない忠告も有効にする-Werrors
忠告をエラーする(結果、ただの忠告でもコンパイル不能)
当然、更に厳しいオプション(言語のスタンダードとか、それに関する追加の忠告等)もあります:
-std=c90
(1990年のC言語のバージョン)あるいは-std=c11
(2011年のC言語のバージョン)-Wpedantic
上記のバージョンにきちんと従うこと(例:// hoge hoge
のコメントは C90 言語ではなくて、C++ なんですよ!
詳しくは、ターミナルの中に、man gcc
を打って調べます。長いけど …