web-dev-qa-db-ja.com

Cでデータのタイプをprintf()に通知する必要があるのはなぜですか?

このCコードを考えてみましょう:

_#include <stdio.h>

main()
{
  int x=5;
  printf("x is ");
  printf("%d",5);
}
_

これで、_int x=5;_を書いたときに、xは整数であることをコンピューターに伝えました。コンピュータはxが整数であることを覚えておく必要があります。しかし、xの値をprintf()に出力するときは、xが整数であることをコンピューターに再度通知する必要があります。何故ですか?

コンピュータがxが整数であることを忘れるのはなぜですか?

8
user106313

ここには、2つの問題があります。

問題#1:Cは静的に型指定された言語です。すべての型情報はコンパイル時に決定されます。タイプ情報は実行時にタイプとサイズを決定できるように、オブジェクトとともにメモリに保存されません。1。プログラムの実行中に特定のアドレスのメモリを調べる場合、表示されるのはバイトのスラッジだけです。特定のアドレスに実際にオブジェクトが含まれているかどうか、そのオブジェクトのタイプまたはサイズは何か、またはそれらのバイトを(整数、浮動小数点タイプ、または文字列内の文字のシーケンスとして)解釈する方法などはわかりません。 )。ソースコードで指定された型情報に基づいて、コードがコンパイルされると、そのすべての情報がマシンコードに組み込まれます。たとえば、関数定義

void foo( int x, double y, char *z )
{
  ...
}

xを整数として、yを浮動小数点値として、zcharへのポインターとして処理する適切なマシンコードを生成するようコンパイラーに指示します。関数呼び出しと関数定義の間の引数の数またはタイプの不一致は、コードのコンパイル時にのみ検出されることに注意してください2;タイプ情報がオブジェクトに関連付けられるのは、コンパイルフェーズ中のみです。

問題#2:printfvariadic関数です。タイプconst char * restrict(フォーマット文字列)の1つの固定パラメーターと、0個以上の追加パラメーターがあり、その数とタイプはnotコンパイル時に既知:

int printf( const char * restrict fmt, ... );

printf関数は、渡された引数自体から、追加の引数の数とタイプを知る方法がありません。スタック(またはレジスター)のバイトのスラッジを解釈する方法を伝えるには、フォーマット文字列に依存する必要があります。さらに良いことに、これは可変個関数であるため、特定の型の引数はデフォルトの型の限られたセットに昇格されます(たとえば、shortintfloatに昇格されます)。 doubleなどに昇格されます)。

繰り返しますが、printfに解釈またはフォーマットの手がかりを与える追加の引数自体に関連する情報はありません。したがって、フォーマット文字列に変換指定子が必要です。

printfに追加の引数の数とタイプを伝えるだけでなく、変換指定子はprintfに出力方法formatを伝える(フィールド幅、精度、パディング)ことに注意してください。 、位置揃え、ベース(整数型の場合は10進数、8進数、16進数)など)。

編集

コメントでの広範な議論を避けるため(そしてチャットページが私の仕事のシステムからブロックされているため-はい、私は悪い子です)、ここで最後の2つの質問に対処します。

私がこれを行うと:
float b;          
float c;           
b=3.1;    
c=(5.0/9.0)*(b);
最後のステートメントで、コンパイラはbがfloat型であることをどのようにして知るのですか?

変換中、コンパイラは、オブジェクトの名前、タイプ、ストレージ期間、スコープなどに関する情報を格納するテーブル(シンボルテーブルと呼ばれることが多い)を維持します。あなた宣言bおよびcfloatとして、したがって、コンパイラが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ソースコードでインターリーブされた生成されたマシンコードのリストを作成しました

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が実行時に実行されると、bcのスペースが(他のデータとともに)スタックポインターを調整してスタックから割り当てられます。

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を浮動小数点数として宣言したため、コンパイラーは浮動小数点値を特別に処理するマシンコードを生成しました。 movsdmulsdcvtss2sd命令はすべて浮動小数点演算に固有であり、レジスタ%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

これは同じ操作ですが、bcが整数として宣言されています。

   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);

コンパイラにうそをついているので機能しません。 bfloatであることを伝えるため、浮動小数点値を処理するマシンコードを生成します。これを初期化すると、定数'H'に対応するビットパターンは、文字値ではなく浮動小数点値として解釈されます。

引数charb型の値を期待する%c変換指定子を使用すると、コンパイラに再びうそをつきます。このため、printfbの内容を正しく解釈せず、ガベージ出力が発生します。5。繰り返しになりますが、printfは、引数自体に基づいて追加の引数の数や型を知ることはできません。表示されるのは、スタック上のアドレス(またはレジスターの束)だけです。渡された追加の引数とその型が何であるかを伝えるために、フォーマット文字列が必要です。


