const w C++ to jeden z najczęstszych modyfikatorów. Jest z nim związane wiele "ciekawostek" i niuansów, więc dzisiaj ten temat w pełni przemielimy ;)
const jak sama nazwa wskazuje służy w oryginalnym zamyśle do definiowania stałych. Maniacy C pewnie nadal używają #define, ale normalni ludzie napiszą w C++:
const int INFINITY = 2000000000;
nad #define INFINITY 2000000000; konstrukcja ta ma jedną, znaczącą przewagę - typ. Dzięki temu, że kompilator wie, że to int, może nas w porę ostrzec o jakiejś głupocie. Ale ludzie od C++ nie byliby sobą, gdyby const miało tylko takie jedno, proste znaczenie.
1. "Wskaźniki na stałe" i "stałe wskaźniki"
Taki kawałek kodu nam nie zadziała:
const int INFINITY = 2000000000;
int* p = &INFINITY;
Dlaczego? Ponieważ p jest wskaźnikiem na liczbę, która jest stała. Nie możemy jej zmienić, a typ wskaźnika p nie zawiera takiej informacji. Dopiero jedna modyfikacja:
const int INFINITY = 2000000000;
const int* p = &INFINITY;
lub jako kto woli:
const int INFINITY = 2000000000;
int const* p = &INFINITY;
definiuje nam p jako wskaźnik na stałą. Typ const T* oznacza, że to, na co wskazuje ten wskaźnik trzeba traktować jak stałą - czyli nie można modyfikować. Nie możemy więc napisać
const int* p = &INFINITY;
*p = 7;
Oczywiście elementem wskazywanym nie musi koniecznie być stała, np.:
int x = 42;
const int* p = &x;
jest jak najbardziej poprawne i logiczne. Możemy też zmieniać elementy na które wskazuje p, ważne jest tylko niemodyfikowanie wskazywanych elementów:
const int* p = &x;
p = &INFINITY;
W powyższych przykładach widzimy jeszcze jedną przewagę const nad #define - z #define INFINITY 2000000000; powyższe przykłady nie są możliwe do skompilowania ;)
"Wskaźnik na stałą" to nie jest jedyna możliwość. Zmodyfikujmy powyższy fragmencik zmieniając położenie modyfikatora const:
int x = 42;
int* const p = &x;
Kiedy const pojawia się po gwiazdce, to mamy stały wskaźnik. Czyli możemy modyfikować element, na który wskazuje p, ale nie możemy zmienić samego p - ten wskaźnik będzie już zawsze wskazywał na ten sam adres w pamięci. Czyli teraz:
p = new int(8);
nie jest poprawne, ponieważ p nie można zmieniać. Natomiast bez problemów możemy zmienić to na co wskazuje p:
*p = 8;
Można spotkać się z połączeniem obu tych koncepcji - "stałym wskaźnikiem do stałej":
const int* const p = &INFINITY;
Łączy on w sobie obostrzenia obu wersji - czyli nie możemy zmieniać zarówno tego na co wskazuje p, jak i samego wskaźnika.
2. Referencje do stałych
Koncepcja jest taka sama jak w przypadku wskaźników na stałe, tylko miejsce gwiazdek zajmują ampersandy ;)
Kiedy więc napiszemy
const int& r = INFINITY;
lub
int const& r = INFINITY;
(const po lewej stronie ampersandu), to już nie możemy napisać:
r = 7;
ponieważ r jest referencją do stałej. Oczywiście podobnie jak przy wskaźnikach to, do czego się odnosi r nie musi być stałą, ale tak będzie traktowane.
OK, skoro jest pełna analogia, to pewnie mamy też "stałe referencje".
int x = 7;
int& const p = x;
To co napisałem powyżej jest niepoprawne - skompiluje się z ostrzeżeniem o anachronizmie, więc dobrze to to nie jest. Dlaczego? Ponieważ referencje z definicji są stałe - dodatkowy modyfikator const jest zbędny.
3. const w deklaracjach metod
Dwa poprzednie punkty mają największe znaczenie dla typów argumentów w deklaracjach metod. I tak, w ramach krótkiego przypomnienia:
void f1(int* const a) - stały wskaźnik pozwoli nam zrobić *a = 8;, ale już nie można a = new int(5);.
void f2(const int* a) - wskaźnik na stałą zabroni nam zrobić *a = 8;, ale można a = new int(5);.
void f3(const int* const a) - stały wskaźnik na stałą zabroni nam zrobić *a = 8;, a także nielegalne będzie a = new int(5);.
void f4(const int& a) - stała referencja uniemożliwi wykonanie a = 8;.
Na const przy referencjach warto uważać - bez tego modyfikatora w funkcji f4 nie można by wykonać następującego kodu:
f4(42);
Warto więc na const uważać ;) I standardowe przypomnienie - const int& a to najszybszy sposób przekazywania argumentów do funkcji :)
Dodatkowa uwaga przy typach zwracanych metod - jeżeli napiszemy funkcję:
char* hello() { return "Hello world!"; }
to ktoś może użyć jej tak:
char* s = hello();
s[1] = 'a';
I bum, bo s tak naprawdę wskazuje na stałą "Hello world!". Rozwiązaniem jest poprawa typu zwracanego funkcji hello():
const char* hello() { return "Hello world!"; }
Warto o tym pamiętać ;)
4. Metody const
Odwołam się tutaj do przykładu z mojego posta sprzed jakiegoś czasu nt. rzadko spotykanych słów kluczowych w C++ (http://wojtek-m.blogspot.com/2010/07/rzadko-spotykane-sowa-kluczowe-c.html). Załóżmy że piszemy klasę do obsługi macierzy - skupmy się na funkcji wyliczającej wyznacznik macierzy:
// Matrix.h
class Matrix {
public:
double getDeterminant();
//...
};
// Matrix.cpp
double Matrix::getDeterminant() {
//...
return 12.0;
}
Teraz mamy funkcję void f(const Matrix& mtrx), która wyświetli na ekranie wartość wyznacznika. Jest pewien problem - macierz mtrx została przekazana jako stała, nie można jej więc modyfikować. A metoda double getDeterminant() teraz może dokonać takiej modyfikacji... Co poradzić?
Otóż wystarczy oznaczyć tą funkcję jako niemodyfikującą obiektu danej klasy - czyli działającą również dla stałych. Służy do tego (jak się słusznie domyśliliście) modyfikator const w odpowiednim miejscu:
// Matrix.h
class Matrix {
public:
double getDeterminant() const;
//...
};
// Matrix.cpp
double Matrix::getDeterminant() const {
//...
return 12.0;
}
Voila! Technicznie rzecz biorąc w metodach bez modyfikatora const wskaźnik this jest typu Matrix* const (czyli stały wskaźnik). Po dodaniu modyfikatora const do metody wskaźnik this w niej jest typu const Matrix* const, czyli jest to stały wskaźnik do stałej.
Można sobie wyobrazić następującą metodę klasy:
const char* f(const int* const a) const;
To na pewno powinien być "stały element" klasy ;)
5. Atrybuty const
O skoro już o stałych elementach klasy mowa, to nie można zapomnieć o stałych atrybutach w klasie. Mają one sens podobny do atrybutów final w Javie - ustawiamy je raz, na początku, a potem nie można ich modyfikować:
class A
{
public:
const int x;
//...
};
Pomysł zacny, ale jak go zrealizować? Pierwsze podejście:
A(int _x)
{
x = _x;
//...
}
skończy się błędem kompilacji. Dlaczego? Jak (jeśli w ogóle) pamiętacie z ostatniego odcinka serii o wydajności w C++, wszystkie pola klasy są inicjowane wartościami domyślnymi przed wejściem do konstruktora. Czyli atrybut x dostał już wartość 0. Wartościami domyślnymi lub wartościami podanymi na liście inicjalizacyjnej. Rozwiązaniem jest więc...
A(int _x) : x(_x)
{
//...
}
Świat ponownie został uratowany ;)
6. Obejścia - mutable i const_cast
Jeżeli przeczytałeś już wszystko, to może nasunęły Ci się dwa pytania-problemy:
1. Co zrobić, kiedy metoda jest i powinna być const, a my chcemy coś jednak zmodyfikować. Dobrym przykładem będzie już wspomniana klasa macierzy - kiedy raz wyliczymy wyznacznik możemy go zapamiętać, a potem zwracać tylko zapamiętaną wartość:
// Matrix.h
class Matrix {
public:
double getDeterminant() const;
//...
private:
double calculateDet() const;
double det;
bool isDetValid;
//...
};
// Matrix.cpp
double Matrix::getDeterminant() const {
if (!isDetValid) {
det = calculateDet();
isDetValid = true;
}
return det;
}
double Matrix::calculateDet() const {
//...
return 12.0;
}
I mamy problem kompilacji. getDeterminant() jest metodą const, czyli nie zmieniającą stanu obiektu. A modyfikuje przecież dwie zmienne.
Rozwiązaniem jest słowo kluczowe mutable - jest to modyfikator, który może być stosowany do składowych klasy, które są niestatycznymi i niestałymi atrybutami. Mówi on: "OK, ten atrybut będzie można modyfikować w metodach const, bo on tak naprawdę nie ustala stanu obiektu".
Kod z tym słowem kluczowym będzie kompilował się już bez problemów i działał tak jak chcemy:
// Matrix.h
class Matrix {
public:
double getDeterminant() const;
//...
private:
double calculateDet() const;
mutable double det;
mutable bool isDetValid;
//...
};
// Matrix.cpp
double Matrix::getDeterminant() const {
if (!isDetValid) {
det = calculateDet();
isDetValid = true;
}
return det;
}
double Matrix::calculateDet() const {
//...
return 12.0;
}
A teraz zagadka: Wiadomo, że modyfikator mutable nie może być stosowany do stałych atrybutów klasy. To dlaczego poniższy kod kompiluje się?
mutable const Matrix* ptr;
Zwycięzca dostanie ode mnie piwo :) Zagadka od półtora miesiąca leży nierozwiązana (była już w poście http://wojtek-m.blogspot.com/2010/07/rzadko-spotykane-sowa-kluczowe-c.html), więc oprócz piwa nagrodą będzie splendor ;)
I drugie pytanie-problem:
2. Co zrobić, kiedy korzystam z biblioteki, której autor nie słyszał o wskaźnikach na stałe? Zwłaszcza boli to w przypadku argumentów metod... Wiem, że dana metoda nie modyfikuje wskazywanego elementu, ale nadal nie mogę jej użyć ze swoim kodem...
Czyli mamy podobną funkcję biblioteczną:
void f(int* a)
{
cout << *a << endl;
}
I chcielibyśmy użyć jej tak:
f(&INFINITY);
ale nie mamy takiej możliwość, bo INFINITY jak wiadomo jest const.
Rozwiązaniem jest operator const_cast. Służy on do specyficznego rzutowania - rzutowania, które usuwa lub dodaje modyfikator const lub volatile. Dzięki temu operatorowi możemy dowolnie dodawać lub usuwać te modyfikatory - i to bezkarnie, w czasie kompilacji. Napiszmy więc:
f(const_cast<int*>(&INFINITY));
i gotowe - nasz const-świadomy kod działa z jakimiś antykami :)
Jedyne na co trzeba uważać, to funkcja f2:
void f2(int* a)
{
*a = 42;
cout << *a << endl;
}
Wykonanie kodu:
f2(const_cast<int*>(&INFINITY));
zakończy się w sposób niekontrolowany - najpewniej wyjątkiem "Access Violation". Dlatego stosujmy operator const_cast tylko kiedy jesteśmy pewni, że dana funkcja nie wykonuje żadnych modyfikacji.
Więcej:
http://duramecho.com/ComputerInformation/WhyHowCppConst.html
http://www.serc.iisc.ernet.in/ComputingFacilities/systems/cluster/vac-7.0/html/language/ref/clrc05keyword_const_cast.htm
http://msdn.microsoft.com/en-us/library/4h2h0ktk%28VS.90%29.aspx
http://en.wikipedia.org/wiki/Const-correctness - ten artykuł warto przeczytać w całości.
niedziela, 29 sierpnia 2010
Subskrybuj:
Komentarze do posta (Atom)
bedzie static w nastepnym odcinku? :D
OdpowiedzUsuńco do wstepu - const ma jeszcze jedna przewage nad #define - mozna podejrzec wartosc w trakcie debugowania ;)
OdpowiedzUsuń:) Zobaczymy co będzie w kolejnym odcinku :)
OdpowiedzUsuńvoid fun(char a[], char b[], char c[]){
OdpowiedzUsuńint i = 0; bool p = false; int pC = 0; int nC = 0;
for(; ;i++){
if(!(a[i])) { c[nC] = b[pC] = a[i]; break; }
(a[i]==' ')?(p=!p,c[nC]=a[i],nC+=1,(!pC)?(pC=pC):(b[pC]=a[i],pC+=1)):(p?(pC+=1,b[pC-1]=a[i]):(nC+=1,c[nC-1]=a[i]));
}
}
jebie mi :D
a to mi dopiero trudność
UsuńBoguś lepiej np. pójdź do kina :P
OdpowiedzUsuńOdpowiedź do zagadki:
OdpowiedzUsuńmutable const Matrix* ptr;
namespace matrix {
class Matrix;
};
class matrix::Matrix {
};
class DeclMatrix : public matrix::Matrix {
mutable const matrix::Matrix* ptr_; //1
mutable matrix::Matrix const* ptr; //2
// powyższe składowe (ptr i ptr_) - są tego samego typu
// (kolejność modyfikatora const jest tutaj
// roźna ale nie wnosi zmian formalnych)
// W odpowiedzi do zagadki przy [*2*] - widać że rozwiązanie nasuwa się samo
// modyfikator mutable odnosi się do wartości pod wskaźnikiem - a nie do wartości
// adresu wskaźnika
mutable const matrix::Matrix*const const_ptr; // nieprawidłowo
// Kod powyżej jest nieprawidłowy - zlepek mutable*const jest niedozwolony
// ale zlepek mutable const const* już jest dozwolony bo znaczeniowo jest róźny
// Wynika to z kolejności rozwinięcia kolejności modyfikataorów względem implementacji
// typów wskaźnikowych
};