15.6 -- Statyczne zmienne składowe

W lekcji 7.4 — Wprowadzenie do zmiennych globalnych wprowadziliśmy zmienne globalne, a na lekcji 7.11 -- Statyczne zmienne lokalne wprowadziliśmy statyczne zmienne lokalne. Obydwa typy zmiennych mają statyczny czas trwania, co oznacza, że ​​są tworzone na początku programu i niszczone na końcu programu. Takie zmienne zachowują swoje wartości, nawet jeśli wyjdą poza zakres.

Na przykład:

#include <iostream>

int generateID()
{
    static int s_id{ 0 }; // static local variable
    return ++s_id;
}

int main()
{
    std::cout << generateID() << '\n';
    std::cout << generateID() << '\n';
    std::cout << generateID() << '\n';

    return 0;
}

Ten program wypisuje:

1
2
3

Zauważ, że statyczna zmienna lokalna s_id zachowała swoją wartość podczas wielu wywołań funkcji.

Typy klas przynoszą static słowo kluczowe jeszcze dwa zastosowania: statyczne zmienne składowe i statyczne funkcje składowe. Na szczęście te zastosowania są dość proste. W tej lekcji porozmawiamy o statycznych zmiennych składowych, a o statycznych funkcjach składowych w następnej.

Statyczne zmienne składowe

Zanim przejdziemy do słowa kluczowego static zastosowanego do zmiennych składowych, najpierw rozważmy następującą klasę:

#include <iostream>

struct Something
{
    int value{ 1 };
};

int main()
{
    Something first{};
    Something second{};
    
    first.value = 2;

    std::cout << first.value << '\n';
    std::cout << second.value << '\n';

    return 0;
}

Kiedy tworzymy instancję obiektu klasy, każdy obiekt otrzymuje własną kopię wszystkich normalnych zmiennych składowych. W tym przypadku, ponieważ zadeklarowaliśmy dwa obiekty klasy Something , otrzymujemy dwie kopie value: first.value, I second.value. first.value różne od second.value. W rezultacie powyższy program wypisuje:

2
1

Zmienne składowe klasy mogą być statyczne przy użyciu słowa kluczowego static . W przeciwieństwie do zwykłych zmiennych składowych, statyczne zmienne składowe są wspólne dla wszystkich obiektów klasy. Rozważmy następujący program, podobny do powyższego:

#include <iostream>

struct Something
{
    static int s_value; // declare s_value as static (initializer moved below)
};

int Something::s_value{ 1 }; // define and initialize s_value to 1 (we'll discuss this section below)

int main()
{
    Something first{};
    Something second{};

    first.s_value = 2;

    std::cout << first.s_value << '\n';
    std::cout << second.s_value << '\n';
    return 0;
}

Ten program generuje następujące dane wyjściowe:

2
2

Ponieważ s_value jest statyczną zmienną składową, s_value jest współdzielona pomiędzy wszystkimi obiektami klasy. W konsekwencji first.s_value jest tą samą zmienną co second.s_value. Powyższy program pokazuje, że dostęp do wartości, którą ustawiamy za pomocą first , można uzyskać za pomocą second!

Statyczne elementy członkowskie nie są powiązane z obiektami klasy

Chociaż dostęp do statycznych elementów można uzyskać poprzez obiekty klasy (jak pokazano za pomocą first.s_value i second.s_value w powyższym przykładzie), elementy statyczne istnieją nawet jeśli nie utworzono instancji obiektów klasy! Ma to sens: są tworzone na początku programu i niszczone na końcu programu, więc ich czas życia nie jest powiązany z obiektem klasy jak zwykły członek.

Zasadniczo elementy statyczne są zmiennymi globalnymi, które znajdują się w obszarze zasięgu klasy. Istnieje bardzo mała różnica pomiędzy statycznym elementem klasy a normalną zmienną w przestrzeni nazw.

Kluczowa informacja

Statyczne elementy składowe to zmienne globalne znajdujące się w obszarze zasięgu klasy.

