Obie wersje kompilują się do tego samego, więc o żadnej optymalizacji nie może być mowy, ale ze względu na gwarancje języka mają odmienną semantykę, przez co są używane w innych sytuacjach. Granica jest subtelna i często płynna i niekiedy świadomie przekraczana, ale pozwala na wyrażenie intencji programisty.
Wersji a używam w dwóch kontekstach:
gdy bar jest dużym obiektem ze sterty oraz gdy foo potrzebuje go użyć wołając metod nie-stałych (wybieram wersję void foo(Bar const* bar) gdy chcę ograniczyć wołalność do stałych metod). W grę wchodzi tylko wołanie metod (względnie dobieranie się do pól, jeśli to struktura). Niedopuszczalne jest nadpisanie ( *bar = bang ) oraz zapamiętanie wskaźnika na później, gdyż dostając goły wskaźnik nie mamy gwarancji jego żywotności – goły wskaźnik wpadający do funkcji każe domniemać, że to obiekt tymczasowy do natychmiastowego użytku. Nawiasem mówiąc goły wskaźnik nigdy nie powinien pojawić się poza argumentem funkcji, gdyż nie ma on semantyki własności (na co C++owcy są bardzo uczuleni), stąd przykazanie użyj – nie zapamiętuj. Jego użycie jest uzasadnione tylko byciem wspólnym mianownikiem dla standardowych wskaźników unique_ptr (o unikalnej właśności) oraz shared_ptr (o dzielonej właśności – jest to wskaźnik z licznikiem odwołań),
gdy bar ma służyć jako parametr opcjonalny i jego pustość niesie dodatkową informację, którą możemy wykorzystać przy projektowaniu algorytmu, a więc tak jak w C.
Wybieram b, gdy intencją jest modyfikacja bar, który najczęściej jest obiektem automatycznym. W szczególności bar może być dużą strukturą, którą potrzebujemy wypełnić w funkcji foo. Skoro rolą foo jest wypełnienie bar, to nie ma ona sensownego zachowania, gdyby bar był pustym wskaźnikiem i tu przydaje się gwarancja istnienia bar.
Nie wiem jak bardzo świadome było pytanie, ale tak samo jak void foo(Bar * bar) ma brata foo(Bar const* bar), to void foo(Bar & bar) ma o wiele popularniejszego brata void foo(Bar const& bar), który zabrania modyfikacji bar. Jest on używany w kontekstach korzystania z bar bez jego modyfikowania gdy operujemy na wartości. Jest to zatem klasyczne wykorzystanie referencji jako aliasa na wartość, co jest niezbędne przy dociążaniu operatorów. Celowo napisałem wartość, gdyż do takiego foo możemy podać literał, czy wyrażenie obliczające się do Bar, a więc niekoniecznie musi być to konkretna zmienna o istniejącym adresie, tak jak przy wskaźnikach. Stałych referencji używamy przy definiowaniu operatorów, konstruktorów kopiujących, klauzul łapania wyjątków itd.
Nie mogę jeszcze nie wspomnieć o kuzynie: r-value referencji, przez co programista C++ widzi jeszcze wersję c:
c. void foo(Bar && bar) { /* kod */ }
Jest to bardzo silny mechanizm, gdyż ta funkcja dowiąże się tylko to wartości, która nie ma nazwy, a więc wartości tymczasowej, która zginie zaraz po zakończeniu działania funkcji foo, dzięki czemu możemy ukraść jej zasoby, bo i tak nikt tego nie zauważy. Na bazie r-value referencji budowane są w C++ konstruktory przenoszące, czyli takie, które tworzą nowy obiekt na bazie starego obiektu tego samego typu, który zaraz zostanie zniszczony – stąd możemy bezpiecznie ukraść mu pamięć, jeśli jest to np. jakiś kontener.
Zagadnienie jak widać jest bardzo szerokie, ale wspomnę, że dzięki konstruktorom przenoszącym wcale nie jest głupia konstrukcja czwarta:
d. void foo(Bar bar) { /* kod */ }
Jest to wzorzec dla settera, czyli metody, która przejmuje bar na własność. foo mogło być wywołane z wartością tymczasową (więc bar „przeniósł” ją do siebie) lub z wartością nazwaną (wówczas wykonała się kopia). Ciało foo w takiej okoliczności powinno przenieść zawartość bar do pola klasy, której jest metodą. Dzięki temu mamy jedną metodę i żadnego kopiowania, jeśli nie jest ono konieczne.
Należy odróżnić błędy od sytuacji wyjątkowych. Gdy parsuję tekst napisany przez użytkownika, błędy w takim tekście nie są niczym wyjątkowym, lecz czymś spodziewanym i aktywnie testowanym. Wyjątki są tu nie na miejscu. Gdy jednak zabraknie mi pamięci, albo nie znajdę karty graficznej, na której ma działać moja gra, to już jest sytuacja wyjątkowa, o którą nie powinienem się martwić w normalnym toku działania programu i tylko skrajni pesymiści przy każdej alokacji itd. sprawdzają czy się udała. C++ jest językiem dla optymistów, których interesuje pozytywna ścieżka wykonania programu (tzw. ścieżka sukcesu) i nieoczekiwane niepowodzenia traktują jako sytuacje wyjątkowe, dla których piszą obsługę w odpowiednich miejscach programu. Wyjątki są mechanizmem na tyle przenośnym na ile wspiera je kompilator i system operacyjny (a poza jakimiś dzikimi embedami są wpierane wszędzie) i powinny być używane wszędzie z wyjątkiem systemów czasu rzeczywistego, czy fragmentów wrażliwych na czas wykonania, gdyż nie istnieje deterministyczna metoda stwierdzenia maksymalnego czasu obsługi wyjątku. Gdy żaden wyjątek nie zostanie rzucony, we współczesnych implementacjach nie ma żadnego narzutu na czas wykonania programu. Dodatkowy czas obsługi możemy spokojnie zaniedbać, gdyż tyczy on się sytuacji… wyjątkowych. Gdy ktoś uważa inaczej, to znaczy że sytuacja, o której myśli, wcale nie jest wyjątkowa i powinien przeprojektować algorytm.
W standardowym C++ nie można ignorować istnienia wyjątków, gdyż standardowy operator new rzuca wyjątek w przypadku braku pamięci, co jest kluczowe w implementacji standardowych kontenerów, które zakładają, że instrukcja zaraz po alokacji, ma zaalokowaną pamięć. Wzorem biblioteki standardowej zakładając, że wszystko się udaje, upraszczamy dramatycznie kod, gdyż nie trzeba sprawdzać, czy udało się to, czy udało się tamto… po prostu piszemy kod, tak jakby się udało, a sytuację, gdy się nie uda, obsługujemy jako sytuację wyjątkową.
Do 90% operacji na łańcuchach znaków użyję std::string. Jest to klasa z semantyką własności względem pamięci zajmowanej przez napis i z podstawowymi operacjami edycyjnymi. Const char * to tylko wskaźnik, więc nie jest de facto łańcuchem znaków, a LPCTSTR ma różne znaczenie w zależności od unicode’owości (i nie jest to typ standardowy), więc trudno je porównywań. Frameworków nie używam, ale zdarza mi się użyć kilku narzędzi napisowych z boosta. Klasy string sam nie implementuję, bo już jest. Co najwyżej mogę zaimplementować coś, co bardziej nadaje się do konkretnego zastosowania i co załatwia kilka procent z pozostałych 10%, gdyż std::string nie jest 100% wpasowana w STL, gdyż nie jest klasycznym kontenerem danych. Jest napisem.
Operatory logiczne i relacyjne zwracają wartość typu bool, a w praktyce wystarczy coś konwertowalnego do bool, gdyż operacjom selekcji wystarczy wyrażenie, które da się do boola skonwertować. Tak działa kompatybilność z C, że taki „if” nie bierze inta równego albo różnego od zera, tylko boola, a int potrafi się do niego skonwertować (oczywiście statycznie, w run time wszystko jest po staremu, C++ tylko lepiej dba o typy wyrażeń).
Użyję C++. Nie znam architektury, którą warto się zainteresować, która nie wspierałaby C++.
Prawda. Wszystkie kompilatory mają błędy. Sam kilka znalazłem i zgłosiłem Microsoftowi. Wskaż mi duży projekt w C, który nie obchodzi błędów kompilatora, szczególnie, że C jako język super przenośny powinien obsługiwać X platform i Y kompilatorów, z których każdy ma swoje kruczki. Prawdę mówiąc nawet zdarzają się błędy języka. Wystarczy zerknąć na issue list komitetu standaryzacyjnego.
Ta składnia stanowi w C++ błąd. W C można było po cichu zrzutować void* na cokolwiek miało się tylko ochotę, dzięki czemu wskaźnik na cokolwiek zwracany przez malloc mógł być wprost zinterpretowany jako wskaźnik na to co chcemy. C++ na to nie pozwala, gdyż w C++ typ jest graczem pierwszoplanowym i bezpieczeństwo typów jest jego kluczową własnością. Dlaczego tak jest lepiej? W C mogłeś np. napisać int *i = malloc(1); co dla początkującego programisty, który nie czyta dokumentacji, wygląda całkiem sensownie, a niesie ze sobą brzemienne skutki. C++ zniechęca do takiej konstrukcji oferując natywny odpowiednik w postaci int * i = new int[1]. Nie jest to najszczęśliwszy zapis, ale jest to prosty odpowiednik, który dba o typ wyrażenia. Operator new jest karmiony typem jaki chcemy zaalokować oraz opcjonalnie liczbą elementów (a nie liczbą bajtów) i zwróci wskaźnik o typie int*, który z ochotą zostanie przypisany do zmiennej i. Nie potrzeba tu żadnego rzutowania, a mamy bezpieczeństwo typów. Dodatkowo new woła konstruktor tworzonego obiektu, co nam załatwia wstępną inicjalizacje pamięci, czego w C nie mieliśmy, a destruktor wołany po delete (bo nota bene zapomniałeś napisać w swoim przykładzie free i doznałeś wycieku ;p) woła destruktor, który sprząta po obiekcie (szczególnie takim nietrywialnym).
A jak już jesteśmy przy alokacji zasobów, żaden (przyzwoity) programista C++ nie trzyma pamięci jako gołe wskaźniki, bo wie, że może zapomnieć o delete (albo nawet jak nie zapomni, to może mu w tym przeszkodzić wyjątek) i skrupulatnie dba o własność zasobu wg metodologii RAII i raczej napisałby std::unique_ptr<int> i( new int[1] ); przez co miałby pewność, że zasób zostanie automatycznie zwolniony (poprzez zawołanie destruktora obiektu std::unique_ptr<int>) w momencie zamknięcia bloku zawierającego i. Programiści C++ są leniwi i nie dbają o te wszystkie detale, z którymi walczą programiści w C. Oni zwalają je na kompilator.
Oczywiście #include <iostream>. iostream nie jest nagłówkiem C, stąd brak rozszerzenia. W momencie, gdy zastanawiano się jakie rozszerzenie dać nagłówkom C++ (hxx? hpp?), ktoś wpadł na pomysł, że nie trzeba dawać żadnego :)
Pytanie jest tendencyjne ;) Wszystkie tak zwane „nowoczesne języki programowania” mają dziedziczenie wielobazowe. Z tą różnica, że np. Java czy C# pozwala na dziedziczenie tylko interfejsów, co nazywa ich implementowaniem. W C++ interfejsy są pod postacią klas czysto wirtualnych, więc de facto można definiować sobie interfejsy i je implementować. C++ pozwala na więcej. Pozwala, aby klasy bazowe miały implementacje metod, a nawet miały pola. Dzięki czemu obiekt pochodny może posiadać wiele podobiektów bazowych. Można nawet zadeklarować niektóre z podobiektów jako wirtualne, przez co będą one występowały tylko raz. Jak nie czujesz się na siłach, aby korzystać z tych mechanizmów – nie rób tego i używaj tylko klas czysto wirtualnych (tzw. interfejsów), ale jeśli wiesz co robisz, to można za pomocą tego systemu zaimplementować wiele ciekawych funkcjonalności, które jednak mogą wykraczać poza „kurs podstawowy”. C++ daje narzędzia i od umiejętności programisty zależy jak ich użyje (łącznie ze zrobieniem sobie krzywdy).
this jest wskaźnikiem i jest wskaźnikiem, bo tak. ;)
Odpowiedź to d. Jest to program, w którym programista C dowodzi, że nie rozumie interakcji dziedziczenia z tablicami w stylu C i pokazuje, że C++ pozwala mu na zrobienie sobie krzywdy. Nieźle mnie to zaskoczyło, bo sam bym nie wpadł na to, żeby napisać coś takiego. Klasa A zawiera jedno pole int. Klasa B zawiera podobiekt A z jednym polem int, oraz pole char*. Deklarując tablicę obiektów typu B przydzielasz każdemu pamięci na dwa pola (int i char*). Tu popełniasz pierwszy błąd. Na razie niewielki. Deklarujesz tablicę jak w starym dobrym C. Drugi błąd (ten już brzemienny) wiąże się z potraktowaniem tablicy jako wskaźnika na (jeden!) obiekt pochodny, który bardzo ładnie i grzecznie potrafi zrzutować się na wskaźnik na (jeden!) obiekt bazowy. Takie rzutowanie jest fajne, bo obiekt pochodny potrafi wszystko to, co bazowy, a nawet więcej, więc możemy traktować jakby był obiektem bazowym. Ale problem jest taki, że przekazałeś ten wskaźnik do funkcji, która oczekuje tablicy (!) obiektów bazowych. Są to dwa różne obiekty, a oszukałeś system typów raz uważając to za wskaźnik, a raz za tablicę. Z pierwszym elementem idzie pięknie, bo ta operacja jest zdefiniowana, ale pakujesz się w problemy wykonując operacje na wskaźnikach do obiektu pochodnego myśląc, że to obiekt bazowy, dzięki czemu pudłujesz w drugi element. Nie jest to poprawny program w C++ i jego wynik jest niezdefiniowany (łącznie z możliwością wytworzenia czarnej dziury pochłaniającej wszechświat). Nadużyłeś dobroduszności C++ w sposób nie do wykrycia przez kompilator i zrobiłeś siebie krzywdę pokroju int * i = malloc( 5 ). Jak poprawnie robi się takie rzeczy? Otóż w C++ nie używamy gołych tablic C w ogólności, a szczególnie jeśli operujemy na obiektach, które nie są obiektami C (a więc obiektach z dziedziczeniem, Ty popełniłeś ten błąd). W C++ można w tym miejscy użyć statycznej tablicy std::array<B,3>, albo kontenera std::vector<B>. Obu obiektów nie da się zrzutować na wskaźnik i nie udałoby Ci się napisać takiego programu, który wygląda sensownie, a takim nie jest. Stroustrup też ma coś do powiedzenia na ten temat.
/Wall w kompilatorze Microsoftu jest bardzo pedantyczne i często wyświetla uwagi, które nie raportują zagrożenia, ale mogą dać wskazówkę programiście o zachowaniu, którego mógł nie być świadom, stąd zaincludowanie iostream wygeneruje stado komunikatów w stylu, że w jakiejś strukturze na końcu dodano trzy bajty paddingu, albo kompilator zadecydował, że nie będzie inlinował jakiejś tam funkcji zadeklarowanej jako inline i tym podobne błahostki. Odpowiedź zatem to b, gdyż w iostream kompilator dodał kilka paddingów i odpuścił sobie kilku inline’owań. Dobrym poziomem warningów jest /W3, ale też nie jest idealnie, bo wśród ostrzeżeń czwartego poziomu jest kilka moim zdaniem przydatnych i te włączam sobie manualnie (wymuszając ich poziom na trzeci).
EDIT: w punkcie 3. 95% + 10% != 100%. Poprawiłem :)