Właściwości podstawowe C++
Użytkownik: Język C jest lepszy dla małych projektów, tak?
Stroustrup: W mojej opinii nie. Nie widziałem jeszcze projektu, dla którego C byłby lepszy niż C++ z jakiegokolwiek powodu z wyjątkiem braku kompilatora C++ na daną platformę.
W tej części postaram się opisać wszystkie właściwości C++ związane tylko i wyłącznie z programowaniem proceduralnym, czyli tzw. "lepszy C". Ten rozdział w szczególności polecam fanatykom języka C, aby udowodnić, że warto pisać w C++ zamiast w C, choćby ze względu na owe właściwości.
Tutaj będą zawierać się wszystkie najważniejsze i najbardziej podstawowe rzeczy dotyczące języka C++, czyli najprostsze operacje wejścia-wyjścia i deklaracje.
Podstawowa budowa programu; strumienie
Najprostszy program w C++, jaki jest, każdy widzi:
#include <iostream> using namespace std; int main() { cout << "Hello, I'm Jan B.\n"; return 0; }
Zastrzegam też od razu, że język o którym piszę jest zgodny ze standardem ANSI C++ oraz ISO C++. Przede wszystkim oznacza to, że na nic się nie zdadzą kompilatory, które nie są zgodne z ANSI C++ (m.in. jeden z nadal popularnych Borland C++ 3.1). Od tamtego czasu C++ przeszedł sporo zmian i wiele rzeczy zostało wycofanych, przez co programy zgodne ze standardem nie będą w nich prawidłowe, ani też programy prawidłowe w tych kompilatorach nie będą zgodne ze standardem. Jeśli chcesz się dowiedzieć więcej nt. najważniejszych zmian, które zostały wprowadzone do ANSI C++ i wycofane z wersji ARM, zajrzyj na koniec tego rozdziału pod "Specjalnie dla ARM-owców".
Nie będę tu omawiać pierwszych dwóch linijek; na razie na użytek pierwszych przykładów przyjmij to za wymóg. Pierwsza, najważniejsza rzecz: wszelkie operacje mogą być zawarte tylko wewnątrz funkcji (no nie do końca oczywiście, ale o szczególnych przypadkach pomówimy kiedy indziej; na razie tak przyjmijmy). W C++ funkcją (a czasem procedurą) nazywa się każdy podprogram (jest to taka uniwersalna nazwa, po angielsku "subroutine"); zazwyczaj jest przez coś (inny podprogram) wywoływany, może przyjmować argumenty i zwracać wyniki, ale przede wszystkim - zawierać instrukcje do wykonania. W każdym razie, jeśli w pliku źródłowym zawiera się jakiś kod, to musi on być zawarty w tzw. BLOKU kodu, który zaczyna się `{' i kończy `}'.
Całość tego przykładu stanowi DEFINICJĘ funkcji (o DEKLARACJACH za chwilę) o nazwie `main'. Jest to specjalnie zastrzeżona nazwa, która oznacza, że funkcja ta zawiera kod głównego programu. W sensie dosłownym można to traktować jak funkcję, która jest wywoływana przez powłokę (ang. shell) uruchamiającą ów program (tak, bo może jej dodatkowo przekazać jakieś argumenty, ale o tym też później ;). Na razie, na użytek przykładów, proszę zapamiętać, że zaczyna się ona od `int main() {' i kończy `}' (koniec linii w C++ nie różni się od spacji w większości przypadków - o czym też później ;).
Pierwsza instrukcja tej funkcji powoduje wysłanie na standardowe wyjście (czyli w normalnych warunkach na ekran) podanego w cudzysłowiu tekstu. Nie wyjaśniam, co to jest `Hello', bo to chyba każdy wie. Kim jest Jan B. też nie wyjaśniam z podobnych powodów, a jeśli ktoś nie wie, to też nic nie szkodzi. Wyjaśnienia jednak wymaga konstrukcja \n. Znak \ oznacza, że po nim znajduje się oznaczenie pewnego znaku specjalnego (znaki te omówię później). Konkretnie zaś znak \n oznacza znak LF ("linefeed", wysunięcie linii) o kodzie 10 (inaczej Control-J). Zgodnie ze standardem (proszę zwrócić uwagę, gdyż wyjątkowo jego interpretacja jest standardem na wszystkich kompilatorach) powoduje on przejście kursora do następnej linii i ustawienie się na jej początku. Tu wywołanie funkcji - jak KAŻDA POJEDYNCZA instrukcja (tu zwracam uwagę wszystkich, którzy znają pascala!) - kończy się średnikiem (niuanse tego również wyjaśnię później ;).
Następna instrukcja - proszę chwilowo zapamiętać, że powinna być na końcu funkcji main - zwraca wartość 0 temu, kto ją wywołał (czyli w tym wypadku powłoce). Proszę również pamiętać, iż wartość 0 informuje powłokę, że wykonanie programu się powiodło (są powłoki, które sprawdzają ten kod, więc lepiej tego dopilnować).
Wyrażenie podane jako `cout << cośtam' oczywiście nie jest żadną szczególną konstrukcją językową. Dlatego właśnie do działania wymaga odpowiedniego pliku nagłówkowego z odpowiednimi deklaracjami. Będzie o nich za jakiś czas; na razie przyjmij konstrukcję z #include za wymóg (za podobny wymóg przyjmij instrukcję using, którą na razie nie musisz się interesować, poza tym, że należy jej używać).
W naszym przykładzie, w celu wypisania tekstu na ekranie użyliśmy operacji "wysyłania do strumienia". Jeśli ktoś uruchamiał czasem programy spod powłoki kierując wyjście (wejście) do (z) pliku, wie że do tego celu używa się konstrukcji `program > plik' (`program < plik'). Skupmy się chwilowo na operatorze wysyłania danych (kierowania wyjścia). Może niektórzy wiedzą również o tym, że istnieje poza operatorem > również operator >>, który tym się różni od >, że nie niszczy istniejącego pliku, lecz dopisuje do niego dane. Podobnie i tu operator taki został wybrany ze względu na podobieństwo; oznacza, że wysyłamy napis "Hello, I'm Jan B.\n" do (istniejącego) strumienia o nazwie `cout' (`cout' wymawia się `si-aut'). Operator << ma oczywiście takie znaczenie tylko w przypadku strumieni; jego "prawdziwe" znaczenie poznamy później. W ten sposób do strumienia możemy wysyłać wiele danych różnych typów, również np. liczbowe i to wielokrotnie:
#include <iostream> using namespace std; int main() { cout << "Mam " << 18 << " lat!\n"; return 0; }
Jak to możliwe? Otóż wynikiem wyrażenia `cout << "Mam " ' jest samo cout (jeśli się ktoś uprze - "cout po wysłaniu do niego podanego tekstu"). Umożliwia to właśnie taki ciąg wywołań. Dla C++ ta możliwość jest dość charakterystyczna; takie ciągi wywołań są dostępne dla bardzo wielu wyrażeń, poczynając od dość starego, dostępnego również w C:
a = b = c;
Z tym, że tutaj kolejność jest zupełnie inna; najpierw wykona się b = c, a potem a = b.
Deklarowanie zmiennych
Przedstawię jeszcze, jak korzystać ze strumienia wejściowego. W tym celu zadeklarujemy sobie zmienną i wczytamy do niej liczbę z tego strumienia.
#include <iostream> using namespace std; int main() { cout << "Ile masz lat? "; // deklaracja zmiennej `ile' typu `int' int ile; cin >> ile; cout << "No no! Już " << ile << " latek Ci stuknęło!\n"; return 0; }
Deklaracja zmiennej ma - jak każdy widzi ;*) - dość prostą postać i - jak również każdy widzi - nie wymaga ona "części deklaracyjnej", jak to ma miejsce np. w pascalu i ANSI C. Każda zmienna może być zadeklarowana niemal w dowolnym miejscu, jednak przed jej pierwszym użyciem.
Choć teoretycznie w ANSI C też nie ma podziału na część deklaracyjną i wykonawczą, to jednak tak naprawdę ten podział istnieje - ostatnia zmienna używana w danym zasięgu musi być zadeklarowana przed pierwszą instrukcją tego zasięgu. C++ nie posiada takiego ograniczenia (zostało ono też usunięte w C99).
Nie dotyczy to jednak inicjalizacji, czyli przypisywania zmiennej odpowiedniej wartości w momencie deklaracji; nie jest to (jak się niektórym wydaje) równoważne przypisaniu (dokładniej o tym będzie w rozdziale 3.b):
int ile = 20;
Taka deklaracja nie jest traktowana jak instrukcja (również w C!) i podlega prawom takim, jak deklaracja. Jednak wyrażenie `cin >> ile;' jest już instrukcją więc `ile' musi być już przed nią zadeklarowane.
Ustalam jeszcze dodatkową regułę. Będę używał określenia "obiekt" raczej, niż "zmienna". Jest ku temu kilka powodów. To, że C++ jest językiem obiektowym to akurat jest tu fakt bez znaczenia ;*). Najważniejszym powodem jest to, że jeśli np. wielkość, która choć fizycznie istnieje, jest przeznaczona tylko do odczytu (nawet jeśli takie przeznaczenie ma nadane tylko lokalnie!), to trudno jest ją nazwać zmienną, skoro jest stałą (z tego co wiem, te dwa wyrazy to antagonizmy ;*). Pojęcie "obiekt" jest więc pojęciem szerszym, niż "zmienna", która oznacza obiekt mogący zmieniać stan. Proszę się więc przyzwyczaić, że każdą fizycznie istniejącą w programie wielkość będzie nazywać się "obiektem".
Żeby można było trochę więcej potrenować, pokażę jeszcze jak używać napisów w C++.
#include <iostream> #include <string> using namespace std; int main() { string imie; cout << "Jak się nazywasz? "; cin >> imie; cout << "Cześć " << imie << "!\n"; return 0; }
Używanie typu string, jak widać, wymaga wczytania dodatkowego nagłówka <string>. Typ ten nie jest bowiem typem wbudowanym języka, lecz bibliotecznym i nie występuje w języku C (język ten bowiem w ogóle nie posiada narzędzi umożliwiających jego stworzenie). Obsługa napisów w C (czyli takiej "surowej" ich postaci) jest trochę bardziej skomplikowana i z tego powodu zajmiemy się nią kiedy indziej. Nadmienię jednak, że wyrażenie "Jak się nazywasz? " nie jest typu string (trochę innego, z którego `string' po prostu korzysta i który obrabia sobie "za naszymi plecami"), później objaśnię, jak to działa. Dla typu string są dostępne m. in. - poza przypisywaniem mu stałej tekstowej - sklejanie (operatorem +) i indeksowanie umożliwiające oglądanie konkretnego znaku ze zmiennej (nawiasy kwadratowe).
Deklarowanie funkcji
Posłużę się tutaj na razie najprostszymi przykładami. Oto przykład użycia funkcji minus_2:
#include <iostream> #include <string> using namespace std; int minus_2( int argument ); int main() { float pi = 3.14; string imie; cout << "Jak się nazywasz? "; cin >> imie; cout << "Cześć " << imie << "!\nWynik naszej funkcji: " << minus_2( pi ) << endl; return 0; } int minus_2( int a ) { return a - 2; }
Funkcja oczywiście nie musi zwracać żadnej wartości (można jej działanie wtedy kończyć instrukcją return bez podania argumentu, poza tym nie jest ona wtedy w ogóle do czegokolwiek konieczna), wtedy jako typ zwracany piszemy `void'. Typ `void' jest zresztą takim samym typem jak każdy inny, poza tym że jest typem abstrakcyjnym, tzn. nie można tworzyć obiektów tego typu. Jednak można, jako argument instrukcji return w funkcji zwracającej void, podać wywołanie innej funkcji, która też zwraca void (ta właściwość jest również nowa w stosunku do C, wprowadzona ze względu na konieczne uogólnienie).
Funkcja nie posiada żadngo oznaczenia, że "oto deklarowana jest funkcja" (np. przy pomocy słowa `function' jak to jest w wielu językach). Deklaracja funkcji jest rozpoznawana po nawiasach. Zatem, nawet jeśli funkcja nie przyjmuje żadnych argumentów, nawiasy muszą pozostać!
Dla ciekawostki dodam, że język C umożliwiał przekazanie jakiekolwiek argumenty do funkcji (tzn. mógł zabronić jakiejkolwiek kontroli typów przekazywanych argumentów), natomiast C++ został tej możliwości prawie całkowicie pozbawiony. W języku C, aby zaznaczyć, że funkcja w ogóle nie przyjmuje argumentów, należało napisać (void), natomiast () oznaczało, że funkcji można przekazać cokolwiek. W C++ wycofano tą regułę, przez co () oznacza to samo co (void), czyli że funkcja nie przyjmuje argumentów. Jednak aby udostępnić tamtą właściwość, w C++ można zastosować deklarację nieokreślonych argumentów (...), która - w odróżnieniu od C - nie wymaga, by podawać jej co najmniej jeden argument jawny. W efekcie więc, aby w C++ uzyskać dostęp do funkcji C, która w nagłówkach C jest deklarowana jako `int Fn();' (lub wcale nie jest), należy napisać:
extern "C" int Fn(...);
Jak używać funkcji o nieokreślonej liczbie argumentów, dowiesz się w rozdziale o standardowej bibliotece C i funkcji "stdarg.h".
Zauważ, że funkcja co prawda nie musi być w pełni zdefiniowana przed jej użyciem, ale musi być choć zadeklarowana. Kompilator bowiem musi wiedzieć, jakich argumentów dla funkcji ma się spodziewać. Nie tylko po to, żeby wykryć błędne wywołania, ale również by dokonać ewentualnych konwersji typów, jak to mamy w tym przykładzie. Proszę spojrzeć, że wywołujemy z argumentem zmiennoprzecinkowym (czy rzeczywistym jak kto woli) funkcję, która przyjmuje argument całkowity (liczba zmiennoprzecinkowa ma całkiem odmienną reprezentację maszynową, niż całkowita; proszę sobie zatem wyobrazić efekty takiego wywołania w "tradycyjnym" C, czyli bez kontroli przekazywanych argumentów!). Kompilator jednak wie, że funkcja przyjmuje taki argument (właśnie dzięki nagłówkowi, który jest obowiązkowy) i wie, że należy dokonać konwersji. Zatem do funkcji `minus_2' zostanie przekazana wartość zmiennej `pi' przekonwertowana na wartość całkowitą - czyli `3'.
Uważni też na pewno dostrzegli, że argument ma inną nazwę w deklaracji funkcji, a inną w definicji. Nie jest to bynajmniej błąd. W deklaracji tej nazwy może w ogóle nie być (jedynie sama nazwa typu), jak również nie musi się ona zgadzać z nazwą w definicji.
Wyjaśnienia wymaga jeszcze symbol `endl'. Nie będę jednak zagłębiał się w szczegóły, czym to jest. Proszę po prostu przyjąć, że jest to synonim literału napisowago "\n" (oczywiście tylko dla operacji wysyłania do strumienia).
Następne rozdziały będą już powoli zagłębiać się w szczegóły języka. Nie byłoby dobrym pomysłem rozkładanie tego na dużo drobnych części, chciałbym bowiem, aby ten dokument stanowił też pewne kompendium języka i nie było problemów ze znalezieniem odpowiednich rzeczy. Jeśli jakieś informacje okażą się nużące, śmiało możesz je opuścić. Zawsze możesz potem wrócić do nich, jeśli okażą się potrzebne.
Specjalnie dla pascalowców
Zostałem poproszony o dopisanie tego rozdziału. Postaram się tu opisać różne rzeczy, które mogą pomóc w przestawieniu się na C++ z pascala. Oczywiście zachęcam wszystkich do przeczytania tego rozdziału; informacje tu zawarte mogą się przydać również osobom, które nie znają pascala. No więc na pewno już się orientujesz, że zamiast begin i end masz klamerki { }. Ale to jest jeszcze prosta sprawa. Najgorzej po pascalu jest sie przestawić na właściwy sposób stawiania średnika. Przekonasz się jednak, że w C++ ten średnik jest trochę lepiej pomyślany.
W C++ średnik stawiamy w następujących sytuacjach:
- po każdej POJEDYNCZEJ instrukcji
- po inicjalizatorze w stylu C zawartym w klamrach (będzie o tym przy typach złożonych)
- po DEKLARACJACH (w odróżnieniu od definicji)
Wiesz już, jak wygląda deklaracja i definicja funkcji. Deklaracja funkcji jest czymś podobnym do pascalowskiego "forward". Z tym tylko, że funkcje można deklarować bez takich ograniczeń, jak w pascalu. A nawet trzeba; deklaracje funkcji zazwyczaj umieszcza się w plikach nagłówkowych. Jak więc "każdy widzi", deklaracja (i tylko deklaracja) funkcji kończy się średnikiem.
Natomiast co z tymi instrukcjami. Średnik jest po pojedynczej instrukcji; nie stawiamy go zatem po zamkniętej klamrze, a także po każdej instrukcji jest on obowiązkowy. Tak samo, jak w pascalu, istnieją instrukcje i bloki podporządkowane jakiejś instrukcji sterującej i tak samo nie ma konieczności ujmowania w klamry jednej instrukcji. Nie zwalnia to jednak ze stawiania po tej pojedynczej instrukcji średnika. Jednym z częstszych przykładów jest coś takiego:
if ( a == 0 ) fnq( 12 ); else return 0;
Mam nadzieję, że ten przykład wyjaśnia wszystko na ten temat.
Ponadto jest jeszcze kilka rzeczy, jeśli chodzi o funkcje. Zauważyłe(a)ś już na pewno, że w C++ nie rozróżnia się "funkcji" i "procedury". Tak naprawdę jest to spore ułatwienie. Ponadto, nie ma czegoś takiego, jak program główny; każdy kod to kod jakiejś funkcji, a program główny to funkcja main. Nie zapominaj również, że funkcja main to funkcja jak każda inna i posiada również własne środowisko (tzn. zmienne lokalne). Na upartego można ją nawet wywoływać, choć przyznam, że się z tym jeszcze nie spotkałem :).
Jedna rzecz, o jakiej trzeba w C++ ZAPOMNIEĆ (wielu uważa to za wadę C++ i po części mają oni rację) to funkcje lokalne. Każda funkcja w C++ posiada tylko i wyłącznie trzy środowiska: zmienne globalne (na plik!), zmienne lokalne wywoływanej funkcji i zmienne statyczne tej funkcji (poznasz je niedługo). C++ niestety nie jest językiem funkcjonalnym i tam funkcje mają sporo ograniczeń; m.in. właśnie to, że każda funkcja ma globalny zasięg (z nazwami to jest tak nie do końca, ale to już dłuższy temat).
Jeśli chodzi o obsługę plików, to z przykrością muszę stwierdzić, że żadne doświadczenia w pascalu w niczym Ci nie pomogą. Przekonasz się jednak, że w C++ na plikach pracuje się w bardziej logiczny, przyjemny i intuicyjny sposób, tak też nie masz się czym przejmować.
Instrukcje sterujące w C++ są zdefiniowane spójniej, niż w pascalu, ale są bardzo podobne. Bardzo specyficzną składnię ma pętla for. Jest ona w pewnym sensie kompromisem pomiędzy wyduszeniem z niej maksimum możliwości, a prostotą składni. Istnieje jej standardowa postać, która jest przekładnią standardowej postaci iterowania po interwale, jest ona jednak dużo bliższa jej wewnętrznemu tłumaczeniu. Poza tym wszystko jest podobne, z tym tylko, że -- jak zobaczysz -- nie ma czegoś takiego jak "then" i "do". Zamiast tego z kolei całe wyrażenie po `if', `while' i `for' należy ująć w nawiasy.
Co co operatorów -- znasz już operator przypisania `=', który jest inny, niż w pascalu `:='. Zastanawiasz się pewnie, jak jest zdefiniowany operator porównania. Mówię więc od razu, że Ci się to nie spodoba. No ale cóż... jest to swego rodzaju kompromis i mnie wydaje się on słuszny. Nie jest bardzo errorogenny, jeśli masz odpowiednio wrażliwy kompilator. Zobaczysz także, że w C++ nie ma wszystkich operatorów słownych, jakie są w pascalu: and, or, not, div, mod, oraz rozszerzeń TurboPascala, and i or (jako bitowe), xor, shl, shr (są tylko and, or, xor i not). Operatory te w C++ istnieją, ale zapisuje się je inaczej. Nieco trudności może sprawić operator div, który w C++ zapisuje się po prostu `/', czyli tak samo, jak zwykłe dzielenie. Dzielenie całkowite (które symbolizuje div) wykonuje się zawsze jeśli oba argumenty są całkowite.
Co na to poradzić? W C++ należy stosować albo zapis "wymuszający odopwiedni typ", jeśli używasz literałów (np. 1.0), albo dokonać konwersji. Tu również wielu początkujących pyta, jak konwertować typ znakowy na liczbę całkowitą lub coś w tym stylu. Akurat C++ jest dość swobodny jeśli chodzi o tą kwestię. Typ char też można "traktować" jak liczbę (tzn. dokonać niejawnej konwersji), tylko ma inny zakres. C++ pozwala zwyczajnie przypisać wartość char do int i odwrotnie (i podobnie z typem bool). Owszem, wielu za to klnie na C++, jednak ja uważam, że kontrola typów, jaką C++ zapewnia na tym poziomie jest wystarczająca i ściślejszej nie potrzeba.
Trudności może sprawić deklaracja typów i zmiennych wskaźnikowych. Znów rodzaj kompromisu; operator ten jest z drugiej strony w stosunku do pascala, co jest czytelniejsze podczas używania, ale fatalne podczas deklaracji. Będzie o tym dokładniej w rozdziale o niezgodnościach.
Dalej: w C++ nie istnieje coś takiego, jak "część deklaracyjna" i "część wykonawcza". Tam jest wszystko "na kupie". Przekonasz się, że jest to duże ułatwienie, bo dzięki temu możesz inaczej rozplanować ułożenie tych deklaracji, jak Ci będzie lepiej pasowało. Feature ten ma jeszcze inny, poważniejszy powód istnienia, niż takie drobne ułatwienia. Poza tym każdy blok kodu ma własny zasięg i może mieć własne zmienne lokalne. Zmienne lokalne mogą też się bez problemów przesłaniać (tzn. spodziewaj się problemów, jeśli zadeklarujesz zmienną wewnątrz bloku o tej samej nazwie, jak zmienna lokalna funkcji ;).
To już chyba wszystko. Jeśli ktoś wymyśli, co można by tu jeszcze dopisać -- zapraszam na pocztę.
Specjalnie dla ARM-owców
Język C++ zanim został po raz pierwszy zestandaryzowany przez oficjalnie w tym celu istniejące organizacje (dokładnie to ANSI i ISO), był opublikowany w takiej formie, w jakiej wyglądał podczas całej swojej ewolucji. Bardzo sztywno się od początku trzymano zgodności z językiem C i z tego względu wiele rozwiązań, które wprowadzono później musiały, czy się tego chciało, czy nie, spowodować wsteczną niezgodność.
Taka pierwsza "oficjalna" wersja C++, na której opierano kompilatory, była opublikowana przez Bjarne Stroustrupa i (bodaj) Margaret Ellis pod nazwą "C++ Annotated Reference Manual", znaną też w skrócie jako "ARM". Na tej wersji C++ (lub którejś wcześniej, później itd.) opiera się wiele kompilatorów, włączając dziś dostępne (watcom, Borland C++ 3.1). Spróbuję wyliczyć tutaj cechy istniejące w ARM, ale wycofane w ANSI C++ i ISO C++:
- Pliki nagłówkowe miały, podobnie jak w C, nazwę z końcówką .h, czyli np. <iostream.h>. Obecnie pliki nagłówkowe nie mają końcówki .h, czyli wyglądają np. jak <iostream>. Pliki nagłówkowe odziedziczone z biblioteki ANSI C też mają swoje odpowiedniki w ANSI C++. Mają one również nazwę bez .h, ale na początku dodano im literę 'c'; zatem <stdio.h> odpowiada <cstdio>. Warto tu też wspomnieć, że w implementacjach ARM-owskich, w których istniał typ string, zawierał się on w <cstring.h>. Obecnie typ string zawiera się w nagłówku <string>, a nagłówek <string.h> z ANSI C jest dostępny w C++ jako <cstring> - to jest już troszkę zamieszane :)
- Elementy biblioteki standardowej (włączając elementy odziedziczone z ANSI C!) są zawarte w przestrzeni nazw std. O przestrzeniach nazw będzie trochę później, na razie zapamiętaj, że to oznacza, że albo musisz używać symboli z poprzedzeniem przestrzeni nazw (np. std::cout), albo dać na początku pliku using namespace std;. Dotyczy to tak samo elementów biblioteki C; czyli jak wczytasz <cstdio> i zechcesz użyć printf, również jeśli nie dasz using namespace std;, to musisz używać std::printf.
- Operator new pierwotnie zachowywał się podobnie jak malloc, to znaczy zwracał pusty wskaźnik, jeśli nie udało się przydzielić pamięci. Obecnie już tak się nie dzieje - operator new w przypadku nieprzydzielenia pamięci rzuca wyjątek typu std::bad_alloc (definicja jest w <new>). Jeśli chcesz uzyskać stare zachowanie operatora new, to w tym samym pliku nagłówkowym jest do tego specjalna definicja dla takiego operatora. Używa się wtedy np. new(std::nothrow) Typ.
- W ARM-ie istniał standardowy strumień strstream (był dostępny w pliku nagłówkowym <strstream.h>). Mówię "strumień", bo chodzi mi o cały zestaw typów oparty o konwencje strumienia skojarzonego z tablicą znaków. Niestety nie wszedł on do standardu w ogóle. Zamiast tego należy używać typu std::stringstream (z nagłówka <sstream>).
- Może to nie jest istotne, ale warto wspomnieć - standardowe strumienie (a także typ string, występujący w niektórych implementacjach ARM C++) były w ARM-ie zwykłymi typami. Obecnie istnieją już wzorce klas z przedrostkiem basic_ - basic_string, basic_ostream itd. i dopiero specjalizowane typem char (we wszystkich pierwszy parametr wzorca to typ znaku, który ma on obsługiwać) mają takie właśnie nazwy (aliasowane przez typedef), czyli string to alias do basic_string<char>. Istnieją też wersje specjalizowane typem wchar_t o nazwach poprzedzonych literą w (np. wstring).
Tyle mniej więcej pamiętam. Poza tym oczywiście istnieje też wiele porad dla wszystkich ARM-owców (czy też inaczej mówiąc "uczniów Grębosza"), czego w ANSI C++ absolutnie należy się WYSTRZEGAĆ:
- NIE UŻYWAJ "OBLEŚNEGO RZUTOWANIA" (tzn. rzutowania wymuszonego "(typ)obiekt"). Rzutowanie to jest niebezpieczne, bo "zabezpiecza" przed zgłaszaniem różnych potencjalnych błędów koncepcyjnych w tej konstrukcji. Używaj standardowych operatorów rzutowania: static_cast, dynamic_cast, const_cast, reinterpret_cast (patrz też "Dodatki: Rzutowanie").
- NIE TWÓRZ WŁASNEGO TYPU STRING (i nie używaj w swoich programach innych klas do obsługi napisów, niż std::string i std::wstring)! Bardzo długo nie istniał w ARM-ie standardowy typ obsługujący napisy (nie ma go np. w Borland C++ 3.1), dlatego obsługa napisów w C++ była długo takim samym koszmarem, jak w C. Niestety spowodowało to z kolei dostarczanie różnych stringowatych klas w najprzeróżniejszych bibliotekach; w starszych projektach niewiele by się znalazło takich, w których ktoś gdzieś nie definiował swojego "stringa" (i tak mamy już AnsiString u Borlanda, CString w MFC, QString w Qt i na pewno znalazłoby się jeszcze kilka innych). Obecnie istnieje już string standardowy (std::string) i należy wyłącznie tego używać do operowania napisami. Jest on zarówno bezpieczniejszy (tablica nie jest terminowana zerem, tylko przechowuje się jej rozmiar), a także w wielu operacjach szybszy (liczenie długości stringa jest stałego czasu, bo jest po prostu odczytywane z wewnętrznyych danych). Dobre implementacje tego typu (jak np. implementacja z gcc) dostarcza również współdzielenie wewnętrznej tablicy znaków z innymi obiektami typu string, które powstały przez skopiowanie (z zastosowaniem licznika referencji i tzw. copy-on-write). Jeśli potrzebujesz koniecznie jakiejś szczególnej właściwości od łańcucha, to wyprowadź swój własny typ z std::string, w miarę możliwości.
- Jeśli potrzebujesz, żeby obiekt sam się usunął (delete) w przypadku natrafienia na wyjątek (np. podczas jego przygotowywania do użytku, kiedy w czasie tego przygotowywania może wystąpić wyjątek), używaj std::auto_ptr. Przydaje się to również jeśli jakaś funkcja zwraca duży obiekt, a ten, kto wywołuje, miałby zadbać o jego usunięcie. Użytkownik jest wtedy zmuszony do przyjęcia rezultatu przez auto_ptr, a jeśli go zignoruje, to obiekt zostanie i tak usunięty.
- UŻYWAJ STANDARDOWYCH ZBIORNIKÓW I ALGORYTMÓW! Minimalizuj użycie wbudowanych tablic. NIGDY NIE UŻYWAJ TABLIC DYNAMICZNYCH TWORZONYCH PRZEZ ZAPISANIE WSKAŹNIKA REZULTATEM WYWOŁANIA new[]! Do tego celu służy całkiem dobrze dynamiczny metatyp standardowy std::vector (patrz "Elementy biblioteki STL"). Nie twórz własnych list, zbiorów, map i tym podobnych tworów, bo to wszystko już dawno istnieje w bibliotece standardowej. Jeżeli już koniecznie chcesz stworzyć jakiś nietypowy zbiornik, to w miarę możliwości oprzyj go o jakiś standardowy zbiornik. Do obliczeń numerycznych także polecam pakiet std::valarray.
- Zachęcam do używania biblioteki boost. Zawiera ona wiele bardzo użytecznych rzeczy, jak np. boost::bind (porównaj z std::bind1st i std::bind2nd z STL), czy tak banalne jak boost::array (typ opakowujący standardowy wbudowany typ tablicowy zgodnie z konwencją STL), dość przydatny boost::shared_ptr (wskaźnik zliczający referencje) plus brakujące wielu w standardzie obsługa systemu plików (np. czytanie katalogu), operacje na datach i czasie, obsługa wyrażeń regularnych, a nawet obsługa wątków. Jest to zresztą chyba jedyna niestandardowa biblioteka, którą można traktować na równi z biblioteką standardową ze względu na jej przenośność.
Specjalnie dla niecierpliwych!
Ten obszar zarezerwowałem sobie na różne rzeczy, które nie bardzo mam gdzie umieścić, a wypadałoby na początku. Chodzi tutaj raczej o przekierowanie do odpowiednich rozdziałów, jeśli ktoś potrzebuje informacji o jakimś szczególe.
- Konwersja znaku na liczbę. W języku C++ wartość typu char, oznaczająca znak, może być konwertowana niejawnie na liczbę (przez przypisanie, albo wymuszone przez zrzutowanie). Wartością liczbową, jaka powstanie po tej konwersji, jest kod znaku, jaki został mu przyznany w bieżącej implementacji (w najczęściej używanych implementacjach jest to ASCII). Tak samo, można przekonwertować liczbę na znak.
- Konwersja napisu na liczbę. Do tego mamy dwie możliwości: albo użyć funkcji atoi albo strtod z biblioteki standardowej C, albo posłużyć się typem stringstream (tzn. po prostu wysłać do strumienia napis i wczytać liczbę albo odwrotnie). Pierwsze jest opisane w rozdziale "Elementy biblioteki standardowej C", drugie pod "Biblioteka wejścia/wyjścia". Biblioteka boost dostarcza również małe opakowanie do tej drugiej możliwości pod nazwą lexical_cast.