Użytkownik: 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.
#include <iostream> using namespace std; int main() { cout << "Hello, I'm Jan B.\n"; return 0; }
Zatrzegam 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;
b =
c
, a potem a = b
.
#include <iostream> using namespace std; int main() { cout << "Ile masz lat? "; int ile; // deklaracja zmiennej `ile' typu `int' 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).
#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ł przekazać jakiekolwiek argumenty do funkcji (tzn. 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 2.8, pod "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 wyrażenia "\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.
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:
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 ntt.
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 2.7.
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. Features 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ę.
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++:
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Ć:
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.