Strona główna » C++ » Kurs » Typowe błędy w C++
 

Typowe błędy w C++

Wstęp

Programowanie polega na zapisu pewnego zestawu instrukcji w postaci zrozumiałej dla kompilatora. Ze względu na różne pomysły twórców języków programowania każdy z nich ma inną składnie. Co za tym idzie programista pisząc w danym języku musi wiedzieć jakich instrukcji może użyć. W przypadku nieznajomości program może się wogle nie uruchomić, albo wykonać nie takie operację jakie chciał autor programu. Poniżej przedstawiam część błędów w C++ z którymi mógł się spotkać każdy programista.

Nie rozumiem co to x

Osoby mające po raz pierwszy styczność z programowaniem mogą twierdzić, że co nie napiszą to kompilator zrozumie. W poniższym przykładzie intencją programisty było wczytanie danych od użytkownika, a następnie ich wyświetlenie.

  1.   cin >> x;
  2.   cout << "Wprowadzono " << x;

Jednak taki program nie może się uruchomić, ponieważ program nie wie co to jest x. Pominięta tutaj została tutaj deklaracja zmiennej. Kod o podobnej liście instrukcji mógły zadziałać np. w PHP gdzie deklaracja zmiennej nie jest potrzebna. Należało by, więc zapisać:

  1.   int x;
  2.   cin >> x;
  3.   cout << "Wprowadzono " << x;

Będąc w temacie zmiennych innym poważnym problemem związanym z nimi jest brak inicjalizacji. Dotyczy to sytuacji kiedy chcemy np. wypisać zmienna do której nie ma przypisanej wartości jak w następującym kodzie:

  1.   int y;
  2.   cout << y;

Obecnie kompilatory nie pozwalają skompilować programu, który wykorzystuje niezainicjalizowaną zmienną. Ma to na celu wyeliminowanie niepożądanego działania programu. W C++ deklaracja zmiennej przypisuje danej nazwie jedynie adres w pamięci, ale dane pod tym adresem nie są ustawiane na żadną domyślną wartość. Oznacza to, że zmienna jedynie o zadeklarowanym typie int może mieć dowolną wartość z zakresu tego typu. Z tego powodu zawsze należy pamiętać o przypisaniu jakiejkolwiek wartości zmiennej. Przykładowo kod możnaby zmienić na następujący:

  1.   int y = 0;
  2.   cout << "Zmienna y: " << y;

Porównywanie zawsze prawdziwe

Zazwyczaj po zapoznaniu się z techniką wczytywania i wypisywania danych oraz przechowywanie danych w zmiennych przychodzi czas na naukę o instrukcji warunkowej. W matematyce znak równości '=' służy podkreśleniu faktu, że to co jest po lewej stronie równa się temu po prawej stronie. W takim razie podczas porównywania części osób na myśl przychodzi następujący kod:

  1.   int z = 0;
  2.   if (z = 5)
  3.     cout << "To bylo 5!";

Kod jednak niezależnie od ustawionej wartości x zawsze wyświetla, że liczba została odgadnięta. Jest to związane z faktem, że do porównywania należy zastosować operator porównania "==", a nie przypisania '='. W tym pierwszym przypadku wartości są porównywane i zwracana jest wartość logiczna czy obie strony są takie same. W drugim przypadku '=' lewej stronie zostaje przypisana prawa. Następnie operator przypisania zwraca nową wartość lewej strony, a to z kolei jest konwertowana na wartość logiczną, gdzie 0 oznacza fałsz, a każda inna wartość prawdę.

Innymi słowy: zawsze należy pamiętać, że do porównania dwa znaki równości jak poniżej:

  1.   int z = 0;
  2.   if (z == 5)
  3.     cout << "To bylo 5!";

Błędne koło

Nieskończona pętla może powstać poprzez nieprawidłowe porównanie wartości jak w instrukcji warunkowej, ale może też wskutek nieprzemyślanego warunku logicznego. Przypatrzmy się poniższemu przykładowi:

  1.   x = 0;
  2.   while (x != 0 || x != 10) {
  3.     cout << x << " ";
  4.     x++;
  5.   }

Pętla nigdy się nie zakończy, ponieważ zmienna x musiałaby mieć równocześnie dwie wartości. Jest to niemożliwe, więc pętla nie ma prawidłowego warunku stopu. Ten przypadek jest akurat jednym z łatwiejszych do wychwycenia. W zależności od przeznaczenie kod możnaby poprawić usuwając zbędny warunek:

  1.   x = 0;
  2.   while (x != 10) {
  3.     cout << x << " ";
  4.     x++;
  5.   }

Czasem jednak zdarzają się bardziej złożone przypadki, gdy nie do końca wiadomo jak się program zapętla.

  1.   int i = 0;
  2.   for (i = 0; i < 10; i++) {
  3.     for (i = 0; i < 5; i++) {
  4.       cout << ".";
  5.     }
  6.     cout << endl;
  7.   }

Tu jednak ponownie okazuje się, że warunek stopu jest nieosiągalny, ale tym razem jest to spowodowane modyfikowaniem wartości zmiennej wewnątrz innej pętli (choć wcale nie musi być to pętla). Z tego powodu należy bardzo uważnie zmieniać wartości licznika pętli wyżej. Mogłoby się wydawać, że takie operacje powinny zostać zakazane przez kompilator, ale są dozwolone, ponieważ z takich przypadków można korzystać na wiele różnych sposobów.

