Zagadnienia

5. Typy

Typy

5.1. Typy

5.1.1. Co można zrobić z typami?

  • 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.

5.2. Typy

5.2.1. Typy podstawowe

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.

5.3. Typy pochodne

5.3.1. Typy pochodne - wskaźniki

  • 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!

5.3.2. Typy pochodne - tablice

  • 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];

5.3.3. Typy pochodne - struktury

  • 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.

5.3.4. Typy pochodne - referencje

  • 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ą).

5.3.5. Definiowanie nazwy typu

  • 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.

5.3.6. Wyliczenia

  • Można definiować wyliczenia np.:
    enum kolor{ czerwony, zielony }.

5.3.7. Kwalifikator 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).

5.3.8. Inicjowanie

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}
  }

5.3.9. Funkcje

  • 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 &.

5.3.10. Wartości domyślne parametrów

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.

5.3.11. Zarządzanie pamięcią

  • 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.

5.3.12. Jawna konwersja typu

Najlepiej unikać

5.3.13. Operatory

  • *, /, \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 \lstCmain()!. 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.

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.