Analyse approfondie de la fonction principale du langage C!

valeur de retour de main

La valeur de retour de la fonction principale est utilisée pour décrire l'état de sortie du programme. S'il renvoie 0, le programme se termine normalement. La signification des autres nombres renvoyés est déterminée par le système. Habituellement, un retour différent de zéro signifie que le programme s'est arrêté anormalement.

void main ()

Certains livres utilisent void main (), mais c'est faux. Void main () n'a jamais été défini en C / C ++.

Bjarne Stroustrup, le père du C ++, a clairement indiqué dans la FAQ sur sa page d'accueil "La définition void main () {/ *… * /} n'est pas et n'a jamais été C ++, ni même C." Cela peut être Parce qu'en C et C ++, le prototype d'une fonction qui ne reçoit aucun paramètre et ne renvoie aucune information est "void foo (void);".

Probablement à cause de cela, beaucoup de gens pensent à tort que la fonction main peut être définie comme void main (void) si la valeur de retour du programme n'est pas requise. Cependant, c'est faux! La valeur de retour de la fonction main doit être définie comme un type int, comme spécifié dans les normes C et C ++.

Bien que dans certains compilateurs, void main () puisse être compilé, tous les compilateurs ne supportent pas void main () car void main n'a jamais été défini dans le standard.

Dans g ++ 3.2, si la valeur de retour de la fonction main n'est pas de type int, elle ne passera pas du tout la compilation. Et gcc3.2 émettra un avertissement. Par conséquent, afin d'avoir une bonne portabilité du programme, int main () doit être utilisé. Le test est le suivant:

#include <stdio.h>

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

Résultat de l'opération: g ++ test.c

principale()

Étant donné que la fonction principale n'a qu'un seul type de valeur de retour, peut-elle être omise? Règlement: Si la valeur de retour n'est pas clairement indiquée, la valeur de retour par défaut est int, ce qui signifie que main () équivaut à int main (), pas à void main ().

En C99, le standard oblige le compilateur à donner au moins un avertissement pour l'utilisation de main (), mais en C89 ce type d'écriture est autorisé. Mais pour la standardisation et la lisibilité du programme, le type de valeur de retour doit être clairement indiqué. Code de test:

#include <stdio.h>

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

résultat de l'opération:

Normes C et C ++

Dans la norme C99, seules les deux définitions suivantes sont correctes:

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

Si vous n'avez pas besoin d'obtenir des paramètres à partir de la ligne de commande, utilisez int main (void); sinon, utilisez int main (int argc, char * argv []). Bien sûr, il existe d'autres façons de passer des paramètres, qui seront discutées séparément dans la section suivante.

Le type de valeur de retour de la fonction principale doit être int, afin que la valeur de retour puisse être transmise à l'appelant du programme (tel que le système d'exploitation), ce qui équivaut à exit (0) pour déterminer le résultat de l'exécution de la fonction.

Les deux fonctions principales suivantes sont définies en C ++ 89:

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

int main () est équivalent à int main (void) en C99; l'utilisation de int main (int argc, char * argv []) est également la même que celle définie en C99. De même, le type de valeur de retour de la fonction principale doit également être int.

déclaration de retour

Si l'instruction return n'est pas écrite à la fin de la fonction main, C99 et C ++ 89 stipulent que le compilateur doit automatiquement ajouter return 0 au fichier objet généré, indiquant que le programme se termine normalement.

Cependant, il est recommandé d'ajouter une instruction return à la fin de la fonction main. Bien que ce ne soit pas nécessaire, c'est une bonne habitude. Sous Linux, nous pouvons utiliser la commande shell: echo $? Pour afficher la valeur de retour de la fonction.

#include <stdio.h>

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

résultat de l'opération:

Dans le même temps, il convient de noter que la valeur de retour de return subira une conversion de type, par exemple: si return 1.2; elle sera forcée à 1, c'est-à-dire que la vraie valeur de retour est 1, la même chose, return'a '; si c'est vrai, return La valeur est 97, mais si retourne "abc", un avertissement sera signalé car la conversion de type implicite ne peut pas être effectuée.

Tester la signification de la valeur de retour de la fonction principale

Comme mentionné précédemment, si la fonction main renvoie 0, cela signifie que le programme se termine normalement. Habituellement, un retour différent de zéro signifie que le programme s'est arrêté anormalement. À la fin de cet article, testez-le: test.c:

#include <stdio.h>

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

Effectuez les opérations suivantes dans le terminal:

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

On peut voir que le système d'exploitation considère que la fonction principale échoue car la valeur de retour de la fonction principale est 11.

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

Si la valeur de retour dans la fonction principale doit être 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

On peut voir que, comme on s'y attend, la fonction principale renvoie 0, ce qui signifie que la fonction se termine normalement et que l'exécution est réussie; si elle retourne une valeur non nulle, cela signifie que la fonction est anormale et que l'exécution échoue.

