Як може працювати програма з глобальною змінною, яка називається main замість основної функції?


97

Розглянемо наступну програму:

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

Використовуючи g ++ 4.8.1 (mingw64) в ОС Windows 7, програма компілює та працює добре, друкуючи:

C ++ відмінна!

до консолі. mainвидається глобальною змінною, а не функцією; як ця програма може виконуватись без функції main()? Чи відповідає цей код стандарту C ++? Чи правильно визначено поведінку програми? Я також використав цей -pedantic-errorsваріант, але програма все ще компілюється та працює.


11
@ πάνταῥεῖ: чому потрібен тег юриста з мови?
Деструктор

14
Зауважте, що 195це опкод для RETінструкції, і що за умовами виклику C, абонент очищає стек.
Брайан

2
@PravasiMeet "тоді як ця програма виконує" - ви не вважаєте, що код ініціалізації змінної повинен бути main()виконаний (навіть без функції? Насправді вони абсолютно не пов'язані між собою)
Парамагнітний круасан

4
Я серед тих, хто виявив, що програма segfault така, як є (64-бітний Linux, g ++ 5.1 / clang 3.6). Але я можу виправити це, змінивши його int main = ( std::cout << "C++ is excellent!\n", exit(0),1 );(і включивши <cstdlib>), хоча програма залишається юридично непрацюючою.
Майк Кінган

11
@Brian Ви повинні згадати архітектуру, коли робите такі заяви. Весь світ - це не VAX. Або x86. Або що завгодно.
dmckee --- кошеня колишнього модератора

Відповіді:


84

Перш ніж переглядати питання щодо того, що відбувається, важливо зазначити, що програма неправильно сформована відповідно до звіту про дефекти 1886: Мовна зв'язок для main () :

[...] Програма, яка оголошує змінну main в глобальному масштабі або яка оголошує ім'я main за допомогою посилання на мову C (у будь-якому просторі імен), неправильно формується. [...]

Останні версії clang і gcc роблять це помилкою, і програма не буде компілюватись ( див. Приклад gcc live ):

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

То чому ж не було діагностики у старих версіях gcc та clang? Цей звіт про дефекти навіть не мав запропонованої постанови до кінця 2014 року, тому цей випадок був зовсім недавно явно неправомірним, що вимагає діагностики.

До цього, здається , що це було б невизначений поведінка , так як ми порушений Shall вимоги проекту C ++ стандарт з розділу 3.6.1 [basic.start.main] :

Програма повинна містити глобальну функцію, яку називають основною, яка позначається початком програми. [...]

Невизначена поведінка непередбачувана і не вимагає діагностики. Невідповідність, яку ми спостерігаємо при відтворенні поведінки, є типовою невизначеною поведінкою.

Отже, що насправді робить код і чому в деяких випадках він дає результати? Давайте подивимось, що у нас є:

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

У нас є, mainщо є int, оголошеним у глобальному просторі імен та ініціалізується, змінна має статичну тривалість зберігання. Це визначається, чи буде ініціалізація відбуватися до того, як буде зроблена спроба виклику main, але, здається, gcc робить це перед тим, як викликати main.

У коді використовується оператор комами , лівий операнд - це вираження відкинутого значення і тут використовується виключно для побічного ефекту виклику std::cout. Результатом оператора кома є правильний операнд, який у цьому випадку є первинним значенням, 195яке присвоюється змінній main.

Ми можемо бачити, як sergej вказує на створені покази збірки, які coutвикликаються під час статичної ініціалізації. Хоча найцікавішим моментом для обговорення дивіться сеанс прямого бомба :

main:
.zero   4

та наступні:

movl    $195, main(%rip)

Ймовірний сценарій полягає в тому, що програма переходить до символу, mainочікуючи, що дійсний код буде там, а в деяких випадках стане seg-fault . Отже, якщо це так, ми очікуємо, що зберігання дійсного машинного коду у змінній mainможе призвести до працездатної програми , припускаючи, що ми знаходимось у сегменті, що дозволяє виконувати код. Ми можемо побачити, що ця запис МОКЦ 1984 року робить саме це .

Здається, ми можемо зробити gcc зробити це в C за допомогою ( дивіться це наживо ):

const int main = 195 ;

Це seg-помилки, якщо змінна mainне є const, мабуть, тому що вона не знаходиться у виконуваному місці, Hat Порада до цього коментаря тут, який дав мені цю ідею.

Також дивіться відповідь FUZxxl тут на C-конкретну версію цього питання.


Чому впровадження також не дає попереджень. (Коли я використовую -Wall & -Wextra, він все ще не дає єдиного попередження). Чому? Що ви думаєте про відповідь @Mark B на це запитання?
Деструктор

IMHO, компілятор не повинен попереджати, оскільки mainце не зарезервований ідентифікатор (3.6.1 / 3). У цьому випадку я вважаю, що поводження з цією справою VS2013 (див. Відповідь Френсіса Куглера) є більш правильним у цій справі, ніж gcc & clang.
cdmh

@PravasiMeet Я оновив свою відповідь wrt на те, чому попередні версії gcc не дали діагностику.
Шафік Ягмур

2
... і справді, коли я тестую програму OP на Linux / x86-64, з g ++ 5.2 (що приймає програму - я думаю, ви не жартували про "останню версію"), вона виходить з ладу саме там, де я цього очікував би.
zwol

1
@Walter Я не вірю, що це дублікати, колишній задає набагато вужче питання. Очевидно, що група користувачів SO, яка має більш редукціоністський погляд на дублікати, що для мене не має особливого сенсу, оскільки ми можемо звести більшість запитань до деяких версій старих запитань.
Шафік Ягмур

20

З 3.6.1 / 1:

Програма повинна містити глобальну функцію, яку називають основною, яка позначається початком програми. Реалізація визначається, чи потрібна програма у вільно розташованому середовищі для визначення основної функції.

З цього виходить, що g ++ трапляється дозволити програмі (імовірно, як "самостійно" пункт) без основної функції.

Тоді з 3.6.1 / 3:

Основна функція не повинна використовуватися (3.2) в межах програми. Визначено зв'язок (3.5) основного. Програма, яка визнає основним вбудованим або статичним, не відповідає. Ім'я main не захищено інакше.

Тож тут ми дізнаємось, що цілком чудово мати цілу змінну main.

Нарешті, якщо вам цікаво, чому виводиться результат, ініціалізація int mainвикористовує оператор комами для виконання coutстатичного init, а потім надає фактичне інтегральне значення для ініціалізації.


7
Цікаво відзначити, що посилання не вдається, якщо ви перейменовуєтесь mainна щось інше: (.text+0x20): undefined reference to main ''
Фред Ларсон

1
Чи не потрібно вказувати gcc, що ваша програма є автономною?
Шафік Ягмур

9

gcc 4.8.1 генерує таку збірку 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

Зверніть увагу, що coutвикликається під час ініціалізації, а не у mainфункції!

.zero 4оголошує 4 (0-ініціалізовані) байти, починаючи з місця main, де mainє ім'я змінної [!] .

mainСимвол інтерпретується як початок програми. Поведінка залежить від платформи.


1
Зауважте, як зазначає Брайан, 195 це код роботи для retдеяких архітектур. Так що вимова нульових інструкцій може бути неточною.
Шафік Ягмур

@ShafikYaghmour Дякую за ваш коментар, ви праві. Я заплутався з директивами асемблера.
sergej

8

Це неправильно сформована програма. Збій у моєму тестовому середовищі, cygwin64 / g ++ 4.9.3.

Зі стандартного:

3.6.1 Основна функція [basic.start.main]

1.Програма повинна містити глобальну функцію, яка називається main, яка є призначеним початком програми.


Я думаю, що до того, як я посилався на повідомлення про дефекти, це була просто не визначена поведінка.
Шафік Ягмур

@ShafikYaghmour, це загальний принцип, який застосовуватиметься у всіх місцях, де повинен застосовуватись стандарт ?
R Sahu

Я хочу сказати так, але я не бачу хорошого опису різниці. З того, що я можу зрозуміти з цієї дискусії , неправильно сформований НДР та невизначена поведінка, ймовірно, є синонімами, оскільки жоден з них не вимагає діагностики. Здається, це означає, що неправильно сформовані, і UB відрізняються, але не впевнені.
Шафік Ягмор

3
Розділ 4 C99 ("Відповідність") робить це однозначним: "Якщо вимога" має "або" не повинна "порушується вимога, яка з'являється поза обмеження, поведінка не визначена". Я не можу знайти еквівалентного формулювання в C ++ 98 або C ++ 11, але я сильно підозрюю, що комітет мав на увазі, що він повинен бути там. (Комітетам С і С ++ дійсно потрібно засісти та згладити всі термінологічні відмінності між двома стандартами.)
zwol

7

Причина, по якій я вважаю, що це працює, полягає в тому, що компілятор не знає, що він компілює main()функцію, тому він компілює глобальне ціле число з побічними ефектами призначення.

Формат об'єкта, в який складається цей блок перекладу, не здатний розрізняти функціональний символ і символ змінної .

Отже, лінкер радісно посилається на головний символ (змінної) і ставиться до нього, як до виклику функції. Але не поки система виконання не запустить глобальний код ініціалізації змінної.

Коли я запустив зразок, його роздрукували, але потім це спричинило помилку сегментації . Я припускаю, що це тоді, коли виконується система намагалася виконати змінну int так, ніби це була функція .


4

Я спробував це на операційній системі Win7 64bit за допомогою VS2013, і вона компілюється правильно, але коли я намагаюся створити додаток, я отримую це повідомлення з вікна виводу.

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, це помилка лінкера, а не повідомлення від налагоджувача. Компіляція вдалася, але лінкер не зміг знайти функцію, main()оскільки це змінна типint
cdmh

Дякую за відповідь, я переформулюю свою первинну відповідь, щоб відобразити це.
Френсіс Куглер

-1

Ви тут робите хитру роботу. Як головне (якось) можна було оголосити цілим числом. Ви використовували оператор списку, щоб надрукувати повідомлення та потім призначити йому 195. Як хтось нижче сказав, що це не комфортно із C ++, це правда. Але оскільки компілятор не знайшов жодного визначеного користувачем імені, головне, він не скаржився. Пам'ятайте, що main - це не визначена системою функція, а її визначена користувачем функція, а саме, з якої програма починає виконувати програму - це головний модуль, а не main (). Знову main () викликається функцією запуску, яка виконується завантажувачем навмисно. Тоді всі ваші змінні ініціалізуються, а під час ініціалізації виводяться так. Це воно. Програма без main () нормальна, але не стандартна.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.