C/C++ - 言語仕様編 第4回 〜関数〜

プリプロセッサとマクロ

プリプロセッサとは、#で始まる一連の命令群であり、C言語の構文とはある程度独立した、 コンパイル前の前処理部分である。

#include ディレクティブ

#include の後に指定したファイルを#includeの位置に読み込む。

#include <filename.h>

#include "filename.h" 

の2つの構文があり、前者はいくつかのデフォルトで指定されているフォルダからファイルを探し、 後者は、その前にまず、ソースのファイル自身があるフォルダから検索を始める。

要するに、標準のヘッダファイルを使用する場合は、前者を、自分で作ったヘッダファイルを使用する場合は、 後者を用いることとなる。

#define ディレクティブ

マクロを定義する。#defineで定義された記号定数をプリプロセッサは単純に置き換える。 使い方として典型的なものは3つある。

#define SYMBOL

SYMBOLという記号定数を定義する。特に置き換えを行うことを目的とはしない。

#define LENGTH 10

記号定数名の後に指定したリテラルに置き換える。LENGTHと書いた部分はすべてそのまま 10 と置き換わる。

列挙型と異なり、本当に完全に置き換えてしまうため、リテラルであれば、整数でなくとも、 浮動小数点数だろうが文字定数だろうが関係なく置き換えることができる。

単純に置き換えを行うことから、あるいは、リテラルでない、通常の C 言語の構文でも 全く問題なく置換が行われる。

#define MULTI(a,b) ((a)*(b))

引数に指定した値を定義中の同じ記号で置き換えつつ、全体を定義に置き換える。 したがって、MULTI(3,4)はプリプロセッサによって、((3)*(4)) と置き換わる。

何度もいうように、単純に置き換えるだけであるため、ただ、

#define MULTI(a,b) a*b

などとすると、例えば、MULTI(a+b,c)などとすると、(a+b)*cとならずに、a+b*cと展開されてしまう。 意図的に動かすためには、必要以上に括弧をつけておくのが無難である。

#undef ディレクティブ

定義された記号定数を未定義とする。

#define LENGTH 10

としているときに、

#undef LENGTH

とすると、この記述以降で、LENGTHは10を表す記号定数ではなくなる。

#if 系ディレクティブ

通常、条件付きコンパイルをするときに使用する。

#ifdef SYMBOL
...
#endif

とするとき、

#define SYMBOL

が、前もって記述されていれば、#ifdefから#endifの間がコンパイルの対象となり、 そうでなければコンパイルの対象とならない。

リテラルに対して記号定数SYMBOLを振っていると、

#if SYMBOL
...
#endif

としたとき、SYMBOLがもつ値が真であればコンパイルの対象となり、そうでない、要するに0ならばコンパイルの 対象から外れることとなる。

条件付きコンパイルの最も基本的な応用の1つに、インクルードガードと呼ばれる使い方がある。 ヘッダファイルの最初に#ifndef と書き、最後に、#endifとしておく。

要するに、その中で#defineで記号定数を定義させることにより、1度ヘッダファイルを読むと、 その後、二度とヘッダファイルの中身を読まなくて済むようになる。

複数個所から#includeすると、当然ながらその回数だけ読まれてしまい、リンカエラーとなるため、それを防ぐための 常套手段である。

関数の宣言と定義・呼び出し

関数の宣言は、戻り値の型の後、関数のシンボル、その後に引数列を書く。

returnType functionName(
	argType1 argValue1, argType2 argValue2
);

関数の定義は宣言の後で行う。戻り値の型や関数の名前、及び引数の型や数は完全に一致しなければならない。

returnType functionName(
	argType1 argValue1, argType2 argValue2
)
{
...
	return returnValue;
} 

{と}の中が1つのスコープとなり、引数であるargValue1とargValue2は、関数スコープ内でのみ局所的に有効となる ローカル変数である。

return で値を返した瞬間に関数の定義スコープから抜け、その中で定義された全ての自動変数は、破壊される。

