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 1: Środowisko programistyczne systemu Unix/Linux

Scenariusz

  1. Student loguje się do systemu LOP (Laboratorium Otwartego Programowania).
  2. Student tworzy katalog roboczy dla zajęć i przechodzi do tego katalogu.
  3. Student pobiera z repozytorium kod programów.
  4. Po wyjaśnieniach prowadzącego student przystępuje do kompilacji i uruchomienia pierwszego programu. Porównuje wynik działania z kodem źródłowym.
  5. Student modyfikuje program według wskazówek prowadzącego (np. zmiana formatu wypisywania wyników). Powtarza próbę kompilacji i uruchomienia.
  6. Student próbuje odtworzyć z pamięci pierwszy program. Kompiluje i uruchamia stworzony kod.
  7. Student powtarza punkty d-f dla kolejnych programów.
  8. Ćwiczenie zaliczeniowe: student pisze program według zamówienia prowadzącego.

Opis szczegółowy

W trakcie zajęć nauczymy się logować do systemu, poruszać w linuksowym (uniksowym) systemie plików, wykonywać podstawową edycję programu przy pomocy edytora vi(m) i kompilować proste programy w C.

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 utworzyć katalog roboczy:

 mkdir lmp

i przejść do niego:

 cd lmp

Inne polecenia powłoki (shella), które mogą być przydatne w trakcie pracy to: ls, cp, mv, rm, pwd, cat, less. Opis każdego polecenia można uzyskać w trakcie pracy przy pomocy komendy man:

 man mkdir

Opisy poleceń można też znaleźć w następujących witrynach:

Kod do zajęć

lmp1.tgz

Praca nad kodem

Większość czasu spędzimy wykonując w kółko trzy polecenia (pX.c to przykładowa nazwa pliku - w rzeczywistości będą to p1.c, p2.c itd.):

vim pX.c
cc -Wall -ansi -pedantic pX.c
./a.out

Pierwsze z tych poleceń:

vim p1.c

to uruchomienie edytora vim, którego będziemy używać (i uczyć się) w trakcie zajęć.

W większości systemów uniksopodobnych istnieje polecenie vimtutor uruchamiające półgodzinną, interaktywna lekcję vi/vim. Więcej o tym edytorze można się dowiedzieć z następujących dokumentów:

Drugie polecenie

cc -Wall -ansi -pedantic pX.c

uruchamia kompilator języka C, który powinien (jeśli kompilacja się uda) utworzyć plik wykonywalny a.out, który uruchomimy trzecim poleceniem.

Kompilator języka C ma całą masę opcji, modyfikujących jego działanie. My będziemy prawie zawsze używać opcji:

Inne opcje kompilatora poznamy w trakcie dalszych zajęć.

Pierwszy program

Prześledźmy teraz kompilację i wykonanie pierwszego programu. Zrzut ekranu, który przedstawia poniższy listing wykonano przy zastąpieniu edytora vim przez program cat, który wypisuje na ekran zawartośc pliku:

volt:~/lmp/lmp1/gr1> cat p1.c
#include <stdio.h>

int main ()
{
  printf ("Dzien dobry!");
  return 0;
}
volt:~/lmp/lmp1/gr1> cc -Wall -ansi -pedantic p1.c
volt:~/lmp/lmp1/gr1> ./a.out
Dzien dobry!volt:~/lmp/lmp1/gr1>

Aby wyjaśnić zawartość powyższego okienka rozpocznijmy od analizy pierwszego programu.

p1.c:

   1 #include <stdio.h>
   2 
   3 int main ()
   4 {
   5   printf ("Dzien dobry!");
   6   return 0;
   7 }

Pierwsza linia pliku p1.c poleca kompilatorowi dołączenie do naszego programu pliku nagłówkowego stdio.h. Jest to potrzebne, jeśli kompilator ma skontrolować, czy poprawnie wywołujemy funkcje ze standardowej biblioteki wejścia/wyjścia. Linie 3-7 definiują funkcję main. Każdy program w C składa się z funkcji, które mogą się wzajemnie wywoływać. Łańcuch tych wywołań rozpoczyna się od funkcji main, która jest wywoływana jako pierwsza, a więc to od niej zaczyna się nasz program. Wynika z tego, że każdy program w C zawiera co najmniej jedną funkcję - main.

