Wykład 6 - 3 godz
Zakres tematyczny
1. Instrukcja typedef
2. struktury, unie, pola bitowe.
3. Jeszcze raz o funkcjach:
- funkcje ze zmienną liczbą parametrów
- przeładowanie nazw funkcji
- rekurencja
1. Instrukcja typedef
W języku C wprowadzono mechanizm zwany typedef, do tworzenia nowych nazw typów danych. Np. deklaracja :
typedef int length;
tworzy dla typu int synonim lenght. Z tego typu można korzystać w deklaracjach, rzutowaniu itp., tak samo jak z int:
lenght dl, maxdl;
lenght *dl[];
Deklaracja typedef w rzadnym wypadku nie tworzy nowego typu, nadaje po prostu inną nazwę dla istniejacego już typu. Za używaniem typedef przemawiają dwa względy:
- parametryzacja w związku z problemem przenośności. Jeśli deklarację typedef stosujemy dla typów zależnych od maszyny, to tylko te deklaracje będą musiały być zmienione przy przenoszeniu programów
- deklaraja lepiej komentuje program. Analiza jego jest znacznie prostsza.
Typ który określamy w deklaracji typedef nie musi być podstawowy, może to być dowolny typ pochodny:
typedef int *wsk_int
Deklaracja typedef nie redefiniuje nazw już istniejących.
2.Inne pochodne typy zmiennych
Należą do nich :
struktury
unie
pola bitowe
Struktury
W klasycznym C struktura była traktowana jako obiekt złożony z kilku lub jednej zmiennej, dla wygody zgrupowanych pod jedną nazwą. Struktury ułatwiają zorganizowanie skomplikowanych danych, ponieważ grupę związanych ze sobą zmiennych (nawet różnych typów) pozwalają traktować jako jeden obiekt, a nie zestaw oddzielnych danych. Typowym przykładem takiej struktury może być np. lista płac. Pracownik opisany jest przez kilka zmiennych: imię, nazwisko, adres, wynagrodzenie itp.
Język C++ poszedł dalej w definiowaniu struktur, i wzbogacił je o możliwość przechowywania tzw. funkcji składowych tzn. funkcji operujących na skladowych struktury. Stąd już tylko krok do definiowania klas. Można nawet powiedzieć, że w pojęciu języka C++ zarówno struktury jak i unie są typami "klasowymi", czyli typu class. Ale o tym pomówimy w przyszłym semestrze. Na tym wykładzie omówimy struktury w starym stylu, natomiast po omówieniu klas powrócimy do struktur w nowym ujęciu.
Deklarację stuktury rozpoczyna słowo kluczowe struct. Podajmy przykład struktury odpowiedniej dla grafiki. Punkt opisują dwie współrzędne, zadeklarowane jako składowe struktury:
struct point
{
int x;
int x;
}
Po słowie struct występuje nazwa struktury (point) zwana etykietą struktury. Nazwy zmiennych występujących w strukturze nazywamy składowymi struktury.
Obiekt typu struktura wymaga tyle pamięci do zapisu, ile wynosi suma pamięci koniecznej do zapisu poszczególnych składowych struktury.
Składowa struktury, normalna zmienna, etykieta mogą mieć te same nazwy bez obawy o konflikt. Deklaracja struct jest definicją nowego typu złożonego.
Po prawej klamrze zamykającej listę zmiennych może występować lista zmiennych, tak jak po każdym podstawowym typie. Zatem definicja:
struct {...} x,y,z
odpowiada składniowo definicji:
int x,y,z;
Deklaracja struktury, która nie zawiera listy zmiennych nie rezerwuje pamięci, jedynie opisuje wzorzec struktury. Mając deklarację struktury można używać jej do deklaracji zmiennych typu struktura np.:
struct point punkt; //równoważne point punkt;
deklaruje zmienną punkt będącą strukturą typu point.
Strukturę można zainicjować dopisując na końcu jej definicji listę wartości początkowych jej składowych np:
struct point maxpoint = {320,200};
W wyrażeniach dostęp do konkretnej składowej struktury umożliwia konstrukcja:
nazwa_struktury.składowa
dla zmiennej strukturalnej punkt owdołania są nastepujące:
a = punkt.x;
b = punkt.y;
Język C umożliwia konstruowanie struktur zagnieżdżonych:
struct rect{
struct point p1;
struct point p2;
}
W takich strukturach dostęp do składowych podstawowych wykonuje się przy pomocy operacji:
struct rect ekran;
a1 = ekran.p1.x
b1 = ekran.p1.y;
Podobnie jak w przypadku zmiennych podstawowych, możemy posługiwać się tablicami struktur. Jest to często spotykane przy tworzeniu rekordów, które mają być zapisane do pliku. Można więc stworzyć tablicę struktur rect:
struct rect{
struct point p1;
struct point p2;
} okno[5];
Każdy element tablicy jest strukturą. Powyższy zapis można zapisać inaczej:
struct rect okno[5];
Inicjowanie tablicy struktur przeprowadza się podobnie jak dla pojedynczych struktur, po definicji podaje się ujętą w nawiasy klamrowe listę wartości początkowych:
struct point {
int x;
int y;
} punkt[ ] = {
0,0,
1,1,
1.5,
16,8 }
Precyzyjniej byłoby ująć wszystkie elementy wierszy tablicy w nawias {}np.:
{1,2},
{1.4},
{1,5},
........
ale gdy podano wszystkie wartości i gdy wartościami są proste stałe lub napisy, wówczas wewnętrzne nawiasy można pominąć. Jak w przypadku normalnych tablic, gdy pominięto wymiar i podano listę wartości początkowych, to liczba elementów tablic struktur zostaje wyliczona automatycznie.
Podczas omawiania wskaźników powiedzieliśmy, że można je tworzyć do większości typów podstawowych i pochodnych (złożonych). Pora wię teraz na omówienie wskaźników do struktur. Są one tak powszchne, że do języka włączono specjalny operator (strzałkę), mający postać: ->. Użycie tego operatora przedstawimy na przykładzie struktury dane:
struct dane{
int klucz;
int tabllica[200];
}dane_dos
Deklarujemy wskażnik wsk_str do zmiennej dane_dos:
struct dane *dane_dos;
inicjujemy go zmienna dane_dos:
wsk_str = &dane_dos;
Od tego czasu do pól struktury można odwoływać sie poprzez wskaźnik w następujący sposób:
wsk_str->klucz = 1; // ekwiwalent dane_dos .klucz = 1;
Zwróćmy uwagę, że nie stosujemy * do odwoływania się do wartości elementu składowego struktury wskazanej przez wskaźnik wsk_str. Powodem wprowadzenia operatora -> do obsługi stuktur jest fakt, że przez operatory * i & nie uzyskamy dostępu do poszczególnych elementów struktury.Nie ma więc czegoś takiego jak:
*wsk_str.klucz;
Unie
Unia jest zmienną, która w różnych momentach może przechowywać w tym samym miejscu pamięci obiekty różnych typów. Obiekty typu unia wymagają do zapisu tyle bajtów pamięci ile wynosi liczba bajtów potrzebna na przechowanie najdłuższego elementu uni. Deklaracja unii podobna jest do deklaracji struktury, tyle tylko, że dokonuje się jej przy pomocy słowa kluczowego union. Operacje dozwolone na strukturach są dozwolone na uniach : kopiowanie i przypisywanie unii traktowanych jako całość, pobieranie adresu, dostęp do ich składowych. Sposób deklaracji i operowania uniami przedstawimy na przykładzie:
#include
#include
#include
union NumericType //deklaracj auni mogącej przechowywać:
{
int iValue; //wartość typu int
long lValue; //wartość typu long
double dValues; //wartość typu double
};
int main(int argc, char *argv[])
{
NumericType *Values = new NumericType[argc-1];
for(int i =1; i
if(strchr(argv[i],'.') != 0) //typ float. użyj składowej dValue do przypisania.
Values[i].dValues = atof(argv[i]);
else //typ nie float
{
if(atol(argv[i]) > INT_MAX) //jeśli dana jest większa niż największy
Values[i].lValues = atol(argv[i]); //int to zapisz ją w lValues
else jeśli nie to w iValues
Values[i].ivalue = atoi(argv[i]);
}
return 0;
}
Przykładowa unia NumericType jest rozmieszczona w pamięci jak przedstawiono na rysunku
Należy pamiętać o tym, aby do przypisywania składników unii zadeklarować taką zmienną, która będzie mogła przechować najdłuższą składowa unii.
Do skladowej uni można się odwoływac także poprzez wskaźnik podobnie jak i w strukturze:
wskaźnik_do_unii -> skladowa.
Unię można zainicjować podobnie jak innej zmienne, należy jednak pamiętać , że można to zrobic jedynie wartością o typie jej pierwszej składowej np.:
NumericType values = 5; //Ok
NumericType values = 14578.9 //Bład - pierwszym elementem unii jest int
Można definiować tzw. unie anomimowe tj. takie które same nie mają nazwy, jak też nie ma nazwy jedyny egzemplarz tej unii. Do składników tej unii odwołujemy się po prostu poprzez nazwę składnika bez operatora '.' np.:
union {
int a;
float b;
char c;
};
int a; //Bład redefinicja zmiennej a co wynika z anonomowosci unii
a = 4; cout<
b = 1.2;
Podobnie jak w przypadku struktur unia może mieć funkcje składowe, wtedy traktuje się ją jak zmienną typu klasa. O tym, później.
Pola bitowe
Struktury i klasy mogą przechowywać składowe zajmujące mniej miejsca niż typ całkowity. Takie składowe nazywane są polami bitowymi. Jest to zbiór przylegających do siebie bitów, znajdujących się w jednej jednostce pamięci zwanej jak państwo wiecie słowem. Składnia deklaracji pola bitowego jest następująca:
deklaratoropt : wyrażenie_stałe
deklarator jest nazwą poprzez którą odwołujemy sie do konkretnego pola bitowego w programie (musi to być typ calkowity), wyrażenie stałe określa liczbę bitów, które zajmuje pole bitowe w strukturze np:
struct Date{
unsigned nWeekDay : 3; //0...7 (3bits)
unsigned nMonthDay : 6; //0...31(6bits)
unsigned NMonth : 5; //0...12(5 bits)
unsigned NYear : 8; //0...100(8 bits)
Możliwe rozmieszczenie bitów w pamięci (zależy to od implementacji) przedstawia rysunek:
Ponieważ pole mYear przekroczyłoby typ unsigned int, dlatego zostało wpisane do następnego słowa typu unsigned int.
Pole bitowe bez nazwy, sluży jako separator (wypełniacz), a jeśli ma dodatkowo 0 bitów to sugeruje, aby następne pole bitowe znalazło się w następnym słowie np.
struct Date{
unsigned nWeekDay : 3; //0...7 (3bits)
unsigned nMonthDay : 6; //0...31(6bits)
unsigned : 0; //przenosi pozostale pola do innego slowa
unsigned NMonth : 5; //0...12(5 bits)
unsigned NYear : 8; //0...100(8 bits)
Możliwe rozmieszczenie bitów w pamięci (zależy to od implementacji) przedstawia rysunek:
Istnieja ograniczenia w używaniu pól bitowych:
1. nie można pobrac adresu do pola bitowego
2. nie można deklarować wskaźników do pól bitowych
3. deklarować referencji do pól bitowych.
3. Jeszce raz o funkcjach
Funkcje o zmiennej liczbie argumentów
Jedną z zalet języka C/C++ jest możliwość definiowania funkcji o zmiennej liczbie parametrów. Spotkaliśmy się już wcześniej z takimi funkcjami: np. funkcja standardowa printf ma zmienną liczbe argumentów, gdyż zależy ona od liczby znaków formatujących znajdujących się w łańcuchu formatującym. Deklaracja takiej funkcji zwykle wygląda następująco:
Informacje o zmiennej liczbie argumentów niosą trzy kropki, występujace na końcu listy parametrów funkcji. Sposób tworzenia i posługiwania się takimi funkcjami podamy na podstawie funkcji wielomian, obliczającej wartość wielomianu danego stopnia st, w danym punkcie x:
#include
#include
#include
double wielomian(double x, int st,...)
{
double wart = 0,wsp;
va_list ap;
va_start(ap,st);
for(;st;--st)
wart += va_arg(ap,double) * pow(x,st);
wart += va_arg(ap,double);
va_end(ap);
return wart;
}
int main(void)
{
printf("
%lf",wielomian(2.0,3.0,1.0,2.0,-3.0,5.0));
return 0;
}
W celu dostępu do parametrów nieustalonych posługujemy się makrodefinicjaami: va_start, va_arg, va_end oraz typem va_list, zdefiniowanym w zbiorze nagłówkowym
1. deklarujemy zmienną ap typu va_list.
2. przed rozpoczęciem dostępu do parametrów nieustalonych, wywołujemy makro: va_start z parametrami: zmienna ap i identyfikatorem ostatniego parametru ustalonego: u nas st:
va_start(ap,st)
3. W celu dostępu do parametrów nieustalonych, wywołujemy makro va_arg z parametrami ap i nazwą typu pobieranego parametru:
va_arg(ap,double)
spowoduje to pobranie nieustalonego parametru i potraktowanie go jako liczby double. Kolejne wywołania makra powodują pobieranie kolejnych parametrów funkcji. Ilość wywołań określa się na podstawie stopnia wielomianu (w przykładowym programie);
4. Po zakończeniu operacji na parametrach nieustalonych należy wywołać makro va_end umożliwiające normalne zakończenie wykonania funkcji o zmiennej liczbie argumentów.
Przeładowanie nazw funkcji
W języku angielskim przeładowanie (overloading) jakiegoś słowa oznacza, że ma ono wiele znaczeń. We wczesniejszych wersjach języka, oraz innych językach programowania z nazwami funkcji związane bylo jedno poważne zastrzeżenie:
w programie mogła być jedna funkcja o danej nazwie. Używając nazwy tej funkcji mówiliśmy kompilatorowi o którą funkcję nam chodzi. Kompilator C++ jest o wiele bardziej inteligentny i dopuszcza użycie tej samej nazwy dla kilku funkcji:np.
void screen(int);
void screen(char, float, int);
Obie te funkcje mimo tej samej nazwy różnią się jednak od siebie: mają inne parametry wywołania. W związku z tym należy przypuszczać, iż kompilator rozpoznaje funkcje nie tylko po nazwie, ale także po liście argumentów tej funkcji. Zjawisko to nazywamy właśnie przeładowaniem nazw funkcji. Funkcje przeładowane, mają tę samą nazwę, ale różnią się liczbą lub typem zmiennych , bądź dla tej samej liczby argumentów kolejnościa typów ich występowania. To, która funkcja zostanie wywołana zależy od kontekstu, czyli od towarzyszących jej argumentów wywołania.
Błędem jest proba definicji dwóch funkcji o identycznej nazwie i tej samej liście argumentów. Przy przeładowaniu istotna jest tylko lista argumentów, natomiast typ zwracany przez funkcje nie jest brany pod uwagę.
O sczegółach dotyczących funkcji przeładowanych musicie państwo przeczytac samodzielnie w podrecznikach.
Rekurencja
Funkcje języka C mogą byc wuwolywane rekurencyjnie, tzn. funkcja może wywoływac sama siebie zarówno bezpośrednio jak i pośrednio. Najprostrzym przykładem funkcji rekurencyjnej może byc funkcja obliczajaca wartość silni dowolnej liczby naturalnej. Porównajmy wersję silnia - nierekurencyjną z wersją silnia_rec - rekurencyjna:
Przykład
#include
#include
long silnia_rec(int);
long silnia(int);
main()
{
int n;
long res;
clrscr();
cin >> n;
res = silnia(n);
cout<<"Silnia = "<
while(!kbhit());
}
long silnia_rec(int n)
{
long sil;
if(n == 1)
sil = 1;
else
sil = n * silnia_rec(n-1);
return sil;
}
long silnia(int n)
{
long sil=1;
int i;
for(i = 1;i<=n;i++)
sil = i * sil;
return sil;
}
Na ćwiczeniach zapoznamy się z jeszcze innymi algorytmami rekurencyjnymi. Rekurencja nie musi przynosić oszczędności pamięci, ponieważ trzeba gdzieś podziać stos używanych wartości. Nie zawsze przyspiesz też działania programu. Dlaczego się więc ja stosuje? Postać rekurencyjna dla wielu algorytmów jest bardziej zwarta i często łatwiejsza do napisania i zrozumienia niz jej wersja nierekurencyjna.