C LANGUAGE TECHNOLOGY

【C言語】可変長引数"..."とは

悩んでいる人

C言語の可変長引数を教えて!

こういった悩みにお答えします.

本記事の信頼性

  • リアルタイムシステムの研究歴12年.
  • 東大教員の時に,英語でOSの授業.
  • 2012年9月~2013年8月にアメリカのノースカロライナ大学チャペルヒル校コンピュータサイエンス学部2021年の世界大学学術ランキングで20位)で客員研究員として勤務.C言語でリアルタイムLinuxの研究開発
  • プログラミング歴15年以上,習得している言語: C/C++Solidity,Java,Python,Ruby,HTML/CSS/JS/PHP,MATLAB,Assembler (x64,ARM).
  • 東大教員の時に,C++言語で開発した「LLVMコンパイラの拡張」,C言語で開発した独自のリアルタイムOS「Mcube Kernel」GitHubにオープンソースとして公開

こういった私から学べます.

可変長引数

可変長引数とは,関数やマクロの引数の個数が固定ではなく任意の個数となっている引数のことです.

これまでに何気なくprintf関数やscanf関数を利用してきましたよね.

これらの関数の引数が不定個(可変長)であることに疑問に思いませんでしたか.

関数のプロトタイプ宣言では,関数の実引数と仮引数の個数と型が厳密にチェックします.

したがって,可変長引数の関数は作れないはずです.

可変長引数を取り扱うためには,関数,引数のスタックへの積み方,マクロ,ポインタ等の正確な基礎知識が必要になります.

これらを順に説明し,実際の可変長引数の手法について説明します.

それでは最初に関数の利用方法について簡単に復習しましょう.

関数のプロトタイプ宣言により,コンパイル時に関数の戻り値以外に引数のチェックを行います.

つまり,引数の並びと型の対応を厳密にチェックします.

実引数と仮引数の個数が違う場合はエラーになります.

型のみが違っていた場合,キャスト可能ならキャストし,キャスト不可能ならエラーになります.

例えば,仮引数がdouble型で実引数がdouble型のポインタならばエラーになります.

この機構により,コンパイル時に引数の個数や型の違いによるエラーを発見できます.

まず,それぞれの書式について簡単に復習するために,int型の値を2つ受け取り,その和を求めるadd関数を作成しましょう.

このadd関数のプロトタイプ宣言は,このように関数の戻り値と引数の型をすべて指定します.

関数のプロトタイプ宣言は,以下のように引数名を省略して記述しても構いません.

コンパイラがチェックするのは引数の並びと型の対応なので,上記のように書くこともできます.

しかし,意味がわかりやすいように名前付けした引数名を書いて,関数プロトタイプ宣言をすることをおすすめします.

プロトタイプ宣言により,この宣言以降では,この関数の戻り値及び引数の並びと型の対応がチェックされます.

add関数を利用するコードは以下になります.

実行結果は以下になります.

可変長引数の関数

"..."とは:printf/scanf関数のプロトタイプ宣言で解説

manコマンドでprintf/scanf関数の表記を確認してみましょう.

プロトタイプ宣言の第2引数が"..."となっていますね.

また,/usr/include/にあるstdio.hの中にあるprintf/scanf関数のプロトタイプ宣言は以下になります.

※__restrictキーワード等で少し異なる表記になっています.

上記の”, ...”が,可変長引数の記述法です.

この”, ...”の部分以降は,引数のチェックを行わない(引数の個数や型が何でもよい)ということを意味します.

また,”, ...”は1つ以上の引数の後に書くという決まりがあります.

可変長引数のプロトタイプ宣言と文法エラーの例

例えば,以下のf関数の第1引数はint型で,その後の引数はチェックしません.

また,以下のg関数の第1引数はint型,第2引数はdouble型で,その後の引数はチェックしません.

以下のh関数は,C言語では文法エラーになりますので注意しましょう.

h関数を利用するコード例は以下になります.

コンパイルの結果は以下になります.

"..."の前に名前入りの引数が要求されている旨のエラーメッセージが表示されます.

ここで,C++言語ではh関数は文法エラーにならず,オーバーロードできる関数がない場合に呼び出されるデフォルト関数になります.

いわゆるswitch文のdefaultのようなものです.

C++のコンパイラg++でコンパイルした結果は以下になります.正常にコンパイルできました.

引数の渡し方

可変長引数のアクセスを理解するためには,C言語での引数の渡し方を理解する必要があります.

引数は,通常はスタックを利用してやりとりします.

アセンブリ言語を勉強したことがある人には馴染み深いかもしれませんが,そうでない人が多いと思います.

スタックとは,後入れ先出しのバッファのことです.

メモリ上にスタック領域という作業領域を確保して,その領域を利用して値を受け渡しします.

後入れ先出しというのは,本を積み上げる(スタックする)ようなものです.

本を積み上げていくと,取り出す時には上から順番に,つまり最後に重ねた本から順番に取り出しますよね.

スタックにデータを積み上げることをpush(プッシュ),スタックからデータを取り出すことをpop(ポップ)といいます.

変数aの値をスタックにプッシュする表記は以下になります.

スタックから変数aに値をポップする表記は以下になります.

ただし,x64では引数の個数が6個以内であれば引数は以下のレジスタに設定されます.

  • %edi:第1引数
  • %esi:第2引数
  • %edx:第3引数
  • %ecx:第4引数
  • %r8d:第5引数
  • %r9d:第6引数

第7引数以降は,push命令で引数はスタックに積まれて,pop命令で引数を取り出します.

可変長引数における暗黙のキャスト(型変換)

可変長引数”, ...”の渡し方を解説します.

可変長引数の場合は,仮引数の型がわからないので,コンパイラは実引数をスタックに積む時にどんな型でプッシュすればよいのかわかりません.

そこで,可変長引数の場合は,暗黙のキャスト(型変換)規則が適用されます.

つまり,整数型はint型もしくはlong型になり,浮動小数点型はdouble型にキャストされます.

実引数から関数に渡される暗黙のキャストは下表になります.

実引数の型関数に渡される型
charint
unsigned charunsigned int
shortint
unsigned shortunsigned int
intint
unsigned intunsigned int
longlong
unsigned longunsigned long
floatdouble
doubledouble
long doublelong double
ポインタ型ポインタ型

stdarg.h

可変引数の関数を作成するためには,まずstdarg.hをインクルードする必要があります.

stdarg.hはSTanDard ARGument Headerの略で,可変引数の関数を作成するための関数がプロトタイプ宣言されています.

”, ...”の部分の実引数の操作は,このヘッダファイル内の関数を利用しています.

manコマンドでstdarg.hのプロトタイプ宣言されている関数を確認できます.

va_start,va_arg,va_end,va_copyの4つの関数は,“, …”の部分の実引数にアクセスするために利用します.

ただし,現在のGCCでは/usr/include/ディレクトリにstdarg.hヘッダファイルは存在せず,コンパイラのビルトインとして実装されています.

※これらの関数は便宜上stdarg.hでプロトタイプ宣言されていると言いますが,実際にはマクロです.

また,これらの関数で利用する引数へのポインタを宣言するためのva_list型を利用します.

va_list型は以下のようにtypedefで定義することが多いです.

va_start関数は,apを初期化し,va_arg関数とva_end関数で利用できるようにします.

va_arg関数は,呼び出し時に指定された引数のうち,次の位置にあるものを指定した型typeの値として取得します.

va_end関数は,va_start関数の終了処理をします.(たいていはapをNULLに設定します.)

va_copy関数は,C99で導入された関数で,(初期化済みの) 可変長引数リストsrcをdestにコピーします.

typedefを学びたいあなたはこちらからどうぞ.

C言語 typedef
【C言語】typedefとは

こういった悩みにお答えします. こういった私から学べます. 目次1 typedef2 typedefで定義されている標準ライブラリのデータ型2.1 stdio.h2.2 stddef.h2.3 std ...

続きを見る

可変長引数を利用するコード例

可変長引数を利用するコード例として,vlaadd関数とvlastddev関数を紹介します.

vlaadd関数

vlaadd関数では,任意の個数のint型の整数の足し算をして,その合計値を返します.

第1引数に足し算をする要素数を指定し,その後に実際に足し算の要素を並べます.

実行結果は以下になります.

vlastddev関数

vlastddev関数では,任意の個数のint型の整数の標準偏差を返します.

28行目でva_copy関数を利用しています.

va_arg関数は呼び出した後に次のデータを読み込む仕様です.

もしva_arg関数を1度のforループで2度呼び出す場合,正しく標準偏差を計算できません.

そこで,41~42行目でva_arg関数の戻り値をtmpに格納して,その平均値との差の2乗を計算します.

実行結果は以下になります.

可変長引数に自作コードでアクセス

可変長引数に自作コードでアクセスするmyvlaadd関数は以下になります.

myvlaadd関数は簡易的な実装なので,以下の制限があります.

将来的には改良するかもしれませんが,ひとまず参考にして下さい.

  • x64のアセンブリ言語を利用していること(AARCH64では実行不可)
  • スタックからマジックナンバを利用して引数を取り出していること(引数の個数は8個,可変長引数の個数は7個が上限)
  • コンパイラの種類やオプションによってはスタックの位置がずれるので正常に動作しない可能性があること
    • GCCのオプションなしでは動作しますが,オプションありでは正常に動作しない可能性があります.
    • Clangではコンパイルできますが,正常に動作しません.
    • Visual Studioでは,GCC拡張のインラインアセンブラを利用しているので,コンパイルできません.

実行結果は以下になります.

限定的ですが,正常に動作していることがわかります.

まとめ

C言語の可変長引数を紹介しました.

具体的には,printf/scanf関数で利用されている可変長引数"..."の意味と,stdarg.hでプロトタイプ宣言されている関数の使い方がわかりました.

また,x64で可変長引数に自作コードでアクセスする処理を実装しました.

printf/scanf関数の自作を知りたいあなたは,以下の記事に詳しく書いてあります.

C言語 printf関数 自作
【C言語】printf関数の自作「myprintf関数」

こういった悩みにお答えします. こういった私から学べます. 目次1 C言語でprintf関数の自作「myprintf関数」2 printf関数の自作「myprintf関数」の作成ルール3 myprin ...

続きを見る

C言語 scanf関数 自作
【C言語】scanf関数の自作「myscanf関数」

こういった悩みにお答えします. こういった私から学べます. 目次1 C言語でscanf関数の自作「myscanf関数」2 scanf関数の自作「myscanf関数」の作成ルール3 myscanf関数の ...

続きを見る

C言語を独学で習得することは難しいです.

私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.

友だち追加

独学が難しいあなたは,C言語を学べるおすすめのオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!

-C LANGUAGE, TECHNOLOGY
-, , , , , , , ,