Come può funzionare un programma con una variabile globale chiamata main invece di una funzione main?


97

Considera il seguente programma:

#include <iostream>
int main = ( std::cout << "C++ is excellent!\n", 195 ); 

Utilizzando g ++ 4.8.1 (mingw64) su sistema operativo Windows 7, il programma si compila e funziona correttamente, stampando:

Il C ++ è eccellente!

alla console. mainsembra essere una variabile globale piuttosto che una funzione; come può essere eseguito questo programma senza la funzione main()? Questo codice è conforme allo standard C ++? Il comportamento del programma è ben definito? Ho anche usato l' -pedantic-errorsopzione, ma il programma viene comunque compilato ed eseguito.


11
@ πάνταῥεῖ: perché è necessario il tag avvocato della lingua?
Distruttore

14
Si noti che 195è il codice operativo per l' RETistruzione e che nella convenzione di chiamata C, il chiamante cancella lo stack.
Brian

2
@PravasiMeet "allora come viene eseguito questo programma" - non pensi che il codice di inizializzazione di una variabile debba essere eseguito (anche senza la main()funzione? In effetti, sono completamente indipendenti)
The Paramagnetic Croissant

4
Sono tra quelli che hanno riscontrato che il programma esegue il segfault come è (Linux a 64 bit, g ++ 5.1 / clang 3.6). Posso rettificare questo problema, tuttavia, modificandolo int main = ( std::cout << "C++ is excellent!\n", exit(0),1 );(e includendo <cstdlib>), sebbene il programma rimanga legalmente mal formato.
Mike Kinghan,

11
@ Brian Dovresti menzionare l'architettura quando fai affermazioni del genere. Tutto il mondo non è un VAX. Oppure x86. O qualunque cosa.
dmckee --- gattino ex moderatore,

Risposte:


84

Prima di entrare nel vivo della domanda su cosa sta succedendo, è importante sottolineare che il programma è mal formato secondo il rapporto sui difetti 1886: Language linkage for main () :

[...] Un programma che dichiara una variabile main a livello globale o che dichiara il nome main con collegamento al linguaggio C (in qualsiasi spazio dei nomi) è mal formato. [...]

