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:

char ch;
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:

char ch; // rezerwuje pamięć dla zmiennej typu char i inicjuje ją wartością 0
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:

double sqrt(double); // deklaracja funkcji
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ć:

double sqrt(double d) { /*...*/ }
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;
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;
extern int error_number; // OK: ponowna deklaracja

W niektórych definicjach bezpośrednio podaje się wartość definiowanego obiektu. Na przykład:

struct Date { int d, m, y; };
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:

void f()
{
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:

char ch;
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: 

const char*kings[] = { "Antigonus", "Seleucus", "Ptolemy" };

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:

const c = 7; // błąd: brak 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:

int x, y; // int x; int y;

Operatory odnoszą się tylko do pojedynczych nazw, tzn. nie dotyczą wszystkich nazw w deklaracji, np.:

int* p, y; // int* p; int y; NIE int* y;
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 

Podobne artykuły

« Nazwy i słowa kluczowe w C++Współbieżność w C++ »

Podziel się ze znajomymi tym artykułem - udostępnij na FB lub wyślij e-maila korzystając z poniższych opcji: