Co to są deklaracje forward w C ++?

215

W: http://www.learncpp.com/cpp-tutorial/19-header-files/

Wymieniono następujące:

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

Użyliśmy deklaracji przekazywania, aby kompilator wiedział, co „ add” jest podczas kompilacji main.cpp. Jak wspomniano wcześniej, pisanie deklaracji przekazywania dla każdej funkcji, która ma zostać użyta w innym pliku, może szybko stać się nudne.

Czy możesz wyjaśnić „ deklarację przekazania ”? Jaki jest problem, jeśli użyjemy go w main()funkcji?

Prostota
źródło
1
„Deklaracja przekazująca” naprawdę jest tylko deklaracją. Zobacz (na końcu) tę odpowiedź: stackoverflow.com/questions/1410563/…
sbi

Odpowiedzi:

381

Dlaczego deklaracja forward jest konieczna w C ++

Kompilator chce się upewnić, że nie popełniłeś błędów ortograficznych ani nie przekazałeś niewłaściwej liczby argumentów do funkcji. Nalega więc, aby przed użyciem zobaczyła deklarację „dodaj” (lub dowolnego innego typu, klasy lub funkcji).

To naprawdę pozwala kompilatorowi lepiej sprawdzać poprawność kodu i porządkować luźne końce, dzięki czemu może tworzyć ładnie wyglądający plik obiektowy. Jeśli nie musisz przekazywać dalej deklaracji, kompilator wygeneruje plik obiektowy, który będzie musiał zawierać informacje o wszystkich możliwych zgadnięciach dotyczących tego, czym może być funkcja „dodaj”. A linker musiałby zawierać bardzo sprytną logikę, aby spróbować ustalić, który „add” rzeczywiście chcesz wywołać, gdy funkcja „add” może znajdować się w innym pliku obiektowym, linker łączy się z tym, który używa add do produkcji dll lub exe. Możliwe, że linker otrzyma niewłaściwe dodanie. Powiedzmy, że chciałeś użyć int add (int a, float b), ale przypadkowo zapomniałeś go napisać, ale linker znalazł już istniejący int add (int a, int b) i pomyślałem, że to było właściwe i użyłem tego zamiast tego. Twój kod się skompiluje, ale nie będzie działał zgodnie z oczekiwaniami.

Tak więc, aby zachować jasność rzeczy i uniknąć zgadywania itp., Kompilator nalega, aby zadeklarować wszystko przed użyciem.

Różnica między deklaracją a definicją

Nawiasem mówiąc, ważne jest, aby znać różnicę między deklaracją a definicją. Deklaracja po prostu daje wystarczającą ilość kodu, aby pokazać, jak coś wygląda, więc dla funkcji jest to typ zwracany, konwencja wywoływania, nazwa metody, argumenty i ich typy. Ale kod metody nie jest wymagany. Do definicji potrzebna jest deklaracja, a także kod funkcji.

W jaki sposób deklaracje forward mogą znacznie skrócić czas kompilacji

Możesz pobrać deklarację funkcji do bieżącego pliku .cpp lub .h, # włączając nagłówek, który już zawiera deklarację funkcji. Może to jednak spowolnić kompilację, zwłaszcza jeśli #włączasz nagłówek do .h zamiast .cpp twojego programu, ponieważ wszystko co #include .h piszesz kończy się # obejmowaniem wszystkich nagłówków też napisałeś #include. Nagle kompilator ma # zawarte strony i strony kodu, które musi skompilować, nawet jeśli chcesz użyć tylko jednej lub dwóch funkcji. Aby tego uniknąć, możesz użyć deklaracji przesyłania dalej i po prostu sam wpisać deklarację funkcji u góry pliku. Jeśli używasz tylko kilku funkcji, może to naprawdę przyspieszyć twoją kompilację w porównaniu do zawsze #włączenie nagłówka. W przypadku naprawdę dużych projektów

Przerywaj odniesienia cykliczne, w których obie definicje wykorzystują się nawzajem

