Análisis en profundidad de la función principal del lenguaje C!

valor de retorno de main

El valor de retorno de la función principal se utiliza para describir el estado de salida del programa. Si devuelve 0, el programa se cierra normalmente. El significado de otros números devueltos está determinado por el sistema. Por lo general, un retorno distinto de cero significa que el programa salió de forma anormal.

vacío principal()

Algunos libros usan void main (), pero esto es incorrecto. Void main () nunca se ha definido en C / C ++.

Bjarne Stroustrup, el padre de C ++, declaró claramente en las preguntas frecuentes de su página de inicio: "La definición void main () {/ *… * /} no es y nunca ha sido C ++, ni siquiera ha sido C." Esto puede ser Porque en C y C ++, el prototipo de una función que no recibe ningún parámetro y no devuelve ninguna información es "void foo (void);".

Probablemente debido a esto, muchas personas piensan erróneamente que la función principal se puede definir como void main (void) si no se requiere el valor de retorno del programa. ¡Sin embargo, esto está mal! El valor de retorno de la función principal debe definirse como un tipo int, como se especifica en los estándares C y C ++.

Aunque en algunos compiladores se puede compilar void main (), no todos los compiladores admiten void main () porque void main nunca se ha definido en el estándar.

En g ++ 3.2, si el valor de retorno de la función principal no es de tipo int, no pasará la compilación en absoluto. Y gcc3.2 emitirá una advertencia. Por tanto, para tener una buena portabilidad del programa, se debe utilizar int main (). La prueba es la siguiente:

#include <stdio.h>

void main()
{
    printf("Hello world\n");
    return;
}

Resultado de la operación: g ++ test.c

principal()

Dado que la función principal solo tiene un tipo de valor de retorno, ¿se puede omitir? Regulación: si el valor de retorno no está claramente marcado, el valor de retorno predeterminado es int, lo que significa que main () es equivalente a int main (), no anular main ().

En C99, el estándar requiere que el compilador dé al menos una advertencia para el uso de main (), pero en C89 este tipo de escritura está permitido. Pero para la estandarización y legibilidad del programa, se debe señalar claramente el tipo de valor de retorno. Código de prueba:

#include <stdio.h>

main()
{
    printf("Hello world\n");
    return 0;
}

resultado de la operación:

Estándares C y C ++

En el estándar C99, solo las dos definiciones siguientes son correctas:

int main( void ) 
int main( int argc, char *argv[] ) 

Si no necesita obtener los parámetros de la línea de comando, use int main (void); de lo contrario, use int main (int argc, char * argv []). Por supuesto, hay otras formas de pasar parámetros, que se discutirán por separado en la siguiente sección.

El tipo de valor de retorno de la función principal debe ser int, de modo que el valor de retorno se pueda pasar al llamador del programa (como el sistema operativo), que es equivalente a exit (0) para determinar el resultado de ejecución de la función.

Las siguientes dos funciones principales se definen en C ++ 89:

int main( ) 
int main( int argc, char *argv[] ) 

int main () es equivalente a int main (void) en C99; el uso de int main (int argc, char * argv []) también es el mismo que el definido en C99. De manera similar, el tipo de valor de retorno de la función principal también debe ser int.

declaración de devolución

Si la declaración de retorno no está escrita al final de la función principal, tanto C99 como C ++ 89 estipulan que el compilador debe agregar automáticamente el retorno 0 al archivo de objeto generado, lo que indica que el programa sale normalmente.

Sin embargo, se recomienda que agregue una declaración de retorno al final de la función principal. Aunque esto no es necesario, es un buen hábito. En Linux, podemos usar el comando de shell: echo $? Para ver el valor de retorno de la función.

#include <stdio.h>

int main()
{
    printf("Hello world\n");
}

resultado de la operación:

Al mismo tiempo, debe tenerse en cuenta que el valor de retorno de retorno se someterá a conversión de tipo, por ejemplo: si devuelve 1.2; será forzado a 1, es decir, el valor de retorno verdadero es 1, lo mismo, return'a '; si es verdadero, devuelve El valor es 97; pero si devuelve "abc", se informará una advertencia porque no se puede realizar la conversión de tipo implícita.

Pruebe el significado del valor de retorno de la función principal

Como se mencionó anteriormente, si la función principal devuelve 0, significa que el programa se cierra normalmente. Por lo general, un retorno distinto de cero significa que el programa salió de forma anormal. Al final de este artículo, pruébelo: test.c:

#include <stdio.h>

int main()
{
    printf("c 语言\n");
    return 11.1; 
}

Realice lo siguiente en la terminal:

