関数の基本編集

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 myFunc(int a, int b)
{
	return a + b;
}

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

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

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


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

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



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

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

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


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

関数を呼び出すとき、 引数と戻り番地(リターンアドレス)がスタックに格納されます[2]。 また、返却値がレジスタや予め呼び出し側の関数が用意した領域に格納されます[3]

引数と返却値編集

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

//例 引数と返却値をもたない関数
#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言語の関数のような「関数スコープの変数」のシステムは、他のプログラム言語(BASICなど)の「サブルーチン」には無いシステムです[4]。BASICのプログラム言語に、C言語でいう「ポインタ」システムが無いのは、そもそも関数スコープの変数のシステムがBASICには無いからです。

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

構造化以前のBASICでサブルーチンを呼び出しても、ルーチン内で使用する変数に呼び出し前の変数とは別アドレスは与えられませんし、そもそもサブルーチン呼び出し時に「引数」のコピーをしませんし、そもそもサブルーチン呼び出しに「引数」は不要ですし、そもそも「引数」のような概念じたいBASICのサブルーチンの仕様には無いのです[4]

BASICでのサブルーチンの呼び出しは、単に「GOSUB 100」の形式で100行目に処理を移し、RETURNで呼び出した行の次の行(GOSUBの次の行)から処理を続行します。


「メソッド」編集

「関数」というが、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 著、古川邦夫 監訳)には、堂々と、カプセル化された部品(文献には「パーツ」とある)的なプログラムを呼び出す方法は「グローバル変数を使う」か「隠されていない一群の関数を使う」と書いてあります[5]。考えてみれば当然で、そもそも外部ファイルから何も変数や関数にアクセスできなければ、そもそもプログラムを起動できません。


さて、ともかく実用では、子サイド(呼び出し先)の関数で、親サイド(呼び出しもと)の変数の値も変更したい場合も多く、その場合には、「グローバル変数としての 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はmain関数の関数スコープな変数
}

C言語では通常、変数をある関数ブロックの内部で宣言した際、その変数は、その関数を抜けると参照できなくなります。 ある関数でしか通用しない変数を関数スコープの変数と呼びます。 これに対して、関数外で宣言した変数をグローバルスコープな変数と呼びプログラム中どこからでも参照できます。

関数スコープな変数は、関数ブロックを抜けると参照できなくなります。 そして、再度関数を呼び出したときには、以前の同名の変数の値は引き継ぎません。

用途によっては、この性質は好ましくなくグローバルスコープの変数の様にプログラムが実行中は値が永続してほしいことがあります。

そこで、関数スコープな変数の値を永続化する「記憶域クラス指定子static」があり、変数宣言に前置します。前の値は保存されていない。

スコープ

変数(などの識別子)の通用範囲のことをスコープと呼びます。 スコープは関数の本体を示す { から } までが代表的ですが、ブロック({ から } まで)もブロックスコープでブロック内で宣言された変数は、ブロックを抜けると参照できなくなります。 また、for文の第一項で宣言された変数のスコープは(たとえ { } で囲まれていなくても)もfor文がスコープになります。

関数原型編集

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のみを書く。 複数の引数がある場合、コンマで区切る。

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


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#がそうである)、そもそもポインタに相当する機能の無い言語も考えられる[7]。そのような場合でも、下記コードのようなアルゴリズムで、変数の交換は可能である。

//例 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


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

関数の引数として配列を渡すには、配列変数名を引数とする(配列変数名は配列の先頭要素へのポインタ)。

関数の引数として配列を渡す
#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 = sum(a, sizeof a / sizeof *a);
  printf("%d\n", s);
}
実行結果
129

ブロックスコープ編集

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

文字列型を引数にする関数編集

脚註・出典など編集

  1. ^ 引数の省略はC23で廃止予定で、voidを明示する必要があります。
  2. ^ ARMアーキテクチャの様にリンクレジスタに戻り番地を記憶するアーキテクチャもあります。
  3. ^ 引数・返却値・戻り番地をどの様に渡したり返したりするかはアーキテクチャごとに異なり、ABI(application binary interface)で定められています。
  4. ^ 4.0 4.1 Full BASIC(ISO/IEC 10279)などの構造化BASICではサブルーチンや関数のなかでローカル変数を使用できます。
  5. ^ Igor Zhirkov 著、古川邦夫 監訳『低レベルプログラミング』、翔泳社、2018年01月19日 初版 第1刷 発行、310ページ
  6. ^ 『JISX3010:2003』p.21「6.2.1 識別子の有効範囲」
  7. ^ 最近は Fortran ですらポインタがあるのでポインタのない言語は非常に限られ、ポインタがなくてもVariant型に類する型がある場合が多い。
  8. ^ 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日改正