Ponadto deklaracje forward mogą pomóc w przerwaniu cykli. To tutaj dwie funkcje próbują się nawzajem używać. Kiedy tak się dzieje (i jest to całkowicie słuszna czynność), możesz # dołączyć jeden plik nagłówka, ale ten plik nagłówka próbuje # dołączyć plik nagłówka, który obecnie piszesz .... który następnie # zawiera drugi nagłówek , który # obejmuje ten, który piszesz. Utknąłeś w sytuacji z kurczakiem i jajkiem, a każdy plik nagłówkowy próbuje ponownie #include drugi. Aby rozwiązać ten problem, możesz zadeklarować w przód potrzebne części w jednym z plików i pozostawić #include poza tym plikiem.

Na przykład:

Plik Car.h

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

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

File Wheel.h

Hmm ... tutaj wymagana jest deklaracja Car, ponieważ Wheel ma wskaźnik do Car, ale Car.h nie może zostać tutaj uwzględniony, ponieważ spowodowałoby to błąd kompilatora. Gdyby uwzględniono Car.h, wówczas spróbowałby uwzględnić Wheel.h, który obejmowałby Car.h, który obejmowałby Wheel.h, i to trwałoby wiecznie, więc zamiast tego kompilator zgłasza błąd. Rozwiązaniem jest zamiast tego zadeklarować samochód zamiast:

class Car;     // forward declaration

class Wheel
{
    Car* car;
};

Jeśli klasa Wheel ma metody, które muszą wywoływać metody car, metody te można by zdefiniować w Wheel.cpp, a Wheel.cpp może teraz obejmować Car.h bez powodowania cyklu.

Scott Langham
źródło
4
Deklaracja przekazywania jest również konieczna, gdy funkcja jest przyjazna dla dwóch lub wielu klas
Barun,
1
Hej Scott, jeśli chodzi o czas kompilacji: czy powiedziałbyś, że powszechną / najlepszą praktyką jest zawsze przesyłanie dalej deklaracji i dołączanie nagłówków w razie potrzeby do pliku .cpp? Po przeczytaniu twojej odpowiedzi wydaje się, że tak powinno być, ale zastanawiam się, czy są jakieś zastrzeżenia?
Zepee
5
@Zepee To równowaga. W przypadku szybkich kompilacji powiedziałbym, że to dobra praktyka i polecam wypróbowanie jej. Może to jednak wymagać trochę wysiłku i dodatkowych wierszy kodu, które mogą wymagać konserwacji i aktualizacji, jeśli nazwy typów itp. Są nadal zmieniane (chociaż narzędzia stają się coraz lepsze w automatycznym zmienianiu nazw rzeczy). Więc jest kompromis. Widziałem podstawy kodu, w których nikt nie przeszkadza. Jeśli zauważysz, że powtarzasz te same definicje przesyłania dalej, zawsze możesz umieścić je w osobnym pliku nagłówkowym i dołączyć taki, coś w rodzaju: stackoverflow.com/questions/4300696/what-is-the-iosfwd-header
Scott Langham
deklaracje przekazywania są wymagane, gdy pliki nagłówkowe odnoszą się do siebie: np. stackoverflow.com/questions/396084/...
Nicholas Hamilton
1
Widzę, że inni deweloperzy w moim zespole byli naprawdę złymi obywatelami bazy kodowej. Jeśli nie potrzebujesz komentarza z deklaracją przekazywania, na przykład // From Car.hmożesz stworzyć pewne owłosione sytuacje, próbując znaleźć definicję w dalszej części drogi, gwarantowane.
Dagrooms
25

Kompilator szuka każdego symbolu używanego w bieżącej jednostce tłumaczenia, który został wcześniej zadeklarowany lub nie w bieżącej jednostce. Jest to tylko kwestia stylu - wszystkie sygnatury metod należy podać na początku pliku źródłowego, a definicje podano później. Znaczącym zastosowaniem jest użycie wskaźnika do klasy jako zmiennej składowej innej klasy.

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

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

Dlatego w miarę możliwości używaj deklaracji forward w klasach. Jeśli twój program ma tylko funkcje (z plikami nagłówkowymi ho), to zapewnienie prototypów na początku jest tylko kwestią stylu. Tak by się stało, gdyby plik nagłówkowy był obecny w normalnym programie z nagłówkiem, który ma tylko funkcje.

Mahesh
źródło
12

Ponieważ C ++ jest analizowany z góry na dół, kompilator musi wiedzieć o rzeczach przed ich użyciem. Więc kiedy odwołujesz się:

int add( int x, int y )