➜  testSigpipe git:(master) ✗ vim test.c
➜  testSigpipe git:(master) ✗ gcc test.c
➜  testSigpipe git:(master) ✗ ./a.out && echo "hello world"  #&&与运算,前面为真,才会执行后边的
c 语言

Se puede ver que el sistema operativo considera que la función principal falla porque el valor de retorno de la función principal es 11.

➜  testSigpipe git:(master) ✗ ./a.out 
➜  testSigpipe git:(master) ✗ echo $?
11

Si el valor de retorno en la función principal debe ser 0:

➜  testSigpipe git:(master) ✗ vim test.c
➜  testSigpipe git:(master) ✗ gcc test.c 
➜  testSigpipe git:(master) ✗ ./a.out && echo "hello world" #hello
c 语言
hello world

Se puede ver que, como esperamos, la función principal devuelve 0, lo que significa que la función sale normalmente y la ejecución es exitosa; si devuelve distinto de cero, significa que la función es anormal y la ejecución falla.

función principal pasando parámetros

Lo primero que hay que tener en cuenta es que algunas personas pueden pensar que la función principal no se puede pasar en parámetros, pero de hecho esto es incorrecto. La función principal puede obtener parámetros de la línea de comandos, mejorando así la reutilización del código.

Prototipo de función

Al pasar parámetros para la función principal, el prototipo de función principal opcional es:

int main(int argc , char* argv[],char* envp[]);

Descripción de parámetros:

①. El primer parámetro argc representa el número de parámetros entrantes.

②. El segundo parámetro char * argv [] es una matriz de cadenas, que se utiliza para almacenar la matriz de punteros al parámetro de cadena.Cada elemento apunta a un parámetro. El significado de cada miembro es el siguiente:

argv [0]: apunte al nombre de ruta completo del programa.

argv [1]: apunta a la primera cadena después del nombre del programa en ejecución, que representa el primer parámetro realmente pasado.

argv [2]: apunta a la segunda cadena después del nombre del programa en ejecución, que representa el segundo parámetro pasado.

…… argv [n]: apunte a la enésima cadena de caracteres después del nombre del programa en ejecución, que representa el enésimo parámetro pasado.

Regulación: argv [argc] es NULL, lo que significa el final del parámetro.

③ El tercer parámetro char * envp [] también es una matriz de cadenas, principalmente para guardar la cadena de variables en el entorno del usuario, que termina en NULL. Cada elemento de envp [] contiene una cadena con el formato ENVVAR = valor, donde ENVVAR es una variable de entorno y valor es su valor correspondiente.

Una vez que se pasa envp, es solo una matriz de cadenas simple y no cambiará con la configuración dinámica del programa. Puede usar la función putenv para modificar las variables de entorno en tiempo real, y también puede usar getenv para ver las variables de entorno en tiempo real, pero envp en sí no cambiará; generalmente se usa menos.

Nota : Los parámetros char * argv [] y char * envp [] de la función principal representan matrices de cadenas, y la forma de escritura es más que char * argv [], los correspondientes argv [] [] y char ** argv pueden ser .

char * envp []

Escriba un pequeño programa de prueba para probar el tercer parámetro de la función principal:

#include <stdio.h>

int main(int argc ,char* argv[] ,char* envp[])
{
    int i = 0;

    while(envp[i++])
    {
        printf("%s\n", envp[i]);
    }

    return 0;
}

Resultados de la ejecución: capturas de pantalla parciales

La información obtenida por envp [] es equivalente al resultado del comando env en Linux.

Versión común

Cuando se usa la versión parametrizada de la función principal, la más comúnmente utilizada es: ** int main (int argc, char * argv []); ** Los nombres de variable argc y argv son nombres convencionales, por supuesto, pueden ser reemplazados por otros nombres .

La forma de ejecución de la línea de comandos es: nombre de archivo ejecutable parámetro 1 parámetro 2 …… parámetro n . Utilice espacios para separar el nombre del archivo ejecutable, los parámetros y los parámetros.

Programa de muestra

#include <stdio.h>

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

    int i;
    printf("Total %d arguments\n",argc);

    for(i = 0; i < argc; i++)
    {
        printf("\nArgument argv[%d]  = %s \n",i, argv[i]);
    }

    return 0;
}

resultado de la operación:

➜  cpp_workspace git:(master) ✗ vim testmain.c 
➜  cpp_workspace git:(master) ✗ gcc testmain.c 
➜  cpp_workspace git:(master) ✗ ./a.out 1 2 3    #./a.out为程序名 1为第一个参数 , 2 为第二个参数, 3 为第三个参数
Total 4 arguments
Argument argv[0]  = ./a.out 
Argument argv[1]  = 1 
Argument argv[2]  = 2 
Argument argv[3]  = 3 
Argument argv[4]  = (null)    #默认argv[argc]为null

