Wie funktioniert die main () -Methode in C?

96

Ich weiß, dass es zwei verschiedene Signaturen gibt, um die Hauptmethode zu schreiben -

int main()
{
   //Code
}

oder für die Behandlung von Befehlszeilenargumenten schreiben wir es als

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

In C++Ich weiß , wir eine Methode überlasten können, aber in Cwie funktioniert der Compiler diese zwei verschiedene Signaturen von handhaben mainFunktion?

Ritesh
quelle
14
Überladen bezieht sich auf zwei Methoden mit demselben Namen im selben Programm. Sie können immer nur eine mainMethode in einem einzelnen Programm in C(oder wirklich in so ziemlich jeder Sprache mit einem solchen Konstrukt) haben.
Kyle Strand
12
C hat keine Methoden; es hat Funktionen. Methoden sind die Backend-Implementierung objektorientierter "generischer" Funktionen. Das Programm ruft eine Funktion mit einigen Objektargumenten auf, und das Objektsystem wählt eine Methode (oder möglicherweise eine Reihe von Methoden) basierend auf ihren Typen aus. C hat nichts davon, es sei denn, Sie simulieren es selbst.
Kaz
4
Für eine eingehende Diskussion über Programmeinstiegspunkte - nicht besonders main- empfehle ich John R. Levines klassisches Buch "Linkers & Loaders".
Andreas Spindler
1
In C ist das erste Formular int main(void)nicht int main()(obwohl ich noch nie einen Compiler gesehen habe, der das int main()Formular ablehnt ).
Keith Thompson
1
@harper: Das ()Formular ist veraltet und es ist nicht klar, ob es überhaupt zulässig ist main(es sei denn, die Implementierung dokumentiert es ausdrücklich als zulässiges Formular). Der C-Standard (siehe 5.1.2.2.1 Programmstart) erwähnt das ()Formular nicht, was dem Formular nicht ganz entspricht (). Die Details sind zu lang für diesen Kommentar.
Keith Thompson

Antworten:

132

Einige der Funktionen der C-Sprache begannen als Hacks, die gerade funktionierten.

Eine dieser Funktionen sind mehrere Signaturen für Hauptargumentlisten sowie Argumentlisten mit variabler Länge.

Programmierer haben festgestellt, dass sie zusätzliche Argumente an eine Funktion übergeben können und mit ihrem angegebenen Compiler nichts Schlimmes passiert.

Dies ist der Fall, wenn die Aufrufkonventionen so sind, dass:

  1. Die aufrufende Funktion bereinigt die Argumente.
  2. Die Argumente ganz links befinden sich näher am oberen Rand des Stapels oder an der Basis des Stapelrahmens, sodass falsche Argumente die Adressierung nicht ungültig machen.

Eine Reihe von Aufrufkonventionen, die diesen Regeln entsprechen, ist die stapelbasierte Parameterübergabe, bei der der Aufrufer die Argumente einfügt und sie von rechts nach links verschoben werden:

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

In Compilern, in denen diese Art von Aufrufkonvention der Fall ist, muss nichts Besonderes getan werden, um die beiden Arten mainoder sogar zusätzliche Arten zu unterstützen. mainkann eine Funktion ohne Argumente sein. In diesem Fall werden die Elemente, die auf den Stapel geschoben wurden, nicht berücksichtigt. Wenn es eine Funktion von zwei Argumenten ist, findet es argcund argvals die beiden obersten Stapelelemente. Wenn es sich um eine plattformspezifische Variante mit drei Argumenten und einem Umgebungszeiger (eine allgemeine Erweiterung) handelt, funktioniert dies ebenfalls: Das dritte Argument wird als drittes Element oben im Stapel gefunden.

Ein fester Aufruf funktioniert also in allen Fällen, sodass ein einzelnes festes Startmodul mit dem Programm verknüpft werden kann. Dieses Modul könnte in C geschrieben werden, als eine Funktion, die dieser ähnelt:

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

Mit anderen Worten, dieses Startmodul ruft immer nur ein Haupt mit drei Argumenten auf. Wenn main keine oder nur keine Argumente int, char **akzeptiert, funktioniert es aufgrund der aufrufenden Konventionen einwandfrei und wenn keine Argumente akzeptiert werden.

