C言語/関数

出典: フリー教科書『ウィキブックス(Wikibooks)』
ナビゲーションに移動 検索に移動

関数の基本[編集]

C言語のプログラムは全て1つ以上の「関数」(かんすう)と呼ばれる処理のまとまりから構成されます。

関数を用いて、いくつかの文をまとめることができます。「main関数」も関数であり、「printf」や「scanf」なども関数であるが、それとは別に、ユーザーがいくつかの処理をまとめて「関数」として定義できます。

C言語の「関数」は、他のプログラム言語でいう「サブルーチン」のような性質がある(値を返すことができるかが関数とサブルルーチンの大きな違いです。)。


Visual Studio で、デバッグ セッションの終了時にコンソールが閉じてしまう場合

Visual Studio で、デバッグ セッションの終了時にコンソールが閉じてしまう場合は、

[ツール] -> [オプション] -> [デバッグ] -> [デバッグの停止時に自動的にコンソールを閉じる]

を無効にします。

関数の定義[編集]

ユーザーが自分で定義する関数について、記法の例を表します。

例 2つの整数を引数にとりその和を返却する関数
int function(int a, int b)
{
	return a + b;
}

上記コードの「function」の部分が、その関数の名前であり、プログラマーが英数字と _ なら自由に関数に名前をつけられます(ただし「if」などのキーワードや「printf」などのライブラリ関数名は使用できません)。「function」でなくても「myFunc」などでも構いません。

以下、特にことわらないかぎり、ユーザーが定義した関数を、単に「関数」と省略する。main関数やprintfなども「関数」と省略する場合がありますが、ユーザーが定義したこと以外に違いはありません。

関数を呼び出す際、関数の 呼び出し元(仮に「親」と呼ぼう) から 呼び出し先関数(仮に「子」と呼ぼう)へ情報を渡すことができ、この渡された情報のことを仮引数(かりひきすう、parameter)、渡した情報のことを実引数(じつひきすう、argument)と呼びます。

関数の定義の記述は次のようになっている。

形式
返却値のデータ型 関数名 ( 引数リスト )
{
	/*いくつかの文*/
}

関数名とはその関数を他の関数と区別するために付ける名前のことで、関数名に使えるのは半角英数、「_(下線)」である。ただし、先頭は非数字でなければならない。 多バイト文字を使用できるかは言語処理系によります。 関数は他の関数の中で呼び出すことができます。

また、引数は省略できる場合があります。

返却値のデータ型 関数名()
{
	/*いくつかの文*/
}

のように関数を記述する場合もある[1]

関数の呼び出し[編集]

定義した関数を呼び出す際の記述は、たとえば次のように行う。

//例 上の例の関数を呼び出す
int function(int a, int b)
{
	return a + b;
}

int main(void){
	int r;
	r = function(1, 2);//rには1と2との和である3が代入されます。
}

関数の定義側で書かれた引数のことを仮引数(かりひきすう、parameter)という。

いっぽう、関数の呼び出し側で書かれた引数のことを実引数(じつひきすう、argument)という。


複数の仮引数を持つ場合、「,(コンマ)」で区切る。仮引数を持たない場合 void を書きます。

仮引数
仮引数のデータ型 仮引数名



任意個数の実引数について、実引数を持たない場合は何も書かきません。 複数の実引数を持つ場合、「,(コンマ)」で区切る。

実引数
関数名(任意個数の実引数);

また、関数の呼び出し先から呼び出し元へ情報を渡すこともでき、この情報を返却値と呼びます。 返却値のデータ型で返却値の有無や種類を決定すします。 任意個数の仮引数で仮引数の数や種類を決定すします。


この例では、function関数の実引数である1,2が、function関数の仮引数であるa,bにコピーされ、a+bがfunction関数の返却値として返され、その値がrに代入されます。

関数を呼び出すとき、 引数と制御が戻るべきアドレス(リターンアドレス)がスタックと呼ばれる領域に格納されます。 また、返却値がレジスタと呼ばれる領域に格納されます。[2]


引数と返却値[編集]

引数と返却値をもたない関数[編集]

//例 引数と返却値をもたない関数
#include <stdio.h>

void HelloWorld()
{
	printf("Hello, World!\n");
}

int main(void)
{
	HelloWorld();
}

この例では自作のHelloWorld関数を使って文字列を表示しています。 このHelloWorld関数は引数と返却値をもたない関数である。 このプログラムでは、 まずmain関数が呼ばれ、 次にmain関数内のHelloWorld関数を呼ぶ文が実行されることでHelloWorld関数が呼ばれ、 HelloWorld関数内のprintf関数を呼ぶ文が実行されることでprintf関数が呼ばれる。 そしてprintf関数が終了してHelloWorld関数に戻り、 HelloWorld関数が終了してmain関数に戻り、 main関数が終了してプログラムが終了する。