1. 1つの例外は可変長配列です。それらのサイズは実行時まで確立されないため、コンパイル時にVLAでsizeofを評価する方法はありません。

2。とにかく、C89以降。それ以前は、コンパイラは関数の戻り値の型の不一致のみを検出できました。関数パラメーターリストの不一致を検出できませんでした。

3。このコードは、gcc 4.1.2を使用して64ビットのSuSE Linux Enterprise 10システムで生成されます。別の実装(コンパイラー/ OS /チップアーキテクチャー)を使用している場合は、正確な機械語命令は異なりますが、一般的な点は変わりません。コンパイラーは、浮動小数点数、整数、文字列などを処理するためのさまざまな命令を生成します。

4。実行中のプログラムで関数を呼び出すと、スタックフレームが作成され、関数の引数、ローカル変数、および次の命令のアドレスが格納されます関数呼び出し。 frame pointerと呼ばれる特殊レジスターは、現在のフレームを追跡するために使用されます。

5。たとえば、上位バイトがアドレス指定されたバイトであるビッグエンディアンシステムを想定します。 Hのビットパターンは、0x00000048としてbに格納されます。ただし、%c変換指定子は引数がcharであることを示しているため、最初のバイトのみが読み取られるため、printfはエンコード0x00に対応する文字を書き込もうとします。
18
John Bode

printfが呼び出されてその機能を実行する時点では、コンパイラーは何をすべきかを指示するために存在しません。

関数は、パラメーターの内容以外の情報を取得せず、varargパラメーターには型がないため、printfは、明示的な指示を取得しなかった場合にそれらを出力する方法の手がかりがありません。フォーマット文字列を介して。コンパイラは(通常)各引数の型を推定できますが、定数テキストに関連して各引数を出力するには、書式文字列を記述してwhereと指示する必要があります。比較"$%d"および"%d$";それらは異なることを行い、コンパイラはあなたがどちらを望んでいるかを推測できません。とにかく手動でフォーマット文字列を作成して引数positionsを指定する必要があるため、引数typesを述べるタスクをユーザーにオフロードすることも明らかです。

別の方法としては、コンパイラーがフォーマット文字列の位置をスキャンし、タイプを推定し、フォーマット文字列を書き換えてタイプ情報を追加し、変更された文字列をバイナリにコンパイルします。しかし、これはliteral形式の文字列に対してのみ機能します。 Cは動的に割り当てられたフォーマット文字列も許可し、コンパイラーが実行時にフォーマット文字列を正確に再構築できない場合が常にあります。 (また、何かを別の関連する型として出力し、ナローイングキャストを効果的に実行したい場合もあります。これもコンパイラが予測できないものです。)

8
Kilian Foth

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が扱えるものへの右辺式.

5
Blrfl

Cの設計者はcompilerをできるだけシンプルにしたいと考えました。他の言語とほとんど同じようにI/Oを処理することが可能であり、渡されたパラメーターのタイプに関する情報をコンパイラーが自動的にI/Oルーチンに提供する必要がある一方で、そのようなアプローチでは多くの場合、 printf(*)で可能なコードよりも効率的なコードを許可しました。そのように定義すると、コンパイラがより複雑になります。

Cの初期の頃には、関数を呼び出すコードは、それが予期している引数を知りませんでした。各引数はそのタイプに応じてスタック上にいくつかのワードをプッシュし、関数は戻りアドレスの下の最上位、2番目から2番目などのスタックスロットで異なるパラメーターを見つけることを期待します。 printfメソッドがスタック上で引数を見つける場所を特定できた場合、コンパイラーが他のメソッドと異なる方法で引数を処理する方法はありませんでした。

実際には、Cによって想定されたパラメーター受け渡しのパターンは、printfのような可変関数を呼び出す場合と、printfが特別なパラメーター受け渡し規則を使用するように定義されている場合を除いて、ほとんど使用されません[たとえば、最初のパラメータはコンパイラが生成したconst char*渡される型に関する自動生成情報が含まれている場合]、コンパイラーはそのためのより適切なコードを生成できたはずです(とりわけ、整数および浮動小数点の昇格の必要性を回避します)。]残念ながら、呼び出されたコードに変数タイプを報告させる機能を追加するコンパイラー。

ヌルポインターは、その有用性を考えると、「10億ドルの間違い」と見なされ、ヌルポインターの算術演算やアクセスをトラップしない言語では一般的に非常に悪い動作しか引き起こさないので、不思議に思います。 printfとゼロで終了する文字列によって引き起こされる害ははるかに悪いと思います。

3
supercat

定義した別の関数に変数を渡すように考えてください。通常は、他の関数に、どのタイプのデータを期待/受信すべきかを伝えます。 printf()でも同じです。これはstdio.hライブラリですでに定義されており、正しい形式で出力できるように、受信しているデータを通知する必要があります(例:int)。

0