情報処理 Ⅱ 第 8 回 2009 年 12 月 7 日 ( 月 )
本日学ぶこと 関数と変数 目的 関数を自分で定義し, 変数の利用方法 範囲を明示的に制限することで, 適切な機能分割 ( モジュール化, 再利用 ) を図る. してはいけないこと 問題 main 関数のみで 100 行以上のプログラム グローバル変数を駆使するプログラム プログラムを読みやすくする 保守性向上 多項式関数 f(x) = c 0 + c 1 x + c 2 x 2 + + c n x n と実数 a が与えられたときに,f(a) を計算できる? 次数 nの異なる様々な関数に対して, 求められる? 入 p.109 2
関数 (Function) 関数の分類 自作関数 : 自分で定義する. 本日のテーマ ライブラリ関数 : 出来合いのもの.printf など. なぜ関数を定義するのか? 処理を共通化 ( 一般化 ) する プログラムの見通しをよくする main 関数がないとプログラムは動かない 入 pp.274-275 リ p.346 3
関数定義の方法 構文 : 型名関数名 ( 引数並び ) { 文...} 型名は, 関数の戻り値の型. 値を返さないときは,void と書く. 引数並びは, 型名変数名 をカンマで区切ったもの. 引数がないときは,void と書く. 例 : double myatof(const char *str0) { } 例 : void procedure(int x, int y) { } 一括の変数宣言と異なり, このintは省略できない. リ pp.347-354 4
void 型 voidは 何もない や 無効な値 を表す型名 void x; 用途 void *x; 関数が引数や戻り値を持たないことを明示するとき void exit(int status); void procedure(void); 任意のポインタ型を表現するとき void *p; void *malloc(size_t size); プログラムを終了するライブラリ関数. 通常,exit(0); もしくは exit(1); のいずれかで呼び出す. 入 p.274 リ p.349, p.300, p.498, p.501 5
関数の呼び出し 関数定義の例 : double f(double x) {return x+1;} 関数呼び出しの例 :b=f(a); 変数 aの値を引数として, 関数 f を呼び出し, その戻り値を, 変数 bに代入する. x を関数 f の仮引数 (parameter), a を関数 f の実引数 (argument) という. これらを区別する必要のないときは, ともに引数引数という. 仮引数の宣言には型名も書く. 実引数には書かない. x = a; の代入を行ってから, 関数本文の処理に入る. 関数の処理が終われば, 変数 x ( のオブジェクト ) は消滅する. x = f(x); と書いてもよい. このとき, 仮引数の x と, 実引数の x ( あらかじめ定義しておく ) は, 別のオブジェクトである. 入 pp.277-278 リ pp.347-348 6
return 関数処理中に return 値 ; があれば, そこで関数の処理を終え, 値を戻り値 (return value) とする. 値の前後にカッコは不要. 戻り値の型が void なら, return; と書ける. 戻り値の型がvoid 以外なら, 必ずreturn 値 ; で処理を終えるように書く. リ p.267 7
引数の授受 Cの関数呼び出しでは必ず値渡し (call by value) になる. 値渡し : 実引数のコピーコピーが仮引数に格納される. その後, 仮引数の値を変更しても, 実引数の値には影響しない. 参照渡し (call by reference) をしたければ, ポインタ値を引数とすればよい. 参照渡し : 仮引数の値を変更すれば, 実引数の値もそれに変わる.CC ではこの意味での参照渡しをすることができないが, ポインタ値を渡すことで, その参照先の値を変えることができる. アドレス渡しともいう. 入 p.277, p.285 リ pp.347-348 8
二つの値を交換する関数 値渡し 失敗 引数は関数内の仮引数にコピーされる. 関数内の仮引数の中で, 値を交換しても, 関数の外のオブジェクトは変更されない. 参照渡し 成功 関数の仮引数は, 指し示す先を持つ. * ポインタ変数 = 値 とすることで, ポインタ変数が指し示す ( 関数の外の ) オブジェクトに値を代入する. swapint.c 9
値渡しで失敗する理由 (1) コード ( 抜粋 ) void swapint_bad(int x, int y) { int tmp; swapint _ bad tmp = x; x = y; y = tmp; } x = 1, y = -1; swapint_bad(x, y); x = 1 y =-1 main 入 p.230 10
値渡しで失敗する理由 (2) コード ( 抜粋 ) void swapint_bad(int x, int y) { int tmp; tmp = x; x = y; y = tmp; } swapint _ bad x = 1 y = -1 x = 1, y = -1; swapint_bad(x, y); x = 1 y =-1 main 入 p.230 11
値渡しで失敗する理由 (3) コード ( 抜粋 ) void swapint_bad(int x, int y) { int tmp; tmp = x; x = y; y = tmp; } swapint _ bad x = 1 y = -1 tmp x = 1, y = -1; swapint_bad(x, y); x = 1 y =-1 main 入 p.230 12
値渡しで失敗する理由 (4) コード ( 抜粋 ) void swapint_bad(int x, int y) { int tmp; } tmp = x; x = y; y = tmp; swapint _ bad x = 1 tmp=1 y = -1 x = 1, y = -1; swapint_bad(x, y); x = 1 y =-1 main 入 p.230 13
値渡しで失敗する理由 (5) コード ( 抜粋 ) void swapint_bad(int x, int y) { int tmp; } tmp = x; x = y; y = tmp; swapint _ bad x = -11 tmp=1 y = -1 x = 1, y = -1; swapint_bad(x, y); x = 1 y =-1 main 入 p.230 14
値渡しで失敗する理由 (6) コード ( 抜粋 ) void swapint_bad(int x, int y) { int tmp; } tmp = x; x = y; y = tmp; swapint _ bad x = -11 y = -11 tmp=1 x = 1, y = -1; swapint_bad(x, y); x = 1 y =-1 main 入 p.230 15
値渡しで失敗する理由 (7) コード ( 抜粋 ) void swapint_bad(int x, int y) { int tmp; } tmp = x; x = y; y = tmp; swapint _ bad x = -11 y = -11 tmp=1 x = 1, y = -1; swapint_bad(x, y); x = 1 y =-1 main 入 p.230 16
参照渡しで成功する理由 (1) コード ( 抜粋 ) void swapint_good(int *px px, int *py py) { int tmp; swapint_good tmp = *px px; *px = *py py; *py = tmp; } x = 1, y = -1; swapint_good(&x, &y); x = 1 y =-1 main リ p.356 17
参照渡しで成功する理由 (2) コード ( 抜粋 ) void swapint_good(int *px px, int *py py) { int tmp; tmp = *px px; *px = *py py; *py = tmp; } swapint_good px py x = 1, y = -1; swapint_good(&x, &y); x = 1 y =-1 main リ p.356 18
参照渡しで成功する理由 (3) コード ( 抜粋 ) void swapint_good(int *px px, int *py py) { int tmp; tmp = *px px; *px = *py py; *py = tmp; } swapint_good px py tmp x = 1, y = -1; swapint_good(&x, &y); x = 1 y =-1 main リ p.356 19
参照渡しで成功する理由 (4) コード ( 抜粋 ) void swapint_good(int *px px, int *py py) { int tmp; } tmp = *px px; *px = *py py; *py = tmp; swapint_good px tmp=1 x = 1, y = -1; swapint_good(&x, &y); x = 1 y =-1 py main リ p.356 20
参照渡しで成功する理由 (5) コード ( 抜粋 ) void swapint_good(int *px px, int *py py) { int tmp; } tmp = *px px; *px = *py py; *py = tmp; swapint_good px tmp=1 x = 1, y = -1; swapint_good(&x, &y); x =-1= y =-1 py main リ p.356 21
参照渡しで成功する理由 (6) コード ( 抜粋 ) void swapint_good(int *px px, int *py py) { int tmp; } tmp = *px px; *px = *py py; *py = tmp; swapint_good px tmp=1 x = 1, y = -1; swapint_good(&x, &y); x =-1= y =-1= 1 py main リ p.356 22
参照渡しで成功する理由 (7) コード ( 抜粋 ) void swapint_good(int *px px, int *py py) { int tmp; } tmp = *px px; *px = *py py; *py = tmp; swapint_good px tmp=1 x = 1, y = -1; swapint_good(&x, &y); x =-1= y =-1= 1 py main リ p.356 23
main 関数の型 main 関数の ( 戻り値の ) 型は,int とする. void main とする本も多いが, 規格上適切ではない. 正常終了は return 0; と書き, 異常終了は return 1; と書くのが一般的. main 関数が返す値, およびexit 関数の引数は, 終了ステータス (exit status) と呼ばれる. Linuxなら, コマンド実行後, echo $? を実行することでその値を確認できる. コンパイルが成功したときに限り, ファイルを実行する が一つのコマンドで書ける リ pp.359-362 24
関数定義の順番 呼び出す関数は, 呼び出す前に ( プログラムファイルの上のほうで ) 宣言されていなければならない. 対策 宣言や定義がない場合は,int 関数名 (); とみなして呼び出しを試みる. 呼び出す順序に注意して関数を並べる. 関数プロトタイプを使用する. 25
関数プロトタイプ (Function prototype) 関数原型 ともいう 構文 : 型名関数名 ( 引数の型の並び ); セミコロンを忘れずに 引数の型の並び は, 引数 ( 型と変数 ) の並び でもよい. このとき変数名は無視される. 一般に, 各関数の定義より前 ( 上 ) に記述する. 例 : int swapint_good(int *, int *); 関数プロトタイプを用いることで, 関数定義の順番を気にすることなくプログラムを記述できる. 関数の入出力が明確になる. 入 pp.278-279 リ pp.350-351 26
変数 (Variable) 識別子 オブジェクト 識別子 (Identifier): 変数名, 関数名, 型定義名などの 名前 識別子とオブジェクト (Object) の違い 識別子は, プログラムファイル ( 静的 ) で記述される ラベル オブジェクトは, プログラム実行中 ( 動的 ) に生成される 実体 同一の識別子に対して複数のオブジェクトが生成されることもある. 宣言により, 変数ならそのオブジェクト, 関数ならその実行コードが, 記憶域 ( メモリ ) 上に割り当てられるとき, その宣言を特に定義定義という. 関数プロトタイプや,externを用いた変数や関数の宣言, 構造体などの独自型定義は, この意味で定義ではない. 入 p.223 リ p.37, pp.102-103 27
変数を宣言 定義する際の注意点 型は何か? グローバルかローカルか? auto( 自動 ) か static( 静的 ) か extern( 外部 ) か? リ pp.104-106 28
グローバル変数とローカル変数 各変数は, 定義された位置によって, 有効範囲 (scope) を持つ. グローバル変数 あらゆるブロックの外で定義された変数. 有効範囲はファイル末尾まで. ローカル変数 ブロックの中で定義された変数. 有効範囲はブロック終了まで. ブロックの中 とは, { と } で挟まれた領域のこと 関数の仮引数もローカル変数 グローバル - global l - 大域的 - あらゆるブロックの外 ローカル - local - 局所的 - あるブロックの中 入 pp.280-281 リ pp.111-114 29
識別子の宣言に関するルールル グローバルに同一の識別子を複数宣言できない. 一つのブロック内に, 同一の識別子を複数宣言できない. 関数はブロック内で定義できない. 必ずグローバル区間での定義となる. GCC( 演習室のコンパイラ ) では, 関数の中に関数を定義できるが, 使用しないこと! ブロック内に変数を定義するときは, それより外にある同一の識別子と重複してもよい. ただし, 外にある同一の識別子は参照できない. モジュール化に関して有用なルール. リ p.114, p.225 30
型の属性 記憶域クラス extern, static, auto, register 型修飾子 const, volatile 例 : extern void function1(const char *); int x(void) {static ti int c=0;...} リ p.104 31
記憶域クラス (1) auto: そのオブジェクトの生存期間は自動記憶域期間である. 積極的に auto を書くことはない. static: そのオブジェクトの生存期間は静的記憶域期間である. 必要なときに使う. extern: 他の場所で宣言された識別子を使用する. 分割コンパイルで不可欠. プログラムが複数のソースファイルで構成されることがある. 大規模プログ externとstaticは, 関数にラミングでは当たり前だが, 本授業で対しても指定できる. は実例を出さない. リ p.107 32
記憶域クラス (2) 静的記憶域期間 ( 静的変数,static 変数 ) static を指定した変数と, グローバル変数が該当する. プログラムの実行に先立ち, オブジェクトが生成され, 初期値が設定される. プログラム終了まで破棄されない. 初期値を指定しないオブジェクトには 0 が代入される. 初期値は, コンパイル時に計算可能な定数式でなければならない. 自動記憶域期間 ( 自動変数,auto 変数 ) auto を指定した変数,static や extern の指定なくブロック内で定義した変数と, 関数の仮引数が該当する. 宣言文を実行するたびに, オブジェクトが生成され, 初期値があれば毎回初期化される. ブロックを終えると, 破棄される. 初期値を指定しないオブジェクトの初期値は不定. 初期値は任意の計算式でよい. 入 pp.226-227 リ p.110, pp.115-120 33
有効範囲? 記憶域クラス? ここで問題 ( 小テストではありません ) グローバルな静的変数は, 定義 できる できない. グローバルな自動変数は, 定義 できる できない. ローカルな静的変数は, 定義 できる できない. ローカルな自動変数は, 定義 できる できない. ローカルな静的変数の用途 ブロック内で情報を保存しておき, あとで利用する. 動的に確保することなく, 配列領域を戻り値とする. 自動変数では扱いきれない大容量オブジェクト (100 万個の int 配列など ) を取り扱う. リ pp.118-119 34
配列の自動変数 (1) 関数の中で配列変数を定義すれば, 関数処理の中で確保され, 関数処理が終わると破棄されるような配列が作られる. void print_message(void) { } char message[] = "Wakayama"; printf("%s n" n", message); print_message message 'W' 'a' 'k' 'a' 'y' 'a' 'm' 'a' ' 0' 35
配列の自動変数 (2) 関数の仮引数に配列変数を書けば, その配列と型が適合するポインタ変数になる. void print_message(char message[]) { printf("%s n", message); } 宣言の形は不完全型実体はポインタ変数 要素数 ( 多次元配列の場合は左端のみ ) は無視される. 要素数なし 初期化なし ( 不完全型 ) でもよい. print_message message 'W' 'a' 'k' 'a' 'y' 'a' 'm' 'a' ' 0' リ pp.357-358 36
多項式の計算 仕様 多項式関数 f(x) = c n n-1 n x + c n-1 x + + c 1 x + c 0 と実数 aが与えられたときに,f(a) を計算する. 考え方 係数 c 0, c 1, c 2, c n を, 配列変数で保持する. c i は,xx の i 次の係数 f(a) = ( (c n * a) + c n-1 ) * a + ) + c 0 により求め ( ホーナー法 ), 乗算の回数を減らす. v 0 = c n v 1 = v 0 * a + c n-1 v 2 = v 1 * a + c n-2 = c n *a*a + c n-1 *a + c n-2 v i = v i-1 * a + c n-i polynomial1.c 37
コード ( 抜粋 ) 多項式の計算を行う関数 係数の配列 ( の先頭を指し示すポインタ変数 ) double calc(double f[], int size, double x) { f の配列の要素数 double val; (xの次数 +1) int i; val = f[size - 1]; v c n for(i = size - 2; i >= 0; i--){ val = val * x + f[i]; } return val; } f(x) を求める (xの次数 + 1) ためのx の値 v v*x + c n-i v = f(x) = c n x n + c n-1 x n-1 + + c n x+c 0 関数定義の効果 : 異なる次数の多項式でも, 一つの (Cの) 関数 calc により, 値を求められる. polynomial2.c 38
変数の有効範囲の補足 破棄されるまでは, ブロックの外からでも ( ポインタなどで間接的に ) オブジェクトの参照や値の書き換えができる. ポインタによる参照渡しが可能となる. 関数内の自動変数に対応するオブジェクトは, 関数処理が行われるたびに生成される. 再帰呼び出しをする関数 が構成できる. 何らかの関数の中 message message[0] = 'w'; としてよい wakayama 'W' 'a'' 'k' 'a'' 'y'' 'a'' 'm'' 'a'' ' 0' リ p.121 39
まとめ 関数を自分で定義し呼び出すとき, どのような点に注意しなければならないか? 参照渡しにはポインタが不可欠であるのはなぜか? グローバル変数とローカル変数, 静的変数と自動変数の違いは何か? グローバルな自動変数は存在するか? 関数の中で配列を扱うとき, どのような点に注意しなければならないか? 40