Chcąc jednak poprawić kod, aby wypisywał dziesięć rzędów po 5 kropek wystarczy dodać oddzielny licznik dla drugiej pętli. Dzięki temu jeden i drugi jest zmieniany niezależnie i szansa na zapętlenie jest zerowa.

  1.   int i = 0;
  2.   for (i = 0; i < 10; i++) {
  3.     for (int j = 0; j < 5; j++) {
  4.       cout << ".";
  5.     }
  6.     cout << endl;
  7.   }

Nadmiarowy średnik

W języku C++ na koniec każdej linijki należy postawić dokładnie jeden średnik. Średnik stawia się jeszcze między innymi na koniec klasy. Czy jednak coś się stanie kiedy zostanie postawiony dodatkowy średnik? Przykładowo w poniższym programie na końcu linijki zostały postawione dwa średniki:

  1.   x = 0;;

Nie jest to błąd, ale zwykle programiści nie stosują tego, ponieważ nie ma to większego sensu. Pomiędzy dwoma średnikami znajduje się według kompilatora pusta instrukcja, która nie ma żadnego wpływu na wywoływanie programu. Jednak są sytuacje kiedy średnik może powodować niewłaściwe działania programu. Zgodnie z zaleceniami młody programista postawił średnik na koniec linijki:

  1.   x = 0;
  2.   while (x++ < 10);
  3.     cout << x;

Nagle się jednak okazuje, że program wypisuje liczbę tylko raz. Jest to spowodowane faktem, że po while(..) został postawiony średnik. Przed tym średnikiem jak w poprzednim przypadku stoi pusta instrukcja, więc instrukcja wypisania tak naprawdę nie jest obejmowana przez pętle. Rozwiązaniem tego problemu jest zastosowanie nawiasów klamrowych w celu określenia wnętrza w pętli. Wtedy postawienie średnika na końcu linijki po while nie wydaje się takie oczywiste, a nawet jeśli zostanie postawiony to stanie się częścią instrukcji do wykonania pętli.

  1.   x = 0;
  2.   while (x++ < 10) {
  3.     cout << x;
  4.   }

Dane poza zakresem

C++ jest językiem pozwalającym tworzyć programy działające w czasie rzeczywistym, ale przez to nie pilnuje on programisty na każdym kroku. Czasem podczas pisania programu dochodzi do sytuacji, gdy chcemy pobrać element, który nie należy do tablicy. Często jest to spowodowane, że jesteśmy do przyzwyczajenia indeksowania elementów od 1 do n, a w rzeczywistości jest od 0 do n - 1.

  1.   int dane[] = {1, 2, 3, 4, 5};
  2.   for (int i = 1; i <= 5; i++) {
  3.     cout << dane[i] << endl;
  4.   }

Program moze wypisać wtedy:

  1. 2
  2. 3
  3. 4
  4. 5
  5. -858993460

Co ciekawe taki program może się uruchomić i zadziałać, ale wtedy zamiast sensownych wartości zasysa bardzo dziwne dane. Jeśli nie skontrolujemy dokładnie działania programu to bardzo długo można nie znaleźć, dlaczego program źle przelicza dane. Jeśli ma się podejrzenie, że wybierany indeks jest poza zakresem to warto dopisać warunek, który sprawdzi czy indeks nie jest poza zakresem.

Jednak najprościej jest pamiętać jakie są poprawne zakresy wypisywania danych z tablicy:

  1.   int dane[] = {1, 2, 3, 4, 5};
  2.   for (int i = 0; i < 5; i++) {
  3.     cout << dane[i] << endl;
  4.   }

Nieznana funkcja

Kompilator analizuje kod linijka po linijce. Na podstawie instrukcji w każdej funkcji generuje odpowiedni kod. Niestety jeśli wewnątrz funkcji znajduje się do funkcji znajdującej się po niej to kompilator wyrzuci błąd. Przyjrzymy się temu kodowi:

  1. void funkcja1() {
  2.   funkcja2();
  3. }
  4. void funkcja2() {
  5.  
  6. }

Istnieja dwa rozwiązania tego problemu. Jeden z nich zakłada zamiane miejscami funkcji i wtedy przed wywołaniem funkcja będzie znana, ale zdecydowanie lepszym pomysłem jest napisanie prototypu funkcji. Wtedy nieważne gdzie się znajdują funkcje program będzie wiedział co ma przekazać do danej funkcji.

  1. void funkcja2();
  2. void funkcja1() {
  3.   funkcja2();
  4. }
  5. void funkcja2() {
  6. }

Zadania

Zadanie 1

Pewien uczeń miał do napisania program, który wczytuje od użytkownika pewną liczbę, a następnie wypisuje wszystkie liczby z zakresu od 1 do x. Niestety program się nie kompiluje. Popraw program tak, aby zaczął działać poprawnie.

  1. #include <iostream>
  2. using namespace std;
  3. int main () {
  4.   int x, y;
  5.   cout << "Ile liczb wypisac?";
  6.   cin >> y;
  7.   for (int i = 1; i < x; i++);
  8.     cout << i << " ";
  9.   system("pause");
  10.   return 0;
  11. }