stdarg y printf() en C




gcc variadic-functions (6)

El archivo de encabezado stdarg se usa para hacer que las funciones acepten un número indefinido de argumentos, ¿verdad?

No, <stdarg.h> simplemente expone una API que debe usarse para acceder a argumentos adicionales. No es necesario incluir ese encabezado si solo desea declarar una función que acepte un número variable de argumentos, como este:

int foo(int a, ...);

Esta es una característica del lenguaje y no requiere declaraciones / definiciones adicionales.

Encontré las siguientes líneas en el archivo stdio.h de gcc:

#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

Supongo que esto es necesario solo para declarar cosas como vprintf() sin incluir interno de <stdarg.h> :

int vprintf(const char *format, va_list ap);

Para acabar:

  • El encabezado que declara la función con un número variable de argumentos no debe incluir <stdarg.h> internamente.
  • La implementación de la función con un número variable de argumentos debe incluir <stdarg.h> y usar la API va_list para acceder a argumentos adicionales.

El archivo de encabezado <stdarg.h> se usa para hacer que las funciones acepten un número indefinido de argumentos, ¿verdad?

Por lo tanto, la printf() de <stdio.h> debe estar usando <stdarg.h> para aceptar un número variable de argumentos ( <stdarg.h> si me equivoco).

Encontré las siguientes líneas en el archivo stdio.h de gcc:

#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

No puedo entender la mayoría de lo que <stdarg.h> , pero parece que incluye <stdarg.h>

Entonces, si printf() usa <stdarg.h> para aceptar el número variable de argumentos y stdio.h tiene printf() , un programa en C que use printf() no necesita incluir <stdarg.h> ¿lo hace?

Probé un programa que tenía printf() y una función definida por el usuario que aceptaba un número variable de argumentos.

El programa que probé es:

#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;
}

Funciona bien si se incluye <stdarg.h> pero de lo contrario genera el siguiente error:

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

¿Cómo es esto?

¿O es que printf() no usa <stdarg.h> para aceptar el número variable de argumentos? Si es así, ¿Cómo se hace?


Bien, existe la familia "regular" de printf: printf, fprintf, dprintf, sprintf y snprintf. Y luego está el número variable de argumentos de la familia printf: vprintf, vfprintf, vdprintf, vsprintf y vsnprintf.

Para usar una lista variable de argumentos con cualquiera de los dos, debe declarar stdarg.h. stdarg.h define todas las macros que está utilizando: va_list, va_start, va_arg, va_end y va_copy.


El archivo <stdarg.h> debe incluirse solo si va a implementar una función de número variable de argumentos . No es necesario poder utilizar printf(3) y amigos. Solo si va a procesar argumentos en un número variable de funciones args, necesitará el tipo va_list y las va_start , va_arg y va_end . Entonces, solo entonces necesitarás incluir ese archivo a la fuerza.

En general, no está garantizado que <stdarg.h> se incluya solo con <stdio.h> De hecho, el código que usted cita solo lo incluye, si __GNU_C__ no está definido (lo cual sospecho que es el caso, así que es no incluido en su caso) y esta macro se define si está utilizando el compilador gcc .

Si va a crear funciones de paso de argumentos variables en su código, el mejor enfoque es no esperar que otro archivo incluido lo incluya, sino que lo haga usted mismo (como cliente para la funcionalidad solicitada que está) donde sea que esté usando el tipo va_list , o va_start , va_arg o va_end macros.

En el pasado, existía cierta confusión acerca de la inclusión doble, ya que algunos archivos de encabezado no estaban protegidos de la inclusión doble (incluyendo dos o más veces el mismo archivo de error incluido en macros definidas de manera doble o similar y tenía que ir con cuidado) pero hoy , esto no es un problema y normalmente todos los campos de encabezado estándar están protegidos de la doble inclusión.


Esta implementación de stdio.h no incluye stdarg.h cuando se compila con gcc. Funciona por la magia que los escritores de compiladores siempre tienen bajo sus mangas.

