const
Typy
Typ określa rozmiar pamięci, dozwolone operacje i ich znaczenie.
Z każdą nazwą w C++ związany jest typ, mamy tu statyczną kontrolę typów.
Typy można nazywać.
Operacje dozwolone na nazwach typów:
podawanie typu innych nazw,
sizeof
,
new
,
specyfikowanie jawnych konwersji.
Liczby całkowite:
char,
signed char,
short int (signed short int),
int (signed int),
long int (signed long int).
Liczby całkowite bez znaku:
unsigned char,
unsigned short int,
unsigned int,
unsigned long int.
(część int można opuścić)
Liczby rzeczywiste:
float,
double,
long double.
W C++ sizeof(char)
wynosi 1 (z definicji),
Typ wartości logicznych bool
,
char
może być typem ze znakiem lub bez znaku,
C++ gwarantuje, że
1 = sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long)
sizeof(float) <= sizeof(double) <= sizeof(long double)
sizeof(T) = sizeof(signed T) = sizeof(unsigned T)
, dla T = char
, short
, int
lub long
,
char
ma co najmniej 8, short
16, a long
32 bity.
Wskaźniki są bardzo często używane w C++.
Wskaźnik do typu T deklarujemy (zwykle) jako T*.
Zwn. składnię C++ (wziętą z C) wskaźniki do funkcji i tablic definiuje się mniej wygodnie.
Operacje na wskaźnikach:
przypisanie,
stała NULL,
* (operator wyłuskania),
p++, p+wyr, p-wyr, p1-p2 gdzie p, p1, p2 to wskaźniki, a wyr to wyrażenie całkowitoliczbowe.
Uwaga na wskaźniki - tu bardzo łatwo o błędy, np.:
char *dest = new char[strlen(src+1)]; strcpy(dest, src); // Błędny fragment programu (ale kompilujący się bez // ostrzeżeń) zwn złe położenie prawego, okrągłego nawiasu.
Zamieszczony przykład pokazuje niezwykle nieprzyjemny i trudny do zlokalizowania błąd związany
ze wskaźnikami.
Funkcje strlen
i strcpy
służą, odpowiednio, do policzenia długości napisu (nie licząc znaku
o kodzie 0, oznaczającego koniec napisu) oraz do skopiowania napisu (wraz ze znakiem o kodzie 0).
Prawy okrągły nawias został przez pomyłkę ustawiony za +1
zamiast przed.
Prawidłowa postać tego fragmentu programu powinna być taka:
char *dest = new char[strlen(src)+1]; // ...Czyli przydzielamy pamięć wystarczającą do zapamiętania wszystkich znaków napisu
src
oraz znaku oznaczającego koniec napisu (znaku o kodzie 0), stąd to dodanie jedynki.
Po przestawieniu nawiasu liczymy długość napisu zaczynającego się od drugiego znaku napisu src
,
co (o ile src
nie było pustym napisem) jest dobrze zdefiniowaną operacją i da wynik o jeden mniejszy
niż długość src
.
Czyli łącznie wyrażenie strlen(src+1)
da wynik za mały o 2 (poza przypadkiem pustego src
,
kiedy to w ogóle nie jesteśmy w stanie przewidzieć jaki będzie wynik).
Zatem pamięć przydzielona na napis dest
może być za krótka.
Może, bo ze względu na specyfikę algorytmów przydzielania pamięci, czasami przydzielają one nieco więcej
bajtów, niż było żądane (np. przydzielają pamięć w blokach po 8 bajtów), więc dla niektórych długości
napisu src
mogą przydzielić wystarczająco dużo pamięci.
Jeśli pamięci będzie za mało, to skopiowanie operacją strcpy
może zamazać dwa bajty pamięci,
należące np. do innej zmiennej.
Zauważmy, że:
Błąd jest trudy do zauważenia w tekście programu.
Wystąpienie błędu jest niezwykle trudne podczas testowania - ten błąd może się ujawnić bądź
nie w zależności od dość przypadkowych czynników (użyty algorytm przydziału pamięci, długość napisu
src
, to czy, a jeśli tak to jaka, zmienna zostanie zamazana w wyniku kopiowania.
Zatem nawet dowolnie wiele razy przeprowadzane testy mogą nie wykryć wystąpienia tego błędu. Taki błąd jest idealnym tematem do najgorszych sennych koszmarów programisty!
T[rozmiar]
jest tablicą rozmiar elementów typu T, indeksowaną od 0 do rozmiar-1.
Odwołanie do elementu tablicy wymaga użycia operatora []
.
Tablice wielowymiarowe deklaruje się wypisując kilka razy po sobie [rozmiar]
(nie można zapisać tego w jednej parze nawiasów kwadratowych).
W C++ nazwy tablicy można używać jako wskaźnika. Oznacza ona (stały) wskaźnik do pierwszego elementu tablicy. Przekazywanie tablicy jako parametru oznacza przekazanie adresu pierwszego elementu.
Nie ma operacji przypisania tablic (przypisanie kopiuje tylko wskaźniki).
Właściwie nie ma tablic:a[i]
oznacza *(a+i)
co z kolei oznacza i[a]
.
Ale uwaga na różnicę:int *p;
oznacza coś zupełnie innego niżint p[100];
Struktura to zestaw elementów dowolnych typów (przynajmniej w tej części wykładu).
Struktury zapisujemy następująco:
struct <nazwa> { typ_1 pole_1; typ_2 pole_2; … typ_k pole_k; };
Do pól struktury odwołujemy się za pomocą:
.
jeśli mamy strukturę,
->
jeśli mamy wskaźnik do struktury.
Struktura może być wynikiem funkcji, parametrem funkcji i można na nią przypisywać.
Nie jest natomiast zdefiniowane porównywanie struktur (==
i !=
).
Można definiować struktury wzajemnie odwołujące się do siebie. Używa się do tego deklaracji:struct <nazwa>;
Tak wstępnie zadeklarowanej struktury można używać tyko tam, gdzie nie jest wymagana znajomość rozmiaru struktury.
Referencja (alias) to inna nazwa już istniejącego obiektu.
Typ referencyjny zapisujemy jako T&
, gdzie T jest jakimś typem (T nie może być typem referencyjnym).
Referencja musi być zainicjalizowana i nie można jej zmienić.
Wszelkie operacje na referencji (poza inicjalizacją) dotyczą obiektu na który wskazuje referencja, a nie samej referencji!
Referencje są szczególnie przydatne dla parametrów funkcji (przekazywanie przez zmienną).
Deklaracja typedef
służy do nazywania typu. Składniowo ma ona postać zwykłej deklaracji poprzedzonej słowem kluczowym typedef
.
Dwa niezależnie zadeklarowane typy są różne, nawet jeśli mają identyczną strukturę, typedef
pozwala ominąć tę niedogodność.
typedef
służy do zadeklarowania identyfikatora, którego można potem używać tak, jak gdyby był nazwą typu.
Można definiować wyliczenia np.:enum kolor{ czerwony, zielony }
.
const
Do deklaracji dowolnego obiektu można dodać słowo kluczowe const
, dzięki czemu uzyskujemy
deklarację stałej, a nie zmiennej (oczywiście taka deklaracja musi zawierać inicjację),
Można używać const
przy deklarowaniu wskaźników:
char *p = ”ala”;
wskaźnik do znaku (napis),
char const *p = ”ala”;
wskaźnik do stałych znaków (stały napis),
char * const p = ”ala”;
stały wskaźnik do znaku (napis),
char const * const p = ”ala”;
stały wskaźnik do stałych znaków (stały napis).
Deklarując zmienne można im nadawać wartości początkowe:
struct S {int a; char* b;}; S s = {1, „Urszula”}; int x[] = {1, 2, 3}; float y[4] [3] = { { 1, 3, 5}, { 2, 4, 6}, {3, 5, 7} }
Deklaracja funkcji ma następującą postać (w pewnym uproszczeniu):
typ_wyniku nazwa ( lista par. ) instrukcja_złożona
Jako typ wyniku można podać void
, co oznacza, że funkcja nie przekazuje wyniku (jest procedurą).
Lista parametrów to ciąg (oddzielonych przecinkami) deklaracji parametrów, postaci (w uproszczeniu):
typ nazwa
Parametry są zawsze przekazywane przez wartość (ale mogą być referencjami lub wskaźnikami).
Jeśli parametrem jest wskaźnik, to jako argument można przekazać adres obiektu, używając operatora &
.
Deklarując parametr funkcji (lub metody), można po jego deklaracji dopisać znak = i wyrażenie. Deklaruje się w ten sposób domyślną wartość argumentu odpowiadającego temu parametrowi. Pozwala to wywoływać tak zadeklarowaną funkcję zarówno z tym argumentem jak i bez niego. W tym drugim przypadku, przy każdym wywołaniu podane wyrażenie będzie wyliczane, a uzyskana w ten sposób wartość będzie traktowana jako brakujący argument:
char* DajTablicę(unsigned rozmiar = 10){ return new char[rozmiar]; } char* p = DajTablicę(100); // Tablica 100-elementowa char* q = DajTablicę(); // Tablica 10-elementowa
Można w jednej funkcji zadeklarować kilka parametrów o wartościach domyślnych, ale muszą to być ostatnie parametry. Oznacza to, że jeśli zadeklarujemy wartość domyślną dla jednego parametru, to wówczas dla wszystkich następnych parametrów również należy określić domyślne wartości (w tej lub jednej z poprzednich deklaracji funkcji):
void f(int, float, int = 3); void f(int, float=2, int); void f(int a=1, float b, int c) // Oczywiście można było od razu napisać: // void f(int a=1, float b=2, int c=3) { cout << endl << a << " " << b << " " << c; } // Wszystkie poniższe wywołania odnoszą się do tej samej funkcji f. f(-1,-2,-3); f(-1,-2); f(-1); f();
Nie można ponownie zdefiniować argumentu domyślnego w dalszej deklaracji (nawet z tą samą wartością).
Przykład zastosowania dodefiniowywania wartości domyślnych poza definicją funkcji: funkcja z innego modułu używana w danym module z domyślną wartością (np. sqrt(double = 2.0)
).
Uwagi techniczne: Wiązanie nazw i kontrola typów wyrażenia określającego wartość domyślną odbywa się w punkcie deklaracji, zaś wartościowanie w każdym punkcie wywołania:
// Przykład z książki Stroustrupa int a = 1; int f(int); int g(int x = f(a)); // argument domyślny: f(::a) void h() { a = 2; { int a = 3; g(); // g(f::a), czyli g(f(2)) ! } }
Wyrażenia określające wartości domyślne:
Nie mogą zawierać zmiennych lokalnych (to naturalne, chcemy w prosty sposób zapewnić, że na pewno w każdym wywołaniu da się obliczyć domyślną wartość argumentu).
Nie mogą używać parametrów formalnych funkcji (bo te wyrażenia wylicza się przed wejściem do funkcji, a porządek wartościowania argumentów funkcji nie jest ustalony (zależy od implementacji)). Wcześniej zadeklarowane parametry formalne są w zasięgu i mogą przesłonić np. nazwy globalne.
Argument domyślny nie stanowi części specyfikacji typu funkcji, zatem funkcja z jednym parametrem, który jednocześnie ma podaną wartość domyślną, może być wywołana z jednym argumentem lub bez argumentu, ale jej typem jest (tylko) funkcja jednoargumentowa (bezargumentowa już nie).
Operator przeciążony nie może mieć argumentów domyślnych.
Operator new
new nazwa_typu
lub
new nazwa_typu [ wyrażenie ]
.
Gdy nie uda się przydzielić pamięci zgłasza wyjątek (bad_alloc
).
Operator delete
delete wskaźnik
lub
delete[] wskaźnik
.
Operator sizeof
sizeof wyr
podanego wyrażenia się nie wylicza, wartością jest rozmiar wartości wyr.
sizeof ( typ )
rozmiar typu typ,
sizeof(char) = 1
wynik jest stałą typu size_t
zależnego od implementacji.
Najlepiej unikać
*, /, \item\lstC
+, -!,
<<, >>
,
<, >, <=, >=,
,
==, !=
,
&&
,
||
,
? :
,
=, +=, /=. \ei\end{frame}
\par\subsection{Preprocesor}
\begin{frame}[fragile]\frametitle{Preprocesor}
\bi\item\lstC{#include <…>},
\item\lstC{#include ”…”}.
\ei\end{frame}
\par\subsection{Program}
\begin{frame}[fragile]\frametitle{Program}
\bi\itemProgram składa się z jednostek translacji. Jednostka translacji to pojedynczy plik
źródłowy (po uwzględnieniu dyrektyw preprocesora: \lstC{#include} oraz tych dotyczących warunkowej kompilacji).
\itemJednostki translacji składające się na jeden program nie muszą być kompilowane w tym samym czasie.
\itemProgram składa się z:
\bi\itemdeklaracji globalnych (zmienne, stałe, typy)
\itemdefinicji funkcji
\ei\itemWśród funkcji musi się znajdować funkcja \lstC
main()!.
Jej typem wyniku jest int
. Obie poniższe definicje funkcji main
są dopuszczalne (i żadne inne):
int main(){ /* … */ }
,
int main(int argc, char* argv[]){ /* … */ }
.
Treść automatycznie generowana z plików źródłowych LaTeXa za pomocą oprogramowania wykorzystującego LaTeXML.
strona główna | webmaster | o portalu | pomoc
© Wydział Matematyki, Informatyki i Mechaniki UW, 2009-2010. Niniejsze materiały są udostępnione bezpłatnie na licencji Creative Commons Uznanie autorstwa-Użycie niekomercyjne-Bez utworów zależnych 3.0 Polska.
Projekt współfinansowany przez Unię Europejską w ramach Europejskiego Funduszu Społecznego.
Projekt współfinansowany przez Ministerstwo Nauki i Szkolnictwa Wyższego i przez Uniwersytet Warszawski.