この例では関数を自作したメリットはないが、 同じ処理がソースコードのあちこちに出てくる場合などは、 その同じ処理を自作の関数にまとめるとメリットがあります。

引数と返却値をもつ関数[編集]

//例 引数と返却値をもつ関数
#include <stdio.h>

double calc(double radius)//「calc関数の定義」
{
	return 3.14*radius*radius;
}

int main(void)
{
	double radius, area;
	printf("円の半径を入力してください。:");
	scanf("%lf", &radius);
	area=calc(radius);//「calc関数の呼び出し」
	printf("円の面積は%fです。\n",area);
}

この例では、自作のcalc関数を用いて、円の半径から円の面積を計算しています。 このcalc関数は引数と返却値をもつ関数である。 「calc関数の呼び出し」のところのradiusを実引数と呼びます。 「calc関数の定義」のところのradiusを仮引数と呼びます。 このプログラムでは、 「calc関数の呼び出し」で、まずmain関数のradiusが実引数としてcalc関数に渡され、 その値がcalc関数の仮引数のradiusにコピーされ、 それから、計算結果が返却値として返され、 その値が変数areaに代入されている。


関数に複数の返却値を持たせる方法[編集]

関数による通常の返却値の方法では、1つの値しか、呼び出し元関数に値を送れない

しかし、特殊な方法を使うことにより、呼び出しもとの関数に、複数の値を送ることができます。

下記に述べるように、少なくとも以下の3つの方法があります。

コラム[編集]

局所変数とアドレス操作の関係
  • 局所変数の実現方法

このようなユーザー定義の関数で、呼び出し先関数での引数(上記の例ではint aとint b)の使用のとき、C言語では「子」(呼び出し先)関数の使用開始のたびに、引数のために新しいアドレスを用意して、その引数に新アドレスを与えるするという仕組みである。

例えるなら、図書館とかで世界に数冊しかないような貴重書を読みたい場合、安全のため図書館は貸し出しさせてくれず、そのかわりコピー機で必要最低限のページだけをコピーさせてくれる場合があるでしょう。関数の引数も、図書館のああいうのと似たような感じで、じつはコピーなのです。

関数の引数(ひきすう)は、じつは原則的には、すべてコピーなのです。(例外は、static変数を使う場合や、ポインタ操作をする場合。)

なぜなら、C言語において、「局所変数」(「ローカル変数」)という安全実現の機能を実現するためである。

「局所変数」(きょくしょ へんすう)とは、ある関数を呼び出したとき、呼び出された子サイド(呼び出し先)で、普通の方法で変数を変更しても、本体である親サイド(呼び出し元)の内容には変化を与えないという機能である。

C言語は局所変数の機能を実現するために、子サイドの関数を呼びだした時には、パソコンはまず、新しいアドレス内を言語処理系に与えており、それから親(呼び出し元)の引数(「実引数」という)のaの値とbの値とをそれぞれ新アドレスに代入するという方法により、子サイドの関数が呼び出された時にaやbの値をコピーしているのである。

このため、子(呼び出し先)の関数内(仮に関数fとする)で、通常の方法でaやbの値を変更しようが、親(呼び出し元)の関数(仮にgとする)で使われている同名の引数aとbのアドレスとは、親(呼び出し先)のアドレスとは別のアドレスなので、よって、子(呼び出し先)は、親(呼び出し元)の内容には、いっさい変化を与えないようにする事ができます。

このようにして、C言語において、局所変数(「ローカル変数」)の機能が実現されます。

このため、「return a;」のような返却値についても、ある関数内で変数が計算結果でa=6となった場合に、関数終了の返却値で「return a;」によって、aの数値を親(呼び出しもとの)関数に返却しても、返却値を受け取った側の親の関数は、単に「6」という数値を受け取ることになり、その数値6が変数aに由来する数値なのか、それとも変数bに由来する数値なのかは、受け取り側の親(呼び出し元)関数にとっては不明なのです。


  • 子(呼び出し先)関数で、親(呼び出し元)にある変数に影響を与える非局所な操作したい場合の対策

しかし、裏を返すと、C言語による「局所変数」機能の実現のための新アドレス準備・配当の仕組みのために、もし呼び出した子サイドの関数内で、親サイド(呼び出し元)の関数内にある値を変更しようとするとき、その実現がややこしくなってしまっている。

もし、子サイド(呼び出された)関数内で、親サイド(呼び出し元)の関数内にある同名の引数を変更したい場合には、下記の2つの方法があります。

static変数という、親子で共有する特殊な型の変数を宣言する方法。
アドレスの値を指定して引数の内容を変化させる方法。なお、アドレス指定を使用するには、ポインタ機能を使うことになる。

では、なぜ、ポインタやアドレスを使うと、呼び出し先関数で、呼び出し元の同名の引数を操作できるようになるのか? 証明は、以下の通り。

まず、パソコン視点から見れば、たとえポインタやアドレス機能を使って子サイドを呼び出したところで(たとえアドレスを実引数(親サイド関数の引数)として呼び出しところで、ポインタを仮引数(子サイド(呼び出し先)関数の引数)として呼び出したところで、)パソコンは、通常の関数呼び出しで引数を新アドレスにコピー代入するのと同様に、その引数にされたポインタの値やアドレスの値も新アドレスにコピー代入しようとするのである。(けっして、呼び出し方法時のコピー作業が、ポインタ使用時と非使用時で有無が変わるわけではない。)
なので、アドレスが引数にされた関数呼び出しのさいにも、パソコンは新アドレスに親(呼び出し元)引数のアドレス値をコピーする。
だが、関数呼び出しのさいに新アドレスに親サイド(呼び出し元)引数のアドレス値をコピーしたところで、けっして、ポインタに格納されている大もとの親サイド(呼び出し元)引数のアドレスの種類が増えるわけではない。
そのため、呼び出し先の関数のポインタに格納されているアドレスの値は、大もとの親サイド(呼び出し元)で使っている引数のアドレスの値と同じ値である。
なので、子サイド(呼び出し先)の関数のポインタにあるアドレス値をつかって、親サイド(呼び出し元)の引数のアドレスを操作できます。


このようにして、アドレスやポインタを使うことにより、子サイド(呼び出し先)の関数内で操作をした場合に、親サイドも含む範囲(子サイドの関数外)にも影響を与える非局所的な操作をすることができます。

「サブルーチン」との違い[編集]

C言語の関数のような「ローカル変数」のシステムは、他のプログラム言語(ベーシックなど)の「サブルーチン」には無いシステムです。ベーシックのプログラム言語に、C言語でいう「ポインタ」システムが無いのは、そもそもローカル変数のシステムがベーシックには無いからです。

このC言語の「ローカル変数」システムこそが、C言語の「関数」システムの真骨頂(しんこっちょう)なのです。

通常仕様のベーシックでサブルーチンを呼び出しても、けっして、ルーチン内で使用する変数に呼び出し前の変数とは別アドレスは与えられませんし、そもそもサブルーチン呼び出し時に「引数」のコピーをしませんし、そもそもサブルーチン呼び出しに「引数」は不要ですし、そもそも「引数」のような概念じたいベーシックのサブルーチンの仕様には無いのです。

ベーシックでのサブルーチンの呼び出しは、単に「GOTO 100」みたいにGOTO文で行指定して指定先にジャンプするかわりに、行番号100にルーチン名を書いておいて、ルーチン名でジャンプ先を指定できるようにしたものに過ぎません。

しかし、C言語では、そもそもGOTO文のようなジャンプ方法が、忌避(きひ)されています。


裏を返せば、C言語で、BASICサブルーチンのような引数もなく「ローカル変数」性もない処理ブロックのあつまりに、C言語で名前を与えようとしても(BASICではそれぞれのサブルーチンに名前をつける事ができるし、そもそもサブルーチンの呼び出し時に名前で呼び出し先を指定する)、そのような構文はC言語には無いのです。

例えるなら、C言語でいう「関数」とは、独立国の国王が領民のために国土を他国から独立して確保しているように、「関数」も領民である変数のためのアドレスを他関数から独立して確保している(正確にいうと、(必要に応じてアドレスを確保できる))、一国一城の主であり、国王さまなのです。


いっぽう、ベーシックでいうサブルーチンは、けっして一国一城の主ではなく、全員、部下です、手下です、サラリーマンです。では、ベーシックの「サブルーチン」は誰の配下かというと、「行番号」さん の手下です。ベーシックのサブルーチンは行番号さんの手下ですので、けっして独立して変数アドレスを持つ事は許されず、そのため、変数のアドレスはべーシックのそのプログラム全体で共有することになるので、そもそも「ローカル変数」というシステムが、ベーシックには無いのです。

C言語で、もし、「main関数の変数アドレスをすべて共有したサブルーチン的なものの処理単位の使用を宣言したいな〜」と思っても、そのような処理単位はC言語の文法には存在しない。一見すると、「main関数の変数アドレスをすべて共有したサブルーチン」は便利そうに思えるかもしれないが、しかし、C言語の開発者はおそらく「このような処理単位は、main関数の変数を誤って書き換えてしまい誤操作につながるだろう」と考えたのだろうか、ともかくC言語にはこのような「変数アドレス共有サブルーチン」(いま勝手に考えた造語)は無いのである。


「メソッド」[編集]

「関数」というが、C言語の関数(function)は、数学の関数(function)とは、まったく違う。

static 変数とポインタの必要性[編集]

このように、C言語の(ユーザーが自分で定義する)「関数」は、かなり特殊なシステムである。


// static変数の使用例(コンパイルできる)
#include <stdio.h>

