¿Cómo funciona el método main () en C?

96

Sé que hay dos firmas diferentes para escribir el método principal:

int main()
{
   //Code
}

o para manejar el argumento de la línea de comando, lo escribimos como-

int main(int argc, char * argv[])
{
   //code
}

En C++Sé que podemos sobrecargar un método, sino en Ccómo manejar el compilador de estas dos firmas diferentes de mainla función?

Ritesh
fuente
14
La sobrecarga se refiere a tener dos métodos con el mismo nombre en el mismo programa. Solo puede tener un mainmétodo en un solo programa en C(o, en realidad, en casi cualquier lenguaje con tal construcción).
Kyle Strand
12
C no tiene métodos; tiene funciones. Los métodos son la implementación de back-end de funciones "genéricas" orientadas a objetos. El programa llama a una función con algunos argumentos de objeto, y el sistema de objetos elige un método (o quizás un conjunto de métodos) según sus tipos. C no tiene nada de esto a menos que lo simule usted mismo.
Kaz
4
Para una discusión profunda sobre los puntos de entrada del programa, no en particular main, recomiendo el libro clásico de John R. Levines "Linkers & Loaders".
Andreas Spindler
1
En C, la primera forma es int main(void)no int main()(aunque nunca he visto un compilador que rechace la int main()forma).
Keith Thompson
1
@harper: el ()formulario es obsoleto y no está claro si siquiera está permitido main(a menos que la implementación lo documente específicamente como un formulario permitido). El estándar C (consulte 5.1.2.2.1 Inicio del programa) no menciona el ()formulario, que no es del todo equivalente al ()formulario. Los detalles son demasiado largos para este comentario.
Keith Thompson

Respuestas:

132

Algunas de las características del lenguaje C comenzaron como hacks que simplemente funcionaron.

Varias firmas para listas de argumentos principales y de longitud variable es una de esas características.

Los programadores notaron que pueden pasar argumentos adicionales a una función y no pasa nada malo con su compilador dado.

Este es el caso si las convenciones de llamada son tales que:

  1. La función de llamada limpia los argumentos.
  2. Los argumentos más a la izquierda están más cerca de la parte superior de la pila, o de la base del marco de la pila, de modo que los argumentos falsos no invalidan el direccionamiento.

Un conjunto de convenciones de llamadas que obedecen estas reglas es el paso de parámetros basados ​​en la pila, mediante el cual el llamador muestra los argumentos y se empujan de derecha a izquierda:

 ;; pseudo-assembly-language
 ;; main(argc, argv, envp); call

 push envp  ;; rightmost argument
 push argv  ;; 
 push argc  ;; leftmost argument ends up on top of stack

 call main

 pop        ;; caller cleans up   
 pop
 pop

En los compiladores en los que se da este tipo de convención de llamada, no es necesario hacer nada especial para admitir los dos tipos maino incluso los tipos adicionales. mainpuede ser una función sin argumentos, en cuyo caso es ajeno a los elementos que se insertaron en la pila. Si es una función de dos argumentos, entonces busca argcy argvcomo los dos elementos de la pila más altos. Si es una variante de tres argumentos específica de la plataforma con un puntero de entorno (una extensión común), eso también funcionará: encontrará ese tercer argumento como el tercer elemento de la parte superior de la pila.

Por tanto, una llamada fija funciona para todos los casos, lo que permite vincular al programa un único módulo fijo de puesta en marcha. Ese módulo podría escribirse en C, como una función parecida a esta:

/* I'm adding envp to show that even a popular platform-specific variant
   can be handled. */
extern int main(int argc, char **argv, char **envp);

void __start(void)
{
  /* This is the real startup function for the executable.
     It performs a bunch of library initialization. */

  /* ... */

  /* And then: */
  exit(main(argc_from_somewhere, argv_from_somewhere, envp_from_somewhere));
}

En otras palabras, este módulo de inicio solo llama a un main de tres argumentos, siempre. Si main no toma argumentos, o solo int, char **, resulta que funciona bien, así como si no toma argumentos, debido a las convenciones de llamada.

Si tuviera que hacer este tipo de cosas en su programa, ISO C no sería portátil y lo consideraría un comportamiento indefinido: declarar y llamar a una función de una manera y definirla de otra. Pero el truco de inicio de un compilador no tiene por qué ser portátil; no se rige por las reglas de los programas portátiles.

Pero suponga que las convenciones de llamada son tales que no puede funcionar de esta manera. En ese caso, el compilador debe tratar mainespecialmente. Cuando se da cuenta de que está compilando la mainfunción, puede generar código que sea compatible con, digamos, una llamada de tres argumentos.

Es decir, escribe esto:

int main(void)
{
   /* ... */
}

Pero cuando el compilador lo ve, esencialmente realiza una transformación de código para que la función que compila se parezca más a esto:

int main(int __argc_ignore, char **__argv_ignore, char **__envp_ignore)
{
   /* ... */
}

