Rok akademicki 1999/2000
Wykład „Języki programowania C/C++”
Prof. dr hab. Z. Gburski, Instytut Fizyki UŚl.
Literatura.
B. Keringham, D. Ritchie, Język C, WNT Warszawa (1987).
J. Bielecki, ANSI C++, Intersoftland, Warszawa (1997).
H. M. Deitel, P. J. Deitel, C++ Programowanie, Arkana, Warszawa (1998).
J. Grębosz, Symfonia C++ tom I, II, III, Oficyna Kallimach, Kraków (1999).
J. Grębosz, Pasja, tom I, II, Oficyna Kallimach, Kraków (1999).
Majczak, Praktyczne programowanie w C++, Intersoftland, Warszawa (1994).
Książki dotyczące środowiska Microsoft Visual C++ 6.0;
J. Bielecki, Visual C++ 6, Helion, Gliwice (1999)
Beck Zaratin, Visual C++ 6.0 Programmer's Guide, MicrosoftPress, Redmonton, Washington, USA (1998).
D. Kruglinski, G. Shepherd, S. Wingo, Programming Visual C++, Fifth Edition,
MicrosoftPress, Redmonton, Washington, USA (1998)
S. Holzner, Visual C++, Helion, Gliwice (1999).
Klasyfikacja języków programowania:
- niskiego poziomu (maszynowe, asemblery), związane z danym typem procesora
- wysokiego poziomu, niezależne od rodzaju procesora
- ogólnego przeznaczenia, do tworzenia bardzo różnorodnego oprogramowania
- specjalizowane, np. do obliczeń inżynierskich i naukowych (Fortran), ekonomicznych (Cobol).
Grupa języków wysokiego poziomu i ogólnego przeznaczenia;
Algol, Ada, Pascal/Delphi, C, C++, Java
Nieco historii.
C rozwinął się z dwóch innych języków BCLP i B. BCLP stworzył M. Ritchards w 1967 r. W 1970 r Ken Thompson opracował język B dla minikomputerów PDP - 7 firmy DEC (USA). B miał wiele ograniczeń, niedoskonałości.
W 1972 r Dennis Ritchie i Ken Thompson stworzyli język C, „spadkobiercę” B. Przez pierwsze 6 lat C nie był bynajmniej popularny, stosowała go wąska grupa programistów. Przełom nastąpił w 1978 r po opublikowaniu książki „The C programming language” (B. Keringhan, D. Ritchie).
1983 Bjarne Stroustrup opublikował pierwszą wersję języka C++
American National Standards Institute ( komisja X3J16) ustala, co kilka lat standard języka C/C++, ostatnia standaryzacja w końcu 1997 r.
Kompilatory C/C++.
Istnieją dla wszystkich typów maszyn, dla superkomputerów, stacji roboczych i PC.
Najbardziej znane komercyjne kompilatory dla PC;
Borland C++ Builder, Visual C++ 6.0 (Microsoft), Watcom, Symantec,
IBM C++ Set
Pierwszy przykład programu w języku C,
/* program pierwszy */
#include <stdio.h>
void main( )
{
printf (”Hello World!”);
}
łańcuch znaków (komentarz) zawarty pomiędzy /* i */ jest pomijany przez kompilator,
służy programiście do opisu wybranych fragmentów programu.
C i C++ rozróżnia duże i małe litery. Format zapisu swobodny, oznacza to, że tak zwane białe znaki (ang. white space characters) są pomijane przez kompilator (podobnie jak komentarze), służą one tylko do zwiększenia czytelności tekstu programu. Białe znaki, nazywane niekiedy odstępami to;
spacja, tab, linefeed, carriage-return, formfeed, vertical-tab, newline.
#include <stdio.h> dyrektywa preprocesora, dołącz plik o nazwie stdio.h do tekstu programu i następnie wykonaj kompilację
main( ) funkcja główna, musi się nazywać main. Wykonywanie każdego programu rozpoczyna się od funkcji main( ), dlatego ta funkcja nazywa się główna. Skompilowany program zaczyna się od wykonywania instrukcji zawartych w funkcji głównej. Słowo void przed nazwą funkcji głównej oznacza, że ta funkcja nie zwraca żadnej wartości, ona coś robi (w tym wypadku wypisuje na ekranie łańcuch znaków zawartych w cudzysłowie), ale w wyniku jej działania nie pojawia się
żadna liczba.
Uwaga: w wielu programach spotkamy funkcję główną poprzedzoną nie przez void lecz przez int, np. int main( ) . Oczywiście int to skrót od ang. integer czyli całkowita.
Funkcja główna może być poprzedzona tylko przez int albo void. Jeżeli występuje sama nazwa main( ) - nie poprzedzona int albo void - to domniema się, że chodzi o int main( ).
Do omówienia funkcji int main( ) wrócimy wkrótce, po omówieniu typów zmiennych występujących w C/C++.
printf (”.......”); wyprowadź na domniemane wyjście (monitor) łańcuch znaków zawarty w cudzysłowie funkcja printf( ) jest zdefiniowana w pliku stdio.h.
Ten sam program w C++,
#include <iostream.h> // pierwszy program w C++
void main ( )
{
cout << ”Hello World!”;
}
W C++ oprócz komentarza zawartego pomiędzy /* */ można także pisać komentarz po // , ale tylko do końca linii (jednowierszowy komentarz). Taki komentarz nie jest dopuszczalny w standardzie C, ale niektóre kompilatory to umożliwiają używanie jednowierszowego komentarza również w C (jako opcja niestandardowa).
Instrukcja cout << ” ......” ; wyprowadza na monitor łańcuch znaków występujący w cudzysłowie, w naszym przykładzie napis: Hello World!
Przy pomocy cout (ang. console output) potrafimy również wyświetlać na ekranie liczby
np. cout << 102; // wyświetli liczbę 102
cout << 1 << 0 << 2; // też wyświetli liczbę 102, podwójny znak mniejszości jest operatorem wstawiania, wstawia znaki do strumienia wyjściowego, można ten operator użyć wielokrotnie w danej instrukcji (instrukcja w C/C++ zwykle kończy się średnikiem).
Przy wyprowadzeniu łańcucha znaków na monitor instrukcja cout << ” .....” ; korzysta z pliku iostream.h, który należy dołączyć dyrektywą #include .
Uwaga: gdy używając zaawansowanych kompilatorów pracujących w środowisku Windows 95/98 lub NT wybieramy jedną z typowych scieżek tworzenia aplikacji, wówczas niekiedy wczytywane są pliki o innych nazwach (np. Visual 6.0 wczytuje często <stdafx.h>). Zwykle wewnątrz tych plików znajdują się dyrektywy #include wczytujące pliki „standardowe” stdio.h, iostream.h, ...itd.
Specjalne znaki stosowane w strumieniu wyjściowym,
\n // nowa linia
np. instrukcja
cout << ”pierwsza linia \n druga linia”;
na powoduje na ekranie napis
pierwsza linia
druga linia
\a alarm (sygnał dźwiękowy)
\b cofnięcie o jedno miejsce (backspace)
\f przejście na początek następnej strony (formfeed)
\r przejście na początek bieżącej linii (powrót karetki)
\\ backslash ( \ )
\' apostrof
\” cudzysłów
\? znak zapytania
\0 znak pusty
\t tabulacja pozioma
\v tabulacja pionowa
Aby przejść do nowej linii, w C++ oprócz znaku specjalnego \n stosuje się endl, oznaczające to samo, np
cout << 1 << endl << 0 << endl << 2 ;
powoduje wyświetlenie
1
0
2
Uwaga: jeżeli stosujemy w strumieniu cout sam znak specjalny, to musi występować w apostrofie np. `\n' lub w cudzysłowie „\n” .
Zmienne.
Nazwy zmiennych w C/C++ mogą zaczynać się od litery (dużej lub małej) lub znaku podkreślenia, nie mogą zaczynać się np. od cyfry, znaków $, @, itp. Nazwy muszą być różne od tzw. słów kluczowych.
Słowa kluczowe w C++;
asm auto bad_cast bad_typeid bool break case catch char class const const_cast continue default delete do double dynamic_cast else enum except explicit extern false
finally float for friend goto if inline int long mutable namespace new operator private protected
public register reinterpret_cast return short signed sizeof static static_cast struct switch template this throw try type_info typedef typeid union unsigned virtual void volatile while
Uwaga: słowa kluczowe mogą być częścią składową nazwy, np. vvoid, a1class, ddo
Są to dopuszczalne nazwy, bo różnią się od słów kluczowych.
Zanim zmienna zostanie użyta, musi zostać zadeklarowana. Deklaracja składa się z typu zmiennej i jej nazwy (identyfikator zmiennej) np
int i; // deklaracja zmiennej typu całkowitego o nazwie i
int i, j, k, l;
Typy zmiennych;
- wbudowane (ang. buid-in) czyli takie, w które język C/C++ jest wyposażony
zdefiniowane przez użytkownika, programista może je sam wymyślać
Typy wbudowane.
short int // inaczej short
int
long int // inaczej long
short k;
int m;
long n;
Np. w kompilatorze Microsoft Visual 6.0 dla komputerów PC
short 2 bajty
int 4 bajty
long 4 bajty
Każdy wariant (short, int, long) może być signed lub unsigned, czyli ze znakiem lub bez znaku.
signed int j; // j może przyjmować wartości dodatnie lub ujemne
unsigned int j; // j może przyjmować tylko wartości dodatnie
Przez domniemanie przyjmuje się, że
int j;
oznacza
signed int j;
czyli typ całkowity ze znakiem (wartości ujemne dopuszczalne)
Liczby rzeczywiste (zmiennoprzecinkowe)
float x;
float x, y, z;
Np. w kompilatorze Microsoft Visual 6.0 dla PC
float 4 bajty
double 8 bajtów
long double 8 bajtów
Deklaracja zmiennej wraz z jej inicjalizacją,
int j=5;
int i=7, j=3, k=100;
float x=10.1;
double x=10.1, y=20.6, z=100.55;
Typ znakowy,
char a;
char a, znak1, znak2;
numer znaku w kodzie ASCII (liczba całkowita)
Na zmienne typu char rezerwuje się 1 bajt pamięci.
char znak; // zarezerwuj w pamięci miejsce dla zmiennej o nazwie znak
znak = `A' ; /* zmiennej o nazwie znak nadaj wartość całkowitą równą numerowi
(kodowi) ASCII litery A, kod ASCII litery A to 65 */
Do wczytywania danych z klawiatury służy cin (ang. console input)
Przykład,
# include <iostream.h>
void main (void) {
int liczba;
cout << ” Type a small integer number and press Enter” ;
cin >> liczba;
cout << ”Your favorite number is ”<< liczba << endl; }
cin >> liczba1 >> liczba2 >> ; // wczytuje dwie liczby
Uwaga: cin używa pierwszego niedrukowanego znaku (spacja, tabulator,...) do ustalenia gdzie kończy się pierwsza i zaczyna druga liczba.
Operator sizeof podaje rozmiar (w bajtach) typu zmiennej lub rozmiar zmiennej np.
sizeof (int); /* podaje rozmiar w bajtach typu int */
sizeof (x); /* podaje rozmiar w bajtach zmiennej x */
Konwersja zmiennych.
To dość obszerne zagadnienie, tutaj tylko sygnalizuję.
Ogólnie, domyślnie następuje „równanie w górę” np. dodanie zmiennej całkowitej do rzeczywistej da w rezultacie liczbę rzeczywistą. Np.dzielenie dwóch liczb całkowitych da liczbę całkowitą, część ułamkowa zostanie obcięta.
int i=3;
float x;
x=i; // x =3.0 bo nastąpi konwersja int do float
int j;
float x=3.14;
j=x; // j=3 bo nastąpi konwersja float do int, bo i ma być integer
register int j;
Jeżeli w rejestrze/rejestrach procesora jest wolne miejsce, to zmienna całkowita “i” ma tam przebywać. Może to niekiedy przyspieszyć działanie programu.
Inkrementacja i dekrementacja.
j = ++i; przedrostkowa, zwiększenie wartości zmiennej całkowitej “i” o 1 przed jej użyciem
j = i++ ; przyrostkowa, zwiększenie wartości całkowitej “i” o 1 po jej użyciu
np.
int i, j=4;
i=++j; // w rezultacie i=5 a od teraz j=5
j=4;
i=j++; // w rezultacie i=4 a od tego momentu j=5
Stałe znakowe.
char z; // deklaracja zmiennej znakowej o nazwie z
z = `a`; // za z podstaw stałą znakową 'a'
Pisze się w apostrofach pojedynczych, `0` oznacza cyfrę/znak zero a nie liczbę zero,
`9` oznacza cyfrę/znak dziewięć a nie liczbę/wartość całkowitą 9. Można stałe znakowe zapisywać bezpośrednio, podając między apostrofami liczbowy kod ASCII znaku, zamiast samego znaku. Kod znaku musi być w zapisie ósemkowym lub szestnastkowym.
Litera a w kodzie ASCII to 97 dlatego
`a` lub `0141` lub `0x61`.
Stałe będące liczbami całkowitymi możemy zapisać w postaci dziesiątkowej, jak do tego przywykliśmy, np. 14, -31, 0, 1001 itd.
Jeżeli zapis stałej całkowitej zaczniemy od cyfry 0 (zero) to kompilator zrozumie, że podajemy mu wartość liczby w zapisie ósemkowym (oktalnym) np.
010 // 1*81 + 0* 80 = 1*8 + 0*1=8, dziesiątkowo 8
014 // dziesiątkowo 8+4 =12
091 // dziesiątkowo
Jeżeli stała całkowita zaczyna się od 0x lub (0X) to kompilator uzna, że zapis jest szestnastkowy (heksadecymalny) np.
0x10 1*16^1 + 0*16^0 = 16
0xa1 10*16^1 +1*16^0 = 161
0xff 15*16+15=255
Znaki a, b, c, d, e, f oznaczają odpowiednio 10, 11, 12, 13, 14, 15 w zapisie dziesiątkowym. W zapisie można posługiwać się małymi lub dużymi literami.
Stałe całkowite traktuje się jak typu int, jeżeli wartość stałej nie mieści się w przydzielonej dla int ilości bajtów, to konwersja do long. Można świadomie zmienić typ stałej z int na long, dopisując na końcu liczby literę l lub L np.
0L, 124L, 55l
także unsigned
57u, 57uL
Łańcuchy.
Łańcuch (ang. string) pisze się w cudzysłowie ”lalala”, łańcuch może zawierać wiele znaków, natomiast stała znakowa tylko jeden znak. Łańcuch jest przechowywany w pamięci jako ciąg znaków, na samym końcu ciągu dodawany jest znak o kodzie 0 (zero) czyli znak NULL, tak kompilator oznacza sobie koniec łańcucha. Wobec tego `a` oraz ”a” to nie jest to samo dla kompilatora.
Tablice
int tab[30]; deklaracja tablicy 30-elementowej (wszystkie elementy tej tablicy o nazwie tab są typu int), elementy numeruje się zaczynając od zera
double dane[100];
int szachy [8] [8]; /* deklaracja tablicy dwuwymiarowej */
double rubik [6] [6] [6] /* deklaracja tablicy 3-wymiarowej */
Słowo kluczowe typedef
nadaje nazwę dowolnemu typowi zmiennych, np.
typedef unsigned long ULONG
nie tworzy nowego typu zmiennych lecz jedynie nadaje nową nazwę istniejącemu typowi zmiennych (synonim typu istniejącego).
np.,
typedef int cena;
Teraz mogę zadeklarować
cena i; // odpowiada int i;
cena j, k, l; // odpowiada int j,k, l;
Po co robić taką gimnastykę ?
Stwierdzamy, że dokładność cen towarów do 1 zł jest za mała, chcemy podawać także grosze, a więc należy przejść do typu float. Co robimy, zmieniamy instrukcję typedef int cena; na
instrukcję
typedef float cena;
Tym sposobem wszystkie miejsca programu, gdzie używamy zmiennych typu cena są float, teraz
cena x; // float x;
cena y, z, a; // float y, z, a;
Nie trzeba nic zmieniać w programie, tylko jedną deklarację.
Modyfikator const (C++)
Mówiliśmy o stałych dosłownych, liczbach. Niekiedy chcemy mieć obiekt, którego wartości nie chcielibyśmy zmieniać, nawet przez nieuwagę, obiekt stały, np. obiekt oznaczający liczbę pi.
Wtedy,
const float pi=3.14; // średnik
Wartość liczbową nazwie/stałej pi możemy nadać tylko teraz, jest to deklaracja z inicjalizacją, póżniej już nie będziemy w stanie podstawić jakiejkolwiek innej wartości pod zmienną/stałą pi.
Obiekt const można inicjalizować, ale nie można mu nic przypisać.
W C i C++ występuje dyrektywa #define np.,
# define pi 3.14 // przy dyrektywie nie ma średnika na końcu
Działanie dyrektywy #define jest takie, że zastępuje w programie wszystkie pi przez 3.14 i zapomina o symbolu pi, nazwa/zmienna pi jest nieznana, kompilator nie potrafi sprawdzić czy dana nazwa została użyta w ramach jej zakresu ważności.
Posłużenie się # define jest jakby zamianą nazwy na stałą liczbę (stałą dosłowną).
Posłużenie się const sprawia, że powstaje w pamięci normalna zmienna, określonego typu (int, float,...), dodatkowo ta zmienna ma jak gdyby ostrzeżenie "nie zmieniać raz nadanej wartości pod żadnym pozorem".
Jeżeli dyrektywą #define określiliśmy jakąś nazwę, to obowiązuje ona od wystąpienia tej dyrektywy do końca pliku. Odwołanie tego następuje dyrektywą
#undef
np.,
#define cztery 4
.....................
undef cztery
Zmienne typu wyliczeniowego
enum nazwa/identyfikator {zbiór elementów};
enum kolor {fiolet, niebieski, zielony, zolty, czerwony};
Typ wyliczeniowy o nazwie kolor z 5 elementami o nazwach wymienionych w { }.
Elementy typu wyliczeniowego przyjmują wartości całkowite (nieujemne), w kolejności wyliczania, poczynając od zera, a więc fiolet==0, niebieski==1,...czerwony==4.
enum tydzien {poniedzialek, wtorek, sroda, czwartek, piatek, sobota, niedziela};
Można wymusić nadanie elementom żądanych wartości całkowitych,
enum plec {mezczyzna=1, kobieta=2, nieznana=0};
przypisane wartości mogą się powtarzać. Jeżeli niektórym elementom nadajemy wartości, innym nie, to te które nie mają nadanej przez programistę wartości przyjmują wartość o 1 większą od wartości nadanej poprzednikowi.
Np.,
enum plec {mezczyzna=1, kobieta=2, nieznana};
wtedy nieznana==3 (domyślnie)
enum temperatura {niska=35, srednia=38, wysoka=40, alarm};
Skoro zdefiniowaliśmy typ o nazwie tydzien, teraz możemy zadeklarować zmienne tego typu np.
tydzien dzien;
oznacza to, że dzien jest zmienną za którą można podstawić tylko jedną z wymienionych w
{ } nazw, tj.
dzien=wtorek; // to jest O.K.
dzien=niedziela; // to jest O.K.
Tylko jedną z wymienionych nazw, nic innego.
dzien=1; // to jest źle, pomimo że wtorek==1 . Dzięki temu nawet przez nieuwagę nie jesteśmy w stanie wpisać do zmiennej dzien czegoś innego, nawet gdyby przez przypadek pasowało jako wartość liczbowa.
int k=3;
dzien=k; // to jest błędne
INSTRUKCJE STERUJĄCE.
W języku C nie ma zmiennych typu logicznego (boolean), które występują w wielu innych językach programowania. Wartość logiczna reprezentowana jest w C w ten sposób, iż wartość wyrażenia równa zero oznacza fałsz, wartość wyrażenia różna od zera oznacza prawdę.
if (warunek)
instrukcja;
Na przykład,
int i, j, k;
i=2;
j=3;
if (i < j) k=i+j;
Pojedyncza instrukcja może być zastąpiona przez blok instrukcji
if (wyrażenie/warunek)
{
// blok instrukcji
}
if ( i >= 0 && j >= 0 )
{
k=i+j;
z=i-j;
}
Bardziej rozbudowana forma
if (warunek/wyrażenie)
instrukcja 1;
else
instrukcja 2;
przykład,
int i,j,k;
if (i<=j)
k=j-i;
else
k=i-j;
Pętle iteracyjne (są dwie).
While (wyrażenie/warunek)
instrukcja ;
Jeżeli wyrażenie ma wartość 1 (warunek jest spełniony/prawdziwy) to instrukcja jest wykonywana i ponownie przystępuje się do testowania prawdziwości warunku w nawiasach itd.
Instrukcja wykonywana jest tak długo, jak długo jest spełniony/prawdziwy warunek, instrukcję wykonuje się dopóty, dopóki wyrażenie przyjmuje wartość różną od 0. Dopiero gdy wyrażenie w nawiasie przyjmie wartość zerową, nastąpi przerwanie pętli. Uw. Jeżeli wartość wyrażenia w nawiasach ( ) od razu jest równa 0, to instrukcja w ogóle nie jest wykonywana.
Przykład,
main (void ) {
int vec[5] = {5 , 10, 15, 20, 25};
int j = 3;
while ( j > 0) {
cout << vec[j];
--j;
}
} // program wyprowadzi na ekran liczby 20, 15, 10
do
{
instrukcja 1;
instrukcja 2;
...............;
...............
instrukcja n;
}
while (wyrażenie/warunek);
Rób ... dopóki
Najpierw wykonywana jest instrukcja, następnie obliczona wartość (sprawdzany warunek), jeżeli wartość różna od zera (prawda), to wykonanie instrukcji zostanie powtórzone i znowu jest sprawdzany warunek (wartość wyrażenia w nawiasach ( ) ) i tak w kółko, dopóki wyrażenie będzie różne od zera. Wartość wyrażenia jest badana po wykonaniu instrukcji, a nie przed, jak w petli while. Instrukcja zostanie wykonana co najmniej jeden raz, nawet gdy wyrażenie nie będzie prawdziwe od początku.
Np.
main ( void) {
int vec[5] = {5, 10, 15, 20, 25 };
int j=0;
do {
cout << vec[j];
j=j+1;
} while (j<4);
} // wyprowadzenie wartości 5, 10, 15, 20
Pętla for.
Ma formę,
for (instr_ini; wyrażenie/warunek; instr_krok)
instrukcja;
Najpierw wykonywana jest instr_ini, potem sprawdza się wyrażenie/warunek, jeżeli wartość wyrażenia/warunku różna od zera (prawda), to wykonywane są instrukcje będące treścią pętli.
Instr_krok jest wykonywana na zakończenie każdego obiegu pętli.
np.
for (i=0 ; i < 11 ; i=i+1)
{
cout << ” a ku-ku !!';
}
Szczegóły:
instr_ini - nie musi być tylko jedną instrukcją, może być ich kilka, wtedy są oddzielone przecinkami. Podobnie jak w instr_krok.
Wyszczególnione elementy: instr_ini, wyraz_warun, instr_krok - nie musza wystąpić. Dowolny z nich można pominąć, zachowując jednak średnik oddzielający go od sąsiada. Opuszczenie wyrażenia warunkowego traktowane jest tak, jakgdyby stało tam wyrażenie zawsze prawdziwe.
Np. zapis
for ( ; ; ) {
.......
{
jest nieskończoną petlą. Inny typ nieskończonej pętli to
while (1)
{
// tutaj jakiekolwiek instrukcje
....... }
Instrukcja wyboru switch.
Switch (wyrażenie)
{
case wart_1:
instr 1;
break;
case wart_2:
instr 2;
break;
.......
case wart_n;
instr n;
break;
default :
instr ;
break;
}
Obliczane jest wyrażenie w nawiasie przy słowie switch, jeżeli jego wartość odpowiada którejś z wartości podanej w jednej z etykiet case, wówczas wykonywane są instrukcje począwszy od tej etykiety, wykonywanie ich kończy się po napotkaniu instrukcji breake. Powoduje to wyskok z instrukcji switch, poza jej końcową klamrę. Jeżeli wartość wyrażenia nie zgadza się z żadną z wartości podanych przy etykietach case, to wykonywane są instrukcje po etykiecie default. Etykieta default może być w dowolnym miejscu instrukcji switch, nawet na jej samym początku. Co więcej, etykiety default może w ogóle nie być. Jeżeli wartość wyrażenie nie zgadza się z żadną z wartości przy etykietach case, a etykiety default nie ma wcale, to opuszcza się instrukcję switch nie wykonując niczego. Instrukcji występujących po etykiecie case nie musi kończyć instrukcja breake, gdy jej nie ma, to wykonywane są instrukcje po następnej etykiecie case. Uwaga, C++ po napotkaniu przypadku zgodnego z badanym wyrażeniem zakłada, że wszystkie umieszczone po tym przypadku wartości wyrażeń przy kolenych słowach case w klamrach instrukcji switch są także równe wyrażeniu, a więc następujące po nich instrukcje są wykonywane.
Np.
int n;
switch (n) {
case 3 : cout << ”*”;
case 2 : cout << “”-“ ;
case 1 : cout << “”!” ;
break; }
W zależności od wartości zmiennej n możliwe są następujące wydruki na ekranie
dla n=3 *-! dla n=2 -! Dla n=1 !
dla innego n nic się nie wydrukuje.
Instrukcja break.
Przerywa instrukcję switch, powoduje także natychmiastowe zakończenie/przerwanie wykonywania pętli; for, while, do ... while.
Jeżeli mamy do czynienia z kilkoma pętlami, zagnieżdżonymi jedna wewnątrz drugiej, to instrukcja break powoduje przerwanie tylko tej najbardziej wewnętrznej pętli, w której występuje, tkwi. Jest to przerwanie z wyjściem o jeden poziom wyżej.
int j=10;
while (5) {
cout << "Petla, j=" << j << "\n";
j=j-1;
if (j < 8 ) {
cout << "Stop !";
break; } }
Wyświetli na ekranie:
Petla, j=10
Petla j=9
Petla j=8
Stop !
Instrukcja continue
Przydaje się wewnątrz pętli for, while, do ... while. Powoduje ona zaniechanie wykonywania instrukcji będących treścią pętli, jednak (w przeciwieństwie do instrukcji break) sama pętla nie zostaje przerwana. continue przerywa tylko bieżący obieg pętli i zaczyna następny obieg, kontynuując pracę pętli. Innymi słowy, napotkanie instrukcji continue odpowiada natychmiastowemu przejściu/skokowi na sam koniec pętli, do etykiety stojącej przed klamrą zamykającą pętlę, komputer "pomyśli", że wykonał treść pętli i przystąpi do następnego obiegu. W ten sposób instrukcje pętli leżące poniżej continue, aż do końca pętli, nie będą realizowane. Przykład,
int i;
for { i=0; i< 10; i=i+1} {
cout << "Hej" ;
if (i>1) continue;
cout << "Ha" << endl;
}
W rezultacie na ekranie pojawi się
HejHa
HejHa
HejHejHejHejHejHejHejHej // 8 razy
Instrukcja goto
goto etykieta;
Po napotkaniu takiej instrukcji program przenosi się do miejsca, gdzie jest dana etykieta.
Etykieta jest to nazwa, po której następuje dwukropek. Wszystkie etykiety w ramach jednej funkcji muszą mieć unikalne nazwy. Instrukcja goto przenosi sterowanie tylko w ramach strefy ważności (np. tej samej funkcji).
Np.
if (jest_blad)
goto koniec;
.....................
koniec:
return jest_blad;
Instrukcja goto przydaje się np. dla natychmiastowego opuszczenia wielokrotnie zagnieżdżonej pętli. Instrukcją break przerwiemy tylko najbardziej zagnieżdżoną pętlę, dzięki goto jesteśmy w stanie wyskoczyć od razu na zewnątrz zagnieżdżonych pętli (zawsze jednak tylko w ramach tego bloku programu, w którym jest znana etykieta).
FUNKCJE.
Funkcja jest grupą powiązanych deklaracji i instrukcji realizujących określone zadanie. Instrukcje funkcji są ujęte w nawiasy klamrowe.
Deklaracja funkcji zawiera;
typ_wartości nazwa_funkcji (lista parametrów); // średnik, bo to tylko deklaracja, niekiedy nazywana też prototypem funkcji
Definicja zawiera
typ_wartości nazwa_funkcji (lista parametrów) // nie ma średnika
{
treść funkcji czyli
deklaracje;
..............
.............
instrukcje;
.............
..............
}
Przykład,
int suma (int i, int j) {
int k;
k=i+j;
return k; }
Typ wartości to typ wartości zwracanej przez funkcję. Instrukcja return zwraca wynik funkcji do programu wywołującego, program po napotkaniu return zwraca wartość stojącą na prawo obok return i kończy działanie funkcji. Jeżeli funkcja nie zwraca żadnej wartości, to należy ją poprzedzić słowem void. Czasem spotyka się instrukcję
return; // bez wyrażenia w nawiasach
wtedy funkcja jest typu void, nie zwraca żadnej wartości, instrukcja return po prostu kończy działanie funkcji, jeżeli za return są jakieś instrukcje, to nie będą wykonane.
Pisanie return; na końcu funkcji typu void nie jest konieczne, ponieważ obecność return; na końcu funkcji bezrezultatowej (void) jest domniemana.
Jeżeli funkcja nie ma parametrów, to programując w C++ umieszczaj w nawiasie (void), słowo void.
Uwaga !
Pusty nawias w deklaracji funkcji f ( ) ; oznacza
- w języku C dowolną liczbę argumentów, to samo co f (...);
- w języku C++ brak jakichkolwiek argumentów, czyli to samo co f (void);
Wywołanie funkcji to po prostu podanie jej nazwy, wraz z parametrami w nawiasach np.
i=50;
j=100;
int m.;
m = suma (i,j);
Zakres ważności nazw deklarowanych w obrębie funkcji ogranicza się tylko do bloku tej funkcji. Nie można więc spoza bloku funkcji dotrzeć za pomocą danej nazwy do zmiennej będącej w obrębie funkcji.
Nazwą deklarowaną w obrębie funkcji jest też etykieta. Nie można do niej skoczyć instrukcją goto spoza tej funkcji. Skoro etykieta jest lokalna dla funkcji, dlatego w dwóch różnych funkcjach mogą istnieć bezkonfliktowo identyczne etykiety.
Gdy funkcja używa parametrów, musimy podać typ każdego z nich, np. zdefiniowaliśmy funkcję,
void write_number (int calkowita) {
cout << "Podana liczba wynosi" << calkowita << endl; }
Parametr funkcji, który nazywa się w tym przykładzie calkowita jest w momencie napisania definicji funkcji parametrem formalnym, nie ma jeszcze ustalonej wartości.
Program wywołujący/korzystający z tej funkcji musi w pewnym momencie przekazać tej funkcji konkretną wartość parametru, tak jak np. tutaj
int k = 105;
write_number ( k );
Uwaga !
Ewentualna zmiana wartości parametrów funkcji wewnątrz tej funkcji nie ma wpływu na ich wartość poza obszarem funkcji. Gdy program przekazuje funkcji parametr, C++ tworzy kopię wartości parametru i umieszcza tę kopię w pamięci tymczasowej (stosie). Funkcja przy wykonywaniu operacji korzysta z kopii wartości parametru, która jest "na stosie". Po zakończeniu wykonywania funkcji C/C++ usuwa kopię parametru ze stosu i w ten sposób wartość parametru, ewentualnie zmieniona przez funkcję, przestaje istnieć.
Przykład,
void write (int i, int j) {
i = 100 ;
j = 100 ;
cout << "Parametry wewnatrz funkcji wynosza: " << i << "oraz" << j << endl; }
void main (void) {
int pierwsza = 200, druga = 1;
cout << "Parametry przed wywolaniem funkcji wynosza: " << pierwsza << "oraz" << druga << endl;
write (pierwsza, druga);
cout << "Parametry po wywolaniu funkcji wynosza:" << pierwsza << "oraz" << druga << endl; }
Po uruchomieniu na ekranie;
Parametry przed wywolaniem funkcji wynosza 200 oraz 1
Parametry wewnatrz funkcji wynosza 100 oraz 100
Parametry po wywolaniu funkcji wynosza 200 oraz 1
Zmienne wewnątrz funkcji to zmienne lokalne, lokalizowane na stosie (część „podręczna” pamięci RAM), po zakończeniu działania funkcji ulegają unicestwieniu, na ich miejsce na stosie wchodzą np. wartości lokalne innej funkcji. Czasami chcielibyśmy, aby wartości zmiennej lokalnej nie uległy unicestwieniu, aby została zapamiętana do następnego użycia tej funkcji.
Taką zmienną musimy zadeklarować wewnątrz funkcji jako statyczną,
static int i = 11;
Taka zmienna statyczna nie jest lokalizowana na stosie (jak zmienne lokalne, czasem nazywane również zmiennymi automatycznymi), ale w tym obszarze pamięci, gdzie zmienne globalne. Gdy po zakończeniu działania funkcji (i ewentualnej modyfikacji wartości zmiennej w wyniku instrukcji zawartych w ciele funkcji) opuścimy funkcję, zmienna statyczna stanie się niedostępna, niemodyfikowalna. Gdy ponownie uruchomimy funkcję, zmienna statyczna będzie miała taką wartość, jaką miała w momencie opuszczania funkcji po jej uprzednim wywołaniu. Wartość zmiennej statycznej nie ulega unicestwieniu po wyjściu z ciała funkcji, jak to się dzieje ze zwykłymi (niestatycznymi, automatycznymi) zmiennymi deklarowanymi w ciele funkcji.
Uwaga: zmienne deklarowane na zewnątrz wszystkich funkcji, w głównym ciągu programu, to tzw. zmienne globalne. Istnieją (są widoczne) od miejsca ich deklaracji w pliku, jako zmienne globalne są widoczne również wewnątrz ciała funkcji. Takie zmienne (globalne) w momencie deklaracji są domyślnie (niejawnie) inicjowane wartością zero, jeżeli programista nie nada im w momencie deklaracji od razu jakiejś wartości. Zmienne lokalne (automatyczne) nie są domyślnie inicjowane w momencie ich deklaracji w ciele funkcji (bloku). W komórkach zarezerwowanych na zmienne lokalne początkowo (przed świadomą inicjalizacją przez programistę) tkwią wartości przypadkowe, „śmieci”.
Wewnątrz bloku funkcji można zadeklarować zmienną (lokalną), która ma taką samą nazwę jak istniejąca na zewnątrz funkcji zmienna globalna. Wewnątrz ciała funkcji zmienna globalna jest wtedy przesłonięta przez zmienną lokalną o tej samej nazwie. Wszystkie operacje z użyciem tej nazwy to operacje na wartościach zmiennej lokalnej. Zmienna globalna o nazwie konfliktowej (tj. takiej samej jak zmienna lokalna) jest nieznana (niewidoczna) wewnątrz funkcji. Wewnątrz funkcji nazwa lokalna ma priorytet, nie dopuszcza globalnej „imienniczki”.
Korzystanie ze zmiennej globalnej o identycznej nazwie jak zmienna lokalna jest jednak możliwe. Gdy nazwę konfliktowej zmiennej poprzedzimy operatorem widoczności (zakresu) globalnej :: , to wtedy wewnątrz funkcji ta zmienna globalna będzie używana, modyfikowana.
Wówczas wewnątrz funkcji zmienna globalna zasłania zmienną lokalną o tej samej nazwie.
int i = 10; // zmienna globalna
.........
void funkcja ( void) {
int i=20, j; // zmienna lokalna
j=i;
cout << ”Wartosc zmiennej lokalnej wewnatrz funkcji wynosi:” << j;
j=::i;
cout << ”Wartosc zmiennej globalnej wewnatrz funkcji wynosi:” << j;
........... }
Uwaga ! Funkcje nie mogą być zagnieżdżone, nie może być deklaracji lub definicji jakiejkolwiek funkcji wewnątrz (w ciele, w treści) funkcji.
Domniemane parametry/argumenty funkcji.
Weźmy funkcję zadeklarowaną następująco,
void write (int i, int j = 0);
sprawi to, że jeśli w programie wywołamy funkcję np. tak,
write (10);
to jest dopuszczalne, kompilator domniema, że drugi argument jest równy zero, tak jak w deklaracji zgłaszaliśmy. Jeżeli definicja funkcji jest później, to w definicji już się tego nie powtarza. Od tej pory wolno wywoływać funkcję z jednym argumentem, chociaż jest ona np. dwuargumentowa, stary sposób wywoływania jest też możliwy, wtedy kompilator nic nie domniemywa, podstawia argumenty tak, jak każemy.
Jeżeli chcemy, by funkcja miała kilka argumentów domniemanych, to argumenty te muszą być na końcu listy.
Nienazwany argument.
Gdy mamy funkcję z jednym tylko argumentem np.
void widmo (char kolor);
Gdy w funkcji tej nie będziemy w ogóle korzystać z parametru funkcji, np. zredukujemy naszą funkcję do postaci
void widmo (char kolor) { }
Argument formalny char kolor nie jest używany, wtedy kompilator będzie nas ostrzegał, że nie użyliśmy w ogóle argumentu, a go zgłaszaliśmy (podejrzenie błędu).
Wówczas można wyrzucić nazwę argumentu, a typ argumentu pozostawić
void widmo (char) { }
Jest to znak dla kompilatora, że funkcja jest wywoływana z jednym argumentem typu char, ale go świadomie nie wywołujemy w ciele funkcji. Kompilator nie będzie generował ostrzeżenia.
Przeciążenie nazw funkcji (overloading), tylko w C++.
W języku angielskim przeciążenie/przeładowanie (overloading) danego słowa oznacza, że ma ono więcej niż jedno znaczenie. Słowo jest przeciążone znaczeniami.
Jesteśmy przyzwyczajeni do sytuacji, gdy każda funkcja ma unikalną nazwę.
Wyobraźmy sobie funkcje
void write (int i);
void write (char a, float x, char b);
gdy kompilator napotka takie wywołanie,
write ('A', 7.13, 'c');
to nie ma wątpliwości, o którą z dwóch funkcji o tej samej nazwie chodzi, rozpoznaje nie tylko po nazwie, ale i po typie argumentów.
Przeciążenie nazwy funkcji polega na tym, że w danym zakresie ważności jest więcej niż jedna funkcja o takiej samej nazwie. To, która z nich zostaje w danym momencie uaktywniona zależy do typu argumentów jej wywołania. Funkcje takie mają tą samą nazwę, ale muszą się różnić typem poszczególnych argumentów. We wcześniejszych wersjach języka istniało słowo kluczowe overload ostrzegające o przeciążeniu funkcji, teraz nie jest zalecane, ale może być użyte, kompilator ostrzega, że jest to słowo "wychodzące z mody/użycia".
W C lub C++ dane jednego typu wygodnie zgromadzić w tablicy, tablicę deklaruje się wg. schematu;
typ_tablicy nazwa_tablicy [ liczba_elementów] ;
np.
int tab [100 ];
float matrix [3] = {1.1, 2.41, 3.178};
int szachy [8] [8];
int rubik [6] [6] [6] ;
Często występuje sytuacja, gdy chcemy dla wygody zgromadzić pod jedną, wspólną nazwą dane różnych typów. Gdy mamy konglomerat danych różnych typów, wtedy C oferuje pojęcie structury.
Schemat deklarowania structury;
struct nazwa_struktury
{
składniki, elementy, zmienne, pola struktury
} lista zmiennych typu strukturalnego (opcjonalnie) ;
Przykłady,
struct data {
int dzien, miesiac, rok ;
char nazwa_miesiaca;
char nazwa_dnia; };
struct pozycja_katalogowa {
int num_pozycji ;
char opis [40] ;
float cena;
int zapas_sztuk ; };
struct poborowy {
char nazwisko [40], imie [20], uwagi_dodatkowe [ 300 ] ;
int iq ;
long pessel;
float waga, wzrost, nr_buta; }niski, wysoki;
Jak się odwołać do pola (składowej, elementu) struktury ?
Pisze się nazwę zmiennej struktury i po kropce nazwę składnika, np.
niski.wzrost = 148.8;
Jeżeli definicja struktury nie zawiera od razu deklaracji zmiennych, to zmienne tej struktury w języku C++ deklaruje się podając nazwę struktury i wymieniając następnie nazwy zmiennych, np.
poborowy wyrosniety, zdolny;
wyrosniety.nr_buta = 48 ;
zdolny.iq = 63;
Tak można to zrobić w C++, w języku C najpierw musimy napisać słowo kluczowe struct, potem nazwę (ang. structure tag ) zdefiniowanej uprzednio struktury, następnie nazwy/identyfikatory zmiennych typu strukturowego, np.
struct poborowy wyrosniety, zdolny;
Naturalnie, taka deklaracja będzie również zrozumiała dla kompilatora języka C++ zgodnie z ogólną ideą, iż C++ jest nadzbiorem C. Użycie słowa struct przy deklaracji zmiennych strukturowych jest w C++ opcjonalne.
Unia.
Definiuje się tak samo, jak strukturę, tylko zamiast słowa struct jest słowo union,
union jaka_liczba {
int i;
long j;
float x;
double y;
} liczba;
Zmienne typu union mają w pamięci zarezerwowane miejsce tylko na jeden, największy składnik unii, w powyższym przykładzie jest to liczba typu double. Wszystkie składniki uni są zapisywane w tym samym miejscu pamięci, poprawną wartość zawiera tylko ostatnio zapisany składnik. W przeciwieństwie do struktury, unia przechowuje w danym momencie wartość tylko jednej składowej, zapisanie bieżącej wartości składowej zastępuje poprzednio istniejącą wartość.
Odwołanie do elementu uni - podajemy nazwę zmiennej i po kropce nazwę elementu/składnika unii, np.
liczba.j = 1001;
Unia bez nazwy, inaczej unia anonimowa.
union {
int kilogramy;
float funty; } ;
W tym przykładzie unia nie ma nazwy, nie ma także deklaracji zmiennych typu unia. Taką unię nazywamy anonimową. Do składników uni anomnimowej odnosimy się podając nazwy tych składników, tak jak zwykłych zmiennych (bez nazwy uni, której zreszta nie ma, więc jak podać jej nazwę !?) bez użycia kropki.
# include <iostream.h>
void main (void) {
union {
int kilogramy;
float funty; } ;
kilogramy = 500;
cout << "Masa w kg wynosi" << kilogramy << endl;
funty = 0.5 * kilogramy ;
cout << "Masa w funtach wynosi " << funty << endl; }
Użycie uni anonimowej oszczędza pamięć bez zaciemniania kodu przez pisanie nazwy unii i kropki przy dostępie do zmiennych uni.
Klasa, nie występuje w C, dopiero w C++, rozszerzenie pojęcia struktury w jej wydaniu w języku C.
Zawiera zmienne (składowe) różnych typów i funkcje działające na tych zmiennych. Funkcje klasy zwane są metodami tej klasy. Klasa grupuje składowe (dane, cechy ) obiektu i funkcje działające na składowych klasy. Rzeczywisty obiekt można w komputerze opisać zespołem zmiennych (liczb) reprezentujących cechy obiektu i zespołem funkcji, reprezentujących zachowanie się obiektu.
Ogólny schemat deklaracji:
class nazwa_klasy {
składowe (pola) klasy
................................
funkcje (metody) klasy };
class pracownik {
public:
char imie_nazwisko [ 40 ];
float zarobek;
void wydruk (void) {
cout << "Imie i Nazwisko"" << imie_nazwisko << endl;
cout << "Pani/Pana zarobek wynosi : " << zarobek << endl; }
public: // etykieta
Składniki klasy mogą być publiczne (public) lub prywatne (private). Jeżeli są public to program ma do nich dostęp z zewnątrz, spoza zakresu klasy przy użyciu operatora wyboru z kropką.
Etykieta private oznacza, że deklarowane zmienne i funkcje klasy są dostępne tylko z wnętrza klasy. Funkcje klasy mogą być wtedy wywołane tylko przez inne funkcje zawarte w tej samej klasie. Etykiety dostępu (public, private, protected) można umieszczać w dowolnej kolejności, mogą się też powtarzać. Zawsze oznaczają, że te składniki klasy, które następują bezpośrednio po etykiecie - mają tak określony dostęp. Domniemywa się, że - dopóki w definicji klasy nie wystąpi żadna z etykiet - składniki klasy mają dostęp private.
Dość często, typowo, postępuje się tak, iż możliwie dużo składowych/zmiennych klasy pozostawia się jako prywatne, a niektóre funkcje tej samej klasy deklaruje się jako publiczne. Dostęp do prywatnych danych/zmiennych tej klasy mamy poprzez wywoływanie jej funkcji/metod publicznych.
Ukrywanie informacji, enkapsulacja. Sterowanie dostępem do składników klasy (składowych, funkcji) jest swego rodzaju dobrodziejstwem, które chroni dobrze napisaną klasę przed przypadkowym "zepsuciem".
Wskazane jest ukrywanie jak największej ilości zmiennych, pozwala oderwać to kod klasy od bieżącego kontekstu i wykorzystać go w przyszłości.
Sama definicja klasy nie definiuje jeszcze żadnych egzemplarzy (obiektów) klasy. Klasa to typ obiektu a nie sam obiekt. Obiekty (egzemplarze) wprowadzamy tak jak zmienne wbudowane, nazwa_klasy nazwa_obiektu
pracownik dyrektor, sekretarka; // deklaracja obiektów klasy pracownik
Dostęp do zmiennych lub metod konkretnego obiektu klasy za pomocą odniesienia
cout << dyrektor.zarobek;
void main (void) {
pracownik dyrektor, sekretarka;
strcpy (sekretarka.nazwisko_imie, "Kowalska Magda");
sekretarka.zarobek = 2900;
strcpy (dyrektor.nazwisko_imie, "Nowak Andrzej" );
dyrektor.zarobek = 1200; }
Niezależnie od miejsca zadeklarowania/zdefiniowania składowej lub funkcji/metody wewnątrz klasy, są one znane w całej klasie. Nazwy deklarowane w klasie mają zakres ważności równy tej klasie. Inaczej jest w zwykłych funkcjach, nie umieszczonych wewnątrz klasy. Wtedy gdy zadeklarowaliśmy zmienną, to jest ona znana od miejsca deklaracji do końca funkcji, w linijkach funkcji powyżej deklaracji danej zmiennej nie jest ona znana. Zwykła funkcja, deklarowana poza klasą, ma zakres pliku w którym występuje. Klasa, w przeciwieństwie do funkcji, nie ma początku i końca, to jakby pudełko na składniki (zmienne i funkcje/metody). Składową klasy może także być obiekt innej klasy, struktury, uni. Składnikami klasy mogą być typy wbudowane: int, float, char,...., jak również typy definiowane: struct, union, class.
Funkcja/metoda klasy może być zdefiniowana wewnątrz klasy lub poza ciałem klasy.
class person {
char name [25];
int age;
public:
void write ( ) {
cout << name << " , age: " << age << endl; }
};
To była funkcja zdefiniowana/umieszczona wewnątrz ciała funkcji. Jest drugi sposób, w definicji klasy umieszcza się tylko deklaracje funkcji, natomiast definicje funkcji pisze się poza ciałem klasy,
class person {
char name [25]
int age;
public :
void write ( );
}; // koniec definicji klasy
......................
.....................
void person:: write ( ) {
cout << name << " , age : " << age << endl; }
Operator widoczności :: , ponieważ funkcja jest zdefiniowana poza ciałem klasy.
Funkcja zdefiniowana poza klasą ma dokładnie taki sam zakres, jakby była zdefiniowana wewnątrz klasy, oba sposoby definiują funkcję o zakresie ważności tej klasy.
// * Przypominam jeszcze raz, funkcje składowe (metody) klasy mają pełny dostęp do wszystkich składników klasy, to znaczy zarówno do danych (zmiennych), jak i do innych funkcji (mogą je wywoływać). Do składnika swojej klasy odwołują się po prostu podając jego nazwę. Metoda klasy jest narzędziem, za pomocą którego dokonujemy operacji na składowych (zmiennych) tej klasy. *//
Jest jednak ogromna różnica dla kompilatora.
Jeżeli bowiem funkcję składową zdefiniujemy wewnątrz klasy, to kompilator uznaje, iż jest to tzw. funkcja typu inline. Pojęcie funkcji inline jest specyficzne dla C++, nie występuje w C.
Co to znaczy, że funkcja jest inline ?
Jeżeli ktoś miał coś wspólnego z programowaniem w asemblerze, to wie, że za każde wywołanie funkcji coś się płaci. Musi na poziomie języka maszynowego wystąpić kilka instrukcji, które obsługują przejście w inne miejsce programu. Po wykonaniu funkcji trzeba trochę posprzątać, to zabiera czas. Nawiasem mówiąc, koszt wywołania funkcji w C/C++ jest stosunkowo niski, C/C++ jest językiem ogólnego przeznaczenia, wysokiego poziomu, ale wśród języków wysokiego poziomu jest bardzo nisko, blisko kodu maszynowego.
Ile razy w programie umieścimy wywołanie funkcji zadeklarowanej jako inline, kompilator dosłownie umieści jej ciało (treść) w linijce, w której to wywołanie nastąpiło. Nie będzie żadnych akcji na poziomie kodu maszynowego związanych z wywoływaniem i powrotem z tej funkcji, w rezultacie taki kod będzie wykonywał się szybciej.
Jeżeli chcemy jakąś funkcję mieć inline, to piszemy przed nią słowo inline, np.
inline int zaok (float liczba) {
return (liczba + 0.5); }
Ta funkcja jest wywoływana z argumentem typu float, a zwraca typ int, jest to funkcja służąca do zaokrąglania liczb rzeczywistych na liczby całkowite. Nie należy nadużywać koncepcji funkcji inline, stosować tylko dla bardzo małych funkcji, o bardzo małym kodzie.
Umiejscowienie funkcji inline.
Jeżeli funkcja jest inline, to kompilator napotykając w jakiejś lini jej wywołanie, musi w tej linii wstawić właściwe instrukcje, definicja, a nie tylko deklaracja funkcji musi być w tym momencie znana. Definicje funkcji typu inline umieszczać na górze tekstu programu.
Wracamy do klas,
jeżeli ciało funkcji wewnątrz klasy (metoda) jest bardzo małe, 2 linijki, definijemy tą funkcję wewnątrz klasy, wówczas jest ona automatycznie inline. Przez domniemanie wszystkie funkcje definiowane wewnątrz klasy są inline. Jeżeli funkcje są dłuższe, to deklarujemy w klasie, a definicja umieszczona poza klasą, przy pomocy operatora widoczności. Czy metoda zdefiniowana poza ciałem klasy nie może być inline ? Może, ale nie przez domniemanie, trzeba to wyraźnie napisać, poprzedzając tą funkcję słowem inline. Np.
inline void person:: write ( ) {
cout << name << " , age : " << age << endl; }
Składniki klasy mogą być private, public, protected. Odwołanie się do składnika prywatnego spoza klasy jest błędne/niedopuszczalne/nieskuteczne, np.
class nowy_model {
long int cena;
public:
int zuzycie_paliwa, predkosc_max, przyspieszenie;
void funkcja (void); };
nowy_model maly, sredni, duzy;
..................
maly.zuzycie_paliwa = 4;
sredni.przyspieszenie = 8;
duzy.predkosc_max = 225;
maly.cena = 99999; /błąd, bo cena jest "prywatna", nie jest składową publiczną
Zasłanianie nazw
Ponieważ nazwy składników klasy (zmiennych i funkcji/metod) mają zakres klasy, w obrębie klasy zasłaniają elementy o takiej samej nazwie leżące poza klasą. Uwaga., zbytnie poleganie na zasłanianiu wpędzić może w tarapaty, prędzej czy póżniej zapomnimy, co i kiedy jest zasłaniane, najlepiej wymyślać inne nazwy. Funkcje wewnątrz klasy mogą być przeciążone, tj może wystąpić kilka tego samego typu, o tej samej nazwie, różniących się liczbą parametrów.
W definicji klasy składowe (zmienne) nie mogą być inicjowane, np.
class dowod {
char nazwisko[50], imie[40], kolor_oczu[15];
int numer, wzrost=179; }; // błąd !!!
Klasa to typ obiektu, a nie sam obiekt, gdyby tak mogła wyglądać definicja klasy, to wszystkie dowody miałyby w pozycji wzrost wpisaną tą samą wartość.
Dane do obiektu klasy można wpisać dopiero po jego zadeklarowaniu, np.
dowod twoj;
twoj.wzrost=179; // to jest O.K.
Przy dużej liczbie obiektów klasy i dużej liczbie zmiennych w klasie, inicjowanie zmiennych w obiektach klasy jest uciążliwe. Czy można w momencie definicji obiektu nadać jednocześnie początkowe/startowe wartości zmiennym obiektu ?
C++ udostępnia specjalną funkcję składową klasy (metodę) zwaną konstruktorem.
Jej nazwa musi być taka sama, jak nazwa klasy, nazwa konstruktora nie może być poprzedzona typem zwracanej wartości, nawet typ void jest niedopuszczalny, przed nazwą konstruktora nic nie stoi.
Mamy klasę,
class numer {
int liczba;
public:
void schowaj (int k) { liczba=k; }
int zwracaj ( ) {return liczba; } };
Klasa ta jest schowkiem na liczę typu int. Metoda chowaj wkłada liczbę do schowka, a metoda zwracaj pokazuje co jest w schowku.
Do tej pory, nie posługując się klasami, gdy chcieliśmy stworzyć obiekt typu int, w którym chowaliśmy liczbę np. 7, to wystarczyło napisać instrukcję podstawienia
int schowek1 = 7;
Gdy to samo zrobic przy pomocy zdefiniowanej klasy, to najpierw należy zadeklarować/zdefiniować obiekt tej klasy
numer schowek2;
schowek2.schowaj (7);
W momencie definicji obiektu nie ma jeszcze inicjalizacji jego składowych. Inicjalizacja wewnątrz definicji klasy jest niedopuszczalna, jak wiemy.
Klasa numer nie jest tak wygodna w użyciu pod tym względem, jak typ wbudowany int.
Aby typy definiowane przez użytkownika były pod tym względem równie wygodne, jak typy wbudowane, wprowadzono do C++ koncepcję konstruktora, specjalnej funkcji klasy.
Oto klasa numer wyposażona w konstruktor,
class numer {
int liczba;
public:
numer (int k) {liczba = k; } // konstruktor
void schowaj (int k) { liczba = k; }
int zwracaj ( ) { return liczba; } };
Przed konstruktorem nic nie stoi, wobec tego w konstruktorze nie może wystąpić instrukcja return zwracająca jakąś wartość, nawet tylko return ; (return ze średnikiem) nie może wystąpić. Oto jak w programie posługujemy się konstruktorem klasy numer,
numer schowek2= numer (7);
lub, drugi sposób
numer schowek2 (7);
W pierwszym sposobie definiujemy w pamięci obiekt schowek2 i dla niego wywołujemy konstruktor z argumentem 7. Druga definicja opuszcza znak równości i nazwę konstruktora - nazwa konstruktora jest identyczna z nazwą klasy.
Konstruktor może być tylko zadeklarowany wewnątrz klasy, a zdefiniowany na zewnątrz, z użyciem operatora widoczności, tak jak każda inna funkcja składowa klasy.
numer::numer (int k) {liczba = k; }.
Uwaga, nazwa konstruktor jest nieco myląca, konstruktor nie definiuje obiektu, tylko nadaje wartości początkowe jego zmiennym, konstruktor nie jest obowiązkowy w klasie.
Gdy nie umieścimy w klasie własnego konstruktora, to podczas powoływania do życia obiektu klasy np.
Pies blutterier;
Tworzony jest automatycznie konstruktor domyślny o nazwie Pies, który nie pobiera parametrów i nic nie robi,
Pies::Pies( ) { }
Jeżeli deklarujemy konstruktor, to kompilator oczywiście nie dopisze nam swojego, domyślnego konstruktora.
Składnik statyczny w klasie.
Każdy obiekt danej klasy ma swój własny zestaw danych/zmiennych/składowych. Załóżmy, że klasa ma 5 zmiennych. Gdy np. zdefiniujemy 500 obiektów klasy, to w pamięci będzie 500 * 5=2500 odrębnych miejsc zarezerwowanych. Są sytuacje, gdy poszczególne obiekty (egzemplarze) klasy powinny posługiwać się tą samą daną. Gdy dana ta dotyczy nie poszczególnych obiektów klasy ale samej klasy jako całości. Np. cena jogurtu o wadze 125 g taka sama, niezależnie czy to jogurt malinowy, egzotyczny, jabłkowy, ...
Dana/zmienna/składowa, która jest określona jako statyczna, jest tworzona w pamięci jednokrotnie i jest wspólna dla wszystkich egzemplarzy (obiektów) danej klasy. Co więcej, rezerwacja pamięci dla niej istnieje nawet wtedy, gdy nie zdefiniowano jeszcze ani jednego egzemplarza obiektu tej klasy.
class Przykladowa {
public:
int i;
static int wazny; };
Deklaracja składnika statycznego w ciele klasy nie jest jego definicją. Definicję musimy umieścić gdzieś tak, aby miała zakres ważności pliku (tak jak zmienna globalna). Definicja taka może zawierać inicjalizację, np.
int nazwa_klasy :: zmienna_statyczna = 11;
Do składnika statycznego odnosimy się tak, jak do zwykłego składnika klasy, np. gdy mamy zdefiniowany obiekt klasy Przykladowa
Przykladowa przyklad1;
przyklad1.wazny = 154;
Jest jeszcze drugi sposób, za pomocą nazwy klasy i operatora widoczności
int Przykladowa :: wazny = 154;
Zastosowanie składnika statycznego klasy - gdy wszystkie obiekty (egzemplarze klasy) mają tę samą cechę, która może się zmieniać, np. podwyżka ceny wszystkich jogurtów, niezależnie od ich smaku, polega na operacji zmiany wartości jednego składnika statycznego, nie trzeba zmieniać składowej cena w każdym obiekcie z osobna.
Dziedziczenie klas.
Gdy chcemy zdefiniować nową klasę, która będzie posiadała wiele, lub nawet wszystkie atrybuty (zmienne i metody) już uprzednio zdefiniowanej, sprawdzonej i działającej klasy, różnić się ma od tej już istniejącej kilkoma dodatkowymi zmiennymi lub metodami. Wtedy C++ pozwala zbudować łatwo klasę dziedziczną (dziedziczącą, pochodną, rozszerzoną, pokrewną, potomną, podklasę) od klasy pierwotnej (wzorcowej, wyjściowej, bazowej, nadklasy, nadrzędnej). Oszczędza się czas, nie trzeba przepisywać kodu klasy pierwotnej w ciele klasy dziedzicznej, zwiększa się czytelność kodu. Np. mamy klasę pierwotną
class Pracownik {
public:
Pracownik (char, long, float);
void info (void);
private:
char nazwisko [20], stanowisko[20];
float pensja; };
Gdy teraz chcemy utworzyć klasę Kierownik, która powinna zawierać dodatkowe elementy w stosunku do klasy Pracownik, np.
char auto_sluzbowe [30];
char telefon_komorkowy[30];
int bonus;
Nową klasę Kierownik utworzymy jako rozszerzenie klasy pierwotnej Pracownik,
class Kierownik : public Pracownik {
// definicje nowych elementów klasy Kierownik, których nie było w klasie Pracownik
};
Słowo kluczowe public przed nazwą klasy Pracownik oznacza, że publiczne składowe klasy Pracownik są również publiczne w klasie Kierownik.
Uwaga; nie należy sądzić, że to obiekty (egzemplarze klasy) moga tworzyć obiekty od siebie pochodne (pokrewne, dziedziczące). Dziedziczenie dotyczy klas (czyli definiowanych typów danych), a nie jakichś konkretnych egzemplarzy obiektów.
Nowa klasa przejmuje/dziedziczy elementy (składniki, atrybuty) klasy pierwotnej.
Klasa pochodna ma naturalnie dostęp do wszystkich składników klasy pierwotnej, które są zdeklarowane jako public, składniki typu public jak gdyby były wewnątrz klasy pochodnej. Prywatne składniki klasy pierwotnej są dziedziczone (jak wszystkie inne składniki klasy pierwotnej), ale w zakresie klasy pochodnej nie ma do nich dostępu, klasa rozszerzona nie ma dostępu do składników prywatnych klasy pierwotnej, tej z której się wywodzi. Składniki prywatne klasy pierwotnej niby są w klasie pochodnej, ale nie można ich ruszać, więc po co takie składniki ? Ma to jednak sens. W klasie pochodnej mogą być bowiem dziedziczone jakieś nieprywatne funkcje składowe. Ponieważ te funkcje mają zakres klasy pierwotnej, mogą pracować na prywatnych składowych tej klasy, z zakresu klasy pochodnej możemy je uruchomić, bo nie są prywatne. Mogą więc robić coś dla klasy pochodnej.
Gdy przewidujemy że będziemy rozszerzać klasę pierwotną, to niektóre składniki klasy pierwotnej wygodnie poprzedzić etykietą protected. Składniki klasy pierwotnej poprzedzone etykietą protected są dostępne/widoczne z klasy rozszerzonej (dziedziczącej) tak samo, jak gdyby były one typu public. Ale są one jak gdyby typu public tylko dla składników klasy pochodnej/dziedziczącej, dla składników innych klas, nie wywodzących się od klasy pierwotnej, są dalej typu private (niedostępne).
Istnieje jeszcze drugi mechanizm za pomocą którego tym razem klasa pochodna decyduje jak chce odziedziczyć nieprywatny składnik klasy pierwotnej. Podkreślam, nieprywatny (public, protected), co do prywatnego to nic się nie da zrobić, jest on dziedziczony, ale niedostępny (zapieczętowany). Składniki typu public i protected są dostępne w zakresie klasy pochodnej, klasa bazowa nie stawia barier przy ich wykorzystywaniu (dostępie do nich) w klasie pochodnej. Klasa pochodna może sobie jednak wybrać, czy chce by odziedziczony składnik typu np. public był u niej także public.Może zdecydować włożyć go u siebie "do szufladki" private i nikomu już go nie pokazywać.
Wyboru takiego dokonuje się przy definiowaniu klasy pochodnej
class Kierownik : public Pracownik {
// definicje nowych elementów klasy Kierownik, których nie było w klasie Pracownik
};
Słowo public poprzedzające nazwę klasy pierwotnej oznacza, że składniki typu public z klasy podstawowej mają w klasie pochodnej również status public (są dostępne poza ciałem klasy pochodnej ), natomiast odziedziczone składniki protected pozostają protected w klasie pochodnej.
Jeżeli definiując klasę pochodną przed nazwą klasy pierwotnej umieścimy słowo protected, to odziedziczone składniki typu public i protected klasy pierwotnej są w klasie pochodnej także protected.
Jeżeli przed nazwą klasy pierwotnej stoi słowo private, to odziedziczone składniki typu public i protected klasy pierwotnej są w klasie pochodnej typu private. Jeżeli w definicji klasy pochodnej przed nazwą klasy pierwotnej nie umieścimy ani słowa private, ani protected, ani public to przez domniemanie zostanie przyjęte dziedziczenie typu private.
Udostępnianie wybiórcze.
Wiemy już, że klasa pochodna może odziedziczone składniki ukryć/chronić (dziedziczenie private lub protected) albo pozostawić do nich taki sam dostęp, jaki miały w klasie bazowej. Czasem taka decyzja nie jest łatwa, chcielibyśmy coś ukryć, coś udostępnić, hurtowe określenie statusu nas nie zadowala, jest zbyt "grube", za mało subtelne.
C++ pozwala i na takie subtelności. Robimy tak, że klasa pochodna dziedziczy klasę podstawową prywatnie, natomiast te nazwy składników, którym mimo wszystko chcemy dać "przepustkę" - należy w klasie pochodnej wyspecyfikować “po imieniu” umieszczając ich nazwy po etykiecie protected lub public.
Same nazwy, nie trzeba przypominać, co jest funkcją a co np. zmienną, tablicą,...
class bazowa {
protected:
int n;
void fun ( );
public:
int k;
float fun1( ); };
class pochodna : private bazowa {
protected:
bazowa :: n; // deklaracje dostępu
bazowa :: fun ;
public:
bazowa :: k; };
Dzięki deklaracjom dostępu mimo, że klasa bazowa została odziedziczona prywatnie - wybrane nazwy mogą być udostępnione jako publiczne lub chronione (protected).
Uw. deklaracja dostępu może tylko powtórzyć dostęp, jaki nazwa miała w swojej klasie podstawowej, nie może ani zaostrzyć, ani osłabić/rozluźnić dostępu. Wybiórcze udostępnianie za pomocą deklaracji dostępu nie dotyczy składników prywatnych klasy podstawowej. Z tymi nic się nie da zrobić. Dotyczy jedynie składników nieprywatnych, które zostały odziedziczone prywatnie.
Uw. konstruktory klasy podstawowej nie są dziedziczone w klasie pochodnej. Powód jest prosty, obiekt klasy pochodnej to jakby obiekt klasy podstawowej plus coś jeszcze (składniki dodane w definicji klasy pochodnej/rozszerzonej ). O tych dodanych składnikach konstruktor klasy podstawowej nic nie wie, inicjowane byłyby tylko zmienne odziedziczone z klasy bazowej, a te dodane w klasie rozszerzonej nie. Na to się nie możemy zgodzić, albo zrobić całość, albo nic. Konstruktory klasy pochodnej trzeba sobie zdefiniować.
Podczas tworzenia klasy pochodnej (rozszerzonej) może się zdarzyć, iż nazwa składnika klasy pochodne jest taka sama, jak nazwa składnika klasy wyjściowej. Wtedy gdy przebywamy w zakresie klasy pochodnej, to przez domniemanie C++ daje priorytet składnikom klasy pochodnej. Np. gdy zarówno klasa pracownik jak i jej podklasa kierownik zawierają zmienną int pensja, to metoda podklasy kierownik użyje takiej wartości zmiennej pensja, która jest wewnątrz tej metody. Gdy chcemy w metodzie zawartej w podklasie kierownik użyć zmiennej pensja podanej w klasie pierwotnej pracownik, wtedy należy nazwę tej zmiennej poprzedzić nazwą klasy i operatorem widoczności
cout << "Wynagrodzenie kierownika:" << pracownik :: pensja << endl;
Dziedziczenie kilkupokoleniowe (kaskadowe),
klasa pochodna może być równocześnie klasą podstawową dla innej klasy.
class A {
...................... // ciało klasy
}
class B : public A {
................... // ciało klasy
}
class C : public B {
........................ // ciało klasy
}
Dziedziczenie wielokrotne, wielobazowe.
C++ pozwala utworzyć klasę na podstawie więcej niż jednej klasy pierwotnej (bazowej). Jeżeli nowa klasy dziedziczy atrybuty więcej niż jednej klasy, to mamy do czynienia z dziedziczeniem wielobazowym.
Przykład,
class monitor {
public:
char nazwa[30]
void infom (void);
private: