1.Wprowadzenie
1.1. Literatura.
Schild Herbert(1998): Informator o języku programowania Borland C++. BUM.
Grębosz Jerzy(1999): Symfonia C++. Kallimach.
Grębosz Jerzy(1999): Pasja C++. Kallimach.
Liberty Jesse(1998): Poznaj C++ w 24 godziny. Intersoftland.
Liberty Jesse(1999): Poznaj C++ w 10 minut. Intersoftland.
Delannoy Claude(1993): Ćwiczenia z języka C++. WNT.
Meyers Scott(1998): Język C++ bardziej efektywny.
Stroupstrup Bjarne(1997): Język C++. WNT
Deitel Harvey M., Deitel Paul(1999): Arkana C++. RM.
Borowikowie Bogdan i Wanda (1998): Meandry języka C++/
Meyers Scott(1998): Język C++ bardziej efektywny. WNT
Sedgewick Robert(1999): Algorytmy w C++. READ ME
Lippman Stanley D.(1998): Model obiektu w C++. WNT
i wiele, wiele innych.
Ellis M. Stroustrup B.(1990): The Annotated C++ Reference Manual. Addison- Wesley.
1.2. Uwagi wstępne.
Język C++ jest rozszerzeniem języka C - "C z klasami". Początkowo - "porządkowanie" bardzo dużych programów (od 20000 instrukcji w górę).. Obecnie wykorzystywany do praktycznie wszystkich zadań. Język hybrydowy (kundlowaty) - można go wykorzystać przy programowaniu strukturalnym (jak w C), jak stosować do tzw. "programowania zorientowanego obiektowo".
"Obiekt" - zmienna typu zdefiniowanego przez użytkownika. W szerszym znaczeniu - każda zmienna. W C typ użytkownik definiował swój typ stosując struktury. Zawierały one tylko dane. W C++ struktury mogą zawierać również funkcje do obsługi tych danych. Przez "programowanie obiektowe" rozumie się często programowanie z użyciem takich zmiennych.
Ellis i Stroupstrup przez "programowanie zorientowane obiektowo" rozumieją coś bardziej ograniczonego. Stwierdzają oni w swej książce:
" Wykorzystanie klas pochodnych i funkcji wirtualnych jest nazywane często programowaniem zorientowanym obiektowo".
(The use of derived classes and virtual functions is often called object-oriented programming").
Podstawowe cechy programowania obiektowego:
1. Kapsułkowanie (enkapsulacja, encapsulation). Przy pracy z wieloma obiektami, potrzebujemy funkcje do obsługi tych obiektów. Włączamy te funkcje do obiektów i powodujemy, że dane są niedostępne z zewnątrz.
2. Polimorfizm. Możliwość stosowania takiego samego interfejsu do obsługi różnych obiektów. Przykładem polimorfizmu w C jest operator /. Oznacza on zarówno dzielenie całkowite jak i dzielenie liczb rzeczywistych. (5/3 to co innego niż 5.0/3). W C++ zjawiska takie występują znacznie częściej.
3. Dziedziczenie. Przyjmowanie przez jeden obiekt właściwości innego. Pozwala to na klasyfikowanie obiektów. Mamy obiekt bazowy - tzn. pewne dane i funkcje składowe (metody), np kot. Jeśli chcemy rozpatrywać obiekty szczególne, np. sjam i pers, nie musimy w tych obiektach przepisywać metod i danych ogólnych - oba obiekty odziedziczą je po obiekcie kot.
1.2. Rzucające się w oczy różnice, między programem w C i C++.
1.2.1. Miejsce deklaracji zmiennych.
W C, zmienne trzeba było deklarować tuz za pierwszym nawiasem otwierającym blok "{". W C++ miejsce deklaracji zmiennej jest dowolne. Trzeba jedynie zadeklarować zmienna wcześniej, niż się z niej skorzysta. W szczególności, możemy zadeklarować zmienną sterującą instrukcji for tuż przed jej uzyciem:
for(int i = 0; i<10; i++){....};
1.2.2 Operacje wejścia i wyjścia.
include <iostream.h>
main()
{
cout <<"Bury kot\n";
}
Funkcje printf i scanf istnieją również w C++. Jednak wygodniej jest często posługiwać się słowami cin i cout (właściwie obiektami cin i cout) i symbolami >> oraz <<. Symbole te oznaczają również, tak jak w C, przesunięcia bitowe. Dla wbudowanych typów nie trzeba stosować kodu konwersji - kompilator ustawi kody domyślne. Wyjście (i również wejście) możemy formatować za pomocą tak zwanych manipulatorów. Na przykład minimalna szerokość pola dla liczby i ustawiamy manipulatorem setw():
cout << setw(3) << i;
Oto najpopularniejsze manipulatory:
setw(n) ustawia szerokość pola na n
setfill(ch) ustawia znak wypełnienia o kodzie ch
dec wyprowadzanie liczb w systemie dziesiętnym (domyślne)
hex wyprowadzanie liczb w systemie szesnastkowym
oct wyprowadzanie liczb w układzie ósemkowym
endl wstawienie nowej linii (zamiast '\n')
ends wprowadzenie znaku NULL
flush wymycie strumienia
setbase(n) ustawienie podstawy liczenia na n (= 8, 10 lub 16)
setprecision(n) liczba cyfr po kropce dziesiętnej,
setiosflags(l) ustawienie bitów formatu w liczbie l typu long l
resetiosflags(l) wyzerowanie bitów formatu ustawionych w argumencie l typu long
Jeśli manipulator ma parametry, do jego realizacji trzeba dołączyć plik nagłówkowy iomanip.h. Oto ważniejsze bity formatowania wyprowadzanych danych.. Na szczęście, w pliku iostream.h zdefiniowane są nazwy flag formatowania (dla setiosflags() i resetiosflags()).
// formatting flags
enum {
skipws = 0x0001, // skip whitespace on
// input
left = 0x0002, // left-adjust output
right = 0x0004, // right-adjust output
internal = 0x0008, // padding after sign or
// base indicator
dec = 0x0010, // decimal conversion
oct = 0x0020, // octal conversion
hex = 0x0040, // hexadecimal conversion
showbase = 0x0080, // use base indicator on
// output
showpoint = 0x0100, // force decimal point
//(floating output)
uppercase = 0x0200, // upper-case hex output
showpos = 0x0400, // add '+' to positive
// integers
scientific= 0x0800, // use 1.2345E2 floating
// notation
fixed = 0x1000, // use 123.45 floating
// notation
unitbuf = 0x2000, // flush all streams after
// insertion
stdio = 0x4000, // flush stdout, stderr
// after insertion
boolalpha = 0x8000 // insert/extract bools as
// text or numeric
};
Każda z tych flag ma własny bit, możemy więc zakodować kilka z nich, stosując bitowy operator OR |. Flagi te możemy wykorzystywać bezpośrednio, pod warunkiem, że postawimy przed nimi ios::. Oto przykład.
//Demonstracja manipulatorów
#include <iostream.h>
#include <iomanip.h>
main()
{
cout << endl;
cout << "Wyrownanie Wyrownanie Heksa-\n";
cout << "lewe prawe decymalne\n";
for(int i=5; i<=10; i++)
{
cout << setiosflags(ios::left);
cout << setw(5) << i << setw(8) << i*i*i;
cout << resetiosflags(ios::left);
cout << setw(2) << i << setw(8) << i*i*i;
cout << setiosflags(ios::hex|ios::showbase);
cout << setw(8) << i << setw(8) << i*i*i << endl;
cout << resetiosflags(ios::hex);
}
}
Program powyższy daje następujące wyniki:
Wyrownanie Wyrownanie Heksa-
lewe prawe decymalne
5 125 5 125 0x5 0x7d
6 216 6 216 0x6 0xd8
7 343 7 343 0x7 0x157
8 512 8 512 0x8 0x200
9 729 9 729 0x9 0x2d9
10 1000 10 1000 0xa 0x3e8
A oto przykład programu formatującego zmiennoprzecinkową tablicę. Zwróćmy uwagę na flagę fixed, która ustawia przecinek w takim miejscu, że liczby wyrównane są do prawej.
//Formatowanie tablicy
#include <iostream.h>
#include <iomanip.h>
main()
{
cout << "x f(x)\n\n";
cout << setiosflags(ios::fixed);
for(int i=20; i<=40; i+=2)
{
double x=i/10.0;
cout << setw(3) << setprecision(1) << x;
cout << setw(15) << setprecision(10)
<< (x*x + x + 1/x) << endl;
}
}
Wynik działania powyższego programu wygląda następująco:
x f(x)
2.0 6.5000000000
2.2 7.4945454545
2.4 8.5766666667
2.6 9.7446153846
2.8 10.9971428571
3.0 12.3333333333
3.2 13.7525000000
3.4 15.2541176471
3.6 16.8377777778
3.8 18.5031578947
4.0 20.2500000000
Do zagadnień związanych z wejściem i wyjściem jeszcze powrócimy.
1.2.3. Dostęp do zmiennych globalnych.
W języku C, w wewnętrznych blokach mogliśmy zasłonić zmienne globalne. Jeśli nazwę globalną przysłoniliśmy nazwa lokalną, z wewnętrznego bloku nie było do niej dostępu. Obecnie mamy operator odsłaniania ::.
int a=5;
main()
{
int a=3;
cout<< a*::a;
}
Operator :: na równi z nawiasami () ma najwyższy priorytet w C++. Nie musimy więc stosować nawiasów. Program wydrukuje 15.
1.2.4. Parametry domyślne funkcji.
Funkcje można deklarować bądź definiować z parametrami domyślnymi. Wgląda to w ten sposób
void fun(int a=0, float b=0, double c=0)
{
...
}
W programie można funkcję wywołać np tak:
fun(); - wtedy wszystkie parametry przyjmą swe wartości domyślne.
fun(5) - parametry b i c przyjmą wartości domyślne.
fun(5, 3.0), fun (1, 3.0, 6.0) itd. Parametry niewymienione w wywołaniu przyjmą swoje wartości domyślne.
Uwaga!
Jeśli w funkcji występują parametry z wartościami domyślnymi, muszą one znajdować się na końcu listy parametrów.
Jeśli parametry domyślne podajemy w deklaracji funkcji, a nie definicji, nazwę parametru można pominąć.
void fkot(int=5, float=3.0, double=5.0);
1.2.5 Przeładowywanie (lub przeciążanie) nazw funkcji.
W języku C każda funkcja powinna mieć swoja indywidualna nazwę. W C++ funkcje mogą mieć takie same nazwy, pod warunkiem, że mają różne parametry. Funkcje różnych typów z jednakowymi parametrami uważane są za tożsame. Przy rozróżnianiu parametrów, musimy brać pod uwagę ich "typy podstawowe" - tzn. typy int, int*, int& są uważane za tożsame.
Przeładowanie nazw funkcji może doprowadzić do kłopotów, jesli jednocześnie stosujemy parametry domyślne czy wskaźniki do funkcji. Reguły są zdroworozsądkowe. Ogólna zasada: przeładowanie nazw funkcji powinno czynić program bardziej jasnym, a nie bardziej zawikłanym.
Przykład
funkcja biblioteczna double sqrt(double);
funkcja przybliżona int sqrt(int);
1.2.6. Dynamiczna alokacja pamięci.
Tak jak w C, w języku C++ możemy posługiwać się funkcjami malloc(), calloc() i realloc(). Istnieje jednak nowy operator new rezerwujący pamięć. Działa on w nastepujacy sposób:
int n;
char *s;
.........
cin >> n;
s = new char[n];
Do stosowania tego operatora nie potrzeba żadnych plików nagłówkowych. Do elementów bufora możemy odwoływać się s[0], s[1] itd. W przypadku, jeśli pamięci nie da się zarezerwować, operator new zwraca NULL.
Zamiast funkcji free() stosujemy operator delete. Na przykład, żeby zwolnić bufor s z przykładu powyżej, piszemy:
delete s;
Odmianą operatora delete jest operator delete[]. Dla bufora s możemy napisać delete[] s, ale wynik jest taki sam, jak dla delete s. W literaturze operator delete[] stosuje się raczej do usuwania tablic obiektów.
Nowe metody przydziału pamięci są łatwiejsze w użyciu od starych. Jednak zmiana rozmiaru uprzednio przydzielonej pamięci łatwiejsza jest przy użyciu funkcji realloc().Niestety mieszanie obydwu sposobów rezerwacji pamięci nie jest wskazane. Ponadto, operator new służy również do innych celów, prócz rezerwacji buforów pamięci, tak że najczęściej z klasycznych funkcji trzeba zrezygnować.
1.2.7. Referencje.
Jeśli deklarujemy zmienną np.
int a;
oznacza to, że pewnemu miejscu w pamięci (o adresie &a) nadaliśmy nazwę a. Gdy piszemy
a=5;
oznacza to, ze do "komórki" pamięci o nazwie a wpisaliśmy liczbę całkowitą 5. W C, każda taka "komórka" pamięci mogła mieć tylko jedną nazwę. W C++ "komórka pamięci" może mieć wiele nazw. W dalszym ciągu, zamiast "zmienna" czy "komórka pamięci" będziemy stosowali nazwę "obiekt".
Jednemu obiektowi możemy nadać kilka nazw w następujący sposób:
int a, &b=a;
b jest drugą nazwą, obiektu a, nazywamy ja referencją lub "odsyłaczem" do tego obiektu. Kiedy w programie zmienimy a, zmienimy też b, i odwrotnie - jeśli zmienimy b, zmienimy a. Również adres b jest tożsamy z adresem a - b jest po prostu jeszcze jedna nazwą zmiennej a.
Pierwsze zastosowanie referencji natychmiast się narzuca - możemy przekazywać do wnętrza funkcji obiekty bezpośrednio - jeśli parametrami formalnymi będą referencje do zmiennych w programie, to gdy zmienimy wewnątrz funkcji te parametry, zmienne w programie głównym zmienią się - w rezultacie otrzymamy podobny mechanizm jak w Pascalu czy Fortranie.
Rozpatrzmy na przykład funkcje swap1 i swap2 w poniższym programie
# include <iostream.h>
swap1(int *x, int *y)
{
int temp;
temp=*x;
*x=*y;
*y=temp;
return 0;
}
void swap2(int &x, int &y)
{
int temp=x;
x=y;
y=temp;
}
main()
{
int a=6,b=3,c=4,d=8;
swap1(&a,&b);
swap2(c,d);
cout<<"Zamienione liczby 6,3\t"<<a<<"\t"<<b<<"\n";
cout<<"Zamienione liczby 4,8\t"<<c<<"\t"<<d;
return 0;
.
Obie funkcje, swap1() i swap2() zamieniają wartości dwóch zmiennych. W funkcji swap1() posługujemy się adresami zmiennych x i y. Zmienne x i y wewnątrz funkcji swap1() są w programie kopiami adresów obiektów a i b. Jednak kopie adresów są w dalszym ciągu adresami i jeżeli zmienimy zmienna o adresie x (tzn. zmienną *x) zmienimy a.
W procedurze swap2() zmienne x, y są odsyłaczami do zmiennych c i d. Jeśli zmienimy x zmienimy też c.
Referencje umożliwiają nam jeszcze jedna konstrukcję programową. Rozpatrzmy funkcję, która coś "zwraca". Zwracanie jest to właściwie podstawianie wartości wyrażenia znajdującego się za słowem kluczowym return pod nazwę funkcji. Jeśli wyrażenie zwracane będzie referencją do zmiennej w programie głównym (lub zmienna globalną), to możemy nazwę funkcji zaopatrzyć w znak referencji &. Po zakończeniu działania funkcji jej nazwa będzie referencja do odpowiedniej zmiennej w programie głównym. Wynika stad, że pod nazwę funkcji możemy coś podstawić - nazwa funkcji jest "L-wartością". Najlepiej widać to na przykładzie:
// Referencja jako wartosc zwracana.
#include <iostream.h>
int &smaller(int &x, int &y)
{ return (x < y ? x : y);
// uwaga: nie mozna tu zwracac zmiennej lokalnej
}
main()
{
int a=23, b=15;
cout << "a = " <<a << " b = " << b <<endl;
cout << "Mniejsza z tych liczb to " << smaller(a, b) << endl;
smaller(a, b) = 0; // wywolanie funkcji po lewej stronie!!
cout << "Mniejsza z liczb a i b jest obecnie rowna zeru. Wobec tego \n";
cout << "a = " << a << " b = " << b << endl;
}
Po wykonaniu programu mniejsza z dwóch liczb a i b będzie równa zeru.
1.2.8. Funkcje wewnętrzne (lub otwarte - inline)
Funkcja wewnętrzna to funkcja, która nie jest wywoływana, lecz której kod jest wstawiany bezpośrednio w miejsce, w którym funkcja ma być wywoływana. Jeśli na przykład chcemy obliczyć sumę n liczb naturalnych 1+2+3+ +n= 0.5n(n +1), piszemy:
inline int sum(int n) {return n*(n+1)/2;}
Później wołamy tę funkcje jak zwykle. Na przykład
y=1.0/sum(k+1);
jest równoważny czemuś takiemu:
{int t; y = 1.0/ (t=k+1, t*(t+1)/2);}
Zauważmy, ze wartość argumentu k+1 obliczana jest tylko raz.
Funkcje wewnętrzne przypominają makra, ale są od nich elastyczniejsze.
makro, realizujące tę sama funkcję miałoby postać:
#define sum(n) ((n) * ((n) + 1) / 2)
i trzeba byłoby na przykład zwracać uwagę na dodatkowe nawiasy.
1.2.9. Struktury.
W C++ podobnie jak w C struktura określa nam typ obiektów. Niech struktura ta wygląda w ten sposób:
struct kot
{
........
};
Jeśli chcieliśmy zadeklarować obiekt Mruczek, w C trzeba by było napisać tak:
struct kot Mruczek;
W C++ wystarczy napisać:
kot Mruczek;
Ponadto w skład struktur w C++ mogą wchodzić funkcje. Pojęcie struktury w C++ jest właściwie zamienne z pojęciem klasy. Jedyna różnica polega na tym, że składowe struktury są domyślnie publiczne, a składowe klasy - domyślnie prywatne.
10
Piotr Staniewski
C++ Studia Zaoczne. Wykład 1.