Що таке попередні декларації в C ++?


215

За адресою : http://www.learncpp.com/cpp-tutorial/19-header-files/

Згадується наступне:

add.cpp:

int add(int x, int y)
{
    return x + y;
}

main.cpp:

#include <iostream>

int add(int x, int y); // forward declaration using function prototype

int main()
{
    using namespace std;
    cout << "The sum of 3 and 4 is " << add(3, 4) << endl;
    return 0;
}

Ми використовували пряму декларацію, щоб компілятор знав, що таке " add" під час компіляції main.cpp. Як уже згадувалося раніше, написання переадресаційних декларацій для кожної функції, яку ви хочете використовувати в іншому файлі, може швидко набриднути.

Чи можете ви пояснити далі "декларацію "? У чому проблема, якщо ми використовуємо її у main()функції?


1
"Попередня декларація" насправді є лише декларацією. Дивіться (в кінці) цю відповідь: stackoverflow.com/questions/1410563/…
sbi

Відповіді:


381

Чому вперед потрібно оголосити в C ++

Компілятор хоче переконатися, що ви не допустили орфографічних помилок або передали неправильну кількість аргументів функції. Отже, він наполягає на тому, що він спочатку бачить декларацію "додати" (або будь-які інші типи, класи або функції) перед її використанням.

Це дійсно просто дозволяє компілятору виконати кращу роботу з перевірки коду і дозволяє йому виправити вільні кінці, щоб він міг створити акуратний об’єктний файл. Якщо вам не потрібно було пересилати декларування речей, компілятор видав би об'єктний файл, який повинен містити інформацію про всі можливі здогадки про те, якою може бути функція "додати". І для того, щоб функція "add" могла жити в іншому об'єктному файлі, він може містити дуже розумну логіку, щоб спробувати розробити "додавання", коли функція "add" може жити в іншому об'єктному файлі. dll або exe. Можливо, що лінкер може отримати неправильне додавання. Скажіть, ви хотіли використовувати int add (int a, float b), але випадково забули його написати, але лінкер знайшов уже існуючий додаток int (int a, int b) і вважав, що це правильно, і використовував це замість цього. Ваш код збирається, але не буде робити те, що ви очікували.

Таким чином, лише для того, щоб все було явним і уникати здогадок тощо, компілятор наполягає на тому, щоб ви оголосили все до того, як воно буде використано.

Різниця між декларацією та визначенням

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

Як вперед-декларації можуть значно скоротити терміни складання

Ви можете отримати декларацію функції у свій поточний .cpp або .h файл, # включивши заголовок, який вже містить оголошення функції. Однак це може уповільнити компіляцію, особливо якщо ви #include заголовок у .h замість .cpp вашої програми, оскільки все, що # включає в себе .h, що ви пишете, закінчилося б # включаючи всі заголовки. Ви теж написали #includes. Раптом у компілятора є # включені сторінки та сторінки коду, які йому потрібно зібрати, навіть коли ви хотіли використовувати лише одну або дві функції. Щоб цього уникнути, ви можете використовувати переадресацію та просто ввести декларацію функції самостійно у верхній частині файлу. Якщо ви використовуєте лише декілька функцій, це дійсно може зробити ваші компіляції швидшими порівняно з завжди # включенням заголовка. Для дійсно великих проектів,

Розбийте циклічні посилання, де два визначення обидва використовують один одного

Крім того, вперед-декларації можуть допомогти вам порушити цикли. Тут обидві функції обидва намагаються використовувати одна одну. Коли це станеться (і це цілком дійсна річ), ви можете # включити один заголовочний файл, але цей заголовок намагається #including файл заголовка, який ви зараз пишете .... що потім #includes інший заголовок , яка # включає ту, яку ви пишете. Ви застрягли в ситуації з куркою і яйцями, коли кожен файл заголовка намагається повторно включити інший. Щоб вирішити це, ви можете переслати декларувати потрібні вам частини в одному з файлів і залишити #include з цього файлу.

Наприклад:

Файл Car.h

#include "Wheel.h"  // Include Wheel's definition so it can be used in Car.
#include <vector>

class Car
{
    std::vector<Wheel> wheels;
};

Файл Wheel.h

Хм ... тут потрібна декларація про автомобіль, оскільки Wheel має вказівник на Автомобіль, але Car.h сюди не може бути включений, оскільки це призведе до помилки компілятора. Якщо Car.h був включений, то він би спробував включити Wheel.h, який би включав Car.h, який включав би Wheel.h, і це продовжуватиметься назавжди, тому натомість компілятор викликає помилку. Рішення полягає в тому, щоб переадресувати заявку Авто замість цього:

class Car;     // forward declaration

class Wheel
{
    Car* car;
};

Якщо клас Wheel мав методи, які потрібно викликати автомобільними методами, ці методи можна було б визначити в Wheel.cpp, а Wheel.cpp тепер може включати Car.h, не викликаючи циклу.


4
попереднє оголошення також необхідне, коли функція є дружньою для двох або багатьох класів
Barun

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

5
@Zepee Це баланс. Для швидкого побудови я б сказав, що це хороша практика, і рекомендую спробувати. Однак це може зайняти певні зусилля та додаткові рядки коду, які, можливо, потрібно буде підтримувати та оновлювати, якщо назви типів і т. Д. Все ще змінюються (хоча інструменти покращуються в автоматичному перейменуванні речей). Тож відбувається компроміс. Я бачив кодові бази, де ніхто не турбує. Якщо ви повторюєте те саме визначення вперед, ви завжди можете помістити їх в окремий файл заголовка і включити його, наприклад: stackoverflow.com/questions/4300696/what-is-the-iosfwd-header
Скотт Ленґем

попередні декларації потрібні, коли файли заголовків посилаються один на одного: тобто stackoverflow.com/questions/396084/…
Ніколас Гамільтон

1
Я бачу, що це дозволяє іншим дияволам у моїй команді бути справді поганими громадянами кодової бази. Якщо вам не потрібен коментар із заявою вперед, наприклад, // From Car.hтоді ви можете створити деякі волохаті ситуації, намагаючись знайти визначення вниз, гарантовано.
Dagrooms

25

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

//foo.h
class bar;    // This is useful
class foo
{
    bar* obj; // Pointer or even a reference.
};

// foo.cpp
#include "bar.h"
#include "foo.h"

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


12

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

int add( int x, int y )

в основній функції компілятору необхідно знати, що вона існує. Щоб довести це, спробуйте перемістити його нижче основної функції, і ви отримаєте помилку компілятора.

Отже, " Декларація вперед " - це саме те, що написано на бляшанці. Він декларує щось перед його використанням.

Як правило, ви включаєте прямі декларації у файл заголовка, а потім включаєте цей заголовочний файл таким же чином, як і iostream .


12

Термін " пряма декларація " в C ++ здебільшого використовується лише для декларацій класу . Дивіться (в кінці) цю відповідь, чому "попереднє оголошення" класу насправді є просто простим оголошенням класу з фантазійною назвою.

Іншими словами, "вперед" просто додає баласт до терміна, оскільки будь-яка заява може розглядатися як передана в тій мірі, в якій вона оголошує якийсь ідентифікатор, перш ніж вона буде використана.

(Щодо декларації на відміну від визначення , ще раз перегляньте, в чому різниця між визначенням і декларацією? )


2

Коли компілятор бачить, add(3, 4)він повинен знати, що це означає. За допомогою прямого оголошення ви в основному повідомляєте компілятору, що addце функція, яка займає два ints і повертає int. Це важлива інформація для компілятора, оскільки йому потрібно поставити 4 та 5 у правильному поданні на стек, і потрібно знати, для якого типу потрібна річ, повернута додаванням.

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


1
int add(int x, int y); // forward declaration using function prototype

Чи можете ви пояснити далі "декларацію"? У чому проблема, якщо ми використовуємо її в функції main ()?

Це те саме, що #include"add.h". Якщо ви знаєте, препроцесор розширює файл, про який ви згадуєте #include, у файл .cpp, де ви пишете #includeдирективу. Це означає, що якщо ви пишете #include"add.h", ви отримуєте те саме, це як ніби ви робите "вперед заяву".

Я припускаю, що add.hмає такий рядок:

int add(int x, int y); 

1

одне швидке доповнення щодо: зазвичай ви розміщуєте ці передні посилання у файл заголовка, що належить до файлу .c (pp), де реалізована функція / змінна тощо. у вашому прикладі це виглядатиме так: add.h:

extern int add (int a, int b);

ключове слово extern визначає, що функція фактично оголошена у зовнішньому файлі (також може бути бібліотека тощо). ваш main.c виглядатиме так:

#включати 
#include "add.h"

int main ()
{
.
.
.


Але хіба ми не поміщаємо лише декларації у файл заголовка? Я думаю, що саме тому функція визначена в "add.cpp", і, таким чином, використовуючи прямі декларації? Дякую.
Простота

0

Одна проблема полягає в тому, що компілятор не знає, яке значення надає ваша функція; припускає, що функція повертає a intв цьому випадку, але це може бути так само правильно, як це може бути неправильно. Інша проблема полягає в тому, що компілятор не знає, яких аргументів очікує ваша функція, і не може попередити вас, якщо ви передаєте значення неправильного виду. Існують спеціальні правила "просування", які застосовуються при передачі, скажімо, значень з плаваючою комою на незадекларовану функцію (компілятор повинен розширити їх, щоб ввести подвійний), що часто не є тим, що функція насправді очікує, що призводить до важкого пошуку помилок під час виконання.

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