El orden de ejecución de main

Algunas personas pueden decir que, en otras palabras, la función principal debe ser la primera función ejecutada por el programa. Entonces, ¿es este realmente el caso? Creo que después de leer esta sección, habrá una comprensión diferente.

Por qué se dice que main () es la entrada del programa

El punto de entrada del programa en el sistema Linux es "_start", esta función es parte de la biblioteca del sistema Linux (Glibc), cuando nuestro programa y Glibc están vinculados para formar el archivo ejecutable final, esta función es la función de entrada para la inicialización de la ejecución del programa. . Para ilustrar a través de un programa de prueba:

#include <stdio.h>

int main()
{
    printf("Hello world\n");
    return 0;
}

Compilar:

gcc testmain.c -nostdlib     # -nostdlib (不链接标准库)

La ejecución del programa provocará un error: / usr / bin / ld: advertencia: no se puede encontrar el símbolo de entrada _start; este símbolo no se encuentra

entonces:

  1. El compilador busca por defecto el símbolo __start, no el principal

  2. __start Este símbolo es el inicio del programa

  3. main es un símbolo llamado por la biblioteca estándar

Entonces, ¿cuál es la relación entre este _start y la función principal? Exploremos más a continuación.

La realización de la función _start La entrada se especifica mediante el script de enlace predeterminado del enlazador ld, por supuesto, el usuario también puede configurarlo a través de parámetros. _start se implementa mediante código ensamblador. Está representado aproximadamente por el siguiente pseudocódigo:

void _start()
{
  %ebp = 0;
  int argc = pop from stack
  char ** argv = top of stack;
  __libc_start_main(main, argc, argv, __libc_csu_init, __linc_csu_fini,
  edx, top of stack);
}

El código de ensamblaje correspondiente es el siguiente:

_start:
 xor ebp, ebp //清空ebp
 pop esi //保存argc,esi = argc
 mov esp, ecx //保存argv, ecx = argv

 push esp //参数7保存当前栈顶
 push edx //参数6
 push __libc_csu_fini//参数5
 push __libc_csu_init//参数4
 push ecx //参数3
 push esi //参数2
 push main//参数1
 call _libc_start_main

hlt

Se puede ver que antes de llamar a _start, el cargador empujará los parámetros del usuario y las variables de entorno a la pila.

Trabajar antes de que se ejecute la función principal

Se puede ver en la implementación de _start que es necesario realizar una serie de trabajo antes de que se ejecute la función principal. Lo principal es inicializar los recursos relacionados con el sistema:

Some of the stuff that has to happen before main():

set up initial stack pointer 

initialize static and global data 

zero out uninitialized data 

run global constructors

Some of this comes with the runtime library's crt0.o file or its __start() function. Some of it you need to do yourself.

Crt0 is a synonym for the C runtime library.

1. Establecer el puntero de la pila

2. Inicializar variables globales estáticas estáticas y globales, es decir, el contenido de la sección de datos

3. Asignar valores iniciales a la parte no inicializada: los valores numéricos de short, int, long, etc. son 0, bool es FALSE, pointer es NULL, etc., es decir, el contenido de la sección .bss

4. Ejecute el constructor global, similar al constructor global en C ++

5. Pase los parámetros de la función principal, argc, argv, etc. a la función principal, y luego ejecute la función principal.

El código que se ejecuta antes de main

A continuación, hablemos sobre qué código se ejecutará antes de que se ejecute la función mian: (1) El constructor del objeto global se ejecutará antes que la función principal.

(2) La asignación de espacio y la asignación de valor inicial de algunas variables globales, objetos y variables estáticas, objetos es antes de la ejecución de la función principal, y después de que se ejecuta la función principal, se deben realizar algunas operaciones como liberar espacio y liberar derechos de uso de recursos.

(3) Después de que se inicia el proceso, se debe ejecutar algún código de inicialización (como establecer variables de entorno, etc.) y luego saltar a main para su ejecución. La construcción del objeto global también está antes de main.

(4) A través del atributo de palabra clave , deje que una función se ejecute antes que la función principal, realice alguna inicialización de datos, verificación de carga del módulo, etc.

Código de muestra

①, a través del atributo de palabra clave

#include <stdio.h>

__attribute__((constructor)) void before_main_to_run() 
{ 
    printf("Hi~,i am called before the main function!\n");
    printf("%s\n",__FUNCTION__); 
} 

