Języki programowania - ćwiczenia 1
Temat zajęć: Powtórka z języka C - kompilacja, typy danych,
struktura programu, instrukcje, funkcje.
Literatura:
- B. Kernighan, D. Ritchie: "Język C"
- B. Stroustryp: "Język C++"
- V. Sthern: "C++ Inżynieria Programowania"
Materiały dodatkowe:
Kompilacja kodu źródłowego w języku C
Przebieg kompilacji programu w języku C (C++):
Na zajęciach będziemy korzystać albo z kompilatora firmy Microsoft
(Visual Studio .NET), albo Borland (Borland C++ 5.5), albo GNU (gcc
3.x / 4.x). Zaczniemy od składni kompilacji. Dla kompilatora BCC wygląda ona
następująco:
bcc32 [-Ikatalog] [-Lkatalog] [-enazwa.exe] źródło.c ... [biblioteka.lib ...]
lub w przypadku, gdy chcemy wykonać tylko kompilację (bez linkowania):
bcc32 -c [-Ikatalog] [-onazwa.obj] źródło1.c ...
Dla kompilatora GCC składnia jest następująca:
gcc [-Ikatalog] [-Lkatalog] [-o nazwa] źródło.c ... [-lbiblioteka ...]
lub w przypadku wyłącznie kompilacji:
gcc -c [-Ikatalog] źródło.c ...
W przypadku MS Visual Studio będą dostarczane gotowe pliki
projektów, które zawierają w sobie instrukcje dla kompilatora.
Możliwe jest jednak korzystanie z kompilatora C++ MS Visual Studio z
wiersza poleceń:
cl [/Fe"nazwa_pliku.exe"] źródło.c ... [/link biblioteki]
Typy danych i definicja zmiennych
Posługując się danym językiem programowania musimy znać typy danych,
jakie ten język oferuje oraz operacje, jakie można wykonywać na danych
konkretnego typu.
Język C oferuje następujące typy proste:
- void - typ pusty (wykorzystywany
głównie w operacjach na wskaźnikach i dla wskazania braku
parametrów i/lub wyniku)
- char - typ znakowy (lub całkowity
8-bitowy)
- short int (lub w skrócie short)
- krótki typ całkowity (minimum 16 bitów)
- int - typ całkowity (minimum 16
bitów, zazwyczaj 32 bity)
- long int (lub w skrócie long)
- długi typ całkowity (minimum 32 bity)
- float - krótki typ
rzeczywisty (zazwyczaj 32 bity)
- double - długi typ rzeczywisty
(zazwyczaj 64 bity)
- long double - długi typ rzeczywisty
(zazwyczaj 80 bitów)
- enum - typ wyliczeniowy
Typy całkowite mogą dodatkowo być poprzedzone kwalifikatorem unsigned
lub signed. Pierwszy z nich każe kompilatorowi
traktować dany typ jako typ bez znaku (wszystkie
bity mogą być wykorzystane do przechowania wartości, lecz można
reprezentować tylko liczby nieujemne), drugi wymusza interpretację ze
znakiem (pierwszy bit jest przeznaczony na znak i liczba jest
przechowywana w formacie U2 - uzupełnienia do dwóch).
Domyślnie typy całkowite są signed
(niektóre kompilatory posiadają specjalny parametr,
który powoduje domyślne traktowanie typów
całkowitych jako unsigned).
Stałe całkowite możemy zapisywać jako:
- liczby w układzie dziesiętnym, np. 123,
-334 lub 65535
- liczby w układzie ósemkowym poprzedzone zerem,
np. 011 (= 17 dziesiętnie)
- liczby w układzie szesnastkowym poprzedzone ciągiem 0x,
np. 0xfe (= 254 dziesiętnie)
Dodatkowo w całkowitych stałych liczbowych można na końcu dodać
kwalifikator L (wymusza traktowanie stałej jako
long) i/lub U (wymusza traktowanie stałej jako unsigned).
Stałe rzeczywiste możemy zapisywać jako:
- liczby w układzie dziesiętnym w notacji pozycyjnej, np. 12.223
- liczby w tzw. notacji naukowej, np. -12233E-3
(= -12.223)
Składnia definicji zmiennej w języku C jest następująca:
typ_zmiennej nazwa_zmiennej1[=wartość], nazwa_zmiennej2[=wartość], ...;
Przykład 1
Przeanalizujemy prosty program w języku C, na przykładzie
którego zobaczymy w jaki sposób konstruowany jest
kod programu, jak deklaruje się zmienne, poznamy podstawowe funkcje
wejścia/wyjścia i przekonamy się, że C jest językiem bezlitosnym dla
nieuważnych programistów.
Plik c1p1.c pobierz lub
gotowy projekt Visual Studio pobierz
#include <stdio.h>
int main()
{
int a=8, b, c;
double d=3, e=100;
b = 0xff;
c = a + b;
printf("c = %d\n", c);
d = e / d;
printf("d wynosi %f\n", d);
a = 5;
b = 2;
d = a / b;
printf("%s %f\n", "d = ", d);
getc(stdin);
return 0;
}
Przeanalizujmy kod z przykładu 1.
Wiersze programu w C zaczynające się znakiem #
zawierają dyrektywy dla preprocesora. W tym przypadku żądamy dołączenia
przed kompilacją pliku nagłówkowego stdio.h,
zawierającego m. in. deklarację funkcji printf, z
której nasz program korzysta (implementacja tej funkcji
znajduje się w standardowej bibliotece C, która jest
linkowana z każdym programem, więc nie musimy specjalnie żądać jej
linkowania, jednak deklaracje funkcji trzeba jawnie dołączać do kodu
źródłowego za pomocą #include).
Każdy fragment kodu do wykonania musi znajdować się w jakiejś funkcji.
Funkcją wyróżnioną jest funkcja main,
od której zaczyna się wykonanie programu (w
szczególności program może składać się z samej funkcji main).
Na początku ciała funkcji znajdują się definicje zmiennych lokalnych
tej funkcji (uwaga: w języku ANSI C definicje zmiennych muszą być
umieszczone przed pierwszą instrukcją; w C++ mamy już większą swobodę).
Konstrukcja wyrażeń arytmetycznych jest dość przejrzysta, jednak zawsze
musimy pamiętać, że operatory mogą się różnie zachowywać w
zależności od typów swoich operandów (sprawdzimy
jaki jest wynik instrukcji d = a / b).
Funkcja printf służy do wysyłania tekstu na
standardowe wyjście programu (zazwyczaj ekran). Jej składnia jest
następująca:
printf(łańcuch_formatujący [,wartość1, ...]);
Liczba parametrów zależy od zawartości łańcucha
formatującego. Łańcuch ten jest zwykłym łańcuchem tekstowym
zawierającym pewne specjalne sekwencje sterujące. Najczęściej używanymi
sekwencjami są:
- \n - przejście do nowego wiersza
- %d - wstawienie w miejsce sekwencji
wartości całkowitej podanej jako parametr
- %f - wstawienie w miejsce sekwencji
wartości rzeczywistej podanej jako parametr
- %s - wstawienie w miejsce sekwencji
łańcucha tekstowego podanego jako parametr
Łańcuchy tekstowe w języku C ograniczone są cudzysłowem "
(w odróżnieniu np. od Pascala); w apostrofach 'x'
umieszczamy pojedynczy znak (wartość typu char).
Odpowiednikiem funkcji printf działającym "w
drugą stronę" jest funkcja scanf,
która pobiera tekst ze standardowego wejścia (zazwyczaj
klawiatura) i po ew. konwersji podstawia do podanej zmiennej (wymaga to
podania wskaźnika na zmienną, jednak ze względów
praktycznych podaję niżej prosty przykład).
int a;
double b;
scanf("%d", &a);
scanf("%f", &b);
Funkcja getc służy do odczytu pojedynczego znaku
ze standardowego wejścia. W naszym przykładzie korzystamy z niej aby
zatrzymać wykonanie programu (które za moment się zakończy)
do momentu wciśnięcia przez użytkownika klawisza Enter. W przeciwnym
wypadku program uruchamiany w środowisku Visual Studio natychmiast się
zakończy, co uniemożliwi nam odczytanie prezentowanych w konsoli
wyników. Formalnie prototypem funkcji getc
(zdefiniowana w stdio.h) jest
int getc(FILE *stream)
W naszym przypadku jako strumień (plik) podajemy predefiniowaną zmienną
globalną stdin, która oznacza
standardowe wejście (zwykle klawiaturę). Zwróćmy uwagę, że
wynik zwracany przez getc jest w naszym programie
ignorowany (czyli traktujemy tą funkcję jakby była procedurą).
Zadanie 1
Skompilować i uruchomić kod z przykładu 1. Zwrócić uwagę na
wynik działań d = e / d oraz d = a / b.
Zadanie 2
W języku C dostępny jest operator rzutowania typów (jest on
dość często wykorzystywany). Ma on postać
(nowy typ) zmienna
np.
double z = 3.3;
int x;
/* nie wolno wykonać x = z, ale można np. tak: */
x = (int) z;
Korzystając z operatora rzutowania typów zmodyfikuj kod z
przykładu 1 tak, aby wynikiem wyrażenia z wiersza 18 było 2.5, a nie 2.
Nie wolno zmieniać niczego poza wierszem 18.
Instrukcje sterujące w języku C:
- instrukcja warunkowa
if (warunek) {
instrukcja1;
...
} else {
instrukcjae1;
...
}
Klauzula else może być pominięta, podobnie jak
operatory blokowe { i }
(jeśli występuje pomiędzy nimi pojedyncza instrukcja). Przykład:
if (a < b)
printf("a mniejsze od b");
else if (a > b)
printf("a większe od b");
else printf("a równe b");
- pętla for
for (warunek_pocz; warunek_końc; mod_zmiennej) {
instrukcja1;
...
}
na przykład
for (i=0; i<10; i++) {
printf("%d\n",i);
}
- pętla while
while (warunek) {
instrukcja1;
...
}
- pętla do ... while:
do {
instrukcja1;
...
} while (warunek)
Zadanie 3
Napisać program, który dla podanej z klawiatury liczby
całkowitej wykona jej test pierwszości i wypisze wynik tego testu
("pierwsza" lub "złożona"). Metoda testowania pierwszości: dowolna.
Operatory arytmetyczne.
- +, -, * : odp. dodawanie, odejmowanie, mnożenie
- / : dzielenie rzeczywiste (jeśli choć
jeden operand jest rzeczywisty) lub dzielenie całkowite (jeśli oba
operandy są całkowite)
- % : reszta z dzielenia całkowitego
- &, |, <<, >>
: operatory bitowe (iloczyn, suma, przesunięcia)
- ++, -- : operatory inkrementacji i
dekrementacji
- !, &&, || : operatory
logiczne (negacja, koniunkcja, alternatywa)
Uwaga: znaczenie niektórych operatorów zależy od kontekstu. Przykładowo, operator &
realizuje iloczyn binarny lub pobranie adresu (wskaźnika na) swojego
operandu. Jednak w tej pierwszej roli wymaga dwóch
operandów, zaś w drugiej - jednego. W kodzie programu łatwo to
rozróżnić, np.
int a = 1, b = 3, c;
int* w;
c = a & b; /* iloczyn binarny, wynik: 1 */
w = &a; /* pobranie adresu; do w zapisany jest adres pamięci zmiennej a */
Relacje.
- == : czy równe
- != : czy różne
- <, >, <=, >=
: czy mniejsze, większe, mniejsze bądź równe, większe bądź
równe
Uwaga: Symbol = jest operatorem
przypisania i nie może być używany jako operator
porównania. Na nieszczęście, jeśli przypadkiem zostanie
użyty w takiej roli, nie musi to spowodować błędu kompilacji. Wynikiem
operacji (wyrażenia) przypisania jest bowiem przypisywana wartość,
która dla niektórych typów może być
zinterpretowana jako prawda lub fałsz (patrz zadanie 6 poniżej).
Zadanie 4
Sprawdzić (przez zaimplementowanie krótkiego przykładu) czy
możliwe jest użycie operatora % (modulo) dla
operandów rzeczywistych.
Zadanie 5
Sprawdzić różnicę w działaniu operatora bitowego & i
logicznego && poprzez następujący eksperyment:
int a,b;
a = 1;
b = 2;
if (a) printf("a = prawda\n");
else printf("a = falsz\n");
if (b) printf("b = prawda\n");
else printf("b = falsz\n");
if (a & b) printf("a & b = prawda\n");
else printf("a & b = falsz\n");
if (a && b) printf("a && b = prawda\n");
else printf("a && b = falsz\n");
Zadanie 6
Sprawdzić niebezpieczeństwo przypadkowego użycia operatora =
jako operatora porównania poprzez następujący eksperyment:
int x, y;
x = 10;
y = 20; /* ewidentnie x != y */
if (x = y) printf("10 jest rowne 20\n");
else printf("10 jest rozne od 20\n");
Definicja funkcji
Definicja funkcji w języku C wygląda następująco:
typ_wyniku nazwa_funkcji([typ_par1 nazwa_par1 [,typ_par2 nazwa_par2[,...]]])
{
ciało funkcji
return [wynik];
}
Jeśli typem zwracanego wyniku jest void, po return
nie można podawać zwracanej wartości. W przeciwnym wypadku zwracana
wartość musi mieć typ zgodny z zadeklarowanym typem wyniku funkcji.
Język C dopuszcza również deklarację
funkcji, której celem jest poinformowanie kompilatora o
możliwości
użycia funkcji, zaś jej implementacja może znajdować się w innym module
lub bibliotece (lub dalej w kodzie źródłowym tego samego
modułu).
Deklarację nazywa się również prototypem funkcji. Postać
prototypu jest
następująca:
typ_wyniku nazwa_funkcji([typ_par1 nazwa_par1 [,typ_par2 nazwa_par2[,...]]]);
Jeśli funkcja nie przyjmuje parametrów, jej prototypem jest
typ_wyniku nazwa_funkcji(void);
natomiast jeśli funkcja nie zwraca żadnej wartości, jako typ zwracanej
wartości podajemy void.
Zadanie 7.
Zaimplementować program zawierający podane niżej funkcje tak, aby dał
się poprawnie skompilować i uruchomić.
#include <stdio.h>
int f1(int x)
{
if (x==0) return x;
else return f2(x-1);
}
int f2(int x)
{
if (x==0) return x;
else return f1(x-1);
}
int main(void)
{
int z = 5;
printf("%d\n",f1(z));
printf("%d\n",f2(z));
return 0;
}