w głównej funkcji kompilator musi wiedzieć, że istnieje. Aby to udowodnić, spróbuj przenieść go poniżej głównej funkcji, a pojawi się błąd kompilatora.

Zatem „ Deklaracja przekazująca ” jest dokładnie tym, co jest napisane na puszce. Deklaruje coś przed użyciem.

Zasadniczo należy zawrzeć deklaracje przekazywania w pliku nagłówkowym, a następnie dołączyć ten plik nagłówkowy w taki sam sposób, w jaki dołączony jest iostream .

Nacięcie
źródło
12

Termin „ deklaracja przekazania ” w C ++ jest najczęściej używany tylko w przypadku deklaracji klas . Zobacz (na końcu) tej odpowiedzi, dlaczego „deklaracja przekazująca” klasy jest tak naprawdę prostą deklaracją klasy o fantazyjnej nazwie.

Innymi słowy, „przekazanie” tylko dodaje balast do terminu, ponieważ każda deklaracja może być postrzegana jako przekazana, o ile deklaruje jakiś identyfikator przed użyciem.

(Co to jest deklaracja w przeciwieństwie do definicji , zobacz ponownie Jaka jest różnica między definicją a deklaracją? )

sbi
źródło
2

Gdy kompilator zobaczy add(3, 4), musi wiedzieć, co to znaczy. Deklaracja przekazująca w zasadzie mówi kompilatorowi, że addjest funkcją, która pobiera dwie liczby całkowite i zwraca liczbę całkowitą. Jest to ważna informacja dla kompilatora, ponieważ musi umieścić 4 i 5 we właściwej reprezentacji na stosie i musi wiedzieć, jakiego typu jest rzecz zwracana przez add.

W tym czasie kompilator nie martwi się o faktyczną implementację add, tj. Gdzie jest (lub jeśli jest nawet jedna) i czy się kompiluje. Pojawia się to później, po skompilowaniu plików źródłowych po wywołaniu konsolidatora.

René Nyffenegger
źródło
1
int add(int x, int y); // forward declaration using function prototype

Czy możesz bardziej szczegółowo wyjaśnić „deklarację przekazania”? Jaki jest problem, jeśli użyjemy go w funkcji main ()?

To jest tak jak #include"add.h". Jeśli wiesz, preprocesor rozszerza plik, o którym wspominasz #include, w pliku .cpp, w którym piszesz #includedyrektywę. Oznacza to, że jeśli napiszesz #include"add.h", otrzymasz to samo, to tak, jakbyś robił „deklarację przekazania”.

Zakładam, że add.hma tę linię:

int add(int x, int y); 
Nawaz
źródło
1

jedno szybkie uzupełnienie dotyczące: zwykle umieszczasz te referencje w pliku nagłówkowym należącym do pliku .c (pp), w którym implementowana jest funkcja / zmienna itp. w twoim przykładzie wyglądałoby to tak: add.h:

extern int add (int a, int b);

słowo kluczowe extern stwierdza, że ​​funkcja jest faktycznie zadeklarowana w zewnętrznym pliku (może to być również biblioteka itp.). Twój plik main.c wyglądałby tak:

#zawierać 
#include „add.h”

int main ()
{
.
.
.

Jacek
źródło
Ale czy nie umieszczamy deklaracji tylko w pliku nagłówkowym? Myślę, że właśnie dlatego funkcja jest zdefiniowana w „add.cpp”, a zatem używa deklaracji forward? Dzięki.
Prostota
0

Jednym z problemów jest to, że kompilator nie wie, jaką wartość dostarcza twoja funkcja; zakłada, że intw tym przypadku funkcja zwraca an , ale może być tak poprawna, jak może być błędna. Innym problemem jest to, że kompilator nie wie, jakich argumentów oczekuje twoja funkcja i nie może cię ostrzec, jeśli przekazujesz wartości niewłaściwego rodzaju. Istnieją specjalne reguły „promocji”, które obowiązują przy przekazywaniu, powiedzmy wartości zmiennoprzecinkowe do niezadeklarowanej funkcji (kompilator musi je rozszerzyć, aby wpisać podwójne), co często nie jest tym, czego funkcja faktycznie oczekuje, co prowadzi do trudnych do znalezienia błędów W czasie wykonywania.

Sztylet
źródło