Deklaracje w C++
Każda nazwa (identyfikator) w programie w języku C++ przed użyciem musi zostać zadeklarowana. W deklaracji określa się typ nazwy, aby poinformować kompilator, z jakiego rodzaju obiektem ma do czynienia. Na przykład:
string s;
auto count = 1;
const double pi {3.1415926535897};
extern int error_number;
const char*name = "Njal";
const char*season[] = { "wiosna", "lato", "jesień", "zima" };
vector people { name, "Skarphedin", "Gunnar" };
struct Date { int d, m, y; };
int day(Date*p) { return p−>d; }
double sqrt(double);
template T abs(T a) { return a<0 ? −a : a; }
constexpr int fac(int n) { return (n<2)?1:n*fac(n−1); } // możliwa ewaluacja w czasie kompilacji (2.2.3)
constexpr double zz { ii*fac(7) }; // inicjacja w czasie kompilacji
using Cmplx = std::complex; // alias typu (3.4.5, 6.5)
struct User; // nazwa typu
enum class Beer { Carlsberg, Tuborg, Thor };
namespace NS { int a; }
Jak wynika z powyższych przykładów, w deklaracji można zrobić znacznie więcej, niż tylko związać typ z nazwą. Większość z tych deklaracji to także definicje. Definicja to deklaracja zawierająca wszystko, co jest w programie potrzebne do użycia danego obiektu. Zwłaszcza jeśli do reprezentacji tego czegoś potrzebna jest pamięć, to definicja powoduje jej zarezerwowanie. Czasami deklaracje uważa się za część interfejsu, a definicje za składniki implementacji. Wówczas interfejsy tworzy się z deklaracji, które można powtórzyć w osobnych plikach. Definicje rezerwujące pamięć nie należą do interfejsów. Przyjmując, że powyższe deklaracje znajdują się w zakresie globalnym, mamy:
auto count = 1; // rezerwuje pamięć dla zmiennej typu int zainicjowanej wartością 1
const char*name = "Njal"; // rezerwuje pamięć dla wskaźnika typu char
// rezerwuje pamięć dla literału łańcuchowego "Njal"
// inicjuje wskaźnik adresem tego literału
struct Date { int d, m, y; }; // Date jest strukturą zawierającą trzy składowe
int day(Date*p) { return p−>d; } // day jest funkcją wykonującą określony kod
using Point = std::complex; // Point jest nazwą dla std::complex
Z wszystkich powyższych deklaracji tylko trzy nie są jednocześnie definicjami:
extern int error_number; // deklaracja zmiennej
struct User; // deklaracja nazwy typu
To znaczy, że obiekt, do którego się odnoszą, musi być zdefiniowany gdzie indziej, aby można go było używać:
int error_number = 1;
struct User { /*...*/ };
W programie C++ każda nazwa może być zdefiniowana tylko raz. Deklaracji natomiast może być wiele. Wszystkie deklaracje obiektu muszą mieć ten sam typ. Dlatego w poniższym przykładzie znajdują się dwa błędy:
int count; // błąd: ponowna definicja
extern int error_number;
extern short error_number; // błąd: różne typy
Tu nie ma żadnego błędu (opis extern znajduje się w podrozdziale 15.2):
extern int error_number; // OK: ponowna deklaracja
W niektórych definicjach bezpośrednio podaje się wartość definiowanego obiektu. Na przykład:
using Point = std::complex; // Point jest nazwą dla std::complex
int day(Date*p) { return p−>d; }
const double pi {3.1415926535897};
Dla typów, aliasów, szablonów, funkcji i stałych wartość jest stała. Wartość niestałych typów danych może się zmienić. Na przykład:
{
int count {1}; // inicjacja count wartością 1
const char*name {"Bjarne"}; // name jest zmienną wskazującą stałą (7.5)
count = 2; // przypisanie 2 do count
name = "Marian";
}
Z definicji tylko dwie nie określają wartości:
string s;
Każda deklaracja określająca wartość jest definicją.
Struktura deklaracji
Struktura deklaracji jest zdefiniowana w gramatyce języka C++ (iso.A). W ciągu czterdziestu lat gramatyka ta znacznie ewoluowała w porównaniu z bazową gramatyką języka C i obecnie jest bardzo skomplikowana. Mimo to unikając zbyt dużych uproszczeń, można powiedzieć, że deklaracja składa się z pięciu części (wymienione w kolejności):
-
opcjonalne specyfikatory przedrostkowe (np. static albo virtual);
-
typ bazowy (np. vector albo const int);
-
deklarator opcjonalnie zawierający nazwę (np. p[7], n albo *(*)[]);
-
opcjonalny przyrostkowy specyfikator funkcji (np. const albo noexcept);
-
opcjonalny inicjator funkcji lub treść funkcji (np. ={7,5,3} lub {return x;}).
Z wyjątkiem definicji funkcji i przestrzeni nazw koniec deklaracji oznacza się średnikiem. Spójrz na przykład definicji tablicy łańcuchów w stylu C:
Tutaj typem bazowym jest const char, deklaratorem *kings[], a inicjatorem znak = wraz z listą w klamrze. Specyfikator to pierwsze słowo kluczowe, np. virtual, extern albo constexpr określające jakiś nie dotyczący typu atrybut deklarowanego obiektu. Deklarator składa się z nazwy i opcjonalnych operatorów deklaracyjnych. Najczęściej używane z tych operatorów zostały wymienione w poniższej tabeli:
Operatory deklaracyjne
- przedrostek * wskaźnik
- przedrostek *const stały wskaźnik
- przedrostek *volatile wskaźnik ulotny
- przedrostek & referencja do wartości lewostronnej (7.7.1)
- przedrostek && referencja do wartości prawostronnej (7.7.2)
- przedrostek auto funkcja (używająca przyrostkowego typu zwrotnego)
- przyrostek [] tablica
- przyrostek () funkcja
- przyrostek −> zwraca z funkcji
Ich stosowanie byłoby o wiele łatwiejszy, gdyby wszystkie były przedrostkami lub przyrostkami. Niestety operatory *, [] i () zgodnie z projektem odzwierciedlają sposób użycia ich w wyrażeniach. Dlatego operator * jest przedrostkiem, a operatory [] i () są przyrostkami. Operatory przyrostkowe wiążą silniej od przedrostkowych. W efekcie char*kings[] jest tablicą wskaźników na typ char, podczas gdy char(*kings)[] jest wskaźnikiem do tablicy char. W celu wyrażenia chęci użycia takich typów jak „wskaźnik do tablicy” czy „wskaźnik do funkcji” trzeba używać nawiasów. Zauważ, że w deklaracji nie można opuścić typu:
gt(int a, int b) // błąd: brak typu zwrotnego
{
return (a>b) ? a : b;
}
unsigned ui; // OK: unsigned oznacza unsigned int
long li; // OK: long oznacza long int
Pod tym względem standard C++ różni się od wczesnych wersji języków C i C++, w których dwa pierwsze przykłady również były poprawne, bo brak określenia typu traktowano jako typ int. Jednak ta zasada niejawnego stosowania typu int była źródłem wielu trudnych do wykrycia błędów i nieporozumień. Nazwy niektórych typów składają się z kilku słów, np. long long czy volatile int. Niektóre nawet nie wyglądają jak nazwy, np. decltype(f(x)) (typ zwrotny wywołania f(x).
Deklarowanie po kilka nazw
W jednej deklaracji można zadeklarować kilka nazw naraz. Deklaracja taka jest po prostu listą oddzielanych przecinkami deklaratorów. Na przykład dwie zmienne całkowitoliczbowe można zadeklarować w następujący sposób:
Operatory odnoszą się tylko do pojedynczych nazw, tzn. nie dotyczą wszystkich nazw w deklaracji, np.:
int x, *q; // int x; int* q;
int v[10], *pv; // int v[10]; int* pv;
Obecność tego rodzaju deklaracji z wieloma nazwami w programie sprawia, że kod jest trudniejszy
do zrozumienia, a więc należy unikać ich stosowania.
Zakres dostępności
Deklaracja wprowadza nazwę do określonego zakresu, tzn. zadeklarowanej nazwy można używać tylko w pewnej części tekstu programu.
- Zakres lokalny: nazwa lokalna to nazwa zadeklarowana w funkcji lub lambdzie. Jej zakres obejmuje obszar od miejsca deklaracji do końca bloku, w którym deklaracja ta się znajduje. Blok to fragment kodu, którego granice wyznaczają znaki { i }. Nazwy parametrów funkcji i lambd są nazwami lokalnymi dla najszerszego bloku ich funkcji lub lambdy.
- Zakres klasowy: nazwa składowa (albo nazwa składowa klasy) to nazwa zdefiniowana w klasie nie znajdującej się w jakiejkolwiek funkcji, klasie, klasie wyliczeniowej lub innej przestrzeni nazw. Jej zakres obejmuje obszar od początku do końca deklaracji klasy.
- Zakres przestrzeni nazw: nazwa jest nazwą składową przestrzeni nazw, jeśli jest zdefiniowana w przestrzeni nazw, ale nie w funkcji, lambdzie, klasie, klasie wyliczeniowej lub innej przestrzeni nazw. Jej zakres obejmuje obszar od miejsca deklaracji do końca przestrzeni nazw. Przestrzeń nazw może być też dostępna w innych jednostkach translacji.
- Zakres globalny: nazwa jest globalna, jeśli nie jest zdefiniowana w żadnej funkcji, klasie, klasie wyliczeniowej ani przestrzeni nazw. Jej zakres dostępności obejmuje obszar od miejsca deklaracji do końca pliku. Nazwa globalna może być także dostępna w innych jednostkach translacji. Zasadniczo globalna przestrzeń nazw również jest przestrzenią nazw, a więc nazwa globalna jest także nazwą składową przestrzeni nazw.
- Zakres instrukcji: nazwa znajduje się w zakresie instrukcji, jeśli jest zdefiniowana w nawiasie () instrukcji for-, while-, if- lub switch-. Jej zakres obejmuje obszar od deklaracji do końca instrukcji. Wszystkie nazwy znajdujące się w zakresie instrukcji są także lokalne.
- Zakres funkcji: etykieta znajduje się w zakresie od deklaracji do końca funkcji.
Język C++. Kompendium wiedzy. Wydanie IV, Autor: Bjarne Stroustrup, Wydawnictwo: Helion