Los archivos de origen de C deben incluir todos los encabezados del sistema al que hacen referencia de todos modos. Es un requisito de la norma C. Es decir, si su código fuente requiere definiciones presentes en stdarg.h, debe contener la directiva #include <stdarg.h> directamente o en uno de sus archivos de encabezado que incluye. No puede depender de que stdarg.h se incluya en otros encabezados estándar , incluso si de hecho lo incluyen.


No, para usar printf() todo lo que necesita es #include <stdio.h> . No hay necesidad de stdarg porque printf ya está compilado. El compilador solo necesita ver un prototipo para printf para saber que es variadic (derivado de los puntos suspensivos ... en el prototipo). Si observa el código fuente de la biblioteca stdio para printf , verá que se <stdarg.h> .

Si desea escribir su propia función variadic, debe #include <stdarg.h> y usar sus macros en consecuencia. Como puede ver, si olvida hacerlo, los va_start/list/end son desconocidos para el compilador.

Si desea ver una implementación real de printf , mire el código en la fuente de E / S estándar de FreeBSD , junto con la fuente para vfprintf .


Primero voy a responder tu pregunta en términos del estándar C, porque eso es lo que te dice cómo debes escribir tu código.

El estándar C requiere que stdio.h "se comporte como si" no incluye stdarg.h . En otras palabras, las macros va_start , va_arg , va_end y va_copy , y el tipo va_list , no deben estar disponibles al incluir stdio.h . En otras palabras, se requiere que este programa no compile:

#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;
}

(Esta es una diferencia con respecto a C ++. En C ++, todos los encabezados de biblioteca estándar pueden, pero no se requieren, incluirse entre sí).

Es cierto que la implementación de printf (probablemente) utiliza el mecanismo stdarg.h para acceder a sus argumentos, pero eso solo significa que algunos archivos en el código fuente de la biblioteca C (" printf.c ", tal vez) deben incluir stdarg.h así como stdio.h ; Eso no afecta tu código.

También es cierto que stdio.h declara funciones que toman los argumentos va_list -typed. Si observa esas declaraciones, verá que realmente usan un nombre typedef que comienza con dos guiones bajos, o un guión bajo y una letra mayúscula: por ejemplo, con el mismo stdio.h que está viendo,

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

Todos los nombres que comienzan con dos guiones bajos, o un guión bajo y una letra mayúscula, están reservados para la implementación: stdio.h puede declarar tantos nombres como desee. A la inversa, usted, el programador de la aplicación, no puede declarar ninguno de estos nombres, o usar los que la implementación declara (excepto el subconjunto que está documentado, como _POSIX_C_SOURCE y __GNUC__ ). El compilador te permitirá hacerlo, pero los efectos no están definidos.

Ahora voy a hablar sobre lo que citó de stdio.h . Aquí está de nuevo:

#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

Para entender lo que esto está haciendo, necesitas saber tres cosas:

  1. Los "problemas" recientes de POSIX.1 , la especificación oficial de lo que significa ser un sistema operativo "Unix", agrega va_list al conjunto de cosas que se supone que stdio.h debe definir. (Específicamente, en el problema 6 , la va_list está definida por stdio.h como una extensión "XSI", y en el problema 7 es obligatoria). Este código define la va_list , pero solo si el programa ha solicitado las funciones del problema 6 + XSI o problema 7; eso es lo que #if defined __USE_XOPEN || defined __USE_XOPEN2K8 #if defined __USE_XOPEN || defined __USE_XOPEN2K8 significa. Tenga en cuenta que está utilizando _G_va_list para definir va_list , al igual que, en otros lugares, utilizó _G_va_list para declarar vprintf . _G_va_list ya está disponible de alguna manera.

  2. No puede escribir el mismo typedef dos veces en la misma unidad de traducción. Si stdio.h definió va_list sin notificar de alguna manera a stdarg.h que no lo vuelva a hacer,

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

    no compilaría

  3. GCC viene con una copia de stdarg.h , pero no viene con una copia de stdio.h . El stdio.h que está citando proviene de GNU libc , que es un proyecto separado bajo el paraguas de GNU, mantenido por un grupo de personas separado (pero superpuesto). De manera crucial, los encabezados de libc de GNU no pueden asumir que están siendo compilados por GCC.