Nasza pierwsza funkcja main nie robi zbyt wiele - w linii piątej wywołuje funkcję printf, polecając jej wypisanie na standardowy strumień wyjściowy komunikatu Dzien dobry!, a w linii 6 zwraca do systemu operacyjnego wartość 0 - zwyczajowo oznacza to, że program zakończył się bez błędów.

Analizując wykonanie naszego programu:

volt:~/lmp/lmp1/gr1> ./a.out
Dzien dobry!volt:~/lmp/lmp1/gr1>

zwracamy uwagę na niezbyt czytelną ostatnią linię. Wyświetlany przez nasz program napis Dzien dobry! zlewa się w niej z tekstem zachęty volt:~/lmp/lmp1/gr1> wypisywanym przez interpreter poleceń (shell). Aby to poprawić, możemy za pomocą edytora dodać na końcu napisu Dzien dobry! specjalną sekwencję znaków polecającą przejście do nowego wiersza. Zmodyfikowany program wygląda następująco:

   1 #include <stdio.h>
   2 
   3 int main ()
   4 {
   5   printf ("Dzien dobry!\n");
   6   return 0;
   7 }

Różnica jest naprawdę niewielka - na końcu napisu w linii 5 dodaliśmy dwa znaki: \n - jest to specjalna sekwencja, mówiąca funkcji printf, że w miejscu jest wystąpienia powinien zostać wyprowadzony do strumienia wyjściowego znak nowej linii.

Oto ślad kompilacji i uruchomienia poprawionego programu:

volt:~/lmp/lmp1/gr1> cc -Wall -ansi  p1.c
volt:~/lmp/lmp1/gr1> ./a.out
Dzien dobry!
volt:~/lmp/lmp1/gr1>

Drugi program

Drugi program pozwoli nam pokazać, w jaki sposób język C manipuluje danymi. W funkcji main pojawiają się zmienne, które inicjujemy, modyfikujemy i wypisujemy.

p2.c

   1 #include <stdio.h>
   2 
   3 int main()
   4 {
   5         int             i = 0;
   6         int             j = 1;
   7 
   8         printf("i=%d\n", i);
   9         printf("j=%d\n", j);
  10         printf("i-j=%d\n", i - j);
  11         i = j + 1;
  12         printf("j+1=%d\n", i);
  13         j -= 3;
  14         printf("j-3=%d\n", j);
  15         printf("j=j-3\n");
  16         printf("j=%d\n", j);
  17         printf("%d+%d=%d\n", i, j, i + j);
  18         printf("%d*%d=%d\n", i, j, i * j);
  19 
  20         return 0;
  21 }

Pierwszą cechą języka C, na którą zwracamy uwagę, to struktura funkcji main.

Definicja każdej funkcji składa się z nagłówka i ciała.

Nagłówek naszej funkcji main to linia 3, w której występują:

Ciało funkcji to ciąg ujętych w nawiasy klamrowe ({ w linii 4 oraz } w linii 21) instrukcji, wśród których wyróżniamy definicje zmiennych. Zgodnie ze standardem ANSI powinny one występować na początku bloku (czyli bezpośrednio po otwierającym nawiasie klamrowym) i tego będziemy się starali trzymać w naszych programach. W naszym przykładzie mamy dwie zmienne, definiowane w 5 i 6 linii. Definicja zmiennej mówi kompilatorowi, że powinien zarezerwować pewien obszar pamięci (odpowiedni do przechowywania obiektu danego typu) i nadać mu nazwę. Dalsze odwołania do tej nazwy pozwalają na modyfikację zawartości określonego przez nazwę fragmentu pamięci zgodnie z regułami określanymi przez typ zmiennej.

Nasze definicje zmiennych uzupełnione są przez nadanie im początkowych wartości - bez tego i oraz j przyjęłyby wartości przypadkowe.

Kolejne linie programu pokazują niektóre możliwości modyfikacji (linie 11 i 13) oraz wypisywania zmiennych. Do wypisywania zmiennych używamy znanej już funkcji printf, ale teraz w napisach podawanych jako pierwszy argument tej funkcji występują sekwencje znaków %d. Jeśli printf napotka taką sekwencję, to pobiera z listy argumentów kolejne, zakładając, że są to liczby całkowite (litera d w %d pochodzi od angielskiego słowa decimal - "dziesiętnie"). W ten sposób odpowiednia zawartość pierwszego argumentu funkcji printf pozwala wypisywać aktualną wartość zmiennych (jak np. w liniach 8 i 9) lub wyrażeń (linie 10, 16, 17).

Oto wynik kompilacji i uruchomienia drugiego programu:

