Ile i jakie są zastosowania „const” w C ++?

129

Jako początkujący programista C ++ istnieją pewne konstrukcje, które nadal wydają mi się bardzo niejasne, jednym z nich jest const. Możesz go używać w tak wielu miejscach i przy tak wielu różnych efektach, że dla początkującego prawie niemożliwe jest wyjść z życiem. Czy jakiś guru C ++ wyjaśni raz na zawsze różne zastosowania i czy i / lub dlaczego ich nie używać?

tunnuz
źródło
dokładnie szukam tego pytania: D
alamin

Odpowiedzi:

100

Próbuję zebrać kilka zastosowań:

Powiązanie niektórych tymczasowych z odniesieniem do const, aby wydłużyć jego żywotność. Odniesienie może być bazą - a jego destruktor nie musi być wirtualny - właściwy destruktor jest nadal nazywany:

ScopeGuard const& guard = MakeGuard(&cleanUpFunction);

Wyjaśnienie za pomocą kodu:

struct ScopeGuard { 
    ~ScopeGuard() { } // not virtual
};

template<typename T> struct Derived : ScopeGuard { 
    T t; 
    Derived(T t):t(t) { }
    ~Derived() {
        t(); // call function
    }
};

template<typename T> Derived<T> MakeGuard(T t) { return Derived<T>(t); }

Ta sztuczka jest używana w klasie narzędziowej ScopeGuard Alexandrescu. Gdy obiekt tymczasowy znajdzie się poza zakresem, Destruktor klasy Derived jest wywoływany poprawnie. W powyższym kodzie brakuje kilku drobnych szczegółów, ale to jest wielka sprawa.


Użyj const, aby powiedzieć innym metodom, że nie zmienią stanu logicznego tego obiektu.

struct SmartPtr {
    int getCopies() const { return mCopiesMade; }
};

Użyj const dla klas kopiowania przy zapisie , aby kompilator pomógł ci zdecydować, kiedy nie chcesz kopiować.

struct MyString {
    char * getData() { /* copy: caller might write */ return mData; }
    char const* getData() const { return mData; }
};

Objaśnienie : Możesz chcieć udostępniać dane podczas kopiowania, o ile dane obiektu oryginalnego i kopiowanego pozostają takie same. Gdy jeden z obiektów zmieni dane, potrzebujesz teraz dwóch wersji: jednej dla oryginału i jednej dla kopii. Oznacza to, że kopiowanie na zapis do obu obiektów, tak, że teraz obie mają własną wersję.

Używając kodu :

int main() {
    string const a = "1234";
    string const b = a;
    // outputs the same address for COW strings
    cout << (void*)&a[0] << ", " << (void*)&b[0];
}

Powyższy fragment kodu wyświetla ten sam adres na moim GCC, ponieważ używana biblioteka C ++ implementuje kopiowanie przy zapisie std::string. Oba łańcuchy, mimo że są odrębnymi obiektami, współużytkują tę samą pamięć dla swoich danych. Utworzenie wartości binnej niż const będzie preferować wersję operator[]inną niż const, a GCC utworzy kopię zapasowego bufora pamięci, ponieważ możemy go zmienić i nie może to wpływać na dane a!

int main() {
    string const a = "1234";
    string b = a;
    // outputs different addresses!
    cout << (void*)&a[0] << ", " << (void*)&b[0];
}

Aby konstruktor kopiujący mógł wykonać kopie z obiektów stałych i tymczasowych :

struct MyClass {
    MyClass(MyClass const& that) { /* make copy of that */ }
};

Do tworzenia stałych, których trywialnie nie można zmienić

double const PI = 3.1415;

Do przekazywania dowolnych obiektów przez odniesienie zamiast według wartości - aby zapobiec potencjalnie kosztownemu lub niemożliwemu przekazywaniu wartości bocznej