__attribute__((destructor)) void after_main_to_run() 
{ 
    printf("%s\n",__FUNCTION__); 
    printf("Hi~,i am called after the main function!\n");
} 

int main( int argc, char ** argv ) 
{ 
    printf("i am main function, and i can get my name(%s) by this way.\n",__FUNCTION__); 
    return 0; 
}

② Inicialización de variables globales

#include <iostream>

using namespace std;

inline int startup_1()
{
    cout<<"startup_1 run"<<endl;
    return 0;
}

int static no_use_variable_startup_1 = startup_1();

int main(int argc, const char * argv[]) 
{
    cout<<"this is main"<<endl;
    return 0;
}

En este punto, hemos terminado de hablar de las cosas antes de que se ejecute la función principal Entonces, ¿crees que la función principal es también la última función del programa?

El resultado, por supuesto, no es. Después de que se ejecuta la función principal, hay otras funciones que se pueden ejecutar. Después de que se ejecuta la función principal, regresa a la función de entrada, y la función de entrada realiza el trabajo de limpieza, incluida la destrucción de variables globales, destrucción de montón, apagado de E / S, etc., y luego La llamada al sistema finaliza el proceso.

Función ejecutada después de la función principal

1. El destructor del objeto global se ejecutará después de la función principal, 2. La función registrada con atexit también se ejecutará después de la principal.

función atexit

Prototipo:

int atexit(void (*func)(void)); 

La función atexit puede "registrar" una función para que esta función sea llamada cuando la función principal termina normalmente, cuando el programa termina anormalmente, las funciones registradas a través de él no serán llamadas.

El compilador debe permitir que el programador registre al menos 32 funciones. Si el registro es exitoso, atexit devuelve 0; de lo contrario, devuelve un valor distinto de cero y no hay forma de cancelar el registro de una función.

Antes de cualquier operación de limpieza estándar realizada por exit, las funciones registradas se llaman secuencialmente en el orden inverso al orden de registro. Cada función llamada no acepta ningún parámetro y el tipo de retorno es nulo. La función registrada no debe intentar hacer referencia a ningún objeto cuya clase de almacenamiento sea auto o registro (por ejemplo, por puntero), a menos que esté definido por sí mismo.

Si registra la misma función varias veces, esta función se llamará varias veces. La operación final de la llamada a la función es el proceso pop. main () también es una función. Al final, la función atexit se llama en el orden de salida de la pila. Por lo tanto, la función atexit es la misma que la función que se apila y extrae. Es la primera en entrar , la última en salir y se registra primero. Después de la ejecución. La función de limpieza de devolución de llamada se puede registrar a través de atexit. Puede agregar algo de trabajo de limpieza a estas funciones, como liberación de memoria, cierre de archivos abiertos, cierre de descriptores de socket, liberación de bloqueos, etc.

#include<stdio.h>
#include<stdlib.h>

void fn0( void ), fn1( void ), fn2( void ), fn3( void ), fn4( void );

int main( void )

{
  //注意使用atexit注册的函数的执行顺序:先注册的后执行
    atexit( fn0 );  
    atexit( fn1 );  
    atexit( fn2 );  
    atexit( fn3 );  
    atexit( fn4 );

    printf( "This is executed first.\n" );
    printf("main will quit now!\n");

    return 0;

}

void fn0()
{
    printf( "first register ,last call\n" );
}

void fn1(
{
    printf( "next.\n" );
}

void fn2()
{
    printf( "executed " );
}

void fn3()
{
    printf( "is " );
}

void fn4()
{
    printf( "This " );
}

Autor: z_ryan

Original: https://blog.csdn.net/z_ryan/category_7316855.html


1. ¡Vista previa del emocionante contenido del curso en línea para desarrolladores de Internet de las cosas de GD32 Arm MCU!

2. Columna Yang Fuyu | Buscando esquinas que se puedan adelantar: El gran hombre dijo que estaba en el medio

3. ¡RISC-V está realmente en contra de la tendencia!

4. ¡No lo ignore! ¡Lagunas fatales en el código incrustado!

5. ¡Extra! MCU "residente" de comunicación de larga distancia LoRa desde entonces

6. ¿Es la tecnología realmente neutral?

Descargo de responsabilidad: este artículo se reproduce en línea y los derechos de autor pertenecen al autor original. Si está involucrado en problemas de derechos de autor, contáctenos, confirmaremos los derechos de autor en función de los materiales de certificación de derechos de autor que proporcione y pagaremos la remuneración del autor o eliminaremos el contenido.

Supongo que te gusta

Origin blog.csdn.net/DP29syM41zyGndVF/article/details/112690324
Recomendado
Clasificación