Le versioni più recenti di clang e gcc rendono questo un errore e il programma non verrà compilato ( vedi l'esempio live di gcc ):

error: cannot declare '::main' to be a global variable
int main = ( std::cout << "C++ is excellent!\n", 195 ); 
    ^

Allora perché non c'era diagnostica nelle versioni precedenti di gcc e clang? Questo rapporto di difetto non aveva nemmeno una proposta di risoluzione fino alla fine del 2014 e quindi questo caso è stato solo di recente esplicitamente mal formato, il che richiede una diagnosi.

Prima di questo, sembra che questo sarebbe un comportamento indefinito poiché stiamo violando una deve requisito del progetto di standard C ++ dalla sezione 3.6.1 [basic.start.main] :

Un programma deve contenere una funzione globale chiamata main, che è l'inizio designato del programma. [...]

Il comportamento indefinito è imprevedibile e non richiede una diagnosi. L'incoerenza che vediamo con la riproduzione del comportamento è un tipico comportamento non definito.

Quindi cosa fa effettivamente il codice e perché in alcuni casi produce risultati? Vediamo cosa abbiamo:

declarator  
|        initializer----------------------------------
|        |                                           |
v        v                                           v
int main = ( std::cout << "C++ is excellent!\n", 195 ); 
    ^      ^                                   ^
    |      |                                   |
    |      |                                   comma operator
    |      primary expression
global variable of type int

Abbiamo mainche è un int dichiarato nello spazio dei nomi globale e viene inizializzato, la variabile ha una durata di archiviazione statica. L'implementazione definisce se l'inizializzazione avverrà prima che mainvenga effettuato un tentativo di chiamata, ma sembra che gcc lo faccia prima di chiamare main.

Il codice utilizza l' operatore virgola , l'operando sinistro è un'espressione di valore scartato e viene utilizzato qui esclusivamente per l'effetto collaterale della chiamata std::cout. Il risultato dell'operatore virgola è l'operando di destra che in questo caso è il prvalue 195assegnato alla variabile main.

Possiamo vedere sergej che indica che l'assembly generato mostra che coutviene chiamato durante l'inizializzazione statica. Anche se il punto più interessante per la discussione vedere la sessione live di godbolt sarebbe questo:

main:
.zero   4

e il successivo:

movl    $195, main(%rip)

Lo scenario probabile è che il programma salti al simbolo mainaspettandosi che ci sia un codice valido e in alcuni casi seg-fault . Quindi, se questo è il caso, ci aspetteremmo che la memorizzazione di un codice macchina valido nella variabile mainpossa portare a un programma funzionante , supponendo che ci troviamo in un segmento che consente l'esecuzione del codice. Possiamo vedere che questa voce IOCCC del 1984 fa proprio questo .

Sembra che possiamo convincere gcc a farlo in C usando ( guardalo dal vivo ):

const int main = 195 ;

È seg-faults se la variabile mainnon è const presumibilmente perché non si trova in una posizione eseguibile, Hat Tip a questo commento qui che mi ha dato questa idea.

Vedi anche la risposta di FUZxxl qui a una versione specifica in C di questa domanda.


Perché anche l'implementazione non fornisce alcun avviso. (Quando uso -Wall e -Wextra non dà ancora un unico avvertimento). Perché? Cosa ne pensi della risposta di @Mark B a questa domanda?
Distruttore

IMHO, il compilatore non dovrebbe dare un avviso perché mainnon è un identificatore riservato (3.6.1 / 3). In questo caso, penso che la gestione di questo caso da parte di VS2013 (vedi la risposta di Francis Cugler) sia più corretta nella sua gestione rispetto a gcc & clang.
cdmh

@PravasiMeet Ho aggiornato la mia risposta rispetto al motivo per cui le versioni precedenti di gcc non fornivano una diagnostica.
Shafik Yaghmour

2
... e in effetti, quando provo il programma dell'OP su Linux / x86-64, con g ++ 5.2 (che accetta il programma - immagino tu non stia scherzando sulla "versione più recente"), si blocca esattamente dove mi aspettavo voluto.
Zwol

1
@ Walter Non credo che questi siano duplicati, il primo sta facendo una domanda molto più ristretta. C'è chiaramente un gruppo di utenti SO che ha una visione più riduzionista dei duplicati che non ha molto senso per me dato che potremmo ridurre la maggior parte delle domande SO a qualche versione di domande più vecchie, ma SO non sarebbe molto utile.
Shafik Yaghmour

20

Dal 3.6.1 / 1:

Un programma deve contenere una funzione globale chiamata main, che è l'inizio designato del programma. È l'implementazione definita se un programma in un ambiente indipendente è necessario per definire una funzione principale.

Da questo sembra che g ++ consenta un programma (presumibilmente come la clausola "indipendente") senza una funzione principale.

Quindi da 3.6.1 / 3:

La funzione main non deve essere utilizzata (3.2) all'interno di un programma. Il collegamento (3.5) di main è definito dall'implementazione. Un programma che dichiara che main è inline o statico è malformato. Il nome main non è altrimenti riservato.

Quindi qui apprendiamo che va benissimo avere una variabile intera denominata main.

Infine, se ti stai chiedendo perché l'output viene stampato, l'inizializzazione di int mainutilizza l'operatore virgola per eseguire coutsu static init e quindi fornire un valore integrale effettivo per eseguire l'inizializzazione.


7
È interessante notare che il collegamento non riesce se si rinomina mainin qualcos'altro: (.text+0x20): undefined reference to main ''
Fred Larson,

1
Non devi specificare a gcc che il tuo programma è indipendente?
Shafik Yaghmour

9

gcc 4.8.1 genera il seguente assembly x86:

.LC0:
    .string "C++ is excellent!\n"
    subq    $8, %rsp    #,
    movl    std::__ioinit, %edi #,
    call    std::ios_base::Init::Init() #
    movl    $__dso_handle, %edx #,
    movl    std::__ioinit, %esi #,
    movl    std::ios_base::Init::~Init(), %edi  #,
    call    __cxa_atexit    #
    movl    $.LC0, %esi #,
    movl    std::cout, %edi #,
    call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)   #
    movl    $195, main(%rip)    #, main
    addq    $8, %rsp    #,
    ret
main:
    .zero   4