excepto que los nombres __argc_ignoreno existen literalmente. No se introducen tales nombres en su ámbito y no habrá ninguna advertencia sobre los argumentos no utilizados. La transformación de código hace que el compilador emita código con el enlace correcto que sabe que tiene que limpiar tres argumentos.

Otra estrategia de implementación es que el compilador o quizás el enlazador genere la __startfunción de manera personalizada (o como se llame), o al menos seleccione una de varias alternativas precompiladas. La información podría almacenarse en el archivo de objeto sobre cuál de las formas admitidas de mainse está utilizando. El vinculador puede ver esta información y seleccionar la versión correcta del módulo de inicio que contiene una llamada a la mainque es compatible con la definición del programa. Las implementaciones de C generalmente tienen solo una pequeña cantidad de formas compatibles, por mainlo que este enfoque es factible.

Los compiladores para el lenguaje C99 siempre tienen que tratar mainespecialmente, hasta cierto punto, para soportar el truco que si la función termina sin una returndeclaración, el comportamiento es como si return 0se hubiera ejecutado. Esto, nuevamente, puede tratarse mediante una transformación de código. El compilador nota que mainse está compilando una función llamada . Luego comprueba si el final del cuerpo es potencialmente alcanzable. Si es así, inserta unreturn 0;

Kaz
fuente
34

NO hay sobrecarga mainincluso en C ++. La función principal es el punto de entrada para un programa y solo debe existir una única definición.

Para el estándar C

Para un entorno alojado (que es el normal), el estándar C99 dice:

5.1.2.2.1 Inicio del programa

La función llamada al inicio del programa se denomina main. La implementación no declara ningún prototipo para esta función. Se definirá con un tipo de retorno de inty sin parámetros:

int main(void) { /* ... */ }

o con dos parámetros (a los que se hace referencia aquí como argcy argv, aunque se pueden utilizar nombres, ya que son locales a la función en la que se declaran):

int main(int argc, char *argv[]) { /* ... */ }

o equivalente; 9) o de alguna otra manera definida por la implementación.

9) Por lo tanto, intse puede reemplazar por un nombre de typedef definido como int, o el tipo de argvse puede escribir como char **argv, y así sucesivamente.

Para C ++ estándar:

3.6.1 Función principal [basic.start.main]

1 Un programa debe contener una función global denominada main, que es el inicio designado del programa. [...]

2 Una implementación no predefinirá la función principal. Esta función no debe sobrecargarse . Tendrá un tipo de retorno de tipo int, pero de lo contrario su tipo está definido por la implementación. Todas las implementaciones permitirán las dos siguientes definiciones de principal:

int main() { /* ... */ }

y

int main(int argc, char* argv[]) { /* ... */ }

El estándar C ++ dice explícitamente "[la función principal] tendrá un tipo de retorno de tipo int, pero de lo contrario su tipo está definido por la implementación", y requiere las mismas dos firmas que el estándar C.

En un entorno alojado ( entorno AC que también admite las bibliotecas C): el sistema operativo llama main.

En un entorno no alojado (uno destinado a aplicaciones integradas) siempre puede cambiar el punto de entrada (o salida) de su programa utilizando las directivas del preprocesador como

#pragma startup [priority]
#pragma exit [priority]

Donde prioridad es un número integral opcional.

El inicio de Pragma ejecuta la función antes de la función principal (prioridad) y la salida de pragma ejecuta la función después de la función principal. Si hay más de una directiva de inicio, la prioridad decide cuál se ejecutará primero.

Sadique
fuente
4
No creo que esta respuesta realmente responda a la pregunta de cómo el compilador maneja realmente la situación. La respuesta dada por @Kaz da más información, en mi opinión.
Tilman Vogel
4
Creo que esta respuesta responde mejor a la pregunta que la de @Kaz. La pregunta original tiene la impresión de que se está produciendo una sobrecarga del operador, y esta respuesta resuelve que al mostrar que en lugar de alguna solución de sobrecarga, el compilador acepta dos firmas diferentes. Los detalles del compilador son interesantes pero no necesarios para responder a la pregunta.
Waleed Khan
1
En el caso de entornos independientes ("no alojados"), hay mucho más en marcha que solo un #pragma. Hay una interrupción de reinicio del hardware y ahí es donde realmente comienza el programa. A partir de ahí, se ejecutan todas las configuraciones fundamentales: pila de configuración, registros, MMU, mapeo de memoria, etc. Luego se realiza la copia de los valores de inicialización de NVM a las variables de almacenamiento estáticas (segmento .data), así como la "puesta a cero" en todos variables de almacenamiento estático que deben establecerse en cero (segmento .bss). En C ++, se llaman constructores de objetos con duración de almacenamiento estático. Y una vez hecho todo esto, se llama a main.
Lundin
8

No hay necesidad de sobrecargar. Sí, hay 2 versiones, pero solo se puede usar una a la vez.

user694733
fuente
5

Esta es una de las extrañas asimetrías y reglas especiales del lenguaje C y C ++.

