web-dev-qa-db-ja.com

Cのstdargとprintf()

_<stdarg.h>_ヘッダーファイルは、関数が未定義の数の引数を受け入れるようにするために使用されますよね?

したがって、_<stdio.h>_のprintf()関数は、可変数の引数を受け入れるために_<stdarg.h>_を使用している必要があります(間違っている場合は修正してください)。

gccのstdio.hファイルに次の行が見つかりました。

_#if defined __USE_XOPEN || defined __USE_XOPEN2K8
# ifdef __GNUC__
#  ifndef _VA_LIST_DEFINED
typedef _G_va_list va_list;
#   define _VA_LIST_DEFINED
#  endif
# else
#  include <stdarg.h>//////////////////////stdarg.h IS INCLUDED!///////////
# endif
#endif
_

何が入っているのかほとんどわかりませんが、_<stdarg.h>_が含まれているようです

したがって、printf()が可変数の引数を受け入れるために_<stdarg.h>_を使用し、_stdio.h_がprintf()を持っている場合、printf()を使用するCプログラムは必要ありません。 _<stdarg.h>_を含めますか?

printf()と可変数の引数を受け入れるユーザー定義関数を持つプログラムを試しました。

私が試したプログラムは次のとおりです。

_#include<stdio.h>
//#include<stdarg.h>///If this is included, the program works fine.

void fn(int num, ...)
{
    va_list vlist;
    va_start(vlist, num);//initialising va_start (predefined)

    int i;

    for(i=0; i<num; ++i)
    {
        printf("%d\n", va_arg(vlist, int));
    }

    va_end(vlist);//clean up memory
}

int main()
{
    fn(3, 18, 11, 12);
    printf("\n");
    fn(6, 18, 11, 32, 46, 01, 12);

    return 0;
}
_

_<stdarg.h>_が含まれている場合は正常に機能しますが、含まれていない場合は次のエラーが生成されます。

_40484293.c:13:38: error: expected expression before ‘int’
         printf("%d\n", va_arg(vlist, int));//////error: expected expression before 'int'/////////
                                      ^~~
_

これはどのように?

それとも、printf()が可変数の引数を受け入れるために_<stdarg.h>_を使用しないということですか?もしそうなら、それはどのように行われますか?

17
J...S

考えてみましょう:

stdio.h:

int my_printf(const char *s, ...); 

<stdarg.h>が必要ですか? いいえ、あなたはしません。 ...は、言語の文法の一部であり、「組み込み」です。ただし、このような引数のリストを使用して意味のある移植可能なものを実行したい場合は、va_listva_startなどで定義されているnamesが必要です。

stdio.c:

#include "stdio.h"
#include "stdarg.h"

int my_printf(const char *s, ...)
{
   va_list va;
   va_start(va, s);

   /* and so on */
}

しかし、これは基本的に、libcのimplementationで必要になります。これは、ライブラリを自分でコンパイルしない限り表示されないものです。代わりに取得するのは、すでにマシンコードにコンパイルされているlibc共有ライブラリです。

したがって、printf()が可変数の引数を受け入れるために使用し、stdio.hにprintf()がある場合、printf()を使用するCプログラムはそれを含める必要はありませんか?

たとえそうだったとしても、あなたはできませんそれに依存します。そうでなければ、コードの形式が正しくありません。実装がすでにそれを行っているかどうかに関係なく、ヘッダーに属する名前を使用する場合は、とにかくすべてのヘッダーを含める必要がありますか否か。

13
edmz

stdargヘッダーファイルは、関数が未定義の数の引数を受け入れるようにするために使用されますよね?

いいえ、_<stdarg.h>_は、追加の引数にアクセスするために使用する必要があるAPIを公開するだけです。次のように、可変数の引数を受け入れる関数を宣言するだけの場合は、そのヘッダーを含める必要はありません。

_int foo(int a, ...);
_

これは言語機能であり、追加の宣言/定義は必要ありません。

Gccのstdio.hファイルに次の行が見つかりました。

_#if defined __USE_XOPEN || defined __USE_XOPEN2K8
# ifdef __GNUC__
#  ifndef _VA_LIST_DEFINED
typedef _G_va_list va_list;
#   define _VA_LIST_DEFINED
#  endif
# else
#  include <stdarg.h>//////////////////////stdarg.h IS INCLUDED!///////////
# endif
#endif
_

このようなものは、_<stdarg.h>_を内部に含めずにvprintf()のようなものを宣言するためにのみ必要だと思います。

