Drobne programowanie

Kaczuś zaprasza do opowieści o algorytmach, językach programowania i strukturach danych

Na stronie stosowane są pliki cookies. Więcej na podstronie.
odsłon: 5666

Skrzynia porad

Przeglądając różne fora i listy dyskusyjne zauważyłem, że część programistów uczących się, bądź z małym doświadczeniem ma ciągle te same problemy. Zazwyczaj odpowiedzi na to są proste, do znalezienia w wielu miejscach, ale widać za słabo powtarzane, gdyż pytania i tak się zdarzają. Postanowiłem więc stworzyć taki mały dział z poradami. Nazywa się Skrzynia Porad.

Pytania:

Odpowiedzi:

Dlaczego wypisując coś za pomocą printfa, bądź couta wynik pojawia się na ekranie dopiero po znaku nowej linii?
Ponieważ domyślnie konsola jest w trybie buforowanym, więc znaki wyświetlą się, albo gdy się bufor zapełni, albo właśnie po pojawieniu się znaku nowej linii. Jest też trzecie wyjście - wymusić takie zachowanie (opróżnienie bufora). dla komendy printf należy po tej komendzie wywołać:
fflush(stdout);
dla obiektu cout:
std::cout.flush();
do góry
Dodaję w pętli wartość 0.00001 i wynik trochę odbiega od rzeczywistości.
W skrócie - taki jest urok liczb zmiennoprzecinkowych. Sposób ich reprezentacji jest dość specyficzny i należy pamiętać, że przy dodawaniu powstają największe błędy, toteż zamiast dodawać w pętli, dużo lepiej jest pomnożyć (błąd będzie mniejszy). Należy tez pamiętać, że po kilku operacjach, choć wynik powinien być 0, to tak niekoniecznie jest. Dlatego zamiast sprawdzać czy wynik naszych operacji jest jet równy 0, powinniśmy sprawdzać, czy nasz wynik jest w przedziale
 -epsilon<nasz wynik<epsilon 
gdzie epsilon jest wartością dokładności. Gdy chcemy sprawdzić czy 2 liczby są równe, to:
if (fabs(liczba1-liczba2) < epsilon)
   puts("Z dużym prawdopodobieństwem są to wartości równe");
do góry

Jak sprawdzić czy liczba jest parzysta?
Są dwie szkoły. Jedni twierdzą, że sprawdzamy, czy ostatni bit jest ustawiony
if ((liczba & 1) == 1)
    puts("liczba nieparzysta");
else
    puts("liczba parzysta");
Według drugiej szkoły, to należy sprawdzić, czy reszta z dzielenia przez 2 jest 1 czy 0. No i tu zaczynają się schody! Ponieważ jeśli byśmy robili test nieparzystości, to tak naprawdę może być jeszcze reszta z dzielenia równa -1! Czyli - albo robimy test parzystości, albo sprawdzamy czy
abs(liczba % 2) == 1
do góry

Alokuje dużą tablicę operatorem new i program zawiesza się mimo, że sprawdzam, czy wskaźnik jest różny od NULL.
Cóż - jeśli nie ustawimy w parametrach kompilatora (jeśli istnieje taka możliwość), by operator new w razie niepowodzenia zamiast rzucać wyjątek zwracał nam NULL, to tak będzie działać. Proszę zapamiętać: domyślnie operator new przy niepowodzeniu rzuca wyjątek. Jeśli chcemy by działało to inaczej - albo przestawiamy parametry kompilacji, albo wymuszamy oczekiwane działanie za pomocą parametru std::nothrow.
do góry

Popularne zadanie z testów o pracę z błędem.
Jest pewne zadanie, które lubi pojawiać się na testach. Co ciekawe, prawidłowa odpowiedź jest różnie oceniana przez egzaminujących. Jest ono w postaci (forma bardzo uproszczona, zazwyczaj jest to trochę bardziej skomplikowane, ale chodzi o zasadę):
char* s = "ala ma kota";
s[0] = 'e';
printf("%s\n", s);
i pytanie co zostanie wypisane na ekranie. Najbardziej prawidłowa odpowiedź to: program pójdzie w krzaki, choć będą tacy, co będą się upierali, że pojawi się napis 'ela ma kota', co oczywiście może się zdarzyć. Może się zdarzyć, że napis zostanie taki jak w oryginale, czyli 'ala ma kota'. Bo to tak naprawdę zależy od systemu. A tu standard mówi, że nie wolno nam zrobić przypisania, bo s jest wskaźnikiem do stałego literału, który może być (i w niektórych systemach jest) obszarem chronionym programu!
Jeśli byśmy chcieli móc zmieniać taki literał, to kod powinien wyglądać tak:
char s[] = "ala ma kota";
s[0] = 'e';
printf("%s\n", s);
do góry

