Як називається файл із додатком після компіляції

Як називається файл із додатком після компіляції



Процес компіляції програм

Найцікавіше, що в жодній літературі про компіляцію не розповідається (у зовсім базовій вважається, що це складно, а в просунутій — що ви знаєте), а всі, хто це знає, кажуть, що прийшло з досвідом.

Базові знання про етапи компіляції.

Зазвичай ми компілюємо програму як g++ program.cpp. А ось чого ми поки не знаємо, так це того, що g++ не робить всю роботу самостійно, а викликає інші команди, які виконують компіляцію частинами. І якщо подивитися, що там, то відбувається. cc1plus, потім as, наприкінці collect2, який викликає ld. Давайте спробуємо це повторити.

Далі буде перерахування стадій із зазначенням двох моментів: як їх можна виконати руками та яке розширення зазвичай має результат цієї стадії.

  1. Препроцесування. Виконується за допомогою g++ -E (якщо додатково передати ключ -P, то висновок буде трохи коротше), вихідний файл зазвичай має розширення .i.На файл з розширенням .i можна й очима подивитися - у ньому буде купа тексту замість #include, а потім наш код. #include — директива препроцесора, яка тупо вставляє вказаний файл у місце, де написанаТакож препроцесор займається макросами (#define).
  2. Трансляція. Виконується за допомогою g++ -Sвихідний файл зазвичай має розширення .s. «Трансляція» — це (з англійської) «переклад». -masm = intel, можна уточнити, який саме асемблер перекладати (як було сказано в 01_asm, асемблери відрізняються залежно від інструмента).
  3. Асемблювання. Виконується спеціальною утилітою. asвихідний файл зазвичай має розширення .o (і називається об'єктним файлом). На даному етапі немає нічого цікавого — просто інструкції, які були в асемблері, переганяються в машинний код. Тому файли .o марно дивитися очима, вони бінарні, для цього є спеціальні утиліти, наприклад, objdump. Про нього буде розказано трохи згодом.
  4. Лінківка. Виконується простим викликом g++ від об'єктного файлу. На виході дає виконуваний файл. Потрібна, якщо файлів кілька: ми запускаємо препроцесор, трансляцію та асемблювання незалежно кожного файлу, а об'єднуються вони лише з етапі лінківки. Незалежні .cpp файли називають одиницями трансляції. Зрозуміло, тільки в одній одиниці має бути main . У цьому main'і, до речі, можна не робити return 0, його туди вставить компілятор.
    Варто сказати, що інформація про лінківці вірна до появи модулів у C++20, де можна діставати дані одного файлу для іншого. Там виникає залежність файлів один від одного, а значить компілювати їх треба в певному порядку.

Класична схема етапів компіляції виглядає так:

Є схожа стаття на хабрі на тему.

Оголошення та визначення.

Дуже хочеться з'єднати ось це:

Це не компілюється, а точніше, помилка відбувається на етапі трансляції. a.cpp. У тексті помилки написано, що f не визначено у сфері видимості. Все тому, що для того, щоб викликати функцію, треба щось про неї знати. Наприклад, якщо ми передаємо в функцію int - це один асемблерний код, а якщо double - то зовсім інший (бо різні calling convention'и можуть бути). Тому на етапі трансляції слід знати сигнатуру функції. Щоб вказати цю сигнатуру, C++ є оголошення:

// a.cpp: void f(); // Ось це оголошення. int main()

Коли ми пишемо функцію та крапку з комою - це оголошення/декларація (declaration). Це означає, що у програмі така функція є. А коли ми пишемо тіло функції у фігурних дужках – це визначення (definition).

До речі, написати оголошення буває корисним навіть якщо у нас один файл. Наприклад, у такому файлі:

#include int main() < f(); >void f()

Це не компілюється, і справа в тому, що компілятор дивиться файл зверху вниз, і коли доходить до виклику функції f всередині main він ще не дійшов до її визначення. Тут можна переставити функції місцями, так, але якщо ми маємо взаєморекурсивні функції, то там переставити їх не вийде — тільки написати декларацію.

Помилки лінківки. Інструменти nm і objdump. Ключове слово static.

Розглянемо такий приклад:

// main.cpp void f(); int main()

Тут на етапі лінківки напишуть, що функція f() визначається двічі. Щоб гарно подивитися, як це працює, можна використати утиліту nm. Коли ви згенеруєте a.o і викличте nm -C a.o, то побачите щось таке:

U puts 0000000000000000 T f()

Що робить ключ -C, залишимо потім. На те, що тут знаходиться puts замість printf, теж звертати увагу не треба, це просто така оптимізація компілятора - коли можна замінити printf на puts, замінюємо.
А звернути увагу треба на те, що puts не визначено (про це нам каже літера U), а функція f() - визначена в секції .text (літера T). У main.cpp, зрозуміло, буде невизначена функція f() та певна main . Тому, маючи ці об'єктні файли, можна об'єднати main.cpp і a.cpp. Або main.cpp і b.cpp. Без перекомпіляції. Але не можна всі три разом, адже f() буде визначено двічі.

Якщо ми хочемо подивитись на об'єктні файли детальніше, нам знадобиться утиліта objdump. Вона має безліч ключів, які кажуть, що ми хочемо побачити. Наприклад, -x - Видати взагалі все. Нам зараз потрібно -d - дизасемблювання та -r - Релокації. Коли ми викличемо objdump -dr -Mintel -C main.o, ми побачимо, що на місці виклику функції f знаходиться call та нулі. Тому що невідомо, де ця функція, треба на етапі лінківки підставити її адресу. А щоб дізнатися, що саме підставити, є релокації, які інформацію про це містять. У загальному випадку релокація — інформація про те, які зміни потрібно зробити із програмою, щоб файл можна було запустити.

Давайте тепер на що подивимося. Нехай у нашому файлі визначено функцію f(). І десь за випадковим збігом далеко-далеко також визначено функцію f() . Зрозуміло, що воно так не з'єднується. Але ми можемо на увазі, що наша функція f потрібна тільки нам і ніяк назовні не стирчить. Для цього є спеціальний модифікатор: static. Якщо зробити такі функції nm, то можна побачити символ t замість T, Який саме означає локальність для одиниці трансляції. Взагалі функції, локальні для одного файлу варто позначати як static у будь-якому випадку, тому що це ще допомагає компілятору зробити оптимізації..

Світові змінні.

Для глобальних змінних все те саме, що й для функцій: наприклад, ми можемо також послатися на глобальну змінну з іншого файла. Тільки тут інший синтаксис:

extern int x; // Оголошення. int x; // Визначення.

І так само в глобальних змінних можна писати static. А тепер приклад:

// a.cpp extern int a; void f(); int main()
// b.cpp #include int a; void f()

Вперше вам виведуть 0, тому що глобальні змінні ініціалізуються нулямиЛокальні змінні зберігаються на стеку, і там які дані були до заходу в функцію, ті там і будуть.

Декорування імен.

Обговорена нами модель компіляції дозволяє використовувати різні мови програмування. Поки ЯП може транслюватися в об'єктні файли, проблеми можуть виникнути тільки на етапі лінківки. Наприклад, ніхто не заважає вам взяти вже готовий асемблерник і скомпілювати його з .cpp файлом. Але в виклику асемблера є одна проблема.

Те ім'я символу, яке ми побачимо в nm буде fooА в C++ з'явилася перевантаження функцій, тобто void foo(int) і void foo(double) - це дві різні функції, обидві з яких можна викликати. компілятор mangle'іт/декорує імена, тобто змінює їх так, щоб символи вийшли унікальними. nm навіть може видати вам ці імена (в даному випадку вийде _Z3fooi і _Z3food). Але у вас є можливість побачити їх по-людськи: для цього існує вже згаданий ключ. -C, який якщо передати програмі nm, то вона роздекорує все назад і видасть вам імена людину. objdumpЦей ключ дати теж можна. c++filt, яка на ім'я символу дає сигнатуру функції.

Так ось, extern "C" каже, що при лінковці нам не потрібно проводити декорацію. І якщо у нас в асемблерному файлі написано fibonacci: , то вам і потрібно залишити ім'я символу як є:

extern "C" uint32_t fibonacci(uint32_t n);

У функцій, що мають різні сигнатури, але позначені як extern "C", після компіляції не буде інформації про типи їх аргументів, тому це з'єднується, але працювати не буде (ну або буде, але тут UB, так як, наприклад, типи аргументів очікуються різні).

Лінківка зі стандартною бібліотекою.

Візьмемо тепер оголошення printf із cstdio і вставимо його оголошення вручну:

extern "C" int printf(const char*, . ); int main()

Така програма також працює. А де визначення printf виникає питання? А ось дивіться. На етапі зв'язування не тільки зв'язуються ваші файли. Крім цього до параметрів зв'язування додаються ще кілька об'єктних файлів і кілька бібліотек. У нашій моделі світу вистачить інформації про те, що бібліотека просто набір об'єктних файлів. І ось при лінківці вам дають стандартну бібліотеку C++ (-lstdc++), математичну бібліотеку (-lm), бібліотеку -libgcc, щоб якщо ви робите арифметику в 128-бітових числах, то компілятор міг викликати функцію __udivti3 (поділ), і купу ще. У нашому випадку потрібна одна - -lc, у якій лежить printf . А ще один з об'єктних файлів, з якими ви лінкуєтеся, містить функцію _start (це може бути файл crt1.o), яка викликає main.

Headers (заголовні файли). Директива #include.

Якщо ми використовуємо одну функцію у багатьох файлах, нам треба писати її сигнатуру скрізь. А якщо ми її змінюємо, то взагалі можна повіситися. Тож так не роблять. А як це роблять? А так: декларація виділяється в окремий файл. Цей файл має розширення .h і називається заголовним. По суті це відбувається в стандартній бібліотеці. Підключаються заголовні файли директивою #include, якщо вони з якоїсь бібліотеки, або #include "filename", якщо вони ваші.У чому різниця? Стандартне пояснення – тим, що трикутні дужки спочатку шукають у бібліотеках, а потім у вашій програмі, а лапки – навпаки. Насправді обидва варіанти просто мають список шляхів, де шукати файл, і ці списки різні.

Але із заголовками потрібно правильно працювати. Наприклад, не можна робити #include "a.cpp" . Чому? Тому що всі визначені в a.cpp функції та змінні проникнуть туди, куди ви його підключили. І якщо файл у вас один, то ще нічого, а якщо більше, то в кожному, де написано #include "a.cpp" буде визначення, а значить визначення одного і того ж об'єкта буде написано кілька разів. Аналогічний ефект буде, якщо писати визначення відразу в заголовному файлі, не треба так.

На жаль, директива #include має кілька нюансів.

Запобігання повторному включенню.

Давайте поговоримо про структури. Що буде, якщо ми в заголовному файлі створимо struct і підключимо цей файл? Та нічого. Абсолютно нічого. Згенерований асемблерний код буде однаковим. У структур немає визначення по суті, тому що вони не генерують код. Тому їх пишуть у заголовках. При цьому їх методи можна (але не потрібно) визначати там, тому що вони сприймаються компілятором як inline. А хто такий цей inline і як він працює, дивись далі. Але із структурами є один нюанс. Розглянемо ось що:

// a.cpp: #include "y.h" // --> `struct x<>;`. #include "z.h" // --> `struct x<>;` помилка компіляції, повторне визначення.

Стандартний спосіб це виправити виглядає так:

// x.h: #ifndef X_H // Якщо ми визначили макрос, то заголовок цілком ігнорується. #define X_H // Якщо ігнорується, то помічаємо, що файл ми підключили. struct x <>; #endif // До блоку #ifndef. #endif полягає весь файл повністю.

Це називається include guard. Ще всі можливі компілятори підтримують #pragma once (ефект як у include guard, Але простіше). І справді #pragma once працює краще, тому що не спирається на ім'я файлу, наприклад. Але його немає у стандарті, що сумно.

Є один нюанс із #pragma once'ом. Якщо у вас є два жорсткі посилання на один файл, то у нього проблеми. Якщо у вас include guard, то інтуїтивно зрозуміло, що таке різні файли – коли макроси вони різні. А ось чи вважати різними файлами два жорсткі посилання на те саме — питання складне. Інша річ, що робити так, щоб джерела містили жорсткі чи символічні посилання вже досить дивно.

Наперед-декларації.

// a.h #ifndef A_H #define A_H #include "b.h" // Nothing, then `struct b < . >;` struct a < b * bb; >; #endif
// b.h #ifndef B_H #define B_H #include "a.h" // Nothing, then `struct a < . >;` struct b < a* aa; >; #endif
// main.cpp #include "a.h" // `struct b <. >; struct a < . >;` #include "b.h" // Nothing.

Зрозуміло, у чому проблема полягає. Ми підключаємо a.h, в ньому - b.h, а в ньому, оскільки ми вже зайшли в a.h, include guard нам його блокує. І ми спочатку визначаємо структуру b, а потім – a. І при перегляді структури b , ми не знатимемо, що таке a .

Для цього є конструкція, яка називається forward-декларацією. Вона виглядає так:

// a.h #ifndef A_H #define A_H struct b; struct a < b * bb; >; #endif
// b.h #ifndef B_H #define B_H struct a; struct b < a * aa; >; #endif

Щоб завести покажчик, нам не потрібно знати вміст структури. Тому ми просто говоримо, що b — це певна структура, яку ми визначимо далі.

Взагалі forward-декларацію у будь-якому випадку краще використовувати замість підключення заголовних файлів (якщо можливо, звісно). Чому?

  • По-перше, через час компіляції.Велика кількість підключень у заголовних файлах негативно впливає на нього, тому що якщо змінюється headerнеобхідно перекомпілювати всі файли, які підключають його (навіть не безпосередньо), що може бути довго.
  • Другий момент – коли у нас цикл із заголовних файлів, це завжди помилканавіть якщо там немає проблем як у прикладі, тому що результат компіляції залежить від того, що ви підключаєте першим.

Поки структуру не визначили, структура це incomplete type. Наприклад, на момент оголошення struct b; у коді вище, b - incomplete. До речі, в той момент, коли ви перебуваєте в середині визначення класу, він все ще вскладне. Все, що можна з incomplete типами робити - це оголошувати функції з їх використанням і створювати покажчик. Уповній тип стає повним типом після визначення. Поки що інформація про incomplete-типи нам ні до чого, але вона вистрілить пізніше.

Правило єдиного визначення.

А тепер такий приклад:

// a.cpp #include struct x < int a; // Padding double b; int c; int d; >; x f(); int main()

Тут варто згадати, що структури при лінковці не відіграють жодної ролі, тобто лінковника все одно, що у нас структура x визначена у двох місцях. Тому така програма відмінно скомпілюється і запуститься, проте вона є некоректною. За стандартом така програма працюватиме невідомо як, а життя дані поїдуть. А саме 2 пропаде через вирівнювання double , 3 і 4 перетворяться на одне число ( double ), а 5 буде на своєму місці, а x::e з файлу a.cpp буде просто не проініціалізовано. Правило, згідно з яким так не можна, називається one-definition rule/правило єдиного визначення. До речі, порушенням ODR є навіть тасування полів.

Inlining.

int foo(int a, int b) < return a + b; >int bar(int a, int b)

Якщо подивитися на асемблер код для bar , то там не буде виклику функції foo , а буде return b; . Це називається inlining коли ми беремо тіло однієї функції і вставляємо всередину іншої як воно є. Це пов'язано, наприклад, зі стилем програмування в поточному світі (багато маленьких функцій, які роблять маленькі речі) — ми прибираємо всі ці абстракції, зливаємо функції в одну і потім оптимізуємо, що там є.

Модифікатор inline.

// a.c void say_hello(); int main()
// b.c #include void say_hello()

Тут не станеться inlining, а чому? А тому що компілятор вміє підставляти тіло функцій лише всередині однієї одиниці трансляції (оскільки inlining відбувається на момент трансляції, а тоді компілятор не має функцій з інших одиниць).

Тут помірковано замовчується, що модель компіляції, яку ми обговорюємо – давня та бородата. Ми можемо передати ключ -flto у компілятор, тоді все буде за'inline'. Справа в тому, що при включеному режимі linking time optimization, ми відкладаємо на потім генерацію коду та генеруємо його на етапі лінківки. У такому разі лінківка може тривати багато часу, тому застосовується при складанні з оптимізаціями. Докладніше про режим LTO – значно пізніше.

Проте давайте розглянемо, як без LTO виправити проблему з відсутністю inlining'а. Ми можемо написати в заголовку тіло, це допоможе, але це, як ми знаємо, помилка компіляції. Хм-м, ну, можна не тільки написати функцію в заголовному файлі, але і позначити її як static, але це дає вам свою функцію на кожну одиницю трансляції, що, по-перше, буває просто не тим, що ви хочете, а в -друге, кратно збільшує розмір вихідного файла.

Тому є модифікатор inline . Він потрібний для того, щоб лінковник не дав помилку порушення ODR. Модифікатор inline безпосередньо не впливає на те, що функції вбудовуються. Якщо подивитися на inline через nm, то там побачимо W (weak) — з кількох функцій можна вибрати будь-яку (передбачається, що вони однакові).

По суті inline – вказівка ​​компілятору, що тепер за дотриманням ODR стежите ви, а не він І якщо ODR ви порушуєте, то це невизначена поведінкаill-formed, не diagnostic required). ill-formed, не diagnostic required - Це ситуація, коли програма некоректна, але ніхто не змушує компілятор вам про це говорити. Він може (у GCC є така можливість: якщо дати g++ ключі -flto -Wodr, він вам про це скаже), але не зобов'язаний. А за життям лінковник вибере довільну з наявних функцій (наприклад, з першої одиниці трансляції або взагалі різні в різних місцях):

// a.cpp #include inline void f() < printf("Hello, a.cpp!\n"); >void g();
// b.cpp inline void f() < printf("Hello, b.cpp!\n");

Якщо скомпілювати цей код з оптимізацією, обидві функції f будуть за'inline'і, і все буде добре, якщо без, то залежить від порядку файлів: g++ a.cpp b.cpp може цілком видавати Hello, a.cpp! двічі, а g++ b.cpp a.cppHello, b.cpp! двічі.

Якщо потрібно саме за 'inline' функцію, тобто нестандартизовані модифікатори типу __forceinline , проте навіть вони можуть ігноруватися компілятором.

Інші команди препроцесора.

#include обговорили вже вздовж і впоперек.Тобто, якщо виконано якусь умову, можна виконати один код, а інакше — інший.

Визначення макросу.

І ще є макроси: визначити макрос ( #define ) і визначити макрос ( #undef ):

#define PI 3.14159265 double circumference(double r)

Текст, що йде після імені макросу, називається replacement. Replacement відокремлюється від імені макросу пропуском і поширюється до кінця рядка. Всі входження ідентифікатора PI нижче цієї директиви будуть замінені на replacement. Найпростіший макрос object-like, його ви бачите вище, трохи складніший function-like:

Що нам потрібно про це знати. макроси працюють із токенами. Вони не знають взагалі нічого про те, що ви робите. Ви можете написати

І отримати відмовлене від реальності повідомлення про помилку. А справа в тому, що це на етапі препроцессингу розкривається, наприклад, так:

І тут компілятор бачить більш запеклий марення, ніж назва змінної так, як не можна.

Що ще не бачить препроцесор, то це синтаксичну структуру і пріоритет операцій. Більш страшні речі виходять, коли пишеться щось таке:

#define MUL(x, y) x * y int main()

Тому що розкривається це в

Це не те, що ви хочете. Тому коли ви таке пишіть, потрібно, по-перше, всі аргументи запихати в дужки, по-друге — вираз тежа по-третє, це вас ніяк не врятує від чогось такого:

Тому перед написанням макросів три рази подумайте, чи потрібно воно, а якщо потрібно, будьте дуже обережні. А ще, якщо ви використовуєте відладчик, він нічого не знає про макроси, навіщо йому знати. Тому в налагоджувачі написати «виклик макросу» Ви зазвичай не можете. Див. також FAQ Б'ярна Страуструпа про те, чому макроси це погано.

Ще #define дозволяє перевизначати макроси.

# define STR "abc" const char * first = STR; // "abc". #define STR "def" const char * second = STR; // "def".

Replacement макросу не препроцессується щодо макросу, але результат розкриття макросу препроцессируется повторно:

#define Y foo #define X Y // Це не `#define X foo`. #define Y bar// Це не `#define foo bar`. X // Розкривається `X` -> `Y` -> `bar`.

Також за специфікацією препроцесор ніколи не повинен розкривати макрос зсередини самого себе, а залишати вкладений ідентифікатор такий:

#define M < M >M // Розкривається в < M >.
#define A a < B >#define B b < C >#define C c < A >A // a < b< c< A >> > B // b < c< a< B >> > C // c < a < b < C >> >

Умовна компіляція. Перевірка макросу.

Директиви #ifdef, #ifndef, #if, #else, #elif, #endif дозволяють відпрепроцесувати частину файлу, лише за певної умови. Директиви #ifdef, #ifndef перевіряють чи визначений зазначений макрос. Наприклад, вони корисні для різної компіляції:

#ifdef __x86_64__ typedef unsigned long uint64_t; #else typedef unsigned long long uint64_t; #endif

Директива #if дозволяє перевірити довільний арифметичний вираз.

#define TWO 2 #if TWO + TWO == 4 // . #endif

Директива #if препроцесує свій аргумент, а потім парсить те, що вийшло як арифметичний вираз. Якщо після препроцессування в аргументі #if залишаються ідентифікатори, вони замінюються на 0, крім ідентифікатора true , який замінюється на 1.

Одне із застосувань #ifndef – це include guard, які вже обговорювалися раніше.

Константи.

Знадобилася нам, наприклад, $pi $. Традиційно в C це робилося через #define. Але препроцесор, як ми знаємо, має купу проблем. У випадку з константою PI нічого не станеться, навряд чи хтось називатиме змінну так, особливо великими літерами, але все ж таки.

А в C ++ (а пізніше і в C) з'явився const. Але все-таки, навіщо він потрібен, чому не можна просто написати глобальну змінну double PI = 3.141592; ?

  1. По-перше, константи можуть бути оптимізовані компілятором. Якщо ви робите нормальну змінну, компілятор повинен її взяти з пам'яті (або регістрів), адже в іншому файлі хтось міг її змінити. А якщо ви напишете const, то у вас не буде проблем ні з оптимізацією (асемблер буде як при #define), ні з адекватністю повідомлень про помилки.
  2. По-друге, вона несе функцію, що документує, коли ви пишете const з покажчиками. Якщо в заголовку функції написано const char*, то ви точно знаєте, що ви передаєте в неї рядок, який не змінюється, а якщо char*, то, швидше за все, змінюється (тобто функція створена для того, щоб змінювати).
  3. По-третє, маючи const компілятор може взагалі не створювати змінну: якщо ми напишемо return PI * 2 , то там буде повертатися константа, і ніякого множення на етапі виконання.

До речі, як взагалі взаємодіє const із покажчиками? Подивимося такий приклад:

Так не можна, це має фундаментальну проблему: ви можете потім записати *p = 3 і це все порушить. Тому другий рядок не компілюється, і його треба замінити на

Але тут треба на що подивитися. У покажчика у певному сенсі два поняття незмінності. Ми можемо зробити так:

Хто нам заважає зробити так? Так ніхто, нам не можна міняти вміст p, а не його самого. А якщо ви хочете написати, що не можна змінювати саме сам покажчик, то це не const int * / int const *, а int * const. Якщо вам потрібно заборонити обидва варіанти використання, те, що логічно, const int * const або int const * const. Тобто

Тепер на що подивимося:

Що з цього містить помилку? Ну, в третьому, точно помилка, це ми вже обговорили. Також перше та четверте точно коректне. А що з другим? Ну, чи порушує друге чиїсь права? Ну ні. Або як би сказали на парадигмах програмування, ніхто не порушує контракт, ми тільки його розширюємо (додатково обіцяючи незмінність), а отже, все має бути добре. Ну, так і працюють неявні перетворення на C++, ви можете навішувати const скрізь, куди хочете, але не можете його прибирати.

Константними можуть бути складові типи (зокрема, структури). Тоді ця структура просто матиме константні всі поля.

Як називається файл із додатком після компіляції?

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

Для компіляції з попередженнями застосовується прапор-Wall:

Є різні версії стандарту мови Сі, і кожен із них може додавати додатковий функціонал, який ми, можливо, захочемо використовувати у програмі. За допомогою прапора -std= можна вказати конкретний стандарт, додавши c++11 c++14 c++17 або c++20 . Наприклад, для компіляції до стандарту c++17 потрібно написати:

Аналогічно для компіляції стандарт C++20 використовується команда:

Щоб гарантувати, що програма суворо відповідатиме певному стандарту, можна вказати прапор -pedantic

g++ -std=c++11 -Wall -pedantic app.cpp g++ -std=c++14 -Wall -pedantic app.cpp g++ -std=c++17 -Wall -pedantic app.cpp g++ -std=c ++20 -Wall-pedantic app.cpp

У цьому випадку компілятор генеруватиме попередження, якщо код не відповідає правилам стандарту.

Для того, щоб автоматично запустити програму після компіляції, можна використати таку команду:

Можна наліпити в одну команду різні опції:

g++ -std=c++20 -Wall -pedantic app.cpp -o app & app

Параметри компілятора clang

Основні параметри компілятора Clang часом повторюють параметри для gcc. Наприклад, компіляція за допомогою Clang під певний стандарт із виведенням помилок:

clang++ -std=c++20 -Wall -pedantic app.cpp -o app.exe & app.exe

Як називається файл із додатком після компіляції?

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

Для компіляції з попередженнями застосовується прапор-Wall:

Є різні версії стандарту мови Сі, і кожен із них може додавати додатковий функціонал, який ми, можливо, захочемо використовувати у програмі. За допомогою прапора -std= можна вказати конкретний стандарт, додавши c99 c11 або c17 . Наприклад, для компіляції до стандарту c99 потрібно написати:

Аналогічно для компіляції стандарт C11 використовується команда:

Щоб гарантувати, що програма суворо відповідатиме певному стандарту, можна вказати прапор -pedantic

gcc -std=c99 -Wall -pedantic source.c gcc -std=c11 -Wall -pedantic source.c gcc -std=c17 -Wall -pedantic source.c gcc -std=c23 -Wall -pedantic source.c

У цьому випадку компілятор генеруватиме попередження, якщо код не відповідає правилам стандарту.

Для того, щоб автоматично запустити програму після компіляції, можна використати таку команду:

Можна наліпити в одну команду різні опції:

gcc -std=c17 -Wall -pedantic app.c -o app.exe & app

Опція -S дозволяє згенерувати файл із асемблерним кодом:

В даному випадку за вмістом app.c буде згенеровано файл, який називається як і вихідний файл, тільки має розширення .s, тобто в даному випадку буде згенеровано файл app.s. Цей файл міститиме код на асемблері, причому як синтаксис застосовується синтаксис асемблера GAS (ассемблера від GNU).

Параметри компілятора clang

Основні параметри компілятора Clang часом повторюють параметри для gcc. Наприклад, компіляція за допомогою Clang під певний стандарт із виведенням помилок:

clang -std=c17 -Wall -pedantic app.c -o app.exe & app.exe

Схожі статті

  • Яка температура називається температурою кристалізації
  • Як називається почуття коли ти відчуваєш людину
  • Що йде після терабайту пам'яті
  • Як називається монастир у Ярославлі
  • Чому кішки стають спокійнішими після стерилізації
  • Чому сниться так називається
  • Як називається завод із виробництва
  • Як називається пов'язка на голову як у хіпі
  • Недавні статті

  • Чому взуття скрипить при ходьбі
  • Коли день народження у стрічці
  • Чи можна кішці їсти сіль
  • Варіанти планування ділянки 15 соток прямокутної форми
  • Що означає півмісяця знак
  • Рейсмусовий верстат для чого
  • У якому віці парують свиней
  • У чому полягає принцип нарахування та у яких випадках він застосовується