C言語の可変長引数を教えて!
こういった悩みにお答えします.
本記事の信頼性
- リアルタイムシステムの研究歴12年.
- 東大教員の時に,英語でOS(Linuxカーネル)の授業.
- 2012年9月~2013年8月にアメリカのノースカロライナ大学チャペルヒル校(UNC)コンピュータサイエンス学部で客員研究員として勤務.C言語でリアルタイムLinuxの研究開発.
- プログラミング歴15年以上,習得している言語: C/C++,Python,Solidity/Vyper,Java,Ruby,Go,Rust,D,HTML/CSS/JS/PHP,MATLAB,Assembler (x64,ARM).
- 東大教員の時に,C++言語で開発した「LLVMコンパイラの拡張」,C言語で開発した独自のリアルタイムOS「Mcube Kernel」をGitHubにオープンソースとして公開.
- 2020年1月~現在はアメリカのノースカロライナ州チャペルヒルにあるGuarantee Happiness LLCのCTOとしてECサイト開発やWeb/SNSマーケティングの業務.2022年6月~現在はアメリカのノースカロライナ州チャペルヒルにあるJapanese Tar Heel, Inc.のCEO兼CTO.
- 最近は自然言語処理AIとイーサリアムに関する有益な情報発信に従事.
- (AI全般を含む)自然言語処理AIの論文の日本語訳や,AIチャットボット(ChatGPT,Auto-GPT,Gemini(旧Bard)など)の記事を50本以上執筆.アメリカのサンフランシスコ(広義のシリコンバレー)の会社でプロンプトエンジニア・マネージャー・Quality Assurance(QA)の業務委託の経験あり.
- (スマートコントラクトのプログラミングを含む)イーサリアムや仮想通貨全般の記事を200本以上執筆.イギリスのロンドンの会社で仮想通貨の英語の記事を日本語に翻訳する業務委託の経験あり.
こういった私から学べます.
C言語を独学で習得することは難しいです.
私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.
私のキャパシティもあり,一定数に達したら終了しますので,今すぐ追加しましょう!
独学が難しいあなたは,元東大教員がおすすめするC言語を学べるオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!
目次
可変長引数とは
可変長引数とは,関数やマクロの引数の個数が固定ではなく任意の個数となっている引数のことです.
これまでに何気なくprintf関数やscanf関数を利用してきましたよね.
これらの関数の引数が不定個(可変長)であることに疑問に思いませんでしたか.
関数のプロトタイプ宣言では,関数の実引数と仮引数の個数と型が厳密にチェックします.
したがって,可変長引数の関数は作れないはずです.
可変長引数を取り扱うためには,関数,引数のスタックへの積み方,マクロ,ポインタ等の正確な基礎知識が必要になります.
これらを順に説明し,実際の可変長引数の手法について説明します.
それでは最初に関数の利用方法について簡単に復習しましょう.
関数のプロトタイプ宣言により,コンパイル時に関数の返り値以外に引数のチェックを行います.
つまり,引数の並びと型の対応を厳密にチェックします.
実引数と仮引数の個数が違う場合はエラーになります.
型のみが違っていた場合,キャスト可能ならキャストし,キャスト不可能ならエラーになります.
例えば,仮引数がdouble型で実引数がdouble型のポインタならばエラーになります.
この機構により,コンパイル時に引数の個数や型の違いによるエラーを発見できます.
まず,それぞれの書式について簡単に復習するために,int型の値を2つ受け取り,その和を求めるadd関数を作成しましょう.
このadd関数のプロトタイプ宣言は,このように関数の返り値と引数の型をすべて指定します.
関数のプロトタイプ宣言は,以下のように引数名を省略して記述しても構いません.
1 |
int add(int, int); |
コンパイラがチェックするのは引数の並びと型の対応なので,上記のように書くこともできます.
しかし,意味がわかりやすいように名前付けした引数名を書いて,関数プロトタイプ宣言をすることをおすすめします.
プロトタイプ宣言により,この宣言以降では,この関数の返り値及び引数の並びと型の対応がチェックされます.
add関数を利用するコードは以下になります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> int add(int a, int b); int main(void) { int a, b; printf("Please input two integers: "); scanf("%d%d", &a, &b); printf("%d + %d = %d\n", a, b, add(a, b)); return 0; } int add(int a, int b) { return a + b; } |
実行結果は以下になります.
1 2 3 4 |
$ gcc add.c $ a.out Please input two integers: 2 3 2 + 3 = 5 |
可変長引数の関数
"..."とは:printf/scanf関数のプロトタイプ宣言で解説
manコマンドでprintf/scanf関数の表記を確認してみましょう.
プロトタイプ宣言の第2引数が"..."となっていますね.
1 2 3 4 5 6 7 8 |
$ man 3 printf ... int printf(const char *format, ...); ... $ man 3 scanf ... int scanf(const char *format, ...); ... |
また,/usr/include/にあるstdio.hの中にあるprintf/scanf関数のプロトタイプ宣言は以下になります.
※__restrictキーワード等で少し異なる表記になっています.
1 2 |
extern int printf (const char *__restrict __format, ...); extern int scanf (const char *__restrict __format, ...) __wur; |
上記の”, ...”が,可変長引数の記述法です.
この”, ...”の部分以降は,引数のチェックを行わない(引数の個数や型が何でもよい)ということを意味します.
また,”, ...”は1つ以上の引数の後に書くという決まりがあります.
可変長引数のプロトタイプ宣言と文法エラーの例
例えば,以下のf関数の第1引数はint型で,その後の引数はチェックしません.
1 |
int f(int a, …); |
また,以下のg関数の第1引数はint型,第2引数はdouble型で,その後の引数はチェックしません.
1 |
int g(int a, double b, …); |
以下のh関数は,C言語では文法エラーになりますので注意しましょう.
1 |
int h(…); |
h関数を利用するコード例は以下になります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> int h(...); int main(void) { return 0; } int h(...) { return 0; } |
コンパイルの結果は以下になります.
"..."の前に名前入りの引数が要求されている旨のエラーメッセージが表示されます.
1 2 3 4 5 6 7 |
$ gcc h.c h.c:7:7: error: ISO C requires a named argument before '...' 7 | int h(...); | ^~~ h.c:14:7: error: ISO C requires a named argument before '...' 14 | int h(...) | |
ここで,C++言語ではh関数は文法エラーにならず,オーバーロードできる関数がない場合に呼び出されるデフォルト関数になります.
いわゆるswitch文のdefaultのようなものです.
C++のコンパイラg++でコンパイルした結果は以下になります.正常にコンパイルできました.
1 |
$ g++ h.c |
引数の渡し方
可変長引数のアクセスを理解するためには,C言語での引数の渡し方を理解する必要があります.
引数は,通常はスタックを利用してやりとりします.
アセンブリ言語を勉強したことがある人には馴染み深いかもしれませんが,そうでない人が多いと思います.
スタックとは,後入れ先出しのバッファのことです.
メモリ上にスタック領域という作業領域を確保して,その領域を利用して値を受け渡しします.
後入れ先出しというのは,本を積み上げる(スタックする)ようなものです.
本を積み上げていくと,取り出す時には上から順番に,つまり最後に重ねた本から順番に取り出しますよね.
スタックにデータを積み上げることをpush(プッシュ),スタックからデータを取り出すことをpop(ポップ)といいます.
変数aの値をスタックにプッシュする表記は以下になります.
1 |
push a |
スタックから変数aに値をポップする表記は以下になります.
1 |
pop a |
ただし,x64では引数の個数が6個以内であれば引数は以下のレジスタに設定されます.
- %edi:第1引数
- %esi:第2引数
- %edx:第3引数
- %ecx:第4引数
- %r8d:第5引数
- %r9d:第6引数
第7引数以降は,push命令で引数はスタックに積まれて,pop命令で引数を取り出します.
可変長引数における暗黙のキャスト(型変換)
可変長引数”, ...”の渡し方を解説します.
可変長引数の場合は,仮引数の型がわからないので,コンパイラは実引数をスタックに積む時にどんな型でプッシュすればよいのかわかりません.
そこで,可変長引数の場合は,暗黙のキャスト(型変換)規則が適用されます.
つまり,整数型はint型もしくはlong型になり,浮動小数点型はdouble型にキャストされます.
実引数から関数に渡される暗黙のキャストは下表になります.
実引数の型 | 関数に渡される型 |
---|---|
char | int |
unsigned char | unsigned int |
short | int |
unsigned short | unsigned int |
int | int |
unsigned int | unsigned int |
long | long |
unsigned long | unsigned long |
float | double |
double | double |
long double | long double |
ポインタ型 | ポインタ型 |
stdarg.h
可変引数の関数を作成するためには,まずstdarg.hをインクルードする必要があります.
stdarg.hはSTanDard ARGument Headerの略で,可変引数の関数を作成するための関数がプロトタイプ宣言されています.
”, ...”の部分の実引数の操作は,このヘッダファイル内の関数を利用しています.
manコマンドでstdarg.hのプロトタイプ宣言されている関数を確認できます.
va_start,va_arg,va_end,va_copyの4つの関数は,“, …”の部分の実引数にアクセスするために利用します.
※これらの関数は便宜上stdarg.hでプロトタイプ宣言されていると言いますが,実際にはマクロです.
stdarg.hは/usr/include以下にはなく,x64の場合は/usr/lib/gcc/x86_64-linux-gnu/XX/include/stdarg.hにあります(XXばGCCのバージョン番号).
1 2 3 4 5 6 7 |
$ man 3 stdarg ... void va_start(va_list ap, last); type va_arg(va_list ap, type); void va_end(va_list ap); void va_copy(va_list dest, va_list src); ... |
また,これらの関数で利用する引数へのポインタを宣言するためのva_list型を利用します.
va_list型は以下のようにtypedefで定義することが多いです.
1 |
typedef void *va_list; |
va_start関数は,apを初期化し,va_arg関数とva_end関数で利用できるようにします.
va_arg関数は,呼び出し時に指定された引数のうち,次の位置にあるものを指定した型typeの値として取得します.
va_end関数は,va_start関数の終了処理をします.(たいていはapをNULLに設定します.)
va_copy関数は,C99で導入された関数で,(初期化済みの) 可変長引数リストsrcをdestにコピーします.
typedefを学びたいあなたはこちらからどうぞ.
可変長引数を利用するコード例
可変長引数を利用するコード例として,vlaadd関数とvlastddev関数を紹介します.
vlaadd関数
vlaadd関数では,任意の個数のint型の整数の足し算をして,その合計値を返します.
第1引数に足し算をする要素数を指定し,その後に実際に足し算の要素を並べます.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> #include <stdarg.h> int vlaadd(int num, ...); int main(void) { printf("1 + 2 + 3 = %d\n", vlaadd(3, 1, 2, 3)); printf("1 + 2 + 3 + 4 = %d\n", vlaadd(4, 1, 2, 3, 4)); printf("1 + 2 + 3 + 4 + 5 = %d\n", vlaadd(5, 1, 2, 3, 4, 5)); printf("1 + 2 + 3 + 4 + 5 + 6 = %d\n", vlaadd(6, 1, 2, 3, 4, 5, 6)); printf("1 + 2 + 3 + 4 + 5 + 6 + 7 = %d\n", vlaadd(7, 1, 2, 3, 4, 5, 6, 7)); return 0; } int vlaadd(int num, ...) { int i, sum; va_list va_ptr; va_start(va_ptr, num); for (i = sum = 0; i < num; i++) { sum += va_arg(va_ptr, int); } va_end(va_ptr); return sum; } |
実行結果は以下になります.
1 2 3 4 5 6 7 |
$ gcc vlaadd.c $ a.out 1 + 2 + 3 = 6 1 + 2 + 3 + 4 = 10 1 + 2 + 3 + 4 + 5 = 15 1 + 2 + 3 + 4 + 5 + 6 = 21 1 + 2 + 3 + 4 + 5 + 6 + 7 = 28 |
vlastddev関数
vlastddev関数では,任意の個数のint型の整数の標準偏差を返します.
28行目でva_copy関数を利用しています.
va_arg関数は呼び出した後に次のデータを読み込む仕様です.
もしva_arg関数を1度のforループで2度呼び出す場合,正しく標準偏差を計算できません.
そこで,41~42行目でva_arg関数の返り値をtmpに格納して,その平均値との差の2乗を計算します.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> #include <stdarg.h> #include <math.h> double vlastddev(int num, ...); int main(void) { printf("stddev of [1, 2, 3] = %lf\n", vlastddev(3, 1, 2, 3)); printf("stddev of [1, 2, 3, 4] = %lf\n", vlastddev(4, 1, 2, 3, 4)); printf("stddev of [1, 2, 3, 4, 5] = %lf\n", vlastddev(5, 1, 2, 3, 4, 5)); printf("stddev of [1, 2, 3, 4, 5, 6] = %lf\n", vlastddev(6, 1, 2, 3, 4, 5, 6)); printf("stddev of [1, 2, 3, 4, 5, 6, 7] = %lf\n", vlastddev(7, 1, 2, 3, 4, 5, 6, 7)); return 0; } double vlastddev(int num, ...) { int i, sum, tmp; va_list va_ptr, va_ptr2; double avg, var, stddev; va_start(va_ptr, num); va_copy(va_ptr2, va_ptr); for (i = sum = 0; i < num; i++) { sum += va_arg(va_ptr, int); } va_end(va_ptr); avg = (double) sum / num; var = 0.0; for (i = 0; i < num; i++) { tmp = va_arg(va_ptr2, int); var += (tmp - avg) * (tmp - avg); } var /= num; stddev = sqrt(var); va_end(va_ptr2); return stddev; } |
実行結果は以下になります.
1 2 3 4 5 6 7 |
$ gcc vlastddev.c -lm $ a.out stddev of [1, 2, 3] = 0.816497 stddev of [1, 2, 3, 4] = 1.118034 stddev of [1, 2, 3, 4, 5] = 1.414214 stddev of [1, 2, 3, 4, 5, 6] = 1.707825 stddev of [1, 2, 3, 4, 5, 6, 7] = 2.000000 |
標準偏差の計算で利用するsqrt関数を知りたいあなたはこちらからどうぞ.
可変長引数に自作コードでアクセス
可変長引数に自作コードでアクセスするmyvlaadd関数は以下になります.
myvlaadd関数は簡易的な実装なので,以下の制限があります.
将来的には改良するかもしれませんが,ひとまず参考にして下さい.
- x64のアセンブリ言語を利用していること(ARM64(AARCH64)では実行不可)
- スタックからマジックナンバを利用して引数を取り出していること(引数の個数は8個,可変長引数の個数は7個が上限)
- コンパイラの種類やオプションによってはスタックの位置がずれるので正常に動作しない可能性があること
- GCCのオプションなしでは動作しますが,オプションありでは正常に動作しない可能性があります.
- Clangではコンパイルできますが,正常に動作しません.
- Visual Studioでは,GCC拡張のインラインアセンブラを利用しているので,コンパイルできません.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> int myvlaadd(int num, ...); int main(void) { printf("1 + 2 + 3 = %d\n", myvlaadd(3, 1, 2, 3)); printf("1 + 2 + 3 + 4 = %d\n", myvlaadd(4, 1, 2, 3, 4)); printf("1 + 2 + 3 + 4 + 5 = %d\n", myvlaadd(5, 1, 2, 3, 4, 5)); printf("1 + 2 + 3 + 4 + 5 + 6 = %d\n", myvlaadd(6, 1, 2, 3, 4, 5, 6)); printf("1 + 2 + 3 + 4 + 5 + 6 + 7 = %d\n", myvlaadd(7, 1, 2, 3, 4, 5, 6, 7)); return 0; } int myvlaadd(int num, ...) { int i, sum; int tmp; for (i = sum = 0; i < num; i++) { switch (i) { case 0: asm volatile("mov -0xa8(%%rbp), %0" : "=r"(tmp)); break; case 1: asm volatile("mov -0xa0(%%rbp), %0" : "=r"(tmp)); break; case 2: asm volatile("mov -0x98(%%rbp), %0" : "=r"(tmp)); break; case 3: asm volatile("mov -0x90(%%rbp), %0" : "=r"(tmp)); break; case 4: asm volatile("mov -0x88(%%rbp), %0" : "=r"(tmp)); break; case 5: asm volatile("mov 0x10(%%rbp), %0" : "=r"(tmp)); break; case 6: asm volatile("mov 0x18(%%rbp), %0" : "=r"(tmp)); break; default: fprintf(stderr, "Error: number of arguments is too many.\n"); break; } sum += tmp; } return sum; } |
実行結果は以下になります.
限定的ですが,正常に動作していることがわかります.
1 2 3 4 5 6 7 |
$ gcc myvlaadd.c $ a.out 1 + 2 + 3 = 6 1 + 2 + 3 + 4 = 10 1 + 2 + 3 + 4 + 5 = 15 1 + 2 + 3 + 4 + 5 + 6 = 21 1 + 2 + 3 + 4 + 5 + 6 + 7 = 28 |
まとめ
C言語の可変長引数を紹介しました.
具体的には,printf/scanf関数で利用されている可変長引数"..."の意味と,stdarg.hでプロトタイプ宣言されている関数の使い方がわかりました.
また,x64で可変長引数に自作コードでアクセスする処理を実装しました.
printf/scanfファミリー関数の使い方を知りたいあなたはこちらからどうぞ.
printf/scanf関数の自作を知りたいあなたは,以下の記事に詳しく書いてあります.
C言語を独学で習得することは難しいです.
私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.
私のキャパシティもあり,一定数に達したら終了しますので,今すぐ追加しましょう!
独学が難しいあなたは,元東大教員がおすすめするC言語を学べるオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!