void func(void) {
  int b = 0;
  static int c = 0;
  b = b + 1;
  c = c + 1;

  printf("b = %d ,c = %d \n\n", b, c);
}

int main(void) {
  func();
  func();
}
実行結果
b = 1 ,c = 1
b = 1 ,c = 2

のようになる。

このように、static宣言された変数(上の例ではc)は、その関数が終わっても、数値が保存されます。

また、初期化がされる(cに0が代入される)のは、最初に呼び出された1回だけである。

いったい何が「静か」(static)なのか不明であるが、おそらく、関数の再度の呼び出し時にも変数を初期化しないままでいるという意味で、「静か」なのだろう。


なお、下記のように書くと、エラーになる

// static宣言でエラーになる例1
#include <stdio.h>

void func(void) {
  int b = 0;
  static int c = 0;
  b = b + 1;
  c = c + 1;
}

int main(void) {
  func();
  printf("b = %d ,c = %d \n\n", b,
         c);  // このprintf関数がstatic宣言されたcにアクセスできない

  func();
  printf("b = %d ,c = %d \n\n", b,
         c);  // このprintf関数がstatic宣言されたcにアクセスできない
  return 0;
}

このように、static変数は、呼び出し元の関数は、存在を感知できない仕組みである。これによって、バグ発生時に原因箇所を特定しやすくしています。


また、以下のコードも、エラーになる。

// static宣言でエラーになる例2
#include <stdio.h>

void func(void) {
  b = b + 1;  // ここでエラー発生。bが何かを判断できない
  c = c + 1;
}

int main(void) {
  int b = 0;  // main関数で定義を行った場合
  static int c = 0;

  func();
  printf("b = %d ,c = %d \n\n", b,
         c);  // このprintf関数がstatic宣言されたcにアクセスできない

  func();
  printf("b = %d ,c = %d \n\n", b,
         c);  // このprintf関数がstatic宣言されたcにアクセスできない
}

コンパイルの仕組みでは、上から順に部品を組み立てていくような仕組みなので、コード上方にある関数はコード下方にある関数の内容を知らない。

たとえ、コード下にある関数がmain関数であっても、その内容を知りません。よって、上記のようなコードはコンパイルエラーになる。

かといって、funcをmain関数よりも下に書くと、今度はmain関数が、funcの内容を感知できません。

main関数といえども、main関数ブロックよりも下に書かれた内容を知ることはできません。

なので通常、main関数は一番下に書かれます。


なお、最初の「例1」で紹介した例では、main関数で使用される変数bとcの上にある関数funcで定義されてるので、順序的には問題ないのですが、しかしC言語では、アクセスできないようになっています。 staticかどうかに関わらずアクセスできません。 funcの終了時に、その関数の型などの情報も消えるので、エラー例1のコードでは変数bとcの宣言内容を発見できず、利用できません。


結局、冒頭に書いたコンパイル成功例のような書き方しか、ありません。


なお、static宣言した変数を、他の関数とも共有したい場合には、さらに「グローバル変数」という物を使います。

すべての関数ブロックの外の、コード前半部の部分(グローバル領域)で、変数宣言をすると、その変数は、すべての関数からアクセスできます。


※ 日本では、なぜか勘違い・生半可な理解で「グローバル変数はなるべく一切(いっさい)使わないで、保守性のために外部ファイルからアクセスできないように、ソフトウェア工学のカプセル化の手法を使うほうがイイ」という迷信があります。しかしこれはマチガイ・勘違いであり、証拠として文献『低レベルプログラミング』(Igor Zhirkov 著、古川邦夫 監訳)には、堂々と、カプセル化された部品(文献には「パーツ」とある)的なプログラムを呼び出す方法は「グローバル変数を使う」か「隠されていない一群の関数を使う」と書いてあります[3]。考えてみれば当然で、そもそも外部ファイルから何も変数や関数にアクセスできなければ、そもそもプログラムを起動できません。


さて、ともかく実用では、子サイド(呼び出し先)の関数で、親サイド(呼び出しもと)の変数の値も変更したい場合も多く、その場合には、「グローバル変数としての static 変数」(静的変数)もしくは「ポインタ」という機能をつかう。

グローバル領域でのstatic 変数は、その名前の変数は、どこの変数で呼び出されても、かつ共通のアドレスで管理しているので、呼び出した(子サイドの)関数の先で、親サイド(呼び出し元の)関数も書き換えできる機能をもつ。


さて、C言語の入門書での「関数」の章の前半に書いてあるプログラムで、static変数やグローバル変数やポインタを使わなくても利用できる(ユーザー定義)関数は、むしろ珍しいプログラムなのである。画面に「こんにちは!」などの文字などを表示するだけなら、static変数などを使わなくても、そのようなアルゴリズムを構築できるので、そのようなプログラムがC言語の入門書の『関数』の章に書かれるのであろう。

なお、「return a;」などのように戻り値をつかって 計算結果の数値を 親(呼び出しもと)の関数に送信したりする方法は、じつは、あまり実用的ではなく、現実には不便な場合が多いのである。

なぜなら、

まず、親(呼び出しもと)の どの変数に値を送るかを、子(呼び出し先)のreturn文からは指定できないし、(けっして、親(呼び出しもと)の変数aなどを指定して、数値を代入してくれるわけではない)
しかも、その関数が終了してしまうので、ほかにも処理をその関数で続行したい場合に不便であるし、
しかも、1つの数値しか送れない、

など、いろんな欠点があります。

また、returnはエラーの有無を判定結果を呼び出しもと関数に送信するときに使用することが、実務では多い。たとえば if文などで、想定外の事態がなければ0を返却するようにプログラムを記述して、いっぽう想定外があれば-1を返却する、などのように、返却値の値により、想定外の有無を送信することが、実務などで多い。

C言語の入門書で、「return a + b;」などのように、return文で計算結果を送信する記述が書かれてるのは、初心者の学習のための配慮(はいりょ)なのであり、じつは、あまり実務的ではないプログラムである。そもそも、わざわざ別関数で「a+b」を計算せずとも、最初からmain関数内で「a+b」を計算すれば済む。

また、入門書で、ポインタの章と関数の章が別々に書かれていることが多いのは、初心者のための配慮(はいりょ)なのであり、じつは、現実では、ポインタと関数を別々に考えるのは、あまり実用的ではないだろう。


ローカル変数とグローバル変数[編集]

//例 グローバル変数とローカル変数

int g; // gはグローバル変数

int main(void)
{
	int l; // lはローカル変数
}

C言語では通常、変数をある関数ブロックの内部で宣言した際、その変数は、その関数ブロックの中でしか通用しない。 ある関数ブロックの内側でしか通用しない変数をローカル変数と呼びます。いっぽう、(プログラム冒頭部(include文の直後など)などの)関数外で宣言した変数をグローバル変数と呼び、すべてのブロック内で通用する。

変数は宣言する場所によってローカル変数とグローバル変数とに分かれる。[4]

ともかく、関数ブロック内で宣言した変数は、原則的にすべてローカル変数になるので、その変数はその関数の中でしか通用しない。

ローカル変数の存在意義は、(誤って他のブロック内の変数を書き換えられてしまうような)バグを未然に防ぐためである。このように変数の仕組みを、その変数を宣言したブロック内でしか通用しないという仕組みにすることにより、他ブロックを書き換えてしまうようなバグを未然に防ぐ・・・というのがローカル変数の意義である。


たとえ、他の関数ブロックで同名の変数が使用されていようが、それぞれの関数ブロックごとに、変数のアドレスは別々である。つまり、人間に例えるなら、同姓同名の別人のようなものである。

なので、通常、ある関数ブロックの中で宣言した変数は、すべてローカル変数である。たとえmain関数内で変数を定義をしても、その(main関数ないで宣言された)変数ですら、main関数内でしか通用しない。

もし、ある関数が、他の関数と値のやりとりをしたい場合は、関数の返却値(「返し値」、「戻り値」ともいう)を仲介して、値を別関数ブロックに送る必要があります。


ローカル変数はその定義により、性質として、その変数を宣言したブロックからのみアクセスでき、 他のブロックで宣言したローカル変数とは一切関係がない(ブロック有効範囲)。[5]

  • static

通常のローカル変数は、関数ブロックが終了するときに、変数の結果が消去されます。そして、再度関数を呼び出したときは、変数の宣言時に変数が作成されたり、あるいは関数ブロックに入るときに作成され、そしてまた、その関数ブロックが終了するときに、消去されます。

この消去により、ある関数が別の関数の変数を書き換えないような仕組みになっている。しかし、このような関数では、もし、ある関数を複数回呼び出しても、前回使用した計算結果が消去されてしまっており、そのため、前回の計算結果を再利用するような作業が困難である。

たとえば、高校数学で習うような数列の漸化式のような計算ですら、困難になってしまう。(数列の項を、関数に見立てている。)


そこで、変数を再利用可能にすることのできる static スタティックという宣言があります。このstaticをつけると、たとえローカル変数でも、関数の終了時に、その変数の値が消去されずにメモリに残りつづけるようになるので、再度その関数を呼び出したときに、前回の(そのstaticのついた変数の)計算結果を再利用できます。

「static」とは「静的」などと訳され、表現はいかめしいが、しかしstaticの実態は「関数が終わっても値を保管しとけ」程度の実態でしかない。


記憶域クラス指定子staticを持たないローカル変数は、 ブロックに入るときに作成され、 ブロックが終了するときに破壊される(自動記憶域期間)。[6]