Tablice o dynamicznie ustalanej wielkości (VLA) w C++
Z racji, że przed kilkunastoma latami do standardu języka C weszły tablice, których wielkość podajemy "dynamicznie" (VLA):
int foo(unsigned int asize)
{
	char arr[asize];

   [...]
}
Niektórzy chcieli by coś takiego w C++, ale komitet standaryzacyjny doszedł do wniosku, że od tego jest typ std::vector i jemu podobne. GCC ma w swoich rozszerzeniach taką możliwość. Czy to dobrze? - Chyba niekoniecznie, ponieważ piszący w C++ korzystają z zasady, że deklaracja zmiennej powinna być jak najbliżej jej użycia, przez co nierzadko robi się okropny bałagan w kodzie (no ale co kto lubi), to już nie raz spotkałem się z kodem typu:
int foo()
{
    int lsize;
    char arr[lsize];
    [...]
    cin>>lsize;
    [...]
}
Niektórzy początkujący (bo to głównie w ich kodach znajduję takie ciekawe konstrukcje) najwidoczniej wierzą, że wielkość tablicy zmieni się jak zostanie podana wartość, która reprezentuje tę wielkość.

Należy jednak zapamiętać: W C++ do obsługi tablic o dynamicznych rozmiarach używamy operatora new (lub innego alokatora), albo typów z rodziny std::vector! Niby GCC pozwala na VLA, ale to nie jest standardowe rozwiązanie, to:
  • zawsze może przestać wspierać
  • będą problemy, jeśli z jakiś powodów będziemy musieli zmienić kompilator.
do góry

Problem z pobieraniem danych przez funkcję getline.
Czasami mamy taki problem, że wprowadzając dane za pomocą funkcji getline mamy puste dane. Związane jest to z tym, że wcześniej korzystaliśmy z metod wejścia formatowanego np:
cin>>lcnt;
Problem jest taki, że metody wejścia sformatowanego zostawiają w buforze białe znaki, które wczytuje najpierw funkcja getline. Przykład programu:
#include <iostream>
#include <string>
using namespace std;
int main()
{
    int i, lcnt;
    string s;
    cout<<"ile powtórzeń "<<flush;
    cin>>lcnt;
    for (i = 0; i < lcnt; ++i)
    {
    	std::getline(cin, s);
        cout<<(i + 1)<<") "<<s<<endl;
    }
    return 0;
}
i pojawia się problem - pierwsza linia będzie pusta, dane pozwoli nam wprowadzić dla kolejnych linii.

Jedni polecają po cin>>lcnt; wstawić funkcję:
fflush(stdin);
jest to z gruntu złe, bo miesza się bufory i wbrew pozorom nie działa na wielu implementacjach (sprawdzałem - u mnie nie zadziałała w żadnym wypadku kompilatory gcc z bibliotekami ixemul i libnix pod MorphOS-em) - odradzam!
Ładniejsze rozwiązanie to:
cin.ignore(std::cin.rdbuf()->in_avail());
Niestety sam Stroustrup ostrzega, że metoda in_avail() w wielu implementacjach będzie zwracała 0 (na testowanych przeze mnie implementacjach wszystkie zwracały 0).
Ostatnia deska ratunku i jak zauważyłem sprawiająca najmniej problemów to:
cin.ignore(INT_MAX, '\n');
niektórzy zalecają tez dodanie przed nią
cin.clear();
Tak więc nasz program prezentuje się:
#include <iostream>
#include <string>
#include <climits>
using namespace std;
int main()
{
    int i, lcnt;
    string s;
    cout<<"Ile powtórzeń "<<flush;
    cin>>lcnt;
    cin.clear();
    cin.ignore(INT_MAX, '\n');
    for (i = 0; i < lcnt; ++i)
    {
    	std::getline(cin, s);
        cout<<(i + 1)<<") "<<s<<endl;
    }
    return 0;
}         

do góry

Funkcja scanf i tablice znakowe.
Używając funkcji scanf niektórzy tak się przyzwyczaili, że dodają operator '&' do zmiennych, że tworzą kod:
char s[10];
scanf("%s", &s);
Przy tablicy statycznej przypadkowo nawet to zadziała, gdyż nazwa tablicy konwertowana jest na wskaźnik na pierwszy element, jedna to jest to błąd! Powinno być:
char s[10];
scanf("%s", s);
a nawet lepiej:
char s[10];
scanf("%9s", s);
Dlaczego tak? - gdyż funkcji scanf przekazujemy wskaźnik na pamięć, która ma być zmieniana, a przekazując &s, funkcja otrzymuje wskaźnik na wskaźnik, czyli będzie nam zmieniała nie zawartość pamięci na który wskazuje wskaźnik, a samego wskaźnika.

Żeby przekonać się, że pierwsza wersja rzeczywiście jest zła (dla tych, którym przypadkowo działa), wystarczy s zadeklarować dynamicznie....

do góry

Wielkość tablicy
Zauważyłem, że ludzie cały czas nie mogą zapamiętać/zrozumieć, ile elementów ma tablica zadeklarowana:
int tab[10];
Ponieważ już starożytni zauważyli, że repetitio est mater studiorum (powtarzanie jest matką nauki). Więc powtórzę jeszcze raz: Tablica tab ma 10 elementów o indeksach 0, 1, 2, 3, 4, 5, 6, 7, 8, 9!

tak więc błędne będzie wykonanie pętli:
for(i = 0; i <= 10; ++i)
{
	tab[i] = i;
}
prawidłowy kod będzie wyglądał następująco:
for(i = 0; i < 10; ++i)
{
	tab[i] = i;
}
Dla tego by lepiej zapamiętać, może mała analogia: tak samo jak w systemie dziesiętnym mamy dziesięć cyfr 0, 1, 2, 3, 4, 5, 6, 7, 8, 9.

do góry
Przydzielenie pamięci w funkcji

Co zrobić w języku C (ewentualnie C++) jeśli z jakiś powodów mamy przydzielić pamięć na jakiś obiekt (dla uproszczenia powiedzmy, że na tablicę charów) w funkcji.
Najprostsze i najlepsze rozwiązanie: zwracamy wskaźnik na zaalokowany obszar. Nasza funkcja wygląda tak:
char *foo(size_t asize)
{
	char *res;
    ...
    res = malloc(asize);
    ...
    return res;
}
wywołanie bedzie proste:
    char *tab;
    ...
	tab = foo(mysize);
Ok, ale co jeśli funkcja ma nie zwracać, ale realokować pamięć, bądź też jako zadanie mamy przydzielić pamięć na zaliczenie. Każą nam wtedy przekazać nasz wskaźnik jako argument funkcji, a funkcja ma np nic nie zwracać (takie pomysły czasami przychodzą do głowy prowadzącym zajęcia - ok to są ćwiczenia, więc czemu nie...).

Należy pamiętać, że operujemy tak na prawdę na zmiennej typu wskaźnikowego (typ to char *), więc jeśli chcemy zmienić taki argument, to musimy przekazać wskaźnik na obiekt na jakim chcemy operować, czyli:
void foo(char **atab, size_t asize)
{
	...
    *atab = malloc(asize);
    ...
}
wywołanie wtedy wygląda tak:
    char *tab;
    ...
	foo(&tab, mysize);
Dla porządku i dla tych co muszą postarać się wykonać ćwiczenia z języka C++ (choć nie jest to tam preferowany sposób zarządzania pamięcią, ale może z jakiś przyczyn być potrzebne), możemy skorzystać z referencji do wskaźnika!
void foo(char *&atab, size_t asize)
{
	...
    atab = new [asize];
    ...
}
wywołanie wtedy wygląda tak:
    char *tab;
    ...
	foo(tab, mysize);
Oczywiście za każdym razem musimy zwolnić przydzielona pamięć!

do góry
Kiedy tworzyć funkcje

Wiele osób, którzy zaczęli przygodę z programowaniem pyta się mnie często kiedy jakiś fragment kodu powinien stać się osobną funkcją (metodą bądź procedurą - w zależności od języka, modelu, potrzeb etc). Podstawowe przyczyny są 3:
  • Tworzymy funkcje zgodnie z projektem (czyli to co ma być np udostępnione innym programistom)
  • Jeśli piszemy kod i w pewnym momencie mamy ochotę większy fragment kodu skopiować i wstawić z drobnymi modyfikacjami (albo nawet często bez nich) w inne miejsce, to warto rozważyć, czy nie zamknąć tego kodu w funkcji
  • Jeśli jakaś część kodu przestaje być czytelna, a możemy w jakiś rozsądny sposób przenosząc jego fragmenty do funkcji zwiększyć jego czytelność.
Jakie są zalety? Na pewno zwiększa się czytelność. Dodatkowo, jeśli funkcja jest wykorzystywana w wielu miejscach, to znalezienie błędu i poprawienie go w jednym miejscu spowoduje poprawę we wszystkich miejscach w których jest użyta. Ułatwia też testy na kodzie. Wadą na pewno jest narzut związany z wywołaniem funkcji, ale wiele języków i kompilatorów podczas kompilacji potrafi "inlane'anować" takie fragmenty kodu, jeśli odpowiednio je napiszemy (w zgodzie z tym co oczekuje dany język).

do góry

2015-03-07 21:17:49


porady programowanie porady dla programistów