以下のコードスニペットからわかるように、1つのchar
変数と1つのint
変数を宣言しました。コードをコンパイルするときは、変数str
およびi
のデータ型を識別する必要があります。
%s
または%d
をscanf
に指定して、変数のスキャン中に文字列または整数変数であることを再度通知する必要があるのはなぜですか?コンパイラは、変数を宣言したときにそれを識別するのに十分成熟していませんか?
#include <stdio.h>
int main ()
{
char str [80];
int i;
printf ("Enter your family name: ");
scanf ("%s",str);
printf ("Enter your age: ");
scanf ("%d",&i);
return 0;
}
scanf
やprintf
のような可変引数関数が、渡される引数の数でさえも、可変引数のタイプを知るための移植可能な方法がないためです。
C FAQを参照してください: 関数が実際に呼び出された引数の数を見つけるにはどうすればよいですか?
これが、可変引数の数、場合によっては型を決定するために、少なくとも1つの固定引数が必要な理由です。そして、この引数(標準ではparmN
と呼ばれています。C11( ISO/IEC 9899:201x を参照)§7.16可変引数)はこの特別な役割を果たし、マクロに渡されますva_start
。別の言葉で言えば、標準Cでは次のようなプロトタイプの関数を持つことはできません。
void foo(...);
コンパイラが必要な情報を提供できない理由は、コンパイラがここに関与していないためです。これらの関数には変数タイプがあるため、関数のプロトタイプはタイプを指定しません。したがって、実際のデータ型はコンパイル時ではなく、実行時に決定されます。次に、関数はスタックから1つの引数を次々に取得します。これらの値には型情報が関連付けられていないため、関数がデータの解釈方法を知る唯一の方法は、呼び出し元から提供された情報(フォーマット文字列)を使用することです。
関数自体は、渡されるデータ型も、渡される引数の数も知らないため、printf
が独自にこれを決定する方法はありません。
C++では、演算子のオーバーロードを使用できますが、これはまったく異なるメカニズムです。ここでは、コンパイラがデータ型と使用可能なオーバーロードされた関数に基づいて適切な関数を選択するためです。
これを説明するために、コンパイルするとprintf
は次のようになります。
Push value1
...
Push valueN
Push format_string
call _printf
そして、printf
のプロトタイプは次のとおりです。
int printf ( const char * format, ... );
したがって、フォーマット文字列で提供されるものを除いて、型情報は持ち越されません。
コンパイラは賢いかもしれませんが、関数printf
またはscanf
はばかげています-呼び出しごとに渡すパラメータのタイプがわからないのです。これが、毎回%s
または%d
を渡す必要がある理由です。
最初のパラメータはフォーマット文字列です。 10進数を印刷する場合は、次のようになります。
"%d"
(10進数)"%5d"
(スペースで幅5に埋め込まれた10進数)"%05d"
(幅5にゼロが埋め込まれた10進数)"%+d"
(10進数、常に記号付き)"Value: %d\n"
(番号の前後の一部のコンテンツ)など、たとえば ウィキペディアのフォーマットプレースホルダー を参照して、どのフォーマット文字列に含めることができるかを把握してください。
また、ここには複数のパラメーターが存在する可能性があります。
"%s - %d"
(文字列、コンテンツ、数値)
コンパイラは、変数を宣言したときにそれを識別するのに十分成熟していませんか?
番号。
数十年前に指定された言語を使用しています。 Cは現代言語ではないため、Cに現代的なデザインの美学を期待しないでください。現代語は、使いやすさや明快さの向上のために、コンパイル、解釈、または実行の効率を少し犠牲にする傾向があります。コンピュータの処理時間が高価で供給が非常に限られていた時代に由来し、その設計はこれを反映しています。
また、CとC++が、高速、効率的、または金属に近いことを本当に気にかけているときに、選択される言語のままである理由でもあります。
GCC(および場合によっては他のCコンパイラ)は、少なくともいくつかの状況では、引数の型を追跡します。しかし、言語はそのように設計されていません。
printf
関数は、可変引数を受け入れる通常の関数です。変数引数には、ある種の実行時型識別スキームが必要ですが、C言語では、値は実行時型情報を伝達しません。 (もちろん、Cプログラマーは、構造体またはビット操作のトリックを使用してランタイムタイピングスキームを作成できますが、これらは言語に統合されていません。)
このような関数を開発するとき:
void foo(int a, int b, ...);
2番目の引数の後に「任意の」数の追加引数を渡すことができます。関数受け渡しメカニズムの外部にあるある種のプロトコルを使用して、引数の数とタイプを決定するのは私たちの責任です。
たとえば、この関数を次のように呼び出すと、次のようになります。
foo(1, 2, 3.0);
foo(1, 2, "abc");
呼び出し先がケースを区別できる方法はありません。パラメータ受け渡し領域にはビットがいくつかあり、それらが文字データへのポインタを表しているのか、浮動小数点数を表しているのかわかりません。
この種の情報を伝達する可能性は数多くあります。たとえば、POSIXでは、exec
ファミリーの関数は、すべて同じタイプの変数引数char *
を使用し、nullポインターを使用してリストの終わりを示します。
#include <stdarg.h>
void my_exec(char *progname, ...)
{
va_list variable_args;
va_start (variable_args, progname);
for (;;) {
char *arg = va_arg(variable_args, char *);
if (arg == 0)
break;
/* process arg */
}
va_end(variable_args);
/*...*/
}
呼び出し元がnullポインターターミネーターを渡すのを忘れた場合、関数はすべての引数を消費した後もva_arg
を呼び出し続けるため、動作は未定義になります。 my_exec
関数は次のように呼び出す必要があります。
my_exec("foo", "bar", "xyzzy", (char *) 0);
0
でのキャストは、nullポインター定数として解釈されるコンテキストがないために必要です。コンパイラーは、その引数の目的の型がポインター型であることを認識していません。さらに、(void *) 0
は、void *
ではなくchar *
タイプとして渡されるため、正しくありません。ただし、2つはバイナリレベルでほぼ確実に互換性があるため、実際には機能します。そのタイプのexec
関数でよくある間違いは次のとおりです。
my_exec("foo", "bar", "xyzzy", NULL);
ここで、コンパイラのNULL
は、0
キャストなしで(void *)
として定義されています。
別の可能なスキームは、引数がいくつあるかを示す番号を呼び出すように呼び出し元に要求することです。もちろん、その数は正しくない可能性があります。
printf
の場合、フォーマット文字列は引数リストを記述します。関数はそれを解析し、それに応じて引数を抽出します。
冒頭で述べたように、一部のコンパイラ、特にGNU Cコンパイラは、コンパイル時にフォーマット文字列を解析し、引数の数と型に対して静的型チェックを実行できます。
ただし、フォーマット文字列はリテラル以外の場合もあり、実行時に計算される可能性があることに注意してください。これは、このような型チェックスキームの影響を受けません。架空の例:
char *fmt_string = message_lookup(current_language, message_code);
/* no type checking from gcc in this case: fmt_string could have
four conversion specifiers, or ones not matching the types of
arg1, arg2, arg3, without generating any diagnostic. */
snprintf(buffer, sizeof buffer, fmt_string, arg1, arg2, arg3);
プロトタイプとしてのscanf
int scanf ( const char * format, ... );
は、パラメータ形式に従って指定されたデータを、追加の引数が指す場所に格納することを示します。
コンパイラとは関係ありません。scanf
に定義された構文がすべてです。入力するデータ用に予約するサイズをscanf
に通知するには、パラメータ形式が必要です。
これは、関数(printf
scanf
など)に、渡す値のタイプを伝える唯一の方法だからです。例えば-
int main()
{
int i=22;
printf("%c",i);
return 0;
}
このコードは、整数22ではなく文字を出力します。printf関数に変数をcharとして扱うように指示したためです。
printf
とscanf
は、制御文字列と引数のリストを受け取るように設計および定義されたI/O関数です。
関数は渡されたパラメーターのタイプを認識せず、コンパイラーもこの情報を渡せません。
Printfではデータ型を指定していないため、データ形式を指定しています。これはどの言語でも重要な違いであり、Cでは二重に重要です。
%s
を使用して文字列をスキャンする場合、「文字列変数の文字列入力を解析する」という意味ではありません。あなたできません Cには文字列型がないので、Cではそう言います。 Cが文字列変数に最も近いのは、文字列を表す文字をたまたま含む固定サイズの文字配列であり、文字列の終わりはヌル文字で示されます。つまり、実際に言っているのは、「文字列を保持する配列です。解析してほしい文字列入力に十分な大きさであると約束します」ということです。
プリミティブ?もちろん。 Cは、一般的なマシンに最大64KのRAMが搭載されていた40年以上前に発明されました。このような環境では、RAMの保存は、高度な文字列操作よりも優先されました。
それでも、%s
スキャナーは、文字列データ型が存在するより高度なプログラミング環境で存続します。入力ではなくスキャンに関するものだからです。