Uwagi o C i nie tylko

Grzegorz Gembała

Kilka uwag o programowaniu w C na Ligę i nie tylko.


W celu ułatwienia życia początkującym "ligowcom" napisałem o kilku rzeczach o których nie wiedziałem, gdy zaczynałem pisać programy na Ligę, a których znajomość bardzo ułatwiłaby mi start. Jest też kilka zadań z rozwiązaniami. Jeżeli uważasz, że na tej stronie powinna się znaleźć jakaś informacja napisz do Kierownika Ligi.
Powrót

Spis treści


Uwagi ogólne czyli jak pisać aby się nie zamęczyć

Poniższe dokument dotyczy pisania programów na Ligę i inne konkursy programistyczne. Jedynymi kryteriami oceniania programów na zawodach są: poprawność działania, szybkość i zużycie pamięci. Poprawność działania jest sprawdzana poprzez uruchomienie programu dla pewnych danych testowych i porównaniu wyników z poprawnymi odpowiedziami. Dla każdego zadania jest ustalany limit czasu. Najczęściej jest to czas działania programu wzorcowego dla danych testowych pomnożony przez pewna stałą. Rozmiar pamięci dostępnej dla programu wynosi najczęściej 16 MB. Styl programowania nie jest oceniany. W związku z tym piszemy program jak nam wygodnie, ale czytelnie. Unikamy dynamicznych struktur danych, tam gdzie można stosujemy tablice - programy są kompilowane przy użyciu kompilatorów 32bitowych, więc tablice mogą być większe niż 64kb. Najlepiej pisać krótkie programy - szybko się je pisze i szybko znajduje się w nich błędy. Często lepiej przemyśleć rozwiązanie niż męczyć się z 700 liniowym programem. Dobrym pomysłem jest stosowanie funkcji, można zaoszczędzić czas w przyszłości. We wszystkich zadaniach można przyjąć, że dane wejściowe są poprawne i zgodne ze specyfikacją podaną w treści zadania. W rozwiązaniach nie trzeba sprawdzać poprawności odczytanych danych wejściowych.
Początek


Kompilatory, edytory, debugery i inne zabawki

Jeżeli nie potrzebujemy debugera możemy pisać w Notatniku i kompilować program z linii poleceń. Wtedy nie ma większych problemów z kompilatorem, wystarczy żeby był 32bitowy. W praktyce pisanie w Notatniku jest niewygodne, brak numeracji linii powoduje trudności przy szukaniu błędów. Wygodniej używać jakiegoś edytora z funkcjami programistów (podświetlanie składni, numeracja linii) . Krótka charakterystyka kilku narzędzi:
  • GNU C (gcc) - kompilator 32 bitowy dla systemu Linux. Nie posiada IDE ani debugera. Dostępny w 99.99% dystrybucji systemu Linux.
  • GDB - standardowy debuger dla kompilatora GNU C. Jest to debuger tekstowy, bardzo niewygodny i trudny w obsłudze.
  • XWPE - IDE dla kompilatora GNU C dostępne dla systemu Linux. Interfejs jest bardzo podobny do IDE z pakietu Borland 3.xx.
  • Kedit,Kate,Gedit - przykładowe edytory dla systemu Linux. Praktycznie w każdej dystrybucji znajdziemy edytor dla pod xwindows z podświetlaniem składni i numeracją linii.
  • Borland 3.xx - jest to kompilator 16 bitowy w związku z tym odradzam jego stosowanie. Nie można w nim tworzyć tablic o rozmiarze większym niż 64kB. W skład pakietu wchodzi wygodne IDE i dobry system pomocy.
  • DJGPP - kompilator GNU C przeniesiony na system Dos/Windows.
  • RHIDE - IDE dla kompilatora GNU C/DJGPP. Interfejs jest bardzo podobny do IDE z pakietu Borland 3.xx. Istnieją duże problemy z konfiguracją w systemach Windows 2000/XP.
  • Borland C++ Compiler 5.5 darmowy kompilator firmy Borland. Nie posiada IDE. Po zainstalowaniu należy przeczytać plik readme.txt i wykonać oposane w nim czynności.
  • Borland Turbo Debugger Version 5.5 darmowy debuger dla kompilatora Borland C++ Compiler 5.5. Posiada wygodny interfejs. Po zainstalowaniu należy przeczytać plik td32read.txt i wykonać oposane w nim czynności.
  • Microsoft Visual C - pakiet przeznaczony do tworzenia aplikacji dla systemu Windows. IDE posiada opcje tworzenia aplikacji konsolowych.
  • ConTEXT - wygodny edytor dla systemu Windows, przeznaczony dla programistów. Podświetla składnie, numeruje linie istnieje możliwość napisania skryptów sterujących kompilacją i uruchamianiem programów. www.fixedsys.com/context/