ローカル変数をはじめとして、引数などは全て、スタック領域に入る。 スタック領域は、その名の通り、値が宣言されると積み上げられて、上に積まれたものを使用しなくなった瞬間に 積み下ろすことで、その領域を自動解放することにより、破壊する。

あるスコープ内から、関数funcionName(value1, value2); と呼ぶ事で、この関数はreturnType型の値を返すこととなる。 これは、関数を呼び出した瞬間にスタック領域にvalue1とvalue2と積み上げて、その後、関数内でいろいろと ごちゃごちゃやった後に、returnで値を返した瞬間に、スタック領域から積み下ろし、 結局、スタック領域は呼び出す前と同じ状態にまで戻る。

可変長引数

引数はスタック領域に詰まれるわけで、第1引数で指定された変数のポインタが分かると、 変数の型さえ分かれば、そこからずりずりと引数をひっぱることが可能となる。

可変長引数を許可する場合、宣言において、

returnType functionName(argType argValue, ...);

として、少なくとも第1引数のみを明示し、その後を...で記述することで、以降の引数の数を任意とすることができる。

実際に可変長引数を用いる場合は、

#include <stdarg.h>

とした後に、va_list型変数とva_start()マクロ、va_end()マクロ、va_arg()マクロが使用できるようになる。

基本的には、va_list型変数 arg_listを宣言した後、va_start(arg_list, argValue);としポインタを取得した後、 va_arg(arg_list,Type)とすることで、Type型の引数を順番にひっぱることが可能となる。 最後にva_end(arg_list);とする。

ポインタ変数と関数

引数として参照子をとる関数

スタック領域に積むという特徴を持つ以上、関数は通常呼び出し元のスコープに対して、 戻り値以外の方法で何らかの影響を及ぼすことはできない。

もう、言ったことだし、答え言っちゃうけど、参照子を引数としてとると、静的変数や動的変数だけでなく、 関数スコープよりも前の段階でスタックに積んだ変数を直接参照してその値を変えることができる。

関数を呼ぶと必ず引数をスタックに積むために、要するに、関数スコープ内では必ず、 その参照子が適切な参照を行っている限りにおいて、正しく参照することができる。

もちろん、参照子はその後、値として持つポインタを一切操作しないという前提の上で安全である。

参照子を返す関数

関数を呼び出す前の時点で生存期間にのこっている参照子であれば、どんなものでも返してよい。 問題は、参照子の参照先が関数スコープ内にある場合であり、何がなんでも絶対に不正である。 関数スコープを抜けた瞬間に、参照子の参照先はスタックから積み下ろされるためにエラーとなる。

動的変数を返す関数

関数スコープ内で動的変数を宣言し、その動的変数をreturn で返すと、 呼び出したスコープで代入しポインタ変数が、新しく動的変数として機能する。

malloc()やcalloc()がその関数の最も典型的な例である。

当然ではあるが、もともと動的変数であったものを、free()することなく代入することは問題外である。 何よりも、このような関数を、受け取るポインタ変数なしで使用してはならない。

引数として動的変数をとる関数

free()関数は引数として動的変数をとる。そして、free()関数は引数として入れたポインタ変数から、 動的変数としての資格を奪う。

このような場合、free()関数を経由した後に、NULLを代入するなどをするようにしておくべきである。

関数が動的変数のやり取りを許可する場合、関数に入る前と返った後で、動的変数としての資格を 持っているのかどうかを常に気にしなくてはならない。

そのポインタ変数が動的変数であるかどうか、つまり、1度free()すべき変数であるかどうかを、 把握することが大切である。

1度free()すべきところでfree()しないバグ、メモリリークと、2度以上のfree()をしてしまうバグ、二重freeは、 ポインタに関連する代表的なバグとして挙げられる最も代表的なもののうちの2種である。

引数として配列をとる関数

そして、ポインタに関する代表的なバグ3種の3つ目が反復子にともなうバグである。 関数の引数には、配列を指定することもできる。

ただし、T [N]型のように、要素数がコンパイル前の段階で決定しているような場合を除き、 C言語において、配列の要素数を決定する方法は存在しない。