Por lo tanto, el código que usted cita define va_list . Si se define __GNUC__ , lo que significa que el compilador es GCC o un clon compatible con peculiaridades, se supone que puede comunicarse con stdarg.h utilizando una macro llamada _VA_LIST_DEFINED , que se define si y solo si está definido va_list , pero es una macro , puedes comprobarlo con #if . stdio.h puede definir va_list y luego definir _VA_LIST_DEFINED , y luego stdarg.h no lo hará, y

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

se compilará bien. (Si observa stdarg.h de GCC, que probablemente se oculta en /usr/lib/gcc/something/something/include en su sistema, verá la imagen de espejo de este código, junto con una lista hilarantemente larga de otras macros eso también significa "no defina va_list , ya lo hice" para otras bibliotecas de C con las que GCC puede, o podría usarse, una vez.)

Pero si __GNUC__ no está definido, entonces stdio.h asume que no sabe cómo comunicarse con stdarg.h . Pero sí sabe que es seguro incluir stdarg.h dos veces en el mismo archivo, porque el estándar C requiere que funcione. Así que para poder definir va_list , simplemente continúa e incluye stdarg.h , y por lo tanto, también se va_* macros va_* que stdio.h no debe definir.

Esto es lo que la gente de HTML5 llamaría una "violación intencional" del estándar C: está mal, a propósito, porque estar equivocado de esta manera es menos probable que rompa el código del mundo real que cualquier otra alternativa disponible. En particular,

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

es abrumadoramente más probable que aparezca en código real que

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

así que es mucho más importante hacer que el primero funcione que el segundo, aunque se supone que ambos funcionan.

Por último, es posible que todavía se pregunte de dónde viene la _G_va_list . No se define en ningún lugar en stdio.h , por lo que debe ser un compilador intrínseco o estar definido por uno de los encabezados que incluye stdio.h . Aquí es cómo se encuentra todo lo que un encabezado del sistema incluye:

$ 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 para asegurarme de que no estaba compilando en POSIX Issue 6 + XSI ni en Issue 7, y sin embargo vemos stdarg.h en esta lista de todos modos, no incluido directamente por stdio.h , sino por libio.h , que no es un encabezado estándar. Echemos un vistazo allí:

#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 */

Por libio.h tanto, libio.h incluye stdarg.h en un modo especial (aquí hay otro caso en el que las macros de implementación se usan para comunicarse entre los encabezados del sistema), y espera que defina __gnuc_va_list , pero lo usa para definir _IO_va_list , no _G_va_list . _G_va_list se define por _G_config.h ...

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

... en términos de __gnuc_va_list . Ese nombre está definido por stdarg.h :

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

Y __builtin_va_list , finalmente, es un intrínseco GCC no documentado, lo que significa "cualquier tipo que sea apropiado para una va_list con el ABI actual".

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

(Sí, la implementación de stdio por parte de GNU libc es mucho más complicada de lo que tiene una excusa para ser. La explicación es que en tiempos pasados ​​las personas intentaron que su objeto FILE pudiera usar directamente como un filebuf C ++. Eso no ha funcionado en décadas. de hecho, no estoy seguro de si alguna vez funcionó, ya que había sido abandonado antes de EGCS , que es tan antiguo como conozco la historia, pero hay muchos, muchos vestigios del intento aún por ahí, ya sea por compatibilidad binaria hacia atrás o porque nadie ha logrado limpiarlos.)

(Sí, si estoy leyendo esto correctamente, el stdio.h GNU libc no funcionará correctamente con un compilador C cuyo stdarg.h no define __gnuc_va_list . Esto es abstractamente incorrecto, pero inofensivo; cualquiera que desee un nuevo y brillante no El compilador compatible con GCC para trabajar con GNU libc tendrá muchas más cosas de las que preocuparse.)







variadic-functions