Notare che coutviene chiamato durante l'inizializzazione, non nella mainfunzione!

.zero 4dichiara 4 byte (inizializzati con 0) a partire dalla posizione main, dove mainè il nome della variabile [!] .

Il mainsimbolo viene interpretato come l'inizio del programma. Il comportamento dipende dalla piattaforma.


1
Nota, come sottolinea Brian, 195 è il codice operativo per retalcune architetture. Quindi dire che le istruzioni zero potrebbero non essere accurate.
Shafik Yaghmour

@ShafikYaghmour Grazie per il tuo commento, hai ragione. Ho incasinato le direttive dell'assembler.
sergej

8

Questo è un programma mal formato. Si blocca nel mio ambiente di test, cygwin64 / g ++ 4.9.3.

Dallo standard:

3.6.1 Funzione principale [basic.start.main]

1 Un programma deve contenere una funzione globale chiamata main, che è l'inizio designato del programma.


Penso che prima del rapporto sui difetti che ho citato, questo fosse solo un comportamento semplice e indefinito.
Shafik Yaghmour

@ShafikYaghmour, È questo il principio generale da applicare in tutti i luoghi in cui devono essere utilizzati gli standard ?
R Sahu,

Vorrei dire di sì ma non ho visto una buona descrizione della differenza. Da quello che posso dire da questa discussione , NDR mal formato e comportamento indefinito sono probabilmente sinonimi poiché nessuno dei due richiede una diagnosi. Ciò sembrerebbe implicare malformato e UB sono distinti ma non sicuri.
Shafik Yaghmour

3
C99 sezione 4 ("Conformità") rende questo inequivocabile: "Se un requisito 'deve' o 'non deve' che appare al di fuori di un vincolo viene violato, il comportamento è indefinito." Non riesco a trovare una formulazione equivalente in C ++ 98 o C ++ 11, ma sospetto fortemente che il comitato intendesse che fosse lì. (I comitati C e C ++ hanno davvero bisogno di sedersi e
appianare

7

Il motivo per cui credo che funzioni è che il compilatore non sa che sta compilando la main()funzione, quindi compila un intero globale con effetti collaterali dell'assegnazione.

Il formato dell'oggetto in cui è compilata questa unità di traduzione non è in grado di distinguere tra un simbolo di funzione e un simbolo di variabile .

Quindi il linker si collega felicemente al simbolo principale (variabile) e lo tratta come una chiamata di funzione. Ma non finché il sistema runtime non ha eseguito il codice di inizializzazione della variabile globale.

Quando ho eseguito il campione, è stato stampato ma poi ha causato un errore di segmentazione . Presumo che sia stato il momento in cui il sistema di runtime ha cercato di eseguire una variabile int come se fosse una funzione .


4

L'ho provato su un sistema operativo Win7 a 64 bit utilizzando VS2013 e si compila correttamente, ma quando provo a creare l'applicazione ottengo questo messaggio dalla finestra di output.

1>------ Build started: Project: tempTest, Configuration: Debug Win32 ------
1>LINK : fatal error LNK1561: entry point must be defined
========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========

2
FWIW, questo è un errore del linker, non un messaggio dal debugger. La compilazione è riuscita, ma il linker non è riuscito a trovare una funzione main()perché è una variabile di tipoint
cdmh

Grazie per la risposta, riformulerò la mia risposta iniziale per riflettere questo.
Francis Cugler

-1

Stai facendo un lavoro complicato qui. Poiché main (in qualche modo) potrebbe essere dichiarato intero. Hai utilizzato list operator per stampare il messaggio e quindi assegnargli 195. Come detto da qualcuno di seguito, che non conforta con C ++, è vero. Ma poiché il compilatore non ha trovato alcun nome definito dall'utente, main, non si è lamentato. Ricorda che main non è una funzione definita dal sistema, la sua funzione definita dall'utente e l'elemento da cui il programma inizia l'esecuzione è Main Module, non main (), in particolare. Anche in questo caso main () viene chiamato dalla funzione di avvio che viene eseguita intenzionalmente dal caricatore. Quindi tutte le variabili vengono inizializzate e durante l'inizializzazione vengono visualizzate in questo modo. Questo è tutto. Il programma senza main () va bene, ma non è standard.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.