Używanie zmiennych bez uprzedniego nadania im wartości to stosunkowo częsty błąd. Wiele języków kompilowanych będzie z tego powodu stroiło fochy wahające się od ostrzeżeń podczas kompilacji przez błędy tejże aż po wyjątki w czasie wykonania programu (przynajmniej w wersji debugowej). To zresztą bardzo dobrze, gdyż alternatywą jest korzystanie ze zmiennej, której zawartością są jakieś losowe śmieci w pamięci.
Tak więc zmienne trzeba inicjalizować i w niemal każdym języku da się to zrobić mniej więcej tak:
W C++ oczywiście też. Jednak w C++ mamy też feature, który nazywa się inicjalizacją domyślną. Opiera się on założeniu, że dany typ (tj. obiekt tego typu) może się sam zainicjalizować bez żadnych dodatkowych informacji – czyli że np. ma rozsądną wartość domyślną albo bezparametrowy konstruktor. Stąd T()
będzie “domyślnym obiektem typu T
“, który możemy łatwo uzyskać, nie mając w ogóle pojęcia, czym ów typ T
w istocie jest (codzienność szablonów, swoją drogą).
Inicjalizacja zmiennej tego typu domyślną wartością będzie więc wyglądała mniej więcej tak:
Co bardziej spostrzegawczy zauważą jednak, że mamy tutaj dwie operacje: konstrukcję obiektu i kopiowanie go. Nie radzę jednak próby “optymalizacji” w postaci zamiany na deklarację T foo();
– można się nielicho zaskoczyć. Najlepiej pozostawić to zadanie kompilatorowi; przynajmniej w teorii powinien on sobie z nim bez problemu poradzić.
W powyższy sposób możemy domyślnie inicjalizować dowolne typy zmiennych. Dla pewnej ich podgrupy – zwanej agregatami – możemy jednak zastosować inne rozwiązanie. Cóż to jednak są te agregaty? Otóż są to typy złożone (tablice, struktury, itp.), które nie posiadają zdefiniowanych przez programistę konstruktorów. Mimo że instrukcja T()
działa zupełnie dobrze również dla nich, dopuszczalne jest stosowanie nieco innej formy inicjalizacji.
Formą ta jest lista inicjalizacyjna. Można ją niekiedy spotkać w kodzie korzystającym z jakiegoś API, które operuje na dużych strukturach:
Chodzi tu po prostu o podanie wartości dla kolejnych elementów agregatu, czyli pól w strukturze/klasie lub komórek tablicy; całą tę listę zamykamy w nawiasy klamrowe. Nie musi ona być przy tym pełna: jeśli jakieś pozycje zostaną pominięte, to odpowiadające im pola/komórki zostaną automatycznie zainicjalizowane w sposób domyślny. W przykładzie powyżej inicjalizujemy więc strukturę tak, że pierwsze pole zawiera jej rozmiar, a reszta domyślne wartości (czyli pewnie zera).
Dane dla pierwszego pola zostały wprawdzie tutaj podane jawnie, jednak w ogólności nie jest to wymogiem. Na liście inicjalizacyjnej możemy równie dobrze opuścić wszystkie pozycje i to właśnie jest ten drugi, uniwersalny sposób inicjalizacji agregatu. Wygląda on więc po prostu tak:
Jakkolwiek nietypowo (jak na kod C++) linijka ta wygląda, jest ona najzupełniej poprawna. W wyniku jej wykonania nowo zadeklarowany wektor v
będzie miał wszystkie współrzędne wyzerowane.
“Co bardziej spostrzegawczy zauważą jednak, że mamy tutaj dwie operacje: konstrukcję obiektu i kopiowanie go.”
Jesteś w 100% pewien ze
Foo bar(69);
i
Foo bar = Foo(69);
się czymś różnią semantycznie? Moim zdaniem dzialaja dokładnie tak samo i nie wynika to z optymalizacji kompilatora tylko z jezyka.
Dowód: powyższa konstrukcja zadziala dla klas bez konstruktora domyslnego. Imo w trakcie optymalizacji nie można przepuścić blednego kodu.
I jeszcze jedno:
“Inicjalizacja zmiennej tego typu domyślną wartością będzie więc wyglądała mniej więcej tak:
T foo = T();”
Będzie wyglądała po prostu tak:
T foo;
;)
Dodam, że deklaracja typu:
int x[100] = {};
jest o tyle fajna, że każdy element tablicy zostanie zainicjowany zerem, mimo że int to typ podstawowy i nie ma z tej racji żadnego konstruktora domyślnego. Można byłoby się zatem spodziewać, że:
int x[100] = {1,2,3};
da nam 3 wartości i dużo śmieci, a tu niespodzianka!
@Dab:
Ad.1 – Tutaj nie wykorzystujesz w ogóle konstruktora domyślnego, więc nie wiem co to za dowód :) W pierwszej linijce masz po prostu stworzenie obiektu używając pewnego konstruktora Foo(int), a w drugiej to samo lub to samo + wywołanie konstruktora kopiującego.
Ad2. – To nie jest żadna inicjalizacja. Jeśli T jest typem z konstruktorem, to owszem, zostanie on wywołany. Jeśli jednak nie, to zawartość foo pozostanie niezainicjowana.
@Kos:
No mniej więcej o tym właśnie pisałem pod koniec ;)
Mogłem tutaj dodać jeszcze jedną “sztuczkę”, ale ona jest raczej z gatunku zagadek, a nie rzeczy przydatnych. Mianowicie: czy coś takiego działa i jeśli tak, to co robi:
Dla ułatwienia dodam, że liczba par nawiasów { } wokół 4 nie ma znaczenia :)
OK, z tym konstruktorem domyślnym się zapędziłem, racja.
Natomiast podtrzymuję zdanie, że
Foo bar = Foo(15);
Nie spowoduje na odpalenie konstruktora kopiującego. Zrób klasę bez konstruktora kopiującego i operatora przypisania (np. umieść je w private). Wszystko działa.
Ad 2:
Owszem, jeżeli T nie ma konstuktora to nie zostanie wywołany. A przy Foo foo = Foo() zostanie? :)))
Ad ad 2.
Zapędziłem się. Przy typach prostych faktycznie jest jak piszesz. int v1 != int v1 = int().
Jeśli faktycznie
konstruuje obiekt Foo(15) od razu w zmiennej bar, to tym lepiej. Wówczas faktycznie T foo = T(); jest poprawnym i efektywnym sposobem domyślnej inicjalizacji zmiennej dowolnego typu T. Czy tak jest w istocie, czy to feature konkretnego kompilatora czy wymóg standardu – nie wiem, ale pewnie się (kiedyś) dowiem ;-)
Nie jest to wymog standardu. Jest to blad kompilatora. Nawet jezeli nie wywoluje konstruktora kopiujacego (optymalizacja), to musi respektowac ograniczenia dostepu.
Racja. Nie wiem co zrobiłem g++, że to łyknął. :)
@Kos
Jakiś czas temu pisałem o tym na blogu (z cytatami z outputu kompilatorka i ze standardu ;>)
http://gynvael.coldwind.pl/?id=280