_int vprintf(const char *format, va_list ap);
_

あげくの果てに:

  • 可変数の引数を持つ関数を宣言するヘッダーには、内部に_<stdarg.h>_を含めないでください。
  • 可変数の引数を使用する関数の実装には、_<stdarg.h>_を含め、_va_list_ APIを使用して追加の引数にアクセスする必要があります。
10
Sergio

最初に、C標準の観点からあなたの質問に答えます。それが、コードの書き方を教えてくれるからです。

C標準では、stdio.hが「あたかも動作する」必要があります not includestdarg.h。言い換えると、マクロva_startva_argva_end、およびva_copy、およびタイプva_listは、stdio.hを含めることによって使用可能にする必要があります not 。言い換えれば、このプログラムはコンパイルするために not が必要です:

#include <stdio.h>

unsigned sum(unsigned n, ...)
{
   unsigned total = 0;
   va_list ap;
   va_start(ap, n);
   while (n--) total += va_arg(ap, unsigned);
   va_end(ap);
   return total;
}

(これはC++との違いです。C++では、すべての標準ライブラリヘッダーで相互に含めることができますが、必須ではありません。)

printf implementation が(おそらく)stdarg.hメカニズムを使用して引数にアクセスするのは事実ですが、それは単に、のソースコード内のいくつかのファイルを意味します。 Cライブラリ( "printf.c"、おそらく)には、stdarg.hstdio.hを含める必要があります。それはあなたのコードに影響を与えません。

stdio.hva_list型の引数を取る関数を宣言していることも事実です。これらの宣言を見ると、実際には2つのアンダースコア、またはアンダースコアと大文字で始まるtypedef名が使用されていることがわかります。たとえば、同じstdio.hで、

$ egrep '\<v(printf|scanf) *\(' /usr/include/stdio.h
extern int vprintf (const char *__restrict __format, _G_va_list __arg);
extern int vscanf (const char *__restrict __format, _G_va_list __arg);

2つのアンダースコア、またはアンダースコアと大文字で始まるすべての名前は、実装用に予約されています-stdio.hは、必要な数の名前を宣言できます。逆に、アプリケーションプログラマーは、 any のような名前を宣言したり、実装で宣言された名前を使用したりすることはできません(_POSIX_C_SOURCE__GNUC__などの文書化されたサブセットを除く) 。コンパイラーはそれを可能にしますが、効果は定義されていません。


次に、stdio.hから引用したことについて説明します。ここに再びあります:

#if defined __USE_XOPEN || defined __USE_XOPEN2K8
# ifdef __GNUC__
#  ifndef _VA_LIST_DEFINED
typedef _G_va_list va_list;
#   define _VA_LIST_DEFINED
#  endif
# else
#  include <stdarg.h>
# endif
#endif

これが何をしているのかを理解するには、次の3つのことを知る必要があります。

  1. POSIX.1 の最近の「問題」、「Unix」オペレーティングシステムの意味の公式仕様では、va_listが定義することになっているもののセットにstdio.hを追加します。 (具体的には、 Issue 6 では、va_liststdio.hによって「XSI」拡張として定義され、 Issue 7 では必須です。)このコードはva_listを定義しますが、プログラムがIssue6 + XSIまたはIssue7の機能を要求しました。それが#if defined __USE_XOPEN || defined __USE_XOPEN2K8の意味です。 _G_va_listを使用してva_listを定義していることに注意してください。他の場所では、_G_va_listを使用してvprintfを宣言しているのと同じです。 _G_va_listはすでに何らかの形で利用可能です。

  2. 同じ変換単位で同じtypedefを2回書き込むことはできません。 stdio.hva_listに再度実行しないように通知せずに、stdarg.hを定義した場合、

    #include <stdio.h>
    #include <stdarg.h>
    

    コンパイルされません。

  3. GCCにはstdarg.hのコピーが付属していますが、 not にはstdio.hのコピーが付属しています。あなたが引用しているstdio.hGNU libc から来ています。これは、GNU傘下の別のプロジェクトであり、別の(しかし重複している)人々のグループによって維持されています。重要なのは、 GNU libcのヘッダーは、GCCによってコンパイルされていると想定することはできません。

したがって、引用したコードはva_listを定義します。 __GNUC__が定義されている場合、つまりコンパイラがGCCまたはquirk互換クローンである場合、stdarg.hという名前のマクロを使用して_VA_LIST_DEFINEDと通信できると想定します。このマクロは、va_listが定義されている場合にのみ定義されますが、マクロである場合は、 #ifで確認できます。 stdio.hva_list自体を定義してから_VA_LIST_DEFINEDを定義できますが、stdarg.hはそれを行いません。

#include <stdio.h>
#include <stdarg.h>

正常にコンパイルされます。 (おそらくシステムのstdarg.hに隠れているGCCの/usr/lib/gcc/something/something/includeを見ると、このコードの鏡像と、 other マクロの非常に長いリストが表示されます。 other GCCで使用できる、または一度使用できるCライブラリの場合、「va_listを定義しないでください。すでに定義しています。)

ただし、__GNUC__が定義されていない場合、stdio.hは、stdarg.hとの通信方法を not 知っていると見なします。ただし、C標準では動作する必要があるため、同じファイルにstdarg.hを2回含めても安全であることはわかっています。したがって、va_listを定義するために、先に進んでstdarg.hを含めます。したがって、va_*が定義しないであるstdio.hマクロも定義されます。

これは、HTML5の人々がC標準の「故意の違反」と呼ぶものです。このように間違っていると、利用可能な他の方法よりも実際のコードが破損する可能性が低いため、意図的に間違っています。特に、

#include <stdio.h>
#include <stdarg.h>

圧倒的に実際のコードに表示される可能性が高い

#include <stdio.h>
#define va_start(x, y) /* something unrelated to variadic functions */

したがって、両方が機能することになっている場合でも、最初の1つを2番目よりも機能させることがはるかに重要です。


最後に、あなたはまだ一体_G_va_listがどこから来たのか疑問に思うかもしれません。 stdio.h自体のどこにも定義されていないため、コンパイラ組み込み関数であるか、stdio.hに含まれるヘッダーの1つで定義されている必要があります。システムヘッダーに含まれるすべてのものを見つける方法は次のとおりです。

$ echo '#include <stdio.h>' | gcc -H -xc -std=c11 -fsyntax-only - 2>&1 | grep '^\.'
. /usr/include/stdio.h
.. /usr/include/features.h
... /usr/include/x86_64-linux-gnu/sys/cdefs.h
.... /usr/include/x86_64-linux-gnu/bits/wordsize.h
... /usr/include/x86_64-linux-gnu/gnu/stubs.h
.... /usr/include/x86_64-linux-gnu/gnu/stubs-64.h
.. /usr/lib/gcc/x86_64-linux-gnu/6/include/stddef.h
.. /usr/include/x86_64-linux-gnu/bits/types.h
... /usr/include/x86_64-linux-gnu/bits/wordsize.h
... /usr/include/x86_64-linux-gnu/bits/typesizes.h
.. /usr/include/libio.h
... /usr/include/_G_config.h
.... /usr/lib/gcc/x86_64-linux-gnu/6/include/stddef.h
.... /usr/include/wchar.h
... /usr/lib/gcc/x86_64-linux-gnu/6/include/stdarg.h
.. /usr/include/x86_64-linux-gnu/bits/stdio_lim.h
.. /usr/include/x86_64-linux-gnu/bits/sys_errlist.h

-std=c11を使用して、POSIX Issue 6 + XSIモードまたはIssue7モードで not コンパイルされていないことを確認しましたが、とにかくこのリストにstdarg.hが表示されます— stdio.hに直接含まれていませんが、 libio.h。これは標準のヘッダーではありません。そこを見てみましょう:

#include <_G_config.h>
/* ALL of these should be defined in _G_config.h */
/* ... */
#define _IO_va_list _G_va_list

/* This define avoids name pollution if we're using GNU stdarg.h */
#define __need___va_list
#include <stdarg.h>
#ifdef __GNUC_VA_LIST
# undef _IO_va_list
# define _IO_va_list __gnuc_va_list
#endif /* __GNUC_VA_LIST */

したがって、libio.hは特別なモードでstdarg.hを含み(実装マクロがシステムヘッダー間の通信に使用される別のケースです)、__gnuc_va_listを定義することを期待しますが、それを使用して_IO_va_listではなく_G_va_listを定義します。 _G_va_list_G_config.h ..によって定義されます。

/* These library features are always available in the GNU C library.  */
#define _G_va_list __gnuc_va_list

...__gnuc_va_listに関して。 その名前stdarg.hによって定義されます:

/* Define __gnuc_va_list.  */
#ifndef __GNUC_VA_LIST
#define __GNUC_VA_LIST
typedef __builtin_va_list __gnuc_va_list;
#endif