Ponieważ statyczny element s_value istnieje niezależnie od jakichkolwiek obiektów klasy, można uzyskać do niego bezpośredni dostęp za pomocą nazwy klasy i operatora rozpoznawania zakresu (w tym przypadku Something::s_value):

class Something
{
public:
    static int s_value; // declare s_value as static
};

int Something::s_value{ 1 }; // define and initialize s_value to 1 (we'll discuss this section below)

int main()
{
    // note: we're not instantiating any objects of type Something

    Something::s_value = 2;
    std::cout << Something::s_value << '\n';
    return 0;
}

W powyższym przykładzie fragment kodu, s_value odwołuje się za pomocą nazwy klasy Something a nie poprzez obiekt. Należy zauważyć, że nie utworzyliśmy nawet instancji obiektu typu Something, ale nadal możemy uzyskiwać dostęp i używać Something::s_value. Jest to preferowana metoda uzyskiwania dostępu do elementów statycznych.

Najlepsza praktyka

Uzyskuj dostęp do elementów statycznych za pomocą nazwy klasy i operatora rozpoznawania zakresu. (::).

Definiowanie i inicjowanie statycznych zmiennych składowych

Kiedy deklarujemy statyczną zmienną składową wewnątrz typu klasy, mówimy kompilatorowi o istnieniu statycznej zmiennej składowej, ale tak naprawdę jej nie definiujemy (podobnie jak deklaracja forward). zakres.

W powyższym przykładzie robimy to za pomocą tej linii:

int Something::s_value{ 1 }; // define and initialize s_value to 1

Ta linia służy dwóm celom: tworzy instancję statycznej zmiennej składowej (podobnie jak zmienna globalna) i inicjuje ją. W tym przypadku podajemy wartość inicjującą 1. Jeśli nie podano inicjatora, statyczne zmienne składowe są domyślnie inicjowane zerem.

Zauważ, że ta statyczna definicja elementu członkowskiego nie podlega kontroli dostępu: możesz zdefiniować i zainicjować wartość, nawet jeśli jest zadeklarowana w klasie jako prywatna (lub chroniona) (ponieważ definicje nie są uważane za formę dostępu).

W przypadku klas innych niż szablon, jeśli klasa jest zdefiniowana w pliku nagłówkowym (.h), definicja statycznego elementu członkowskiego jest zwykle umieszczana w powiązanym pliku kodu dla klasy (np. Something.cpp). Alternatywnie element można również zdefiniować jako inline i umieścić poniżej definicji klasy w nagłówku (jest to przydatne w przypadku bibliotek zawierających tylko nagłówek). Jeśli klasa jest zdefiniowana w pliku źródłowym (.cpp), definicja statycznego elementu członkowskiego jest zwykle umieszczana bezpośrednio pod klasą. Nie umieszczaj definicji elementu statycznego w pliku nagłówkowym (podobnie jak zmienna globalna, jeśli ten plik nagłówkowy zostanie dołączony więcej niż raz, otrzymasz wiele definicji, co spowoduje błąd linkera).

W przypadku klas szablonowych (szablonowy) definicja statycznego elementu członkowskiego jest zwykle umieszczana bezpośrednio pod definicją klasy szablonu w pliku nagłówkowym (nie narusza to ODR, ponieważ takie definicje są domyślnie wbudowane).

Inicjalizacja statycznych zmiennych składowych wewnątrz definicji klasy

Istnieje kilka skrótów do powyższego. Po pierwsze, gdy element statyczny jest stałym typem całkowitym (który zawiera char i bool) lub const enum, element statyczny można zainicjować w definicji klasy:

class Whatever
{
public:
    static const int s_value{ 4 }; // a static const int can be defined and initialized directly
};

W powyższym przykładzie, ponieważ zmienna składowa statyczna jest stałą int, nie jest wymagana żadna jawna linia definicji. Ten skrót jest dozwolony, ponieważ te konkretne typy const są stałymi czasu kompilacji.

W lekcji 7.10 — Współdzielenie stałych globalnych w wielu plikach (przy użyciu zmiennych wbudowanych), wprowadziliśmy zmienne wbudowane, które mogą mieć wiele definicji. C++17 pozwala statycznym członom być zmiennymi wbudowanymi:

