このCコードを考えてみましょう:
_#include <stdio.h>
main()
{
int x=5;
printf("x is ");
printf("%d",5);
}
_
これで、_int x=5;
_を書いたときに、x
は整数であることをコンピューターに伝えました。コンピュータはx
が整数であることを覚えておく必要があります。しかし、x
の値をprintf()
に出力するときは、x
が整数であることをコンピューターに再度通知する必要があります。何故ですか?
コンピュータがx
が整数であることを忘れるのはなぜですか?
ここには、2つの問題があります。
問題#1:Cは静的に型指定された言語です。すべての型情報はコンパイル時に決定されます。タイプ情報は実行時にタイプとサイズを決定できるように、オブジェクトとともにメモリに保存されません。1。プログラムの実行中に特定のアドレスのメモリを調べる場合、表示されるのはバイトのスラッジだけです。特定のアドレスに実際にオブジェクトが含まれているかどうか、そのオブジェクトのタイプまたはサイズは何か、またはそれらのバイトを(整数、浮動小数点タイプ、または文字列内の文字のシーケンスとして)解釈する方法などはわかりません。 )。ソースコードで指定された型情報に基づいて、コードがコンパイルされると、そのすべての情報がマシンコードに組み込まれます。たとえば、関数定義
void foo( int x, double y, char *z )
{
...
}
x
を整数として、y
を浮動小数点値として、z
をchar
へのポインターとして処理する適切なマシンコードを生成するようコンパイラーに指示します。関数呼び出しと関数定義の間の引数の数またはタイプの不一致は、コードのコンパイル時にのみ検出されることに注意してください2;タイプ情報がオブジェクトに関連付けられるのは、コンパイルフェーズ中のみです。
問題#2:printf
はvariadic関数です。タイプconst char * restrict
(フォーマット文字列)の1つの固定パラメーターと、0個以上の追加パラメーターがあり、その数とタイプはnotコンパイル時に既知:
int printf( const char * restrict fmt, ... );
printf
関数は、渡された引数自体から、追加の引数の数とタイプを知る方法がありません。スタック(またはレジスター)のバイトのスラッジを解釈する方法を伝えるには、フォーマット文字列に依存する必要があります。さらに良いことに、これは可変個関数であるため、特定の型の引数はデフォルトの型の限られたセットに昇格されます(たとえば、short
はint
、float
に昇格されます)。 double
などに昇格されます)。
繰り返しますが、printf
に解釈またはフォーマットの手がかりを与える追加の引数自体に関連する情報はありません。したがって、フォーマット文字列に変換指定子が必要です。
printf
に追加の引数の数とタイプを伝えるだけでなく、変換指定子はprintf
に出力方法formatを伝える(フィールド幅、精度、パディング)ことに注意してください。 、位置揃え、ベース(整数型の場合は10進数、8進数、16進数)など)。
編集
コメントでの広範な議論を避けるため(そしてチャットページが私の仕事のシステムからブロックされているため-はい、私は悪い子です)、ここで最後の2つの質問に対処します。
私がこれを行うと:最後のステートメントで、コンパイラはbがfloat型であることをどのようにして知るのですか?float b; float c; b=3.1; c=(5.0/9.0)*(b);
変換中、コンパイラは、オブジェクトの名前、タイプ、ストレージ期間、スコープなどに関する情報を格納するテーブル(シンボルテーブルと呼ばれることが多い)を維持します。あなた宣言b
およびc
をfloat
として、したがって、コンパイラがb
またはc
を含む式を見ると、マシンコードを生成して、浮動小数点値。
上記のコードを使用して、プログラム全体をラップしました。
/**
* c1.c
*/
#include <stdio.h>
int main( void )
{
float b;
float c;
b = 3.1;
c = (5.0 / 9.0) * b;
printf( "c = %f\n", c );
return 0;
}
-g
および-Wa,-aldh
オプションをgccで使用して、Cソースコードでインターリーブされた生成されたマシンコードのリストを作成しました3:
GAS LISTING /tmp/ccmGgGG2.s page 1
1 .file "c1.c"
9 .Ltext0:
10 .section .rodata
11 .LC2:
12 0000 63203D20 .string "c = %f\n"
12 25660A00
13 .align 8
14 .LC1:
15 0008 721CC771 .long 1908874354
16 000c 1CC7E13F .long 1071761180
17 .text
18 .globl main
20 main:
21 .LFB2:
22 .file 1 "c1.c"
1:c1.c **** #include <stdio.h>
2:c1.c **** int main( void )
3:c1.c **** {
23 .loc 1 3 0
24 0000 55 pushq %rbp
25 .LCFI0:
26 0001 4889E5 movq %rsp, %rbp
27 .LCFI1:
28 0004 4883EC10 subq $16, %rsp
29 .LCFI2:
4:c1.c **** float b;
5:c1.c **** float c;
6:c1.c **** b = 3.1;
30 .loc 1 6 0
31 0008 B8666646 movl $0x40466666, %eax
31 40
32 000d 8945F8 movl %eax, -8(%rbp)
7:c1.c **** c = (5.0 / 9.0) * b;
33 .loc 1 7 0
34 0010 F30F5A4D cvtss2sd -8(%rbp), %xmm1
34 F8
35 0015 F20F1005 movsd .LC1(%rip), %xmm0
35 00000000
36 001d F20F59C1 mulsd %xmm1, %xmm0
37 0021 F20F5AC0 cvtsd2ss %xmm0, %xmm0
38 0025 F30F1145 movss %xmm0, -4(%rbp)
38 FC
8:c1.c ****
9:c1.c **** printf( "c = %f\n", c );
39 .loc 1 9 0
40 002a F30F5A45 cvtss2sd -4(%rbp), %xmm0
40 FC
41 002f BF000000 movl $.LC2, %edi
41 00
42 0034 B8010000 movl $1, %eax
42 00
43 0039 E8000000 call printf
43 00
10:c1.c **** return 0;
44 .loc 1 10 0
45 003e B8000000 movl $0, %eax
GAS LISTING /tmp/ccmGgGG2.s page 2
11:c1.c **** }
46 .loc 1 11 0
47 0043 C9 leave
48 0044 C3 ret
アセンブリリストの読み方は次のとおりです。
40 002a F30F5A45 cvtss2sd -4(%rbp), %xmm0
40 FC
^ ^ ^ ^ ^
| | | | |
| | | | +-- Instruction operands
| | | +------------------ Instruction mnemonic
| | +---------------------------------------- Actual machine code (instruction and operands)
| +--------------------------------------------- Byte offset of instruction from subroutine entry point
+------------------------------------------------ Line number of Assembly listing
ここで注意すべきことが1つあります。生成されたアセンブリコードには、b
またはc
の記号はありません。それらはソースコードリストにのみ存在します。 main
が実行時に実行されると、b
とc
のスペースが(他のデータとともに)スタックポインターを調整してスタックから割り当てられます。
subq $16, %rsp
コードは、フレームポインターからのオフセットによってこれらのオブジェクトを参照します4、次のように、b
はフレームポインタに格納されているアドレスから-8バイト、c
は-4バイトです。
7:c1.c **** c = (5.0 / 9.0) * b;
.loc 1 7 0
cvtss2sd -8(%rbp), %xmm1 ;; converts contents of b from single- to double-
;; precision float, stores result to floating-
;; point register xmm1
movsd .LC1(%rip), %xmm0 ;; writes the pre-computed value of 5.0/9.0
;; to floating point register xmm0
mulsd %xmm1, %xmm0 ;; multiply contents of xmm1 by xmm0, store result
;; in xmm0
cvtsd2ss %xmm0, %xmm0 ;; convert result in xmm0 from double- to single-
;; precision float
movss %xmm0, -4(%rbp) ;; save result to c
b
およびc
を浮動小数点数として宣言したため、コンパイラーは浮動小数点値を特別に処理するマシンコードを生成しました。 movsd
、mulsd
、cvtss2sd
命令はすべて浮動小数点演算に固有であり、レジスタ%xmm0
および%xmm1
は倍精度浮動小数点値を格納するために使用されます。
b
およびc
が浮動小数点数ではなく整数になるようにソースコードを変更すると、コンパイラーは異なるマシンコードを生成します。
/**
* c2.c
*/
#include <stdio.h>
int main( void )
{
int b;
int c;
b = 3;
c = (9 / 4) * b; // changed these values since integer 5/9 == 0, making for
// some really boring machine code.
printf( "c = %d\n", c );
return 0;
}
gcc -o c2 -g -std=c99 -pedantic -Wall -Werror -Wa,-aldh=c2.lst c2.c
でコンパイルすると、次のようになります。
GAS LISTING /tmp/ccyxHwid.s page 1
1 .file "c2.c"
9 .Ltext0:
10 .section .rodata
11 .LC0:
12 0000 63203D20 .string "c = %d\n"
12 25640A00
13 .text
14 .globl main
16 main:
17 .LFB2:
18 .file 1 "c2.c"
1:c2.c **** #include <stdio.h>
2:c2.c **** int main( void )
3:c2.c **** {
19 .loc 1 3 0
20 0000 55 pushq %rbp
21 .LCFI0:
22 0001 4889E5 movq %rsp, %rbp
23 .LCFI1:
24 0004 4883EC10 subq $16, %rsp
25 .LCFI2:
4:c2.c **** int b;
5:c2.c **** int c;
6:c2.c **** b = 3;
26 .loc 1 6 0
27 0008 C745F803 movl $3, -8(%rbp)
27 000000
7:c2.c **** c = (9 / 4) * b;
28 .loc 1 7 0
29 000f 8B45F8 movl -8(%rbp), %eax
30 0012 01C0 addl %eax, %eax
31 0014 8945FC movl %eax, -4(%rbp)
8:c2.c ****
9:c2.c **** printf( "c = %d\n", c );
32 .loc 1 9 0
33 0017 8B75FC movl -4(%rbp), %esi
34 001a BF000000 movl $.LC0, %edi
34 00
35 001f B8000000 movl $0, %eax
35 00
36 0024 E8000000 call printf
36 00
10:c2.c **** return 0;
37 .loc 1 10 0
38 0029 B8000000 movl $0, %eax
38 00
11:c2.c **** }
39 .loc 1 11 0
40 002e C9 leave
41 002f C3 ret
これは同じ操作ですが、b
とc
が整数として宣言されています。
7:c2.c **** c = (9 / 4) * b;
.loc 1 7 0
movl -8(%rbp), %eax ;; copy value of b to register eax
addl %eax, %eax ;; since 9/4 == 2 (integer arithmetic), double the
;; value in eax
movl %eax, -4(%rbp) ;; write result to c
これは、タイプ情報がマシンコードに「組み込まれた」と言ったときに私が以前に意味したものです。プログラムの実行時に、b
またはc
を調べて型を判別することはありません。生成されたマシンコードに基づいて、それらのタイプがどうあるべきかをすでに知っています。
コンパイラが実行時にタイプとサイズを決定する場合、次のプログラムが機能しないのはなぜですか。float b='H'; printf(" value of b is %c \n",b);
コンパイラにうそをついているので機能しません。 b
はfloat
であることを伝えるため、浮動小数点値を処理するマシンコードを生成します。これを初期化すると、定数'H'
に対応するビットパターンは、文字値ではなく浮動小数点値として解釈されます。
引数char
にb
型の値を期待する%c
変換指定子を使用すると、コンパイラに再びうそをつきます。このため、printf
はb
の内容を正しく解釈せず、ガベージ出力が発生します。5。繰り返しになりますが、printf
は、引数自体に基づいて追加の引数の数や型を知ることはできません。表示されるのは、スタック上のアドレス(またはレジスターの束)だけです。渡された追加の引数とその型が何であるかを伝えるために、フォーマット文字列が必要です。
sizeof
を評価する方法はありません。H
のビットパターンは、0x00000048
としてb
に格納されます。ただし、%c
変換指定子は引数がchar
であることを示しているため、最初のバイトのみが読み取られるため、printf
はエンコード0x00
に対応する文字を書き込もうとします。printf
が呼び出されてその機能を実行する時点では、コンパイラーは何をすべきかを指示するために存在しません。
関数は、パラメーターの内容以外の情報を取得せず、varargパラメーターには型がないため、printf
は、明示的な指示を取得しなかった場合にそれらを出力する方法の手がかりがありません。フォーマット文字列を介して。コンパイラは(通常)各引数の型を推定できますが、定数テキストに関連して各引数を出力するには、書式文字列を記述してwhereと指示する必要があります。比較"$%d"
および"%d$"
;それらは異なることを行い、コンパイラはあなたがどちらを望んでいるかを推測できません。とにかく手動でフォーマット文字列を作成して引数positionsを指定する必要があるため、引数typesを述べるタスクをユーザーにオフロードすることも明らかです。
別の方法としては、コンパイラーがフォーマット文字列の位置をスキャンし、タイプを推定し、フォーマット文字列を書き換えてタイプ情報を追加し、変更された文字列をバイナリにコンパイルします。しかし、これはliteral形式の文字列に対してのみ機能します。 Cは動的に割り当てられたフォーマット文字列も許可し、コンパイラーが実行時にフォーマット文字列を正確に再構築できない場合が常にあります。 (また、何かを別の関連する型として出力し、ナローイングキャストを効果的に実行したい場合もあります。これもコンパイラが予測できないものです。)
printf()
は variadic function と呼ばれ、可変数の引数を受け入れるものです。
CのVariadic関数は、特別なプロトタイプを使用して、引数のリストの長さが不明であることをコンパイラーに伝えます。
_int printf(const char *format, ...);
_
標準Cは、_stdarg.h
_で関数のセットを提供します。これを使用して、引数を一度に1つずつ取得し、それらを特定の型にキャストできます。つまり、可変個引数関数は、各引数の型を自分で決定する必要があります。 printf()
は、フォーマット文字列の内容に基づいてこの決定を行います。
これはprintf()
が実際に機能する方法を大幅に簡略化したものですが、プロセスは次のようになります。
_int printf(const char *format, ...) {
/* Get ready to process arguments that follow 'format' */
va_list ap;
va_start(ap, format);
/* Deep in the function, something that's dissected the
format string has decided that the next argument is a
string. Grab the next argument, cast it to char * and
write it to wherever it should go.
*/
char *string = va_arg(ap, char *);
write_string_to_output(string);
/* Conclude processing of arguments */
va_end(ap);
}
_
printf()
が変換可能なすべてのタイプで同じプロセスが発生します。この例は、OpenBSDのvfprintf()
の実装の ソースコード で確認できます。これは、printf()
をサポートする関数です。
一部のCコンパイラーは、printf()
への呼び出しを見つけ、フォーマット文字列が定数であるかどうかを評価し、残りの引数の型が指定された変換と互換性があることを確認するのに十分スマートです。この動作は必須ではありません。そのため、標準ではフォーマット文字列の一部としてタイプを指定する必要があります。これらの種類のチェックが行われる前は、フォーマット文字列と引数リストの不一致により、誤った出力が生成されていました。
C++では、_<<
_は演算子であり、_cout << foo << bar
_などのcout
を使用します infix expression これは、コンパイル時に正確性を評価し、キャストするコードに変換できますcout
が扱えるものへの右辺式.
Cの設計者はcompilerをできるだけシンプルにしたいと考えました。他の言語とほとんど同じようにI/Oを処理することが可能であり、渡されたパラメーターのタイプに関する情報をコンパイラーが自動的にI/Oルーチンに提供する必要がある一方で、そのようなアプローチでは多くの場合、 printf
(*)で可能なコードよりも効率的なコードを許可しました。そのように定義すると、コンパイラがより複雑になります。
Cの初期の頃には、関数を呼び出すコードは、それが予期している引数を知りませんでした。各引数はそのタイプに応じてスタック上にいくつかのワードをプッシュし、関数は戻りアドレスの下の最上位、2番目から2番目などのスタックスロットで異なるパラメーターを見つけることを期待します。 printf
メソッドがスタック上で引数を見つける場所を特定できた場合、コンパイラーが他のメソッドと異なる方法で引数を処理する方法はありませんでした。
実際には、Cによって想定されたパラメーター受け渡しのパターンは、printf
のような可変関数を呼び出す場合と、printf
が特別なパラメーター受け渡し規則を使用するように定義されている場合を除いて、ほとんど使用されません[たとえば、最初のパラメータはコンパイラが生成したconst char*
渡される型に関する自動生成情報が含まれている場合]、コンパイラーはそのためのより適切なコードを生成できたはずです(とりわけ、整数および浮動小数点の昇格の必要性を回避します)。]残念ながら、呼び出されたコードに変数タイプを報告させる機能を追加するコンパイラー。
ヌルポインターは、その有用性を考えると、「10億ドルの間違い」と見なされ、ヌルポインターの算術演算やアクセスをトラップしない言語では一般的に非常に悪い動作しか引き起こさないので、不思議に思います。 printf
とゼロで終了する文字列によって引き起こされる害ははるかに悪いと思います。
定義した別の関数に変数を渡すように考えてください。通常は、他の関数に、どのタイプのデータを期待/受信すべきかを伝えます。 printf()
でも同じです。これはstdio.h
ライブラリですでに定義されており、正しい形式で出力できるように、受信しているデータを通知する必要があります(例:int
)。