Przyjrzyj się temu pozornie niewinnemu przykładowemu programowi:
#include <iostream>
int main()
{
std::cout << "The sum of 3 and 4 is: " << add(3, 4) << '\n';
return 0;
}
int add(int x, int y)
{
return x + y;
}Można by się spodziewać, że ten program wygeneruje wynik:
The sum of 3 and 4 is: 7
Ale w rzeczywistości w ogóle się nie kompiluje! Visual Studio generuje następujący błąd kompilacji:
add.cpp(5) : error C3861: 'add': identifier not found
Powodem, dla którego ten program się nie kompiluje, jest to, że kompilator sekwencyjnie kompiluje zawartość plików kodu. Kiedy kompilator osiąga wywołanie funkcji add() w linii 5 głównego, nie wie, co add jest, ponieważ zdefiniowaliśmy add aż do linii 9! Powoduje to błąd: nie znaleziono identyfikatora.
Starsze wersje programu Visual Studio powodują dodatkowy błąd:
add.cpp(9) : error C2365: 'add'; : redefinition; previous definition was 'formerly unknown identifier'
Jest to nieco mylące, biorąc pod uwagę, że add nie został nigdy zdefiniowany. Mimo to warto ogólnie zauważyć, że pojedynczy błąd często powoduje powstanie wielu zbędnych lub powiązanych błędów lub ostrzeżeń. Czasami może być trudno stwierdzić, czy jakikolwiek błąd lub ostrzeżenie poza pierwszym jest konsekwencją pierwszego problemu, czy też jest to niezależny problem, który należy rozwiązać osobno.
Najlepsza praktyka
W przypadku rozwiązywania błędów kompilacji lub ostrzeżeń w programach rozwiąż pierwszy wymieniony problem, a następnie skompiluj ponownie.
Aby rozwiązać ten problem, musimy uwzględnić fakt, że kompilator nie wie, co to jest add. Istnieją dwa typowe sposoby rozwiązania tego problemu.
Opcja 1: Zmiana kolejności definicji funkcji
Jednym ze sposobów rozwiązania tego problemu jest zmiana kolejności definicji funkcji tak, add jest zdefiniowane przed głównego:
#include <iostream>
int add(int x, int y)
{
return x + y;
}
int main()
{
std::cout << "The sum of 3 and 4 is: " << add(3, 4) << '\n';
return 0;
}W ten sposób do czasu głównego wywołania add kompilator będzie już wiedział, co add jest. Ponieważ jest to tak prosty program, zmiana ta jest stosunkowo łatwa do wykonania. Jednakże w większym programie może być żmudne ustalenie, które funkcje wywołują inne funkcje (iw jakiej kolejności), aby można było je zadeklarować sekwencyjnie.
Ponadto ta opcja nie zawsze jest możliwa. Załóżmy, że piszemy program, który ma dwie funkcje A i B. Jeśli funkcja A wywołuje funkcję B, a funkcja B wywołuje funkcję A, to nie ma możliwości uporządkowania funkcji w sposób, który zadowoli kompilator. Jeśli najpierw zdefiniujesz A , kompilator będzie narzekał, że nie wie, co B to jest. Jeśli zdefiniujesz B najpierw kompilator będzie narzekał, że nie wie co A jest.
Opcja 2: Użyj deklaracji forward
Możemy to również naprawić za pomocą deklaracji forward.
A deklaracja naprzód pozwala nam poinformować kompilator o istnieniu identyfikatora przed faktycznie definiując identyfikator.
W przypadku pozwala nam to poinformować kompilator o istnieniu funkcji, zanim zdefiniujemy jej treść. W ten sposób, gdy kompilator napotka wywołanie funkcji, zrozumie, że wykonujemy wywołanie funkcji i może sprawdzić, czy wywołujemy ją poprawnie, nawet jeśli nie wie jeszcze, jak i gdzie funkcja jest zdefiniowana.
>Aby napisać deklarację forward dla funkcji, używamy deklaracji funkcji (zwanej także prototyp funkcji). Deklaracja funkcji składa się z typu zwracanego przez funkcję, nazwy i typów parametrów, zakończonych średnikiem. Opcjonalnie można dołączyć nazwy parametrów. Treść funkcji nie jest zawarta w deklaracji.
Oto deklaracja funkcji dla add funkcji:
int add(int x, int y); // function declaration includes return type, name, parameters, and semicolon. No function body!Teraz oto nasz oryginalny program, który się nie skompilował, używając deklaracji funkcji jako deklaracji forward dla funkcji add:
#include <iostream>
int add(int x, int y); // forward declaration of add() (using a function declaration)
int main()
{
std::cout << "The sum of 3 and 4 is: " << add(3, 4) << '\n'; // this works because we forward declared add() above
return 0;
}
int add(int x, int y) // even though the body of add() isn't defined until here
{
return x + y;
}Teraz, gdy kompilator osiągnie wywołanie add w main, będzie wiedział, jak add wygląda (funkcja, która przyjmuje dwa parametry całkowite i zwraca liczbę całkowitą) i nie będzie narzekać.
Warto zwrócić uwagę na tę funkcję deklaracje nie muszą podawać nazw parametrów (ponieważ nie są one uważane za część deklaracji funkcji). W powyższym kodzie możesz także zadeklarować swoją funkcję w następujący sposób:
int add(int, int); // valid function declaration Wolimy jednak nazywać nasze parametry (używając tych samych nazw, co rzeczywista funkcja). Pozwala to zrozumieć, jakie są parametry funkcji, po prostu patrząc na deklarację. Na przykład, jeśli zobaczysz deklarację void doSomething(int, int, int), możesz pomyśleć, że pamiętasz, co reprezentuje każdy z parametrów, ale możesz też to zrozumieć błędnie.
Również wiele narzędzi do automatycznego generowania dokumentacji będzie generować dokumentację na podstawie zawartości plików nagłówkowych, w których często umieszczane są deklaracje. Pliki nagłówkowe i deklaracje omawiamy na lekcji 2.11 — Pliki nagłówkowe.
Najlepsza praktyka
Zachowaj nazwy parametrów w deklaracjach funkcji.
Wskazówka
Możesz łatwo tworzyć deklaracje funkcji, kopiując/wklejając nagłówek funkcji i dodając średnik.
Po co deklaracje forward?
Możesz się zastanawiać, dlaczego mielibyśmy używać deklaracji forward, gdybyśmy mogli po prostu zmienić kolejność funkcji w naszych programach work.
Najczęściej deklaracje forward służą do poinformowania kompilatora o istnieniu jakiejś funkcji, która została zdefiniowana w innym pliku kodu. Zmiana kolejności nie jest możliwa w tym scenariuszu, ponieważ osoba wywołująca i osoba wywoływana znajdują się w zupełnie różnych plikach! Omówimy to bardziej szczegółowo na następnej lekcji (2.8 -- Programy z wieloma plikami kodu).
Deklaracja forward może być również używana do definiowania naszych funkcji w sposób niezależny od kolejności. Pozwala nam to definiować funkcje w dowolnej kolejności, która maksymalizuje organizację (np. poprzez grupowanie powiązanych funkcji razem) lub zrozumienie czytelnika.
Rzadziej zdarzają się sytuacje, gdy mamy dwie funkcje, które się wzajemnie wywołują. Zmiana kolejności również w tym przypadku nie jest możliwa, ponieważ nie ma możliwości zmień kolejność funkcji tak, aby każda z nich znajdowała się przed drugą. Deklaracje forward umożliwiają rozwiązanie takich zależności cyklicznych.
Zapominanie o treści funkcji
Nowi programiści często zastanawiają się, co się stanie, jeśli przekażą dalej deklarację funkcji, ale jej nie zdefiniują.
Odpowiedź brzmi: to zależy, jeśli złożona zostanie deklaracja forward, ale funkcja nigdy nie zostanie wywołana, program skompiluje się i zadziała poprawnie, jeśli jednak zostanie wykonana deklaracja forward i funkcja zostanie wywołana, ale program nigdy jej nie zdefiniuje, program skompiluje się poprawnie, ale linker będzie narzekał, że nie może rozwiązać wywołania funkcji.
Rozważ następujący program:
#include <iostream>
int add(int x, int y); // forward declaration of add()
int main()
{
std::cout << "The sum of 3 and 4 is: " << add(3, 4) << '\n';
return 0;
}
// note: No definition for function addW tym programie przekazujemy dalej deklarację add i wywołujemy add, ale nigdy nigdzie nie definiujemy add Gdy próbujemy skompilować ten program, Visual Studio wyświetla następujący komunikat:
Compiling... add.cpp Linking... add.obj : error LNK2001: unresolved external symbol "int __cdecl add(int,int)" (?add@@YAHHH@Z) add.exe : fatal error LNK1120: 1 unresolved externals
Jak możesz widzisz, program skompilował się poprawnie, ale nie powiódł się na etapie łączenia, ponieważ int add(int, int) nie został zdefiniowany.
Inne typy deklaracji forward
Deklaracja forward jest najczęściej używana z funkcjami. Jednak deklaracji forward można używać także z innymi identyfikatorami w C++, takimi jak zmienne i typy. Zmienne i typy mają inną składnię do deklaracji forward, więc omówimy je w przyszłych lekcjach.
Deklaracje a definicje
W C++ często słyszysz słowa „deklaracja” i „definicja”, często zamiennie. Co one oznaczają. Masz teraz wystarczającą wiedzę podstawową, aby zrozumieć różnicę między nimi.
A deklaracja mówi kompilator o istnieniu identyfikatora i powiązanej z nim informacji o typie. Oto kilka przykładów deklaracji:
int add(int x, int y); // tells the compiler about a function named "add" that takes two int parameters and returns an int. No body!
int x; // tells the compiler about an integer variable named xA definicja to deklaracja, która faktycznie implementuje (dla funkcji i typów) lub tworzy instancję (dla zmiennych) identyfikatora.
Oto kilka przykładów definicji:
// because this function has a body, it is an implementation of function add()
int add(int x, int y)
{
int z{ x + y }; // instantiates variable z
return z;
}
int x; // instantiates variable xW C++ wszystkie definicje są deklaracjami. Dlatego int x; jest zarówno definicją, jak i deklaracją.
I odwrotnie, nie wszystkie deklaracje są definicjami. Deklaracje niebędące definicjami nazywane są czystymi deklaracjami. Typy czystych deklaracji obejmują deklaracje forward dotyczące funkcji, zmiennych i typów.
Nomenklatura
W języku potocznym termin „deklaracja” jest zwykle używany w znaczeniu „czystej deklaracji”, a „definicja” oznacza „definicję, która służy również jako deklaracja”. Dlatego zazwyczaj wywołujemy int x; definicję, nawet jeśli jest to zarówno definicja, jak i deklaracja.
Gdy kompilator napotka identyfikator, sprawdzi, czy użycie tego identyfikatora jest prawidłowe (np. czy identyfikator mieści się w zakresie, czy jest używany w sposób poprawny składniowo itp.).
W większości przypadków wystarczy deklaracja, aby umożliwić kompilatorowi upewnienie się, że identyfikator jest używany prawidłowo. Na przykład, gdy kompilator napotka wywołanie funkcji add(5, 6), jeśli widział już deklarację dla add(int, int), może sprawdzić, czy add jest w rzeczywistości funkcją, która przyjmuje dwa int parametry. Nie musi faktycznie widzieć definicji funkcji add (która może istnieć w jakimś innym pliku).
Jednak istnieje kilka przypadków, w których kompilator musi zobaczyć pełną definicję, aby użyć identyfikatora (np. definicje szablonów i definicje typów, które omówimy w przyszłych lekcjach).
Oto tabela podsumowująca:
| Termin | Techniczne Znaczenie | Przykłady |
|---|---|---|
| Deklaracja | Informuje kompilator o identyfikatorze i powiązanych z nim informacjach o typie. | void foo(); // deklaracja funkcji do przodu (bez treści) void goo() {}; // definicja funkcji (ma treść) int x; // definicja zmiennej |
| Definicja | Implementuje funkcję lub tworzy instancję zmiennej. Definicje są także deklaracjami. | void foo() { } // definicja funkcji (ma treść) int x; // definicja zmiennej |
| Czysta deklaracja | Deklaracja niebędąca definicją. | void foo(); // deklaracja funkcji do przodu (bez treści) |
| Inicjalizacja | Udostępnia wartość początkową dla zdefiniowanego obiektu. | int x { 2 }; // x jest inicjalizowane wartością 2 |
Termin „deklaracja” jest powszechnie używany w znaczeniu „czystej deklaracji”, a termin „definicja” używany jest do określenia wszystkiego, co jest zarówno definicją, jak i deklaracją. Używamy tej wspólnej nomenklatury w przykładowych komentarzach do kolumn.
Klasa reguła jednej definicji (lub w skrócie ODR) jest dobrze znaną regułą w C++. ODR składa się z trzech części:
- W obrębie pliku każda funkcja, zmienna, typ lub szablon w danym zakresie może mieć tylko jedną definicję. Definicje występujące w różnych zakresach (np. zmienne lokalne zdefiniowane w różnych funkcjach, lub funkcje zdefiniowane w różnych przestrzeniach nazw) nie naruszają tej zasady.
- W obrębie programu każda funkcja lub zmienna w danym zakresie może mieć tylko jedną definicję. Zasada ta istnieje, ponieważ programy mogą mieć więcej niż jeden plik (omówimy to w następnej lekcji). Funkcje i zmienne niewidoczne dla linkera są wyłączone z tej reguły (omówione w dalszej części lekcji 7.6 – Powiązania wewnętrzne).
- Typy, szablony, funkcje wbudowane i zmienne wbudowane mogą mieć zduplikowane definicje w różnych plikach, pod warunkiem, że każda definicja jest identyczna. Nie omówiliśmy jeszcze większości z tych rzeczy, więc na razie się tym nie martw — wrócimy do tego, gdy będzie to istotne.
Powiązana treść
W kolejnych lekcjach omawiamy zwolnienia z części 3 ODR:
- Typy (13.1 -- Wprowadzenie do typów zdefiniowanych przez program (zdefiniowanych przez użytkownika)).
- Szablony funkcji (11.6 -- Szablony funkcji i 11.7 -- Szablon funkcji instancja).
- Funkcje i zmienne wbudowane (7.9 -- Funkcje wbudowane i zmienne).
Naruszenie części 1 ODR spowoduje, że kompilator wygeneruje błąd redefinicji. Naruszenie części 2 ODR spowoduje, że linker wygeneruje błąd redefinicji. Naruszenie części 3 ODR spowoduje niezdefiniowane zachowanie.
Oto przykład naruszenia części 1:
int add(int x, int y)
{
return x + y;
}
int add(int x, int y) // violation of ODR, we've already defined function add(int, int)
{
return x + y;
}
int main()
{
int x{};
int x{ 5 }; // violation of ODR, we've already defined x
}W tym przykładzie: funkcja add(int, int) jest zdefiniowana dwukrotnie (w zakresie globalnym), a zmienna lokalna int x jest zdefiniowana dwukrotnie (w zakresie main()). Kompilator Visual Studio powoduje zatem następujące błędy kompilacji:
project3.cpp(9): error C2084: function 'int add(int,int)' already has a body project3.cpp(3): note: see previous definition of 'add' project3.cpp(16): error C2086: 'int x': redefinition project3.cpp(15): note: see declaration of 'x'
Jednakże nie jest naruszeniem części 1 ODR, main() posiadanie zmiennej lokalnej zdefiniowanej jako int x i add() posiadanie również zdefiniowanego parametru funkcji jak int x. Definicje te występują w różnych zakresach (w zakresie każdej odpowiedniej funkcji), dlatego uważa się je za oddzielne definicje dwóch odrębnych obiektów, a nie definicję i redefinicję tego samego obiektu.
Dla zaawansowanych czytelników
Funkcje, które mają wspólny identyfikator, ale mają różne zestawy parametrów, są również uważane za różne funkcje, więc takie definicje nie naruszają ODR Omówimy to w dalszej części lekcji 11.1 -- Wprowadzenie do funkcji przeciążenie.
Czas quizu
Pytanie nr 1
Co to jest prototyp funkcji?
Pytanie nr 2
Co to jest deklaracja forward?
Pytanie nr 3
Jak deklarujemy deklarację forward dla funkcji?
Pytanie nr 4
Napisz deklarację funkcji dla tej funkcji (użyj preferowanej formy z nazwami):
int doMath(int first, int second, int third, int fourth)
{
return first + second * third / fourth;
}Pytanie #5
Dla każdego z poniższych programów określ, czy nie udało się ich skompilować, nie udało się połączyć, czy skompilować i połączyć pomyślnie. Jeśli nie jesteś pewien, spróbuj skompilować je!
A)
#include <iostream>
int add(int x, int y);
int main()
{
std::cout << "3 + 4 + 5 = " << add(3, 4, 5) << '\n';
return 0;
}
int add(int x, int y)
{
return x + y;
}B)
#include <iostream>
int add(int x, int y);
int main()
{
std::cout << "3 + 4 + 5 = " << add(3, 4, 5) << '\n';
return 0;
}
int add(int x, int y, int z)
{
return x + y + z;
}C)
#include <iostream>
int add(int x, int y);
int main()
{
std::cout << "3 + 4 = " << add(3, 4) << '\n';
return 0;
}
int add(int x, int y, int z)
{
return x + y + z;
}D)
#include <iostream>
int add(int x, int y, int z);
int main()
{
std::cout << "3 + 4 + 5 = " << add(3, 4, 5) << '\n';
return 0;
}
int add(int z, int y, int x) // names don't match the declaration
{
return x + y + z;
}mi)
#include <iostream>
int add(int, int, int); // no parameter names
int main()
{
std::cout << "3 + 4 + 5 = " << add(3, 4, 5) << '\n';
return 0;
}
int add(int x, int y, int z)
{
return x + y + z;
}f)
#include <iostream>
int add(int x, int y);
int add(int x, int y, int z)
{
return x + y + z;
}
int main()
{
std::cout << "3 + 4 + 5 = " << add(3, 4, 5) << '\n';
return 0;
}
