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


8

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

#include <stdio.h>

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

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

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


5
コンパイラはxが整数であることを認識していますが、printfはそうではありません。
luiscubal 2014年

それは、呼び出し規約の作品は、唯一の1の実装が存在するかだけだprintf(char*, ...)とそれだけでデータの収集に(に相当するもの)のポインタを取得します
ラチェットフリーク

@ratchetfreakなぜそうなのですか?「cout」関数がデータ型を自動的に認識するC ++のように、printfがデータ型のポインターを自動的に取得しないのはなぜですか?
user106313 2014年

5
検討しましたprintf("x is %x in hex, and %d in decimal and %o as octal",x,x,x);か?

1
数値163で試してください。この状況では、8未満の値はあまり面白くありません。あるいは、0から255までループして、数値を確認します。ポイントは、printfにはタイプするだけではないということです。

回答:


18

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

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

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

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

問題#2:printfある可変引数関数は、型const char * restrict(フォーマット文字列)の1つの固定パラメーターと、0個以上の追加パラメーターを受け取ります。これらのパラメーターの数と型は、コンパイル時に不明です。

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

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

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

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

編集する

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

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

変換中、コンパイラは、オブジェクトの名前、タイプ、格納期間、スコープなどに関する情報を格納するテーブル(シンボルテーブルと呼ばれることが多い)を維持します。宣言した bcas 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,-aldhCのソースコードと交互に生成されたマシンコードのリスト作成にはgccでオプションを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、実行時に実行するには、のためのスペースbc(いくつかの他のものと一緒に)は、スタックポインタを調整することによって、スタックから割り当てられます。

subq    $16, %rsp

コードは、それらのフレームポインタからのオフセットによってそれらのオブジェクトを指す4bある-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

フロートとして宣言bcているため、コンパイラーは特に浮動小数点値を処理するマシンコードを生成しました。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);

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

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


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

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

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

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

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


Cが静的に型付けされた言語である場合、putchar()は型に言及せずに正しいデータ型をどのように出力しますか?
user106313 14

@ user31782:putchar関数の定義では、型の引数が1つ必要intです。コンパイラがマシンコードを生成するとき、そのマシンコードは常に単一の整数引数を受け取ると想定します。実行時にタイプを指定する必要はありません。
John Bode 14

putchar()を使用してアルファベットを出力できます。
user106313 14

2
@ user31782:printfすべての出力をテキスト(ASCIIまたはその他)としてフォーマットします。変換指定子は、出力をフォーマットする方法を指示します。 printf( "%d\n", 65 );書き込む文字の配列 '6''5'するので、標準出力への%d変換指定は、10進数として対応する引数の書式を設定するためにそれを伝えます。 printf( "%c\n", 65 );は、引数を実行文字セットの文字としてフォーマットするよう指示する'A'ため、文字を標準出力に書き込みます。%cprintf
John Bode 14

1
@ user31782:言語定義の変更なしではありません。もちろん可能です(C ++も静的に型指定されますが、<<and >>演算子とI / O演算子の型を推論することができます)が、言語が多少複雑になります。慣性は時々克服するのが難しいです。
John Bode

8

というのは、printfが呼び出されてその仕事をするとき、コンパイラーはもはや何をすべきかを指示するためにそこにいないからです。

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

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


したがって、printf()関数は "x"が変数であることを認識していますが、そのタイプが何であるかは認識していませんが、コンパイラは認識しています。printf()を更新してデータのタイプを認識できないようにすることはできません。さらに、C ++では、「cout」は自動的にそのタイプを知ることでデータを印刷できることを思い出します。
user106313 14年

@ user31782 C関数呼び出しは非常に簡単です。そのすべてprintf()渡される書式文字列と引数を見つけることができるバッファへのポインタへのポインタです。このバッファーの長ささえ渡されません!これが、Cが他の言語よりもはるかに高速になる理由の1つです。あなたが提案しているのは、桁違いに複雑です。
grahamparks 2014年

@grahamparksは、C ++では「cout」が「printf」よりも低速です。「cout」のようにprintf()を作成すると遅くなるでしょうか?
user106313 2014年

3
@ user31782:実行時に必ずしも遅くなるとは限りません(いつものように異なります)が、Cにはない言語機能が必要です。Cにはcout、C ++で使用されているテンプレートメカニズムはもちろんのこと、関数のオーバーロードがありません。
マット

5
はい。ただし、Cは1972年に登場したことを覚えておいてください。これらの機能は、かなり後に発明されました。
Kilian Foth、2014年

5

printf()可変引数関数と呼ばれるもので、可変数の引数を受け入れる関数です。

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ようなの中置式コンパイル時に誤りがないか評価し、何かに右辺の式をキャストするコードに変換することができますに対処することができます。cout << foo << barcout


3

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

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

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

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


0

定義した別の関数に変数を渡すように考えてください。通常は、他の関数に、予想/受信する必要があるデータのタイプを通知します。と同じ方法printf()。それはすでにstdio.hライブラリで定義されており、正しい形式で出力できるように、受信するデータを通知する必要があります(あなたの場合のようにint)。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.