fonction principale passant des paramètres

La première chose à noter est que certaines personnes peuvent penser que la fonction principale ne peut pas être passée en paramètres, mais en fait c'est faux. La fonction principale peut obtenir des paramètres à partir de la ligne de commande, améliorant ainsi la réutilisabilité du code.

Prototype de fonction

Lors du passage des paramètres de la fonction principale, le prototype de la fonction principale facultative est:

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

Description du paramètre:

① Le premier paramètre argc représente le nombre de paramètres entrants.

Le deuxième paramètre char * argv [] est un tableau de chaînes, qui est utilisé pour stocker le tableau de pointeurs sur le paramètre de chaîne. Chaque élément pointe vers un paramètre. La signification de chaque membre est la suivante:

argv [0]: pointez sur le chemin complet du programme.

argv [1]: pointe sur la première chaîne après le nom du programme en cours d'exécution, qui représente le premier paramètre réellement passé.

argv [2]: pointe vers la deuxième chaîne après le nom du programme en cours d'exécution, qui représente le deuxième paramètre passé.

…… argv [n]: pointe sur la nième chaîne de caractères après le nom du programme en cours d'exécution, représentant le nième paramètre transmis.

Régulation: argv [argc] est NULL, ce qui signifie la fin du paramètre.

Le troisième paramètre char * envp [] est également un tableau de chaînes, principalement pour enregistrer la chaîne de variables dans l'environnement utilisateur, se terminant par NULL. Chaque élément de envp [] contient une chaîne sous la forme ENVVAR = valeur, où ENVVAR est une variable d'environnement et value est sa valeur correspondante.

Une fois envp passé, il ne s'agit que d'un simple tableau de chaînes et ne changera pas avec les paramètres dynamiques du programme. Vous pouvez utiliser la fonction putenv pour modifier les variables d'environnement en temps réel, et vous pouvez également utiliser getenv pour afficher les variables d'environnement en temps réel, mais envp lui-même ne changera pas; il est généralement moins utilisé.

Remarque : Les paramètres char * argv [] et char * envp [] de la fonction principale représentent des tableaux de chaînes, et la forme d'écriture est plus que char * argv [], les argv [] [] et char ** argv correspondants peuvent être .

char * envp []

Ecrivez un petit programme de test pour tester le troisième paramètre de la fonction principale:

#include <stdio.h>

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

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

    return 0;
}

Résultats de l'exécution: captures d'écran partielles

Les informations obtenues par envp [] sont équivalentes au résultat de la commande env sous Linux.

Version commune

Lors de l'utilisation de la version paramétrée de la fonction main, la plus couramment utilisée est: ** int main (int argc, char * argv []); ** Les noms de variables argc et argv sont des noms conventionnels, bien sûr, ils peuvent être remplacés par d'autres noms .

La forme d'exécution de la ligne de commande est: nom du fichier exécutable paramètre 1 paramètre 2 …… paramètre n . Utilisez des espaces pour séparer le nom du fichier exécutable, les paramètres et les paramètres.

Exemple de programme

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

résultat de l'opération:

➜  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

L'ordre d'exécution de main

Certaines personnes peuvent dire qu'en d'autres termes, la fonction principale doit être la première fonction exécutée par le programme. Alors, est-ce vraiment le cas? Je crois qu'après avoir lu cette section, il y aura une compréhension différente.

Pourquoi on dit que main () est l'entrée du programme

Le point d'entrée du programme sous le système linux est "_start", cette fonction fait partie de la bibliothèque système linux (Glibc), lorsque notre programme et Glibc sont liés ensemble pour former le fichier exécutable final, cette fonction est la fonction d'entrée pour l'initialisation de l'exécution du programme . Pour illustrer à travers un programme de test:

#include <stdio.h>

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

Compiler:

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

L'exécution du programme provoquera une erreur: / usr / bin / ld: avertissement: impossible de trouver le symbole d'entrée _start; Ce symbole est introuvable

alors:

  1. Le compilateur recherche par défaut le symbole __start, pas main

  2. __start Ce symbole est le début du programme

  3. main est un symbole appelé par la bibliothèque standard

Alors, quelle est la relation entre ce _start et la fonction principale? Explorons plus en détail ci-dessous.

La réalisation de la fonction _start L'entrée est spécifiée par le script de lien par défaut du linker ld, bien sûr, l'utilisateur peut également le paramétrer via des paramètres. _start est implémenté par le code assembleur. Il est grossièrement représenté par le pseudo code suivant:

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

Le code d'assemblage correspondant est le suivant:

_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

On peut voir qu'avant d'appeler _start, le chargeur va pousser les paramètres de l'utilisateur et les variables d'environnement sur la pile.

Travailler avant l'exécution de la fonction principale

Il ressort de l'implémentation de _start qu'une série de travaux doit être effectuée avant que la fonction principale ne soit exécutée. L'essentiel est d'initialiser les ressources liées au système:

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. Définissez le pointeur de pile

2. Initialisez les variables globales statiques statiques et globales, c'est-à-dire le contenu de la section de données

3. Attribuez des valeurs initiales à la partie non initialisée: les valeurs numériques de short, int, long, etc. sont 0, bool is FALSE, le pointeur est NULL, etc., c'est-à-dire le contenu de la section .bss

4. Exécutez le constructeur global, similaire au constructeur global en C ++

5. Passez les paramètres de la fonction principale, argc, argv, etc. à la fonction principale, puis exécutez réellement la fonction principale

Le code qui s'exécute avant main

Ci-dessous, parlons du code qui sera exécuté avant l'exécution de la fonction mian: (1) Le constructeur de l'objet global sera exécuté avant la fonction principale.

(2) L'allocation d'espace et l'attribution de valeur initiale de certaines variables globales, objets et variables statiques, objets se fait avant l'exécution de la fonction principale, et après l'exécution de la fonction principale, certaines opérations telles que la libération d'espace et la libération des droits d'utilisation des ressources doivent être effectuées.

(3) Une fois le processus démarré, un code d'initialisation (tel que la définition des variables d'environnement, etc.) doit être exécuté, puis passer à main pour l'exécution. La construction de l'objet global est également avant main.

(4) Grâce à l' attribut mot-clé , laissez une fonction s'exécuter avant la fonction principale, effectuez une initialisation des données, une vérification du chargement du module, etc.

Exemple de code

①, via l' attribut mot-clé

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

② Initialisation des 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;
}

À ce stade, nous avons fini de parler des choses avant que la fonction principale ne soit exécutée.Alors, pensez-vous que la fonction principale est aussi la dernière fonction du programme?

Le résultat n'est bien sûr pas. Une fois la fonction principale exécutée, d'autres fonctions peuvent être exécutées. Une fois la fonction principale exécutée, elle revient à la fonction d'entrée et la fonction d'entrée effectue un travail de nettoyage, y compris la destruction de variables globales, la destruction de tas, l'arrêt des E / S, etc., puis continuez L'appel système met fin au processus.

Fonction exécutée après la fonction principale

1. Le destructeur de l'objet global sera exécuté après la fonction main 2. La fonction enregistrée avec atexit sera également exécutée après la fonction main.

fonction atexit

Prototype:

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

La fonction atexit peut "enregistrer" une fonction afin que cette fonction soit appelée lorsque la fonction principale se termine normalement.

Le compilateur doit permettre au programmeur d'enregistrer au moins 32 fonctions. Si l'enregistrement réussit, atexit renvoie 0, sinon il renvoie une valeur différente de zéro et il n'y a aucun moyen d'annuler l'enregistrement d'une fonction.

Avant toute opération de nettoyage standard effectuée par exit, les fonctions enregistrées sont appelées séquentiellement dans l'ordre inverse de l'ordre d'enregistrement. Chaque fonction appelée n'accepte aucun paramètre et le type de retour est void. La fonction enregistrée ne doit pas essayer de faire référence à un objet dont la classe de stockage est auto ou register (par exemple, par un pointeur), sauf s'il est défini par lui-même.

L'enregistrement de la même fonction plusieurs fois entraînera l'appel de cette fonction plusieurs fois. L'opération finale de l'appel de fonction est le processus pop. main () est également une fonction. À la fin, la fonction atexit est appelée dans l'ordre de sortie de la pile. Par conséquent, la fonction atexit est la même que la fonction empilée et sautée. Elle est le premier entré-dernier sorti et enregistrée en premier. Après exécution. La fonction de nettoyage de rappel peut être enregistrée via atexit. Vous pouvez ajouter des travaux de nettoyage à ces fonctions, comme la libération de la mémoire, la fermeture de fichiers ouverts, la fermeture de descripteurs de socket, la libération de verrous, 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 " );
}

Auteur: z_ryan

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


1. Aperçu du contenu passionnant du cours en ligne pour développeurs GD32 Arm MCU Internet of Things!

2. Colonne Yang Fuyu | À la recherche de coins qui peuvent être dépassés: le grand homme a dit qu'il était au milieu

3. RISC-V est en fait contre la tendance!

4. Ne l'ignorez pas! Des failles fatales dans le code embarqué!

5. Extra! MCU «résident» de communication longue distance LoRa depuis lors

6. La technologie est-elle vraiment neutre?

Avis de non-responsabilité: cet article est reproduit en ligne et le droit d'auteur appartient à l'auteur original. Si vous êtes impliqué dans des problèmes de droits d'auteur, veuillez nous contacter, nous confirmerons le droit d'auteur sur la base des documents de certification des droits d'auteur que vous fournissez et paierons la rémunération de l'auteur ou supprimerons le contenu.

Je suppose que tu aimes

Origine blog.csdn.net/DP29syM41zyGndVF/article/details/112690324
conseillé
Classement