そして最後に、__builtin_va_listは文書化されていないGCC固有のものであり、「現在のABIでva_listに適切なタイプは何でも」を意味します。

$ echo 'void foo(__builtin_va_list x) {}' |
    gcc -xc -std=c11 -fsyntax-only -; echo $?
0

(はい、GNU libcのstdioの実装は、それが存在するための言い訳よりもはるかに複雑です。説明は、昔の人々が試したことですFILEオブジェクトをC++ filebufとして直接使用できるようにするため。これは何十年も機能していません。実際、これまでが機能したかどうかはわかりません。以前は放棄されていました- [〜#〜] egcs [〜#〜] 、これは私が歴史を知っている限りさかのぼります—しかし、試行の痕跡はたくさんあります。バイナリの後方互換性のため、または誰もそれらをクリーンアップすることに慣れていないために、まだぶらぶらしています。)

(はい、これを正しく読んでいると、GNU libcのstdio.hは、stdarg.h__gnuc_va_listを定義していないCコンパイラでは正しく機能しません。これは抽象的に間違っていますが、無害です。 GNU libcには、さらに多くの心配事があります。)で動作する光沢のある新しい非GCC互換コンパイラ。

10
zwol

いいえ、printf()を使用するために必要なのは#include <stdio.h>だけです。 printfはすでにコンパイルされているため、stdargは必要ありません。コンパイラーは、printfのプロトタイプを見て、それが可変個引数であることを知る必要があります(プロトタイプの省略記号...から派生)。 printfstdioライブラリのソースコードを見ると、<stdarg.h>が含まれていることがわかります。

独自の可変個引数関数を作成する可変個引数関数を使用する場合は、必須#include <stdarg.h>し、それに応じてマクロを使用します。ご覧のとおり、これを忘れると、va_start/list/endシンボルはコンパイラーに認識されません。

printfの実際の実装を見たい場合は、 FreeBSDの標準I/Oソースのコードvfprintfのソース)を見てください。

9
Jens

モジュールをヘッダーファイルとソースファイルに分割するための基本:

  • ヘッダーファイルには、モジュールのインターフェイスのみを配置します
  • ソースファイルに、モジュールの実装を配置します

したがって、printfの実装が_va_arg_を利用している場合でも、次のように推測します。

  • _stdio.h_では、作成者はint printf(const char* format, ...);のみを宣言しました
  • _stdio.c_では、作成者は_va_arg_を使用してprintfを実装しました
2
barak manos

このstdio.hの実装には、gccでコンパイルした場合のstdarg.hは含まれません。コンパイラの作成者が常に気を配っているのは魔法のように機能します。

Cソースファイルには、とにかく参照するすべてのシステムヘッダーが含まれている必要があります。これはC標準の要件です。つまり、ソースコードでstdarg.hに存在する定義が必要な場合は、直接、または含まれているyourヘッダーファイルのいずれかに#include <stdarg.h>ディレクティブを含める必要があります。 stdarg.hが実際に含まれている場合でも、他のstandardヘッダーに含まれていることに依存することはできません。

2

_<stdarg.h>_ファイルは、可変数の引数関数を実装するの場合にのみ含める必要があります。 printf(3)やその仲間を使用できる必要はありません。可変数のargs関数で引数を処理する場合にのみ、_va_list_タイプと、_va_start_、_va_arg_、および_va_end_マクロが必要になります。したがって、その場合にのみ、そのファイルを強制的に含める必要があります。

一般に、_<stdarg.h>_が_<stdio.h>_を含めるだけで含まれることは保証されません。実際、引用するコードonlyには___GNU_C___ 定義されていない(これは事実であると思われるため、ケースには含まれていません)の場合、このマクロはgccコンパイラを使用している場合に定義されます。

コードで可変引数受け渡し関数を作成する場合、最善のアプローチは、別のインクルードファイルにそれが含まれることを期待しないことですが、自分で行います (要求された機能のクライアントとして)_va_list_タイプ、または_va_start_、_va_arg_、または_va_end_マクロを使用しているすべての場所。

以前は、一部のヘッダーファイルが二重インクルージョンから保護されていなかったため(同じインクルードファイルが二重に定義されたマクロなどに関するエラーを2回以上生成し、注意が必要だったため)、二重インクルージョンについて混乱がありましたが、今日は、これは問題ではなく、通常、すべての標準ヘッダーフィールドが二重インクルードから保護されています。

1
Luis Colorado