したがって、関数に呼ぶ場合で、T []型不定配列として呼ぶ場合と、T *型反復子として呼ぶ場合で、 必ず、要素数を決定できる手段を用意する必要がある。

不定配列の場合、もっとも手っ取り早い方法は、int型変数を用いて値の数を渡す方法である。 反復子の場合、先頭の反復子と終端の反復子の2つを与えることで、その間を走査する方法をとることができる。

後者は、事前に数を知っておく必要はまったくなく、必ずしも配列に対する反復子である必要もない、 汎用的な方法である。

そして、このような手段を用いず、適切に走査しなかったがために、本来参照可能である領域以外を、 参照した瞬間に、バグが発生する。これがバッファオーバーランと呼ばれる第3のバグである。

引数や返り値としてポインタ変数を用いると、このような潜在的なバグを引き起こしやすくなるため、 細心の注意をもって取り扱わなければならない。

引数としてポインタ変数のポインタをとる関数

余程の事情がない限り、なんらかのポインタ変数の参照子としてとることとなる。 関数を呼ぶ際に、ポインタ変数のポインタを&演算子でとり渡すことになる。

ポインタ変数への参照子である以上、参照子は、ポインタに対する演算を自由に行うことが出来、 かつその演算結果は、呼び出し元のスコープに対し副作用をもたらす。

逆に、ポインタ変数をそのまま渡すだけでは、呼び出し元のポインタ変数に副作用は起こらない。 引数として反復子を入れた場合、関数内でいかに演算しようと、元のポインタ変数の、すくなくとも、 保持している値であるポインタに対しては何も変化が起こらない。

もし、変化させたければ、繰り返すようではあるが、ポインタ変数のポインタを参照子として渡す必要がある。

関数ポインタ

参照子は4つの記憶領域を参照する。静的領域に記憶されている静的変数への参照子、 スタック領域に記憶されている自動変数への参照子、ヒープ領域に記憶されている動的変数への参照子、 そして、プログラム領域に記憶されている、関数への参照子として機能する関数ポインタである。

関数ポインタ型変数

returnType functionName(
	argType1 argValue1, argType2 argValue2
);

と宣言される関数に対する関数ポインタ型変数pは、

returnType (*p)(
	argType1 argValue1, argType2 argValue2
);

として宣言される。なんていうか、すごく不気味な構文ではある。

このpに代入できるものは、当然、関数のポインタなわけだが、配列のシンボルが参照子の役目を果たすのと 同じ考え方で、関数のシンボルが参照子として機能する関数ポインタを表す。

p=functionName;

そんなわけで、functionName(value1,value2); と、p(value1, value2); は全く同じ動作をする。

参照子としての関数ポインタ

関数から参照子として関数ポインタを引っ張る場合には、当然型が必要なわけだが、 なんていっても、関数ポインタの型ってのは、また、不気味な構文である。 要するに、さっきの宣言から、シンボルを除いたもの、

returnType (*)(argType1, argType2); 

が型になるわけだが、普通は、

typedef returnType (*functionPtr)(argType1, argType2);

などとして、functionPtr型を定義するべきである。

再帰的呼び出し

スタックに積むC言語の、いい意味で最も変態的なプログラミングスタイルである。 方法自体はいたって単純であり、関数のreturn から自分自身を再び呼び出す。

returnType functionName(
	argType1 argValue1, argType2 argValue2
){
	...
	return functionName(argValue1, argValue2);
}

もちろん、returnが1つしかない場合は、何をどうやっても絶対に無限ループになる。 必ず、スタックの終端を用意して、なんらかのreturnType型変数を返すreturnが必要となる。

このようにして値が決定された位置から順番にスタックを積み下ろしていくことで、最終的に 呼び出し元のreturnから決定された値を返すこととなる。

以上で基本的なメモリ操作の説明を一度終え、以降、ファイル入出力操作に進む。


文章作成 : yukki-ts (-+-twilight serenade-+- [stage])