W praktyce korzysta się z "zestawów":
  • Borland C++ Compiler 5.5 + Borland Turbo Debugger Version 5.5 + ConTEXT(lub inny edytor)
  • Visual C
  • DJGPP + Rhide
  • GNU C + XWPE
  • GNU C + Kate(lub inny edytor pod xwindows)
Dobrym sposobem na znajdowanie błędów jest wyświetlanie wartości zmiennych podczas działania programu. Robimy tak gdy nie mamy debugera (lub mamy taki z którego nie lubimy korzystać).
Początek


Kompilacja - kompilator gcc

Program kompilujemy poleceniem gcc plik.c. Wykonanie komendy powoduje utworzenie pliku a.exe w systemie Dos lub a.out w systemie Linux. Nazwę pliku wykonywalnego możemy wskazać następująco gcc plik.c -o plik.exe. Dobrze stosować opcje -Wall, powoduje ona wyświetlanie wszystkich ostrzeżeń. Pozwala ona uniknąć prostych błędów: przypisanie zamiast testowania równości, parametr funkcji scanf nie będący wskaźnikiem itp. Osoby korzystające z IDE lub innego kompilatora też mogą włączyć opcje generującą wszystkie ostrzeżenia.
Początek


Standardowe wejście i wyjście

Programy na Ligę czytają dane ze standardowego wejścia i wyświetlają wynik na standardowe wyjście. Służą do tego procedury zdefiniowane w pliku nagłówkowym stdio.h. Program powinien czytać dane ze standardowego wejścia tak z pliku. Nie wolno czekać na naciśniecie klawisza. Program wysyła wyniki na standardowe wyjście. Należy wysyłać tylko wyniki, pytania czy chcesz zakończyć działanie programu lub podobne spowodują odesłanie programu z oceną wrong answer, ewentualnie time limit exceeded gdy program będzie czekał na odpowiedź użytkownika. Program nie musi przeczytać wszystkich danych wejściowych przed wypisaniem wyników można przeczytać cześć danych i zacząć wypisywać rozwiązanie. Najczęściej dane testowe są podzielone na zestawy, program czyta zestaw oblicza i wypisuje odpowiedź czyta następny zestaw ... . Standardowe wejście i wyjście są języku C plikami o nazwach: stdio, stdout. Do czytania używamy funkcji getchar scanf ... , a do wyświetlania putchar, puts , printf .... . Aby sprawdzić czy wystąpił koniec danych można używać funkcji feof z parametrem stdin: feof(stdin). Aby ułatwić sobie testowanie dane testowe zapisujemy do pliku. Aby sprawdzić jak działa program dla danych testowych wydajemy komendę plik < dane_testowe.txt . Jeżeli chcemy aby wynik został zapisany do pliku stosujemy polecenie plik < dane_testowe.txt > wynik.txt . Powyższy efekty możemy osiągnąć dopisując następujące linie na początku funkcji main:
 freopen("e:\\dane\\dane.txt","rt",stdin);
 freopen("e:\\dane\\wynik.txt","wt",stdout);
Oczywiście należy usunąć te linie przed wysłaniem zadania do oceny. W przypadku wątpliwości najlepiej zobaczyć przykładowe zadania.
Początek


Jakich funkcji wolno używać

Aby stwierdzić, jakich funkcji można używać należy zapoznać się z regulaminem zawodów. W praktyce zawsze można i wystarczy używać funkcji zdefiniowanych w standardzie ANSI C. W większości help'ów jest napisane, które funkcje należą do standardu ANSI C. Pisząc program zgodny ze standardem ANSI C mamy pewność, że będzie się kompilował na innym kompilatorze. Ponadto na programy są nałożone pewne ograniczenia. W praktyce programowi nie wolno: wykonywać jakichkolwiek operacji na plikach (nie dotyczy standardowego wejścia i wyjścia), używać sieci, tworzyć wątków.
Początek


Jak łatwo sortować tablice - funkcja qsort

Do sortowania tablic w języku C można używać funkcji qsort ze standardowej biblioteki ANSI C. Argumentami funkcji są wskaźnik na tablicę, którą sortujemy, ilość elementów do posortowania, rozmiar elementu tablicy i wskaźnik na funkcje porównującą. Argumentami funkcji porównującej są dwa wskaźniki na elementy tablicy, funkcja zwraca wartość typu int. Zwracana wartość powinna być dodatnia gdy pierwszy element jest większy od drugiego, zero gdy elementy, wartość gdy drugi element jest większy od pierwszego. Funkcja qsort sortuje tablice w porządku rosnącym, aby posortować tablice w porządku malejącym zmieniamy funkcje porównującą. Najlepiej stosować funkcję qsort w następujący sposób:
int tab[1000000],n;

int comp(int *a,int *b)
{
 return *a-*b;
}

...
/*Sortowanie n pierwszych elementów tablicy tab w porządku rosnącym*/
qsort(tab,n,sizeof(int),(int(*)(const void *,const void *))comp);
...
Funkcja porównująca jest rzutowana na typ (int(*)(const void *,const void *)), więc kompilator nie sprawdza czy funkcja porównująca ma prawidłowe argumenty.
Początek


Jak szybko zaimplementować stos i kolejkę na tablicy

Pisząc zadania na Ligę i inne konkursy znamy rozmiar danych wejściowych. Powoduje to, że stosy i kolejki można łatwo zaimplementować na tablicy. Załóżmy że na stos nie zostanie odłożonych więcej niż STACK_SIZE elementów typu int. Operacje na stosie możemy wykonywać w następujący sposób:
int  stack[STACK_SIZE+1],*top;

top=stack;         /*inicjalizacja*/
*++top=i;          /*odłożenie elementu i na stos*/
*++top=i;
x=*top;            /*przypisanie zmiennej x wartości elementu z wierzchołka stosu*/
top--;             /*zdjecie elementu ze stosu*/
if (top>stack)     /*sprawdzenie czy na stosie jest jakis element*/
 y=*top--;         /*przypisanie zmiennej y wartości elementu z wierzchołka stosu
                     i zdjęcie elementu ze stosu*/
Podobnie prosto możemy zaimplementować kolejkę. Zakładamy, że do kolejki zostanie włożone najwyżej QUEUE_SIZE elementów.
int queue[QUEUE_SIZE],*front,*back;

front=back=queue;  /*inicjalizacja*/
*back++=i;         /*włożenie elementu do kolejki*/
*back++=i;
x=*front;          /*odczytanie elementu z początku kolejki*/
front++;           /*usunięcie elementu z kolejki*/
if (front<back)    /*sprawdzenie czy w kolejce znajduje się element*/
 x=*front++;       /*odczytanie elementu z początku kolejki i usunięcie go z kolejki*/
Praktyczne zastosowanie kolejki na tablicy można zobaczyć w zadaniu 2 (patrz punk Przykładowe zadania).
Początek


Tablicowanie

W niektórych zadaniach dobrze stablicować pewne dane. W skrajnym przypadku można stablicować odpowiedzi dla wszystkich możliwych danych wejściowych, ale zdarza się to rzadko. Należy pamiętać, że w większości konkursów rozmiar pliku z kodem źródłowym jest ograniczony. Wartości pewnych tablic możemy obliczyć na początku programu (przykładowo współczynniki Newtona, początkowe liczby pierwsze).
Początek


Trzy słowa o popularnych błędach

Wiele błędów może zostać wykrytych przez kompilator. W tym celu należy włączyć wszystkie opcje generowania ostrzeżeń (warning). W GCC/DJGPP kompilujemy program z opcja -Wall. Zdarzają się błędy związane z przepełnieniem stosu. Tablice o dużym rozmiarze należy deklarować globalnie, jeżeli je zadeklarujemy w funkcji main to zostaną utworzone na stosie. W skrajnym przypadku program się nie uruchomi. Nie należy przesadzać z głębokością przy rekurencji, tu też stos może się przepełnić. W niektórych konkursach wymagane jest by program zwrócił wartość zero do systemu wiec piszemy tak:
int main()
 {
  ....
  return 0;
 }
W przypadku braku return 0 można otrzymać ocenę runtime error.
Pisząc w języku C lepiej nie stosować komentarzy typu C++ : //. Wysyłając program z takimi komentarzami można otrzymać ocenę Compile Error.
Sprawdź czy wyniki obliczeń nie są za duże dla stosowanego przez Ciebie typu danych.
Do wykrywania błędów możesz stosować standardowe makro assert.
Początek


Przykładowe zadania z rozwiązaniami

Zamieszczam rozwiązania kilku zadań ligowych.
Zawody
Treść
Rozwiązanie
Poziom trudności
Uwagi
Liga Zadaniowa 2001/2002 Zadanie 2 Rozwiązanie zadania 2
średni
Przeszukiwanie wszerz
Liga Zadaniowa 2001/2002 Zadanie 7 Rozwiązanie zadania 7
średni
 
Liga Zadaniowa 2001/2002 Zadanie 9 Rozwiązanie zadania 9
trudne
Programowanie dynamiczne
Liga Zadaniowa 2001/2002 Zadanie 13 Rozwiązanie zadania 13
łatwe
 

Początek


17.5.2024 - 15:05:34