class Whatever
{
public:
    static inline int s_value{ 4 }; // a static inline variable can be defined and initialized directly
};

Takie zmienne można inicjalizować wewnątrz definicji klasy niezależnie od tego, czy są stałe, czy nie. Jest to preferowana metoda definiowania i inicjowania składowych statycznych.

Ponieważ constexpr Elementy są domyślnie wbudowane (od C++17), statyczne constexpr elementy można także inicjować wewnątrz definicji klasy bez jawnego użycia inline słowem kluczowym:

#include <string_view>

class Whatever
{
public:
    static constexpr double s_value{ 2.2 }; // ok
    static constexpr std::string_view s_view{ "Hello" }; // this even works for classes that support constexpr initialization
};

Najlepsza praktyka

Utwórz swoje statyczne elementy inline lub constexpr , aby można je było inicjalizować w definicji klasy.

Przykład statyczne zmienne składowe

Po co używać zmiennych statycznych wewnątrz klas? Jednym z zastosowań jest przypisanie unikalnego identyfikatora do każdej instancji klasy. Oto przykład:

#include <iostream>

class Something
{
private:
    static inline int s_idGenerator { 1 };
    int m_id {};

public:
    // grab the next value from the id generator
    Something() : m_id { s_idGenerator++ } 
    {    
    }

    int getID() const { return m_id; }
};

int main()
{
    Something first{};
    Something second{};
    Something third{};

    std::cout << first.getID() << '\n';
    std::cout << second.getID() << '\n';
    std::cout << third.getID() << '\n';
    return 0;
}

Ten program wypisuje:

1
2
3

Ponieważ s_idGenerator jest wspólny dla wszystkich Something obiektów, kiedy tworzony jest nowy Something obiekt, konstruktor inicjuje m_id bieżącą wartością s_idGenerator a następnie zwiększa wartość dla następnego obiektu. Gwarantuje to, że każdy utworzony Something obiekt otrzyma unikalny identyfikator (zwiększany w kolejności tworzenia).

Nadanie każdemu obiektowi unikalnego identyfikatora może być pomocne podczas debugowania, ponieważ można go wykorzystać do rozróżnienia obiektów, które w przeciwnym razie miałyby identyczne dane. Jest to szczególnie prawdziwe podczas pracy z tablicami danych.

Statyczne zmienne składowe są również przydatne, gdy klasa musi skorzystać z tabeli przeglądowej (np. tablicy używanej do przechowywania zestawu wstępnie obliczonych wartości). Dzięki uczynieniu tabeli przeglądowej statyczną istnieje tylko jedna kopia dla wszystkich obiektów, zamiast tworzyć kopię dla każdej utworzonej instancji obiektu. Może to zaoszczędzić znaczną ilość pamięci.

Tylko statyczne elementy członkowskie mogą używać dedukcji typu (auto i CTAD)

Statyczny element składowy może używać auto do dedukcji swojego typu na podstawie inicjatora, lub klasowego odliczania argumentów szablonu (CTAD) do dedukowania argumentów typu szablonu z inicjatora.

Niestatyczne elementy członkowskie mogą nie używaj auto ani CTAD.

Powody dokonania tego rozróżnienia są dość skomplikowane, ale sprowadzają się do tego, że w przypadku elementów niestatycznych mogą wystąpić pewne przypadki, które prowadzą do niejednoznaczności lub nieintuicyjnych wyników. Nie dotyczy to elementów statycznych. Zatem elementy niestatyczne mają ograniczone możliwości korzystania z tych funkcji, podczas gdy elementy statyczne nie.

#include <utility> // for std::pair<T, U>

class Foo
{
private:
    auto m_x { 5 };           // auto not allowed for non-static members
    std::pair m_v { 1, 2.3 }; // CTAD not allowed for non-static members

    static inline auto s_x { 5 };           // auto allowed for static members
    static inline std::pair s_v { 1, 2.3 }; // CTAD allowed for static members

public:
    Foo() {};
};

int main()
{
    Foo foo{};
    
    return 0;
}
guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
315 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze