見出し画像

C言語教室 第38回 関数を呼ぶ手順

さて、今回は少し脱線気味の話題です。C言語で「関数を呼ぶ」という時に、実際にはどんな処理をしているのかを追ってみます。
まずC言語では関数呼び出しの際の引き数を評価する(コピーする)順序は決まっていません。実際に以下のような「副作用」(評価することで変数の値が変化してしまうこと)のある関数呼び出しを行うと、処理系によって結果が変わります。

#include <stdio.h>

void func(int i, int j) {
  printf("%d,%d\n", i, j);
}

void main() {
  int x = 0;
  func(x++, x++);
}

これを実行するとブラウザ環境では

0,1

が表示されますが、x86のgccでは

1,0

と表示されます。つまり引き数の左から評価されるか、右から評価されるかが異なっているのですね。同様に

#include <stdio.h>

void func(int i, int j) {
  printf("%d,%d\n", i, j);
}

void main() {
  int x = 0;
  func(++x, ++x);
}

は、ブラウザ環境では

1,2

ですが、gccでは

2,2

となります。前置インクリメントは評価に先立ってインクリメントが実行されているのですね。

演算子の前置きと後置きについて

つまり「副作用のある式を関数を呼び出す時に使うな」ということではあるのですが、同じ処理系を使っている限りにおいては、これが発覚することはなく(エンディアンとかもそうですが)、異なる処理系へ移植する時にいつもトラブルが起きるところでもあります。

未規定の動作

古のUNIX時代からのC言語の関数呼び出しは、最期の引き数から順に評価されてスタックに積んで、呼ばれた関数は最初の引き数から順に取り出すというルールしかなかったような気がします。もし引き数が余っても無視されるし、関数が終わればスタックは呼び出し前の状態に戻るので影響はないということになっていたはずです。ただし呼ばれた関数で積んだ引き数よりも多くの変数を取り出すと「何が入っているかわからない」のは確かで、この値に代入したりすると、それこそ恐ろしいことが起こった記憶もあります。何せその時代は引き数のチェックは自己責任だったもので。

これと違うのがPASCALで、こちらは最初の引き数から評価されて、引き数の数は予め確定している必要がありました。どうしてこんなことがわかっていたのかというと、大抵の高級言語は、それだけでシステム周りの処理ができないことも多く、アセンブラなりで自分で関数を書いて呼び出すということが普通に行われていたからです。ですから関数呼び出しでは、どのレジスタに何を入れてというアセンブラ・レイヤでの知識が必要でした。

OSのAPI呼び出しや、ライブラリとしてコンパイル済みの関数を呼び出すときには、必ずしも使っている処理系と同じ方法で引き数を受け取ってくれるとは限りません。そこでプロトタイプ宣言に、どの手順で引き数を処理しているのかを書くことができるように拡張されることになりました。

呼出規約

もちろん、これらの宣言はアンダースコアで始まるキーワードであることから分かる通り、C言語自身で規定されているものではなく、処理系依存です。ただ多くの処理系でサポートされており、広く使われているのはヘッダファイルを開いてみればよく分かるとは思います。ただキーワードがどのように評価されるかは、コンパイルオプションによっても変化するのと、よくよく見ると、先頭のアンダースコアがひとつであったりふたつであったり、似ているけど違うキーワードが混ざっているので、プリプロセッサのマクロが複雑に絡み合って定義されていることも見て取れるかもしれません。

異なる処理系で作成されたライブラリを呼び出すには、この定義を駆使して目的の形式になるように適切なキーワードを「調整」することが求められているのです。これを確認することで引き数が右から評価されるのか左から評価されるのかは、ほぼ決まります。もっとも最近は、このキーワードも増えていて、正しく理解するにはCPUのレジスタの構成であったり、メモリ管理ルールまで理解する必要も出てくるなど、なかなか厳しいものがあります。

#ifndef _EXFUN
# define _EXFUN(N,P) N P
#endif
int _EXFUN(printf, (const char *, ...));

gcc(標準)のstdio.hにあるprintf付近の例

_CRTIMP int mingw_stdio_redirect(printf)(const char *, ...);

windows mingw の stdio.h にあるprintfはもっと複雑

ここまで追う必要が出ることは普通は滅多にないのですが、DLLやシェアードライブラリを作ったり、組み込み系では特定のルールに従う必要が出るので、プロトタイプ宣言に謎の文字が出てきたら、きっとこういうことが書いてあるんだなとだけは覚えておいてください。

なお、extern “C” というワードを見ることもあるかもしれませんが、これはC言語とC++が混在しているヘッダファイルで、この関数は「C形式の名前付けルールなんだよ」という意味です。CとC++は良く混ぜて使われるので、こんなところにも顔を出すことがあるんですね。

期待されている方には申し訳ないのですが、今回も課題はありません。次回は、宣言文と実行文と初期化について触れるつもりです。

ヘッダ画像は、いらすとや さんよりhttps://www.irasutoya.com/2015/09/blog-post_73.html

#C言語 #プログラミング講座 #関数呼び出し #副作用 #プロトタイプ宣言 #ヘッダファイル #呼出規約

この記事が気に入ったらサポートをしてみませんか?