En mi opinión, existe solo por razones históricas y no hay una lógica seria real detrás de él. Tenga en cuenta que maintambién es especial por otras razones (por ejemplo, mainen C ++ no puede ser recursivo y no puede tomar su dirección y en C99 / C ++ se le permite omitir una returndeclaración final ).

Tenga en cuenta también que incluso en C ++ no es una sobrecarga ... o un programa tiene la primera forma o tiene la segunda forma; no puede tener ambos.

6502
fuente
También puede omitir la returndeclaración en C (desde C99).
dreamlax
En C, puede llamar main()y tomar su dirección; C ++ aplica límites que C no aplica.
Jonathan Leffler
@JonathanLeffler: tienes razón, arreglado. Lo único gracioso de main que encontré en las especificaciones de C99, además de la posibilidad de omitir el valor de retorno, es que como el estándar está redactado como IIUC, no se puede pasar un valor negativo argccuando se repite (5.1.2.2.1 no especifica limitaciones en argcy argvaplicar solo a la llamada inicial a main).
6502
4

Lo que es inusual mainno es que se pueda definir de más de una manera, es que solo se puede definir de dos maneras diferentes.

maines una función definida por el usuario; la implementación no declara un prototipo para él.

Lo mismo es cierto para fooo bar, pero puede definir funciones con esos nombres de la forma que desee.

La diferencia es que maines invocada por la implementación (el entorno de ejecución), no solo por su propio código. La implementación no se limita a la semántica ordinaria de llamadas a funciones de C, por lo que puede (y debe) lidiar con algunas variaciones, pero no es necesario que maneje infinitas posibilidades. El int main(int argc, char *argv[])formulario permite argumentos de línea de comandos, y int main(void)en C o int main()en C ++ es solo una conveniencia para programas simples que no necesitan procesar argumentos de línea de comandos.

En cuanto a cómo el compilador maneja esto, depende de la implementación. La mayoría de los sistemas probablemente tienen convenciones de llamada que hacen que las dos formas sean efectivamente compatibles, y cualquier argumento que se pase a un maindefinido sin parámetros se ignora en silencio. Si no, no sería difícil para un compilador o enlazador tratarlo de manera mainespecial. Si tiene curiosidad por saber cómo funciona en su sistema , puede consultar algunas listas de ensamblaje.

Y como muchas cosas en C y C ++, los detalles son en gran parte el resultado de la historia y las decisiones arbitrarias tomadas por los diseñadores de los lenguajes y sus predecesores.

Tenga en cuenta que tanto C como C ++ permiten otras definiciones definidas por la implementación para main, pero rara vez hay una buena razón para usarlas. Y para las implementaciones independientes (como los sistemas integrados sin SO), el punto de entrada del programa está definido por la implementación y ni siquiera se llama necesariamente main.

Keith Thompson
fuente
3

El maines solo un nombre para una dirección de inicio decidida por el vinculador, donde maines el nombre predeterminado. Todos los nombres de funciones en un programa son direcciones de inicio donde comienza la función.

Los argumentos de la función se insertan / aparecen en / desde la pila, por lo que si no hay argumentos especificados para la función, no hay argumentos presionados / activados / desactivados en la pila. Así es como main puede funcionar tanto con argumentos como sin ellos.

AndersK
fuente
2

Bueno, las dos firmas diferentes de la misma función main () aparecen en la imagen solo cuando las desea, es decir, si su programa necesita datos antes de cualquier procesamiento real de su código, puede pasarlos mediante el uso de -

    int main(int argc, char * argv[])
    {
       //code
    }

donde la variable argc almacena el recuento de datos que se pasan y argv es una matriz de punteros a char que apunta a los valores pasados ​​desde la consola. De lo contrario, siempre es bueno ir con

    int main()
    {
       //Code
    }

Sin embargo, en cualquier caso, puede haber uno y solo un main () en un programa, ya que ese es el único punto en el que desde un programa comienza su ejecución y, por lo tanto, no puede haber más de uno. (espero que sea digno)

manish
fuente
2

Se hizo una pregunta similar antes: ¿Por qué se compila una función sin parámetros (en comparación con la definición de función real)?

Una de las respuestas mejor clasificadas fue:

En C func()significa que puede pasar cualquier número de argumentos. Si no quieres argumentos, debes declarar comofunc(void)

Entonces, supongo que es cómo mainse declara (si puede aplicar el término "declarado" a main). De hecho, puedes escribir algo como esto:

int main(int only_one_argument) {
    // code
}

y aún se compilará y ejecutará.

varepsilon
fuente
1
¡Excelente observación! Parece que el enlazador es bastante indulgente main, ya que hay un problema que aún no se menciona: ¡aún más argumentos a favor main! "Unix (pero no Posix.1) y Microsoft Windows" agregan char **envp(recuerdo que DOS también lo permitió, ¿no?), Y Mac OS X y Darwin agregan otro puntero char * de "información arbitraria proporcionada por el sistema operativo". wikipedia
usr2564301
0

No es necesario que anule esto, ya que solo se usará una a la vez. Sí, hay 2 versiones diferentes de la función principal.

gautam
fuente