Wenn Sie so etwas in Ihrem Programm tun würden, wäre es nicht portierbar und würde von ISO C als undefiniertes Verhalten angesehen: Deklarieren und Aufrufen einer Funktion auf eine Weise und Definieren in einer anderen. Der Starttrick eines Compilers muss jedoch nicht portabel sein. Es richtet sich nicht nach den Regeln für tragbare Programme.

Angenommen, die Aufrufkonventionen sind so, dass sie nicht so funktionieren können. In diesem Fall muss der Compiler mainspeziell behandeln . Wenn es bemerkt, dass es die mainFunktion kompiliert , kann es Code generieren, der beispielsweise mit einem Aufruf mit drei Argumenten kompatibel ist.

Das heißt, Sie schreiben Folgendes:

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

Wenn der Compiler es sieht, führt er im Wesentlichen eine Codetransformation durch, sodass die von ihm kompilierte Funktion folgendermaßen aussieht:

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

außer dass die Namen __argc_ignorenicht buchstäblich existieren. In Ihren Bereich werden keine solchen Namen aufgenommen, und es wird keine Warnung vor nicht verwendeten Argumenten angezeigt. Die Codetransformation bewirkt, dass der Compiler Code mit der richtigen Verknüpfung ausgibt, die weiß, dass drei Argumente bereinigt werden müssen.

Eine andere Implementierungsstrategie besteht darin, dass der Compiler oder Linker die __startFunktion (oder wie auch immer sie genannt wird) benutzerdefiniert generiert oder zumindest eine aus mehreren vorkompilierten Alternativen auswählt. In der Objektdatei können Informationen darüber gespeichert werden, welche der unterstützten Formen mainverwendet werden. Der Linker kann sich diese Informationen ansehen und die richtige Version des Startmoduls auswählen, die einen Aufruf enthält, mainder mit der Programmdefinition kompatibel ist. C-Implementierungen haben normalerweise nur eine geringe Anzahl unterstützter Formen, mainsodass dieser Ansatz möglich ist.

Compiler für die C99-Sprache müssen immer bis mainzu einem gewissen Grad speziell behandeln , um den Hack zu unterstützen, dass returndas Verhalten so return 0ausgeführt wird, als ob sie ausgeführt würden , wenn die Funktion ohne Anweisung beendet wird . Dies kann wiederum durch eine Codetransformation behandelt werden. Der Compiler bemerkt, dass eine aufgerufene Funktion mainkompiliert wird. Dann wird geprüft, ob das Ende des Körpers möglicherweise erreichbar ist. Wenn ja, wird a eingefügtreturn 0;

Kaz
quelle
34

mainSelbst in C ++ gibt es KEINE Überladung . Hauptfunktion ist der Einstiegspunkt für ein Programm und es sollte nur eine einzige Definition existieren.

Für Standard C.

Für eine gehostete Umgebung (das ist die normale) lautet der C99-Standard:

5.1.2.2.1 Programmstart

Die beim Programmstart aufgerufene Funktion wird benannt main. Die Implementierung deklariert keinen Prototyp für diese Funktion. Es muss mit einem Rückgabetyp von intund ohne Parameter definiert werden:

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

oder mit zwei Parametern (hier als argcund bezeichnet argv, obwohl beliebige Namen verwendet werden können, da sie lokal für die Funktion sind, in der sie deklariert sind):

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

oder gleichwertig; 9) oder auf eine andere implementierungsdefinierte Weise.

9) Somit intkann durch einen Typedef-Namen ersetzt werden, der als definiert ist int, oder der Typ von argvkann geschrieben werden als char **argvund so weiter.

Für Standard-C ++:

3.6.1 Hauptfunktion [basic.start.main]

1 Ein Programm muss eine globale Funktion namens main enthalten, die den festgelegten Start des Programms darstellt. [...]

2 Eine Implementierung darf die Hauptfunktion nicht vordefinieren. Diese Funktion darf nicht überlastet werden . Es muss einen Rückgabetyp vom Typ int haben, ansonsten ist sein Typ implementierungsdefiniert. Alle Implementierungen müssen beide der folgenden Definitionen von main zulassen:

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

und

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

Der C ++ - Standard sagt ausdrücklich "Es [die Hauptfunktion] muss einen Rückgabetyp vom Typ int haben, aber ansonsten ist sein Typ implementierungsdefiniert" und erfordert dieselben zwei Signaturen wie der C-Standard.

In einer gehosteten Umgebung (AC-Umgebung, die auch die C-Bibliotheken unterstützt) ruft das Betriebssystem auf main.

