[WikiDyd] [TitleIndex] [WordIndex

Materiały zostały opracowane w ramach realizacji Programu Rozwojowego Politechniki Warszawskiej.

http://prpw.iem.pw.edu.pl/images/KAPITAL_LUDZKI.gif http://prpw.iem.pw.edu.pl/images/EU+EFS_P-kolor.gif

http://www.pr.pw.edu.pl/ jest projektem współfinansowanym przez Unię Europejską w ramach Europejskiego Funduszu społecznego (działanie 4.1.1 Programu Operacyjnego Kapitał Ludzki) i ma na celu poprawę jakości kształcenia oraz dostosowanie oferty dydaktycznej Politechniki Warszawskiej do potrzeb rynku pracy. Będzie on realizowany przez Uczelnię w latach 2008-2015.


Laboratorium metodyki programowania

Ćwiczenie 2: Podstawy programowania w języku C

Scenariusz

  1. Student loguje się do systemu LOP (Laboratorium Otwartego Programowania).
  2. Student przechodzi do katalogu roboczego dla zajęć LMP.
  3. Student pobiera z repozytorium kod programów.
  4. Po wyjaśnieniach prowadzącego student przystępuje do kompilacji i uruchomienia programów. Porównuje wynik działania z kodem źródłowym.
  5. Student modyfikuje programy według wskazówek prowadzącego (patrz opis szczegółowy). Powtarza próbę kompilacji i uruchomienia.
  6. Ćwiczenie zaliczeniowe: student rozszerza funkcjonalność programu według zamówienia prowadzącego.

Opis szczegółowy

W trakcie zajęć nauczymy się podstaw tworzenia programów w języku C. Przeanalizujemy i zmodyfikujemy programy pobierające z pliku tekstowego ciąg liczb rzeczywistych i obliczające wartość minimalną, maksymalną, sumę itp. Przeanalizujemy następujące zagadnienia:

Rozpoczęcie pracy

Zajęcia będą prowadzone na komputerze stud.iem.pw.edu.pl. W celu rozpoczęcia pracy należy zalogować się na tym komputerze za pomocą usługi ssh. Można w tym celu wykorzystać albo dostępny w każdym praktycznie systemie Unix/Linux program ssh albo klienta SSH dla systemu Windows - PuTTY. Na maszynach studenckich w IETiSIP PW można podnieść różne dystrybucje Uniksa/Linuksa lub system Windows, ale w każdej z nich jest zainstalowane oprogramowanie ssh.

Do logowania na maszynie stud.iem.pw.edu.pl należy użyć takiego samego loginu i hasła jakie wykorzystywane sa do dostępu do usług wydziałowych (poczta, e-dziekanat, itd.).

Po zalogowaniu się należy przejść do utworzonego na pierwszych zajęciach katalogu lmp:

 cd lmp

Kod do zajęć

lmp2.tgz

Praca nad kodem

Pierwszy program

Pierwszy program oblicza sumę argumentów, z którymi został wywołany. Argumenty muszą być liczbami.

Funkcja main przetwarza tablicę argv (pamiętamy, że zawiera ona argumenty w postaci napisów) na liczby rzeczywiste (przy pomocy funkcji atof podobnej do poznanej na poprzednich zajęciach funkcji atoi) i pakuje je do wektora (który może pomieścić nie więcej, niż 1000 liczb - ale szansa, że ktoś poda więcej argumentów jest niewielka).

Po przetworzeniu argumentów main wywołuje funkcję pwekt (Pisz WEKTor), która wypisze na ekran zawartość wektora, po czym, za pomocą kolejnej funkcji (sum) obliczy sumę wszystkich elementów wektora i wypisze ją na ekran.

Funkcja sum pokazuje dość istotny aspekt języka C - otóż do funkcji przekazywane są kopie aktualnych argumentów wywołania (nazywamy to przekazywaniem argumentów przez wartość). Można to zaobserwować śledząc wartość zmiennej l z funkcji main. Zmienna ta jest przekazywana jako argument do funkcji sum i tam utożsamiana z argumentem formalnym n, który jest modyfikowany w trakcie obliczania sumy (każdy obieg pętli while z linii 10 zmniejsza zmienną n o 1). Mimo tego wartość zmiennej l w funkcji main pozostaje nie zmieniona, o czym przekonuje nas ostatnia instrukcja printf.

Oto program p1.c:

   1 #include <stdio.h>              /* dla printf */
   2 #include <stdlib.h>             /* dla atof */
   3 
   4 double
   5 sum(double v[], int n)
   6 {
   7         /*
   8          * Funkcja oblicza i zwraca sume elementow n-elementowego wektora v
   9          */
  10         double          s = v[0];
  11 
  12         while (--n)
  13                 s += v[n];
  14 
  15         return s;
  16 }
  17 
  18 void
  19 pwekt(double v[], int n)
  20 {
  21         /* Ladne wypisywanie wektora na standardowe wyjscie */
  22         int             i;
  23         printf("[");
  24         for (i = 0; i < n; i++)
  25                 printf(" %g", v[i]);
  26         printf(" ]");
  27 }
  28 
  29 int
  30 main(int argc, char *argv[])
  31 {
  32         double          r       [1000]; /* jesli ktos poda wiecej argumentow
  33                                          * - bedzie bieda! */
  34         int             i;
  35 
  36         int             l = argc - 1;   /* liczba el. wektora */
  37 
  38         for (i = 1; i < argc; i++)      /* zaczynamy od drugiego el. argv */
  39                 r[i - 1] = atof(argv[i]);       /* atof: ASCII to Float */
  40 
  41         pwekt(r, argc - 1);     /* można tak, ale lepiej l zmiast argc-1 */
  42 
  43         printf(", suma elementow = %g\n", sum(r, l));
  44 
  45         printf("wektor ma %d elementow\n", l);  /* zwróćmy uwagę, że
  46                                                  * funkcja sum nie "popsula"
  47                                                  * zmiennej j */
  48 
  49         return 0;
  50 }

A tutaj ślad wykonania programu:

volt:~/lmp/lmp2/gr1> cc -Wall -pedantic -ansi p1.c
volt:~/lmp/lmp2/gr1> ./a.out 1 2 3 4 5.5 6.6 7.7
[ 1 2 3 4 5.5 6.6 7.7 ], suma elementow = 29.8
wektor ma 7 elementow
volt:~/lmp/lmp2/gr1>

Ćwiczenia do wykonania:

Drugi program

Kolejny program to generator, który przygotuje zbiór liczb losowych. Posłużymy się w tym celu funkcją rand ze standardowej biblioteki. Funkcja ta przystosowana jest do testowania kodu i dlatego generuje sekwencje pseudolosowe - powtarzające się. Ponieważ chcemy generować różne sekwencje przy kolejnych wywołaniach generatora - posługujemy się funkcją srand, która pozwala zainicjować generator rand. Różne wartości inicjujące powodują wypisywanie różnych ciągów danych i aby dodatkowo zrandomizować wyniki, używamy jako argumentu srand wartości zwracanej przez kolejną funkcję z biblioteki standardowej - time. Funkcja ta, wywołana w taki sposób, jak w naszym programie, zwraca liczbę sekund, które upłynęły od 1 stycznia 1970.

Nasz generator oczekuje co najwyżej czterech argumentów, ale są one opcjonalne. Pierwszy argument określa, ile liczb ma wygenerować program. Jeżeli nie podamy pierwszego argumentu, to program wygeneruje 10 liczb. Drugi argument określa kres dolny przedziału, w którym zawarte będą generowane wartości. Domyślna wartość, przyjmowana gdy nie podamy drugiego argumentu, to 0. Trzeci argument określa górny kres przedziału wartości. Jego domyślna wartość to 1. Ostatni, czwarty argument, to nazwa (ścieżka) pliku, do którego mają zostać zapisane wyniki (czyli liczby). Jeśli argument ten nie zostanie podany, to liczby zostaną wypisane do stdout (czyli domyślnie na ekran).

Ponieważ argumenty są opcjonalne, a więc przed ich użyciem - odwołaniem do odpowiedniego elementu tablicy argv sprawdzamy, czy element ten w ogóle istnieje. I tak na przykład sprawdzenie w wyrażeniu warunkowym w linii 7, czy argc > 1 (czyli równe co najmniej 2) upewnia nas, że argv[1] istnieje. Podobnie postępujemy w kolejnych liniach dla dalszych elementów argv.

Funkcja rand zwraca liczbę całkowitą z zakresu od 0 do RAND_MAX (RAND_MAX to stała (tak naprawdę to tzw. makro, ale o tym później) zdefiniowana w bibliotece standardowej). Ponieważ nasz program ma zwracać wartości rzeczywiste, a więc przeliczymy zwróconą przez rand wartość na odpowiadający nam zakres.

Oto program p2.c:

   1 #include <stdio.h>           
   2 #include <stdlib.h>          
   3 #include <time.h>            
   4 
   5 int main(int argc, char *argv[])
   6 {
   7         int             n = argc > 1 ? atoi(argv[1]) : 10;      /* ile liczb? domyslnie
   8                                                                  * 10 */
   9         double          min = argc > 2 ? atof(argv[2]) : 0.0;   /* minimalna możliwa
  10                                                                  * wartość? domyślnie
  11                                                                  * 0 */
  12         double          max = argc > 3 ? atof(argv[3]) : 1.0;   /* maksymalna możliwa
  13                                                                  * wartość? domyślnie
  14                                                                  * 1 */
  15 
  16         FILE           *out = argc > 4 ? fopen(argv[4], "w") : stdout;  /* do jakiego pliku
  17                                                                          * pisać? domyślnie na
  18                                                                          * konsolę */
  19         /*
  20          * fopen otwiera plik, którego nazwę określa pierwszy argument
  21          * drugi argument mówi, w jakim celu plik otwieramy - tutaj "w"
  22          * (write) określa, że chcemy coś do niego zapisać. fopen zwróci
  23          * specjalną wartość - NULL, jeśli pliku nie uda się otworzyć
  24          */
  25 
  26         int             i;
  27 
  28         if (out == NULL) {      /* sprawdzamy, czy udało się otworzyć plik
  29                                  * podany jako argv[4] */
  30                 /* Uwaga: stdout != NULL (na pewno) */
  31                 fprintf(stderr, "%s: nie moge pisac do %s\n", argv[0], argv[4]);
  32                 return 1;
  33         }
  34         srand(time(NULL));      /* inicjujemy gen. losowy wartością z
  35                                  * zegara */
  36 
  37         /*
  38          * Petla drukujaca po LL liczb w jednej linii Do zmiany linii
  39          * używana jest sztuczka z formatem drukujacym \n gdy i jest
  40          * wielokrotnoscia LL
  41          */
  42         for (i = 0; i < n; i++) {
  43                 double          ulamek = (double)rand() / RAND_MAX;
  44                 fprintf(out, "%g%c",
  45                         min + (max - min) * ulamek, (i % LL == LL - 1 ? '\n' : ' ')
  46                         );
  47         }
  48         if (i % LL != 0)
  49                 fprintf(out, "\n");
  50 
  51         /*
  52          * W zasadzie powinniśmy zamknąc plik, ale zaraz kończymy, więc i
  53          * tak zrobi to za nas system. A niedobrze byłoby zamknąć stdout !
  54          *
  55          * fclose( out );
  56          */
  57 
  58         return 0;
  59 }

Przeanalizujmy dokładnie kluczowy fragment kodu p2.c:

        for (i = 0; i < n; i++) {
                double          ulamek = (double)rand() / RAND_MAX;
                fprintf(out, "%g%c",
                        min + (max - min) * ulamek, (i % LL == LL - 1 ? '\n' : ' ')
                        );
        }

Pierwsza linia wewnątrz pętli for to wywołanie funkcji rand generującej kolejną liczbę losową, przekształcenie zwracanego wyniku na typ double (przez operację rzutowania (double)), podzielenie rezultatu przez RAND_MAX (dzięki wcześniejszemu przekształceniu zwróconej przez rand wartości to dzielenie wykonywane jest na liczbach zmiennoprzecinkowych - działanie na liczbach całkowitych dawałoby wynik 0 lub 1) i wreszcie podstawienie rezultatu pod zmienną ulamek. W kolejnej linii wartość ta jest odwzorowywana na podany przez użytkownika zakres min-max przez wyrażenie

   1    min + (max - min) * ulamek

i następnie wypisywana przez instrukcję wywołującą systemową funkcję fprintf do strumienia out. Użyty w wywołaniu fprintf format "%g%c" powoduje wypisanie liczby rzeczywistej w postaci wygodnej dla człowieka oraz dopisanie po niej pojedynczego znaku, określanego przez kolejny (czwarty) argument funkcji fprintf:

   (i % LL == LL - 1 ? '\n' : ' ')

To wyrażenie warunkowe, które zwraca znak nowej linii (\n) jeśli zmienna i dzieli się bez reszty przez stałą LL i spację w innym przypadku. Taka "sztuczka" spowoduje, że nasz program będzie wypisywał po LL liczb w każdej linii pliku wynikowego.

Uważny obserwator zauważył pewnie, że stała LL nie została w programie zdefiniowana. Można to zrobić przez umieszczenie na początku pliku instrukcji preprocesora define, np. aby wypisywac po 5 liczb w jednej linii:

   1    #define LL 5
   2 

ale ten sam efekt możemy osiągnąć na etapie kompilacji programu przy pomocy opcji -D kompilatora:

volt:~/lmp/lmp2/gr1> cc -Wall -ansi -pedantic -DLL=5 p2.c
volt:~/lmp/lmp2/gr1> ./a.out
0.777234 0.974562 0.47057 0.870039 0.74157
0.559945 0.00300529 0.509885 0.629421 0.673225
volt:~/lmp/lmp2/gr1> ./a.out 22
0.777273 0.632251 0.248597 0.163289 0.405406
0.654741 0.238228 0.904208 0.0259027 0.346689
0.808119 0.0560304 0.703224 0.0933425 0.807951
0.226861 0.845113 0.814212 0.454461 0.132674
0.850929 0.561807
volt:~/lmp/lmp2/gr1>

Ćwiczenia do wykonania:

Trzeci program

Trzeci program przeczyta wszystkie liczby z wygenerowanego przez nas pliku z danymi, obliczy ich liczbę, sumę, wartość średnią i odchylenie standardowe.

Oto on (p3.c):

   1 #include <stdio.h>
   2 #include <math.h>
   3 
   4 int
   5 main(int argc, char *argv[])
   6 {
   7         FILE           *in = argc > 1 ? fopen(argv[1], "r") : stdin;
   8 
   9         if (in != NULL) {
  10                 double          sum = 0;
  11                 double          sum2 = 0;
  12                 double          x;
  13                 int             n = 0;
  14 
  15                 while (fscanf(in, "%lf", &x) == 1) {
  16                         sum += x;
  17                         sum2 += x * x;
  18                         n++;
  19                 }
  20 
  21                 fclose(in);
  22 
  23                 printf("\tn=%d, sum=%g, avg=%g std_dev=%g\n",
  24                        n, sum, sum / n, sqrt(n * sum2 - sum * sum) / n
  25                       );
  26                 return 0;
  27         } else
  28                 return 1;
  29 }

Pewnych wyjaśnień wymagają instrukcje umieszczone w liniach 7,15 i 24.

W linii 7 otwieramy do odczytu (argument "r") plik, którego nazwę (ścieżkę) podał użytkownik jako pierwszy argument programu. Oczywiście musimy się wcześniej upewnić, czy użytkownik w ogóle jakiś argument podał (argc > 1). Jeżeli nie podał, to będziemy czytać liczby ze standardowego strumienia wejściowego stdin.

W linii 15 próbujemy czytać kolejne liczby ze strumienia in. Jeżeli funkcji fscanf uda się przeczytać kolejną liczbę double (format "%lf") to zwróci jako wynik 1 (liczbę przeczytanych danych) - zwrócenie wartości innej niż 1 oznacza, że plik się "skończył" lub napotkano coś innego, niż liczba. Przeczytana liczba jest "pakowana" do zmiennej x. Nieco tajemniczo wyglądające oznaczenie &x pozwala funkcji fscanf zmodyfikować zmienną x (a nie jej kopię). Więcej o symbolu & powiemy na kolejnych zajęciach.

Linia 24 to obliczenie i wypisanie wartości średniej i odchylenia standardowego. Ta druga wielkość liczona jest tutaj w pewien specjalny sposób, który pozwala uniknąć dwukrotnego przeglądania ciągu liczb (jak wymagałby definicyjny wzór na odchylenie standardowe). Algorytm ten nie jest pozbawiony wad, ale to raczej temat na wykład z metod numerycznych lub obliczeń naukowych. Zainteresowanych problemem odsyłamy do wykładu Advanced Scientific Computations. Zwróćmy jeszcze tylko uwagę, że w wyrażeniu używamy standardowej funkcji matematycznej sqrt obliczającej pierwiastek kwadratowy. To właśnie ze względu na tę funkcję wciągamy do naszego programu plik nagłówkowy math.h (linia nr 2).

Przed kompilacją i uruchomieniem trzeciego programu przygotujemy dla niego dane testowe. Korzystając z programu drugiego utworzymy trzy pliki o różnej długości:

volt:~/lmp/lmp2/gr1> ./a.out 1000 0 10 > tysiac
volt:~/lmp/lmp2/gr1> ./a.out 1000000 0 10 > milion
volt:~/lmp/lmp2/gr1> ./a.out 10000000 0 10 > 10milionow

Przy okazji kompilacji trzeciego programu posnamy dwie kolejne opcje kompilatora:

Po tych wyjaśnieniach powinni Państwo bez problemów zrozumieć poniższy kod:

volt:~/lmp/lmp2/gr1> cc -o stat p3.c -lm -Wall -ansi -pedantic
volt:~/lmp/lmp2/gr1> ./stat 10milionow
        n=10000000, sum=5.00144e+07, avg=5.00144 std_dev=2.88669
volt:~/lmp/lmp2/gr1> ./stat milion
        n=1000000, sum=5.00101e+06, avg=5.00101 std_dev=2.88681
volt:~/lmp/lmp2/gr1> ./stat tysiac
        n=1000, sum=5093.66, avg=5.09366 std_dev=2.83198
volt:~/lmp/lmp2/gr1>

Przy pomocy polecenia time sprawdźmy jeszcze, jak zmienia się czas wykonania naszego programu w zależności od wielkości danych:

volt:~/lmp/lmp2/gr1> time ./stat tysiac
        n=1000, sum=5093.66, avg=5.09366 std_dev=2.83198
./stat tysiac  0,00s user 0,00s system 62% cpu 0,005 total
volt:~/lmp/lmp2/gr1> time ./stat milion
        n=1000000, sum=5.00101e+06, avg=5.00101 std_dev=2.88681
./stat milion  0,29s user 0,02s system 99% cpu 0,308 total
volt:~/lmp/lmp2/gr1> time ./stat 10milionow
        n=10000000, sum=5.00144e+07, avg=5.00144 std_dev=2.88669
./stat 10milionow  2,87s user 0,14s system 99% cpu 3,015 total
volt:~/lmp/lmp2/gr1>

Program na zaliczenie

Do wyboru przez prowadzącego


To już prawie wszystko, ale nie wolno nam zapomnieć, że

materiały zostały opracowane w ramach realizacji Programu Rozwojowego Politechniki Warszawskiej.

http://prpw.iem.pw.edu.pl/images/KAPITAL_LUDZKI.gif http://prpw.iem.pw.edu.pl/images/EU+EFS_P-kolor.gif

http://www.pr.pw.edu.pl/ jest projektem współfinansowanym przez Unię Europejską w ramach Europejskiego Funduszu społecznego (działanie 4.1.1 Programu Operacyjnego Kapitał Ludzki) i ma na celu poprawę jakości kształcenia oraz dostosowanie oferty dydaktycznej Politechniki Warszawskiej do potrzeb rynku pracy. Będzie on realizowany przez Uczelnię w latach 2008-2015.


2015-09-23 06:44