Kontakt  |  PRS 170  |  ALK 420  |  PZR 420  |  SIK 420  |  JPR 222  |  SOP 121


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++):


Schemat kompilacji programu w 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;
}


Valid HTML 4.01!