In einer nicht gehosteten Umgebung (eine für eingebettete Anwendungen vorgesehene) können Sie den Einstiegspunkt (oder den Ausgang) Ihres Programms jederzeit mithilfe der Anweisungen des Vorprozessors wie ändern

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

Wobei Priorität eine optionale ganzzahlige Zahl ist.

Der Pragma-Start führt die Funktion vor der Hauptfunktion aus (vorrangig), und der Pragma-Exit führt die Funktion nach der Hauptfunktion aus. Wenn es mehr als eine Startanweisung gibt, entscheidet die Priorität, welche zuerst ausgeführt wird.

Sadique
quelle
4
Ich glaube nicht, dass diese Antwort tatsächlich die Frage beantwortet, wie der Compiler tatsächlich mit der Situation umgeht. Die Antwort von @Kaz gibt meiner Meinung nach mehr Einblick.
Tilman Vogel
4
Ich denke, diese Antwort beantwortet die Frage besser als die von @Kaz. Die ursprüngliche Frage hat den Eindruck, dass eine Überladung des Operators auftritt, und diese Antwort behebt, dass der Compiler anstelle einer Überladungslösung zwei verschiedene Signaturen akzeptiert. Die Compiler-Details sind interessant, aber nicht erforderlich, um die Frage zu beantworten.
Waleed Khan
1
In freistehenden Umgebungen ("nicht gehostet") ist viel mehr los als nur ein #pragma. Es gibt einen Reset-Interrupt von der Hardware und genau hier beginnt das Programm. Von dort aus werden alle grundlegenden Setups ausgeführt: Setup-Stack, Register, MMU, Speicherzuordnung usw. Anschließend erfolgt ein Herunterkopieren der Init-Werte von NVM in statische Speichervariablen (.data-Segment) sowie ein "Zero-Out" für alle statische Speichervariablen, die auf Null gesetzt werden sollten (.bss-Segment). In C ++ werden Konstruktoren von Objekten mit statischer Speicherdauer aufgerufen. Und wenn alles erledigt ist, wird main aufgerufen.
Lundin
8

Eine Überlastung ist nicht erforderlich. Ja, es gibt 2 Versionen, aber es kann jeweils nur eine verwendet werden.

user694733
quelle
5

Dies ist eine der seltsamen Asymmetrien und Sonderregeln der C- und C ++ - Sprache.

Meiner Meinung nach existiert es nur aus historischen Gründen und es steckt keine wirklich ernsthafte Logik dahinter. Beachten Sie, dass dies mainauch aus anderen Gründen besonders ist (z. B. kann mainC ++ nicht rekursiv sein und Sie können seine Adresse nicht übernehmen, und in C99 / C ++ dürfen Sie eine endgültige returnAnweisung weglassen ).

Beachten Sie auch, dass es auch in C ++ keine Überladung ist ... entweder hat ein Programm die erste Form oder es hat die zweite Form; es kann nicht beides haben.

6502
quelle
Sie können die returnAnweisung auch in C weglassen (seit C99).
Dreamlax
In C können Sie anrufen main()und die Adresse übernehmen. C ++ wendet Grenzwerte an, die C nicht hat.
Jonathan Leffler
@ JonathanLeffler: Sie haben Recht, behoben. Das einzig lustige an main, das ich in C99-Spezifikationen gefunden habe, ist neben der Möglichkeit, den Rückgabewert wegzulassen, dass Sie argcbeim Rekursieren keinen negativen Wert an den Standard IIUC übergeben können (5.1.2.2.1 gibt keine Einschränkungen für an argcund argvgelten nur für den ersten Anruf bei main).
6502
4

Was ungewöhnlich mainist, ist nicht, dass es auf mehr als eine Weise definiert werden kann, sondern dass es nur auf eine von zwei verschiedenen Arten definiert werden kann.

mainist eine benutzerdefinierte Funktion; Die Implementierung deklariert keinen Prototyp dafür.

Das Gleiche gilt für foooder bar, aber Sie können Funktionen mit diesen Namen beliebig definieren.