つまり、そのブロックを含む同じ関数が再び呼び出されても、staticをつけてないかぎり、 前の値は保存されていない。

また、初期化していないローカル変数の値は不定であり、その値をそのまま「参照」してはならない。 (値を代入した後になら「参照」してもよい。)

なお、「参照」という言葉の意味が、C言語では、日常用語とは微妙に違います。C言語でいう「参照」とは、日常語でいう「内容確認」みたいな意味のほかにも、「ブロック外にある変数への、遠隔アクセスと(変数書き換え などの)遠隔操作」みたいな意味があります。「いま居る関数ブロックの場所とは別の、他の関数ブロック内で宣言された変数について、その他ブロックの関数内で定義された変数に与えられたアドレス(変数の宣言時に自動的にアドレス番号が割り振られる)に格納された値の内容にアクセスし、必要ならば、遠隔操作で(= いま居る関数ブロックにいるままで)他ブロックの変数を書き換える」というような意味です。
もし、他ブロックの変数書き換えをしないで、単にアドレス内容を確認するためにアクセスした場合だけなら、日常語でいう「参照」と同じ意味になるわけです。


  • グローバル変数との違い

すべてのブロックからアクセスできる変数を「グローバル変数」と呼びます。

C言語プログラミングでは一般に、コードファイルの冒頭の、どこの関数ブロック内でもない場所で変数宣言をすることにより、グローバル変数として宣言されたことになり、どこの関数からでもアクセスできます。

しかし、その変数にアクセスできることと、各関数ブロックの実行時に領域を新規作成しないかは別のことである。

なので、グローバル変数といえども、各関数ブロックの計算結果を共有したい場合には、さらに宣言時に static を接頭辞につけて宣言する必要があります。


グローバル変数は全てのブロックからアクセスできる(ファイル有効範囲)。[5] グローバル変数はプログラムが開始されたときに作成され、 プログラムが終了したときに破壊される(静的記憶域期間)。[6] また、グローバル変数は、定数式でのみ初期化することができ、 明示的に初期化しない場合、0で初期化されます。

注意!
グローバル変数の乱用は避けるべきです。

理由は、どこからでもアクセスできることでバグの発見が困難になったり、
プログラムの実行中ずっと存在することでメモリの使用効率が落ちたりするからです。

なので、他の関数と計算結果を共有する必要のない変数であれば、なるべく、個別の関数の内部で変数宣言をするのが良い。

いっぽう、他の関数と計算結果を共通したい場合、どうしてもグローバル変数を使わざるを得ない場合も多い。(もし、グローバル変数を用いないと、代わりにポインタを使わざるを得ない場合があります。)

これはつまり、そもそも、複数の関数どうしでの変数の共有は、なるべく、ひかえる必要があります、という事である。


  • ローカル変数の寿命

関数ブロックの内部で宣言された変数を「ローカル変数」と言う。

ローカル変数は、その関数が終わるときに消えるが、正確に言うと、そのローカル変数を宣言したブロックが終了するときに、そのローカル変数も消去されます。[7]

ブロックとは、記号 { と } とで挟まれた処理単位である。

なので、関数内に、さらにブロックが入れ子になっていて、その入れ子内で宣言された変数は、その入れ子ブロックの終了時にその入れ子内にあった変数も消去されます。

if文やfor文の{}もブロックと見なされるので、注意が必要である。[8]


関数原型[編集]

main関数から呼び出される別の関数は、main関数よりも前で、存在が宣言されていなければならない。

そのため、main関数以外の関数は、main関数よりも前で、宣言されていなければならない。

しかし、main関数から呼び出される関数は、mainよりも前で宣言さえされていれば、たとえ具体的な定義内容がmain関数のあとで書かれていても、その関数をmain関数から呼び出して使用できます。そのため、以下のように、main関数から呼び出される関数の存在の宣言だけを先に行うという記法が許されており、この記法では、呼び出し先の関数の具体的な定義内容はmain関数のあとで定義する事が許されている。

このような関数の宣言方法のことを「関数のプロトタイプ宣言」などという。

(関数のプロトタイプ宣言の例)

//例 関数原型の使用例
#include <stdio.h>

double function(double a, double b);//関数原型

int main(void)
{
	printf("%f", function(0.12, 3.45));
}

//2つの浮動小数点数の和を求めるfunction関数
//関数原型があるおかげで、
//関数の使用の後に関数の定義を書くことができます。
double function(double a, double b)
{
	return a + b;
}

関数原型(関数プロトタイプ)とは、関数の宣言である。 ある関数が使われる前に、その関数名、引数、返却値を示し、その関数が使えるようにする。 関数原型を使うと、引数の個数とデータ型がチェックされます。