volt:~/lmp/lmp1/gr1> cc -Wall -ansi  p2.c
volt:~/lmp/lmp1/gr1> ./a.out
i=0
j=1
i-j=-1
j+1=2
j-3=-2
j=j-3
j=-2
2+-2=0
2*-2=-4
volt:~/lmp/lmp1/gr1>

Trzeci program

Trzeci program wypisuje kody ASCII odpowiadające wielkim literom alfabetu angielskiego. Demonstruje on najczęściej używaną w języku C pętlę - for oraz pokazuje odpowiedniość małych liczb całkowitych i znaków:

p3.c

   1 #include <stdio.h>
   2 
   3 int main()
   4 {
   5         int             i;
   6 
   7         printf("Kody dziesiętne wielkich liter:\n");
   8         for (i = 'A'; i <= 'Z'; i++)
   9                 printf("%c - %d\n", i, i);
  10 
  11         return 0;
  12 }

Zwróćmy uwagę, że w 9 linii wypisywana jest dwukrotnie wartość zmiennej i, ale najpierw interpretowana jest jako znak (sekwencja %c) a następnie jako liczba (%d). Skutkuje to następującymi wynikami działania programu:

volt:~/lmp/lmp1/gr1> cc -Wall -ansi  p3.c
volt:~/lmp/lmp1/gr1> ./a.out
Kody dziesiętne wielkich liter:
A - 65
B - 66
C - 67
D - 68
E - 69
F - 70
G - 71
H - 72
I - 73
J - 74
K - 75
L - 76
M - 77
N - 78
O - 79
P - 80
Q - 81
R - 82
S - 83
T - 84
U - 85
V - 86
W - 87
X - 88
Y - 89
Z - 90
volt:~/lmp/lmp1/gr1>

Czwarty program

Czwarty program ma wypisywać znaki odpowiadające określonemu przez użytkownika zakresowi kodów ASCII. Pokazuje on możliwość komunikacji użytkownika z programem w C oraz nieco bardziej złożoną strukturę funkcji main wymaganą ze względu na konieczność sprawdzenia poprawności danych wprowadzanych przez użytkownika. Program wygląda następująco:

p4.c

   1 #include <stdio.h>                       
   2 #include <stdlib.h>                      
   3 
   4 int main(int argc, char *argv[])
   5 {                           
   6 
   7         if (argc == 3) {
   8                 int             a = atoi(argv[1]);
   9                 int             b = atoi(argv[2]);
  10                 int             i;                
  11 
  12                 if (a <= 0 || b <= a || b > 255)
  13                         return 2;               
  14 
  15                 printf("Znaki odpowiadające kodom z zakresu %d - %d:\n", a, b);
  16                 for (i = a; i <= b; i++)
  17                         printf("%d ->  '%c`\n", i, i);
  18 
  19                 return 0;
  20         } else {
  21                 return 1;
  22         }
  23 }

Pierwszą istotną różnicą w stosunku do poprzednich programów są argumenty funkcji main (linia 4):

   1 int main(int argc, char *argv[])

Całkowita zmienna argc określa liczbę argumentów podanych przy uruchomieniu programu. Zmienna ta ma zawsze wartość dodatnią - 1 oznacza, że program został uruchomiony bez argumentów, 2 - z jednym argumentem, 3 - z dwoma, itd.

Zmienna argv to wektor (tablica) zawierająca napisy. Dokładniejsze wyjaśnienie znaczenia char * musimy odłożyć na później, ale na ten moment zupełnie wystarczy Państwu przekonanie, że char * to "napis". Język C dostarcza pokaźnego zbioru funkcji, które potrafią przetwarzać napisy - jedną z takich funkcji jest atoi, która potrafi zamienić napis na liczbę całkowitą (o ile oczywiście napis składa się wyłącznie z cyfr, ewentualnie poprzedzonych znakiem + lub -). Umieszczenie na początku naszego programu polecenia

   1 #include <stdlib.h>
   2 

umożliwia kompilatorowi sprawdzenie, czy prawidłowo wywołujemy tę funkcję.

Ponieważ nasz program wymaga podania przez użytkownika dwóch argumentów (początku i końca zakresu kodów ASCII), a więc działanie funkcji main rozpoczyna się od sprawdzenia, czy zmienna argc ma wartość 3 (linia 7). Jeżeli tak, to przystępujemy do działania, jeśli nie - kończymy funkcję main, sygnalizując błąd przez zwrócenie wartości różnej od zera (linia 21).

Działanie naszego programu będzie wymagało zadeklarowania trzech zmiennych (wystarczyłyby dwie, ale trzy pozwolą zapisać kod łatwiejszy do zrozumienia), a więc definiujemy na początku bloku (zwróćmy uwagę, że nie musi to być blok - ciało funkcji) zmienne a i b (początek i koniec przedziału) oraz i (posłuży do iterowania po kodach z przedziału). Zmienne a i b są natychmiast inicjowane przy pomocy wartości zwracanych przez funkcję atoi. Jak już wspomnieliśmy wyżej, funkcja ta przekształca napis na liczbę - zakładamy, że użytkownik powinien określić początek przedziału jako pierwszy, a koniec jako drugi argument wywołania. Tablice w języku C są indeksowane poczynając od zera i w naszym przypadku argv[0]' powinien zawierać nazwę programu, który wywołana (zapewne będzie to napis ./a.out), argv[1] - napis określający początek i wreszcie argv[2]` - napis określający koniec przedziału. Dla ułatwienia zrozumienia tych wywodów spróbujcie sobie Państwo wyobrazić, że wywołanie programu w postaci:

volt:~/lmp/lmp1/gr1> ./a.out 48 57

Powoduje wypełnienie argc i argv następującymi wartościami:

 argc     <-- 3
 argv[0]  <--- "./a.out"
 argv[1]  <--- "48"
 argv[2]  <--- "57"

Zwróćmy też uwagę, że 48 i "48" to dwa zupełnie różne obiekty - pierwszy jest liczbą całkowitą (48), a drugi to trzy bajty: pierwszy bajt zawiera znak '4' (czyli liczbę 52, która jest kodem tego znaku w ASCII), drugi bajt zawiera znak '8' (czyli liczbę 56), a trzeci bajt liczbę 0 oznaczającą koniec napisu.

W 12 linii naszego programu sprawdzamy, czy użytkownik podał właściwe wartości a i b. Oczekujemy, że 0 < a < b < 255, w przeciwnym razie kończymy program zwracając różną od zera wartość 2, sygnalizującą kolejny błąd (linia 13).

Jeżeli warunek instrukcji if z linii 12 okazał się fałszywy i nie wykonaliśmy instrukcji return z linii 13, to możemy przejść do zasadniczych obliczeń, realizowanych w liniach 15-17 i po ich zakończeniu zwrócić wartość 0 (linia 19) oznaczającą, że program zakończył się pomyślnie.

Obejrzyjmy teraz ślad kompilacji i kilku uruchomień naszego programu. Występujące w kilku liniach poniższego śladu polecenie echo $? pozwala wypisać na ekran wartość zwróconą przez ostatnio uruchomiony program - możemy się w ten sposób przekonać, że zwracanie warości sygnalizujących błędne dane wejściowe rzeczywiście działa:

volt:~/lmp/lmp1/gr1> cc -Wall -ansi  p4.c
volt:~/lmp/lmp1/gr1> ./a.out              
volt:~/lmp/lmp1/gr1> echo $?              
1                                         
volt:~/lmp/lmp1/gr1> ./a.out 10 9         
volt:~/lmp/lmp1/gr1> echo $?              
2                                         
volt:~/lmp/lmp1/gr1> ./a.out 65 70        
Znaki odpowiadające kodom z zakresu 65 - 70:
65 ->  'A`                                  
66 ->  'B`                                  
67 ->  'C`                                  
68 ->  'D`                                  
69 ->  'E`                                  
70 ->  'F`                                  
volt:~/lmp/lmp1/gr1> ./a.out 48 57        
Znaki odpowiadające kodom z zakresu 48 - 57:
48 ->  '0`                                  
49 ->  '1`                                  
50 ->  '2`                                  
51 ->  '3`                                  
52 ->  '4`                                  
53 ->  '5`                                  
54 ->  '6`                                  
55 ->  '7`                                  
56 ->  '8`                                  
57 ->  '9`                                  
volt:~/lmp/lmp1/gr1> echo $?
0

Piąty program

Ostatni program to zadanie, które rozwiązują Państwo jako test zaliczeniowy. Polega ono na napisaniu prostego programu realizującego określoną przez prowadzącego funkcjonalność. Na przykład:


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

wikidyd - IETiSIP, Wydzial Elektryczny Politechniki Warszawskiej: LMP/1 (last edited 2011-09-28 09:30:36 by JacekStarzyński)