Der Unterschied besteht darin, dass maindie Implementierung (die Laufzeitumgebung) nicht nur Ihren eigenen Code aufruft. Die Implementierung ist nicht auf die gewöhnliche Semantik von C-Funktionsaufrufen beschränkt, daher kann (und muss) sie mit einigen Variationen umgehen - es ist jedoch nicht erforderlich, unendlich viele Möglichkeiten zu handhaben. Das int main(int argc, char *argv[])Formular ermöglicht Befehlszeilenargumente und ist int main(void)in C oder int main()C ++ nur eine Annehmlichkeit für einfache Programme, die keine Befehlszeilenargumente verarbeiten müssen.

Wie der Compiler damit umgeht, hängt von der Implementierung ab. Die meisten Systeme haben wahrscheinlich Aufrufkonventionen, die die beiden Formulare effektiv kompatibel machen, und alle Argumente, die an eine mainDefinition ohne Parameter übergeben werden, werden stillschweigend ignoriert. Wenn nicht, wäre es für einen Compiler oder Linker nicht schwierig, mainspeziell zu behandeln . Wenn Sie neugierig sind, wie es auf Ihrem System funktioniert , sehen Sie sich möglicherweise einige Baugruppenlisten an.

Und wie viele Dinge in C und C ++ sind die Details größtenteils ein Ergebnis der Geschichte und willkürlicher Entscheidungen, die von den Designern der Sprachen und ihren Vorgängern getroffen wurden.

Beachten Sie, dass sowohl C als auch C ++ andere implementierungsdefinierte Definitionen für zulassen main- aber es gibt selten einen guten Grund, sie zu verwenden. Bei freistehenden Implementierungen (z. B. eingebetteten Systemen ohne Betriebssystem) ist der Programmeinstiegspunkt implementierungsdefiniert und wird nicht unbedingt aufgerufen main.

Keith Thompson
quelle
3

Dies mainist nur ein Name für eine vom Linker festgelegte Startadresse, wobei mainder Standardname ist. Alle Funktionsnamen in einem Programm sind Startadressen, an denen die Funktion startet.

Die Funktionsargumente werden auf den Stapel geschoben / verschoben. Wenn für die Funktion keine Argumente angegeben sind, werden keine Argumente auf den Stapel geschoben / verschoben. So kann main sowohl mit als auch ohne Argumente arbeiten.

AndersK
quelle
2

Nun, die zwei verschiedenen Signaturen derselben Funktion main () werden nur dann angezeigt, wenn Sie sie möchten. Wenn Ihr Programm also Daten benötigt, bevor Ihr Code tatsächlich verarbeitet wird, können Sie sie über - übergeben.

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

Dabei speichert die Variable argc die Anzahl der übergebenen Daten und argv ist ein Array von Zeigern auf char, die auf die von der Konsole übergebenen Werte verweisen. Ansonsten ist es immer gut zu gehen

    int main()
    {
       //Code
    }

In jedem Fall kann es jedoch nur ein main () in einem Programm geben, da dies der einzige Punkt ist, an dem ein Programm seine Ausführung beginnt und daher nicht mehr als eins sein kann. (hoffe es ist würdig)

manisch
quelle
2

Eine ähnliche Frage wurde bereits gestellt: Warum wird eine Funktion ohne Parameter (im Vergleich zur tatsächlichen Funktionsdefinition) kompiliert?

Eine der am besten bewerteten Antworten war:

In C func()bedeutet, dass Sie beliebig viele Argumente übergeben können. Wenn Sie keine Argumente wollen, müssen Sie als deklarierenfunc(void)

Also, ich denke es ist wie maindeklariert wird (wenn Sie den Begriff "deklariert" anwenden können main). In der Tat können Sie so etwas schreiben:

int main(int only_one_argument) {
    // code
}

und es wird immer noch kompiliert und ausgeführt.

Varepsilon
quelle
1
Hervorragende Beobachtung! Es scheint, dass der Linker ziemlich verzeihend ist main, da es ein Problem gibt, das noch nicht erwähnt wurde: noch mehr Argumente für main! "Unix (aber nicht Posix.1) und Microsoft Windows" fügen hinzu char **envp(ich erinnere mich, dass DOS das auch erlaubt hat, nicht wahr ?), Und Mac OS X und Darwin fügen noch einen weiteren char * Zeiger für "willkürliche vom Betriebssystem bereitgestellte Informationen" hinzu. wikipedia
usr2564301
0

Sie müssen dies nicht überschreiben, da jeweils nur eine verwendet wird. Ja, es gibt zwei verschiedene Versionen der Hauptfunktion

Gautam
quelle