void PrintIt(Object const& obj) {
    // ...
}
Johannes Schaub - litb
źródło
2
Czy możesz wyjaśnić pierwsze i trzecie użycie w twoich przykładach?
tunnuz
„W celu zagwarantowania odbiorcy, że parametr nie może mieć wartości NULL”, nie rozumiem, jak const ma cokolwiek wspólnego z tym przykładem.
Logan Capaldo
ups, tak mi się nie udaje. jakoś zacząłem pisać o referencjach. dziękuję bardzo za jęki :) oczywiście usunę to teraz :)
Johannes Schaub - litb
3
Proszę wyjaśnić pierwszy przykład. Nie ma to dla mnie większego sensu.
chikuba
28

W C ++ są naprawdę 2 główne zastosowania const.

Wartości stałe

Jeśli wartość ma postać zmiennej, składowej lub parametru, który nie będzie (lub nie powinien) być zmieniany podczas swojego życia, należy oznaczyć ją jako stałą. Pomaga to zapobiegać mutacjom na obiekcie. Na przykład w poniższej funkcji nie muszę zmieniać przekazanej instancji Student, więc oznaczam ją jako const.

void PrintStudent(const Student& student) {
  cout << student.GetName();
}

Dlaczego miałbyś to zrobić. O wiele łatwiej jest rozumować algorytm, jeśli wiesz, że podstawowe dane nie mogą się zmienić. „const” pomaga, ale nie gwarantuje, że zostanie to osiągnięte.

Oczywiście drukowanie danych na żądanie nie wymaga zbytniego myślenia :)

Oznaczanie metody składowej jako const

W poprzednim przykładzie oznaczyłem Student jako const. Ale skąd C ++ wiedział, że wywołanie metody GetName () na uczniu nie spowoduje mutacji obiektu? Odpowiedź jest taka, że ​​metoda została oznaczona jako const.

class Student {
  public:
    string GetName() const { ... }
};

Oznaczenie metody „const” ma 2 rzeczy. Przede wszystkim mówi C ++, że ta metoda nie spowoduje mutacji mojego obiektu. Po drugie, wszystkie zmienne składowe będą teraz traktowane tak, jakby były oznaczone jako stałe. Pomaga to, ale nie zapobiega modyfikowaniu instancji Twojej klasy.

To niezwykle prosty przykład, ale miejmy nadzieję, że pomoże on odpowiedzieć na Twoje pytania.

JaredPar
źródło
16

Zwróć uwagę, aby zrozumieć różnicę między tymi 4 deklaracjami:

Następujące 2 deklaracje są identyczne pod względem semantycznym. Można zmienić gdzie ccp1 i CCP2 punkt, ale nie można zmienić rzeczy oni wskazującego.

const char* ccp1;
char const* ccp2;

Następnie wskaźnik jest stały, więc aby był sensowny, musi zostać zainicjalizowany, aby wskazywał na coś. Nie możesz wskazać na coś innego, jednak to, na co wskazuje, można zmienić.

char* const cpc = &something_possibly_not_const;

Na koniec łączymy te dwa elementy - więc wskazywana rzecz nie może być modyfikowana, a wskaźnik nie może wskazywać nigdzie indziej.

const char* const ccpc = &const_obj;

Reguła spirali zgodnej z ruchem wskazówek zegara może pomóc rozplątać deklarację http://c-faq.com/decl/spiral.anderson.html

Steve Folly
źródło
W sposób okrężny, tak! Reguła spirali zgodnej z ruchem wskazówek zegara opisuje to lepiej - zacznij od nazwy (kpPointer) i narysuj spiralę zgodną z ruchem wskazówek zegara przechodzącą przez token i wypowiedz każdy token. Oczywiście po prawej stronie kpPointer nie ma nic, ale nadal działa.
Steve Folly
3

Na marginesie, gdy czytam tutaj , warto to zauważyć

const ma zastosowanie do wszystkiego, co znajduje się bezpośrednio po jego lewej stronie (z wyjątkiem sytuacji, gdy nic tam nie ma, w takim przypadku ma zastosowanie do tego, co jest jego bezpośrednim prawem).

JoePerkins
źródło