関数原型を使わなくても、関数が使われる前に、その関数の定義を書くことで、その関数を使うことができます。 だが、ソースコードが大規模になるとそれも難しくなる。 また、関数原型を使わず、関数が使われる前にその関数の定義を書かなかった場合、返却値は暗黙のうちint型とされ引数のデータ型もチェックされません(C11 では、関数宣言において型指定子を必須とし、暗黙的な型指定を禁じています)。

関数原型の記述は次のようになっている。

返却値のデータ型 関数名(引数のリスト);

引数のリストの記述は次のようになっている。

引数のデータ型 引数名

引数のリストには、引数がない場合はvoidのみを書く。 複数の引数がある場合、コンマで区切る。

関数原型の引数の有効範囲は、関数宣言子の最後で終了する(関数原型有効範囲)。 [9]


static変数を使う[編集]

(※ 未記述)

上述したとおり。

ポインタを使う方法[編集]

ポインタを使う方法である。しかし、ポインタを使えば、2つ以上の数値を操作する事ができるので、ポインタを使って、親(呼び出し元)関数の複数の値を操作する事ができるので、それを「複数の返却値である」と解釈する事もできる

このような事を、「ポインタによる参照渡しの引数を、返却値として用いる」などと言ったりするが、要するにポインタを使って親(呼び出し元)の変数の値を操作するというだけの事であり、あまり深い意味はない。

//例 ポインタを使って関数に複数の返却値を持たせる。
#include <stdio.h>

int function(int *a, int *b)
{
	*a = 1234;
	*b = 5678;
}

int main(void)
{
	int i,j;
	function(&i, &j);
	printf("iの値は%d、jの値は%d。", i, j);
}

構造体を使う方法[編集]

別の方法として、返却値のデータ型に構造体を使って、 その構造体で複数の返却値を表現する方法もある。

#include <stdio.h>
#include <string.h>

//例 構造体を使って関数に複数の返却値を持たせる。
typedef struct 
{
  int i;
  double d;
  char c;
  char str[32];
} sKouzoutai;

sKouzoutai function()
{
  sKouzoutai ret;
  ret.i = 1234;
  ret.d = 3.14;
  ret.c = 'a';
  strcpy(ret.str, "Hello. World!");
  return ret;
}

int main(void)
{
  sKouzoutai kouzoutai;
  kouzoutai = function();
  printf("kouzoutaiのメンバの値は、%d %f %c %sです。\n", kouzoutai.i, kouzoutai.d, kouzoutai.c, kouzoutai.str);
  return 0;
}

しかし、関数の返却値は、エラー判別などに利用される場合も多いので(関数によって「エラーなら返却値に 0 以外を返却する」などの仕様のある場合も多い)、よって上述の手法は、あまりオススメできません。

なお、strcpyは、文字列をコピーするための関数。strcpyを使うには、string.hのインクルードが必要である。

swap関数[編集]

aとbとの値を入れ替えるswap関数を考える。下記コードは誤った例である。

//例 誤ったswap関数
#include <stdio.h>

// グロ-バル変数を使って、交換用の変数をswap外部と共有しようと意図しているが・・・
int x;
int a;
int b;

void swap(int a, int b)
{
	x = a;
	a = b;
	b = x;
}

int main()
{
	a = 1;
    b = 2;
	printf("swap前のa=%d, b=%d\n", a, b);
	swap(a, b);
	printf("swap後のa=%d, b=%d\n", a, b);
}


この例は意図通りに動作しない。結果は

swap前のa=1, b=2
swap後のa=1, b=2

と表示され、なにも交換されていない。

こう失敗する原因は、呼び出され側のswap()の引数で用いられている値を格納する変数(上記コード例の swap(int a,int b) のaおよびb )によって、グローバル変数の a ,b が隠されてしまっているからである。 たとえグローバル領域で「a」「b」と同名の変数があっても、swap関数内では同名なだけの別変数としての仮引数「a」「b」が新たに関数swap内では用意されてしまう。

この様に、外部のスコープのインスタンスがより内側のスコープで同じ識別子を持つインスタンスによって隠されてしまう事を、シャドーイング(Shadowing)と呼びます。

ユーザ定義関数内で変数の値を交換させるためには、一例として、下記のようにポインタを使ってmain関数の変数a,bを書き換える方法があります。

//例 正しいswap関数
#include <stdio.h>

void swap(int *a, int *b)
{
	int x = *a;
	*a = *b;
	*b = x;
}

int main()
{
	int a = 1, b = 2;
	printf("swap前のa=%d, b=%d\n", a, b);
	swap(&a, &b);
	printf("swap後のa=%d, b=%d\n", a, b);
}

この例は意図通りに動作する。

swap前のa=1, b=2
swap後のa=2, b=1

と表示されます。

ポインタを使うと成功する理由は、main関数のローカル変数のアドレスが仮引数にコピーされたため、swap関数がmain関数のローカル変数a,bにアクセスできているからである(ポインタを介したswap関数内のa,bはもはやローカル変数だけでなくグローバル変数にもアクセスできる)。



ユーザ定義関数を何も書かずに全てmain関数に書く方法を使えば、ポインタを使わずとも交換できます。

標準C言語では考える必要は無いが、別のプログラム言語だと、言語によってはポインタが標準設定では使用禁止されていたり(たとえばC#がそうである)、そもそもポインタに相当する機能の無い言語も考えられる[10]。そのような場合でも、下記コードのようなアルゴリズムで、変数の交換は可能である。

//例 swap関数を使わない場合
#include <stdio.h>

int main()
{
	int a = 1, b = 2;
	printf("swap前のa=%d, b=%d\n", a, b);
	
	int x = a;
	a = b;
	b = x;
		
	printf("swap後のa=%d, b=%d\n", a, b);
}
swap前のa=1, b=2
swap後のa=2, b=1

この処理を、マクロを使って一般化すると

//例 マクロを使った場合
#include <stdio.h>
#define swap_int(a,b) do {int __temp=a;a=b;b=__temp;}while(0)
int main()
{
	int a = 1, b = 2;
	printf("swap前のa=%d, b=%d\n", a, b);
	
	swap_int(a,b);
		
	printf("swap後のa=%d, b=%d\n", a, b);
}

※ 一度も反復しないdo { ... } while(0)は、変数__temp のスコープを切るため。

swap前のa=1, b=2
swap後のa=2, b=1
新たに変数を用意せず2つの変数を入れ替える
#include <stdio.h>

int main()
{
	int a = 1, b = 2; /* a の初期値を A、b の初期値を B として... */
	printf("swap前のa=%d, b=%d\n", a, b);
	a ^= b;           /* a = A ^ B */
    b ^= a;           /* b = B ^ A ^ B ... A */
	a ^= b;           /* a = A ^ B ^ A ... B */
	printf("swap後のa=%d, b=%d\n", a, b);
}

は、排他的論理和演算子。

結果

swap前のa=1, b=2
swap後のa=2, b=1

関数の引数として配列を渡す[編集]

関数の引数として配列を渡すには、 先頭要素へのポインタを渡せばよい。 [11]

//例 関数の引数として配列を渡す
#include <stdio.h>

int sum(int *array, int size)
{
	int s = 0;
	for(int i = 0; i < size; ++i)
		s += array[i];
	return s;
}

int main(void)
{
	int a[10] = {2,3,5,7,11,13,17,19,23,29};
	int s = 0;
	s = sum(a, 10);	

	printf("%d\n", s);
}


ブロックスコープ[編集]

複合文( Compound statement )は、ブロックとも呼ばれ、宣言や文を1つの構文単位にまとめたものです。自動保存期間を持つオブジェクトの初期化子や、ブロックスコープを持つ通常の識別子の可変長配列宣言子は、宣言が実行順に到達するたびに、あたかも文のように評価されてオブジェクトに値が格納され(初期化子のないオブジェクトに不確定な値が格納されることも含む)、各宣言内では宣言子が現れる順に評価されます[12]

脚註・出典など[編集]

  1. ^ 引数の省略はC23で廃止予定で、voidを明示する必要があります。
  2. ^ 矢沢久雄、原田英生『日経 BP パソコンベストムック C言語とC++がわかる本』日経ソフトウェア、2013年5月15日発行、096項
  3. ^ Igor Zhirkov 著、古川邦夫 監訳『低レベルプログラミング』、翔泳社、2018年01月19日 初版 第1刷 発行、310ページ
  4. ^ ローカル変数とグローバル変数という用語はよく使われるが、『JISX3010:2003』には出てこない。
  5. ^ 5.0 5.1 『JISX3010:2003』p.21「6.2.1 識別子の有効範囲」
  6. ^ 6.0 6.1 『JISX3010:2003』p.24「6.2.4 オブジェクトの記憶域期間」
  7. ^ ウェブページ『関数内で寿命が尽きる変数 』 http://9cguide.appspot.com/12-01.html
  8. ^ ウェブページ『ロベールのC++教室 - 第68章 寿命 -』 http://www7b.biglobe.ne.jp/~robe/cpphtml/html01/cpp01068.html
  9. ^ 『JISX3010:2003』p.21「6.2.1 識別子の有効範囲」
  10. ^ 最近は Fortran ですらポインタがあるのでポインタのない言語は非常に限られ、ポインタがなくてもVariant型に類する型がある場合が多い。
  11. ^ 前橋和弥著『C言語 ポインタ完全制覇』p.068 平成13年6月25日初版第4刷発行
  12. ^ N2596 working draft — December 11, 2020 ISO/IEC 9899:202x (E). ISO/IEC JTC1/SC22/WG14. p. 122, §6.8 Statements and blocks. http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2596.pdf. 

参考文献[編集]

  • 日本工業標準調査会『JISX3010 プログラム言語C』2003年12月20日改正