CodeGym /Kursy /C++ SELF /Typowe błędy: = vs ==, sprawdzanie wejścia

Typowe błędy: = vs ==, sprawdzanie wejścia

C++ SELF
Poziom 3, Lekcja 5
Dostępny

1. Błąd stulecia: = zamiast ==

Wygląda na drobiazg: jeden znak równości albo dwa. Ale znaczeniowo to dwie zupełnie inne operacje. == — to pytanie „czy równe?”, a = — to polecenie „wstaw tę wartość do zmiennej”.

Podstęp polega na tym, że przypisanie (=) też jest wyrażeniem: zwraca przypisaną wartość. Dlatego kompilator często nie musi ostrzegać: widzi warunek, który daje liczbę, a liczba w warunku traktowana jest jako „0 — fałsz, nie 0 — prawda”.

Wyobraź sobie, że na egzaminie zamiast „Czy to prawda?” mówisz „Zrób to prawdą!”. Nauczyciel się zdziwi, ale formalnie wypowiedziałeś zdanie. Tak samo = w if — to podobny trik.

Mini-demo: jak błąd niszczy dane i logikę

Poniżej przykład, który się kompiluje, ale robi nie to, czego oczekiwano:

#include <iostream>

int main() {
    int x = 5;

    if (x = 0) {                 // BŁĄD: przypisanie zamiast porównania
        std::cout << "x is zero\n";
    }

    std::cout << x << '\n';      // 0
}

Istota zdarzenia: x = 0 przypisuje zero, wyrażenie ma wartość 0, warunek jest fałszywy, gałąź if nie wykona się, ale x już jest zepsute.

Prawidłowy wariant

#include <iostream>

int main() {
    int x = 5;

    if (x == 0) {                // porównanie
        std::cout << "x is zero\n";
    } else {
        std::cout << "x is not zero\n"; // x is not zero
    }
}

Dlaczego kompilator nie zawsze ratuje

Ponieważ z punktu widzenia języka to dopuszczalne: warunek if (...) musi obliczyć coś, co da się rzutować na bool. Liczba jest rzutowana na bool. Zatem — formalnie poprawne.

Tak, wiele kompilatorów ostrzega (warning), ale ostrzeżenie to nie gwarancja, zwłaszcza w edukacyjnych Web‑IDE, gdzie ustawienia mogą być łagodne. Twoim zabezpieczeniem jest uwaga na operator w środku warunku.

Przyzwyczajenie: porównujemy jawnie, bez „sugestii”

Zła praktyka dla początkującego — pisać „sprytnie”:

if (x) { /* ... */ } // działa, ale gorzej czyta się na początku

Dobra praktyka — pisać „na wprost”:

if (x != 0) { /* ... */ }

W kodzie edukacyjnym to niemal zawsze wygrywa czytelnością. A czytelność to połowa błędów mniej.

2. Sprawdzanie wejścia: if (std::cin >> x)

Gdy czytasz liczbę przez std::cin >> x, cicho zakładasz, że użytkownik poda faktycznie liczbę. Ale użytkownik bywa kreatywny. Może wpisać qwe, nacisnąć Enter w złym miejscu, skopiować 10.5 zamiast 10. Wtedy std::cin „obrazi się”: odczyt się nie powiedzie, zmienna może pozostać bez zmian, a dalsze odczyty będą się dziwnie zachowywać.

Póki nie omawiamy szczegółowych stanów strumienia (fail, bad, clear, ignore) — to zostanie w innych tematach. Dziś wystarczy nam minimalny szablon: sprawdzenie samego faktu odczytu.

Najprostszy i najprzydatniejszy wzorzec

#include <iostream>

int main() {
    int n = 0;

    if (std::cin >> n) {
        std::cout << "read: " << n << '\n';   // np.: read: 42
    } else {
        std::cout << "input error\n";         // gdy wpisano nie liczbę
    }
}

W warunku if (std::cin >> n) dzieją się dwie rzeczy: najpierw próba odczytu liczby, potem — sprawdzenie, czy się udało. To kompaktowe i na twoim obecnym etapie — idealne.

Mini-przykład: komenda i liczba, bez nadziei „jakoś się odczyta”

Kontynuujemy pomysł prostego „konsolowego pomocnika”, który czyta komendę i wykonuje akcję. Na razie bez pętli: jedno uruchomienie — jedna komenda.

#include <iostream>
#include <string>

int main() {
    std::string cmd;
    std::cin >> cmd;

    if (cmd == "square") {
        int x = 0;
        if (std::cin >> x) {
            std::cout << x * x << '\n';       // np.: 25
        } else {
            std::cout << "input error\n";
        }
    }
}

Zwróć uwagę: sprawdzanie wejścia znajduje się tam, gdzie jest potrzebne — blisko użycia liczby. Nie „liczymy”, że się odczytało.

3. Przypadki brzegowe: wiek, zakresy i łańcuchy

Granice to wartości, na których program najczęściej się łamie: 0, 1, -1, „dokładnie 18”, pusty łańcuch, ostatni znak łańcucha, dzielenie przez zero. Początkujący zwykle testuje „środek” (np. 20 lat), a błąd ukrywa się na „krawędzi” (18 lat).

Dlatego warto wypowiadać warunek słowami i sprawdzać, czy granica jest wliczana, czy nie.

Aby umysł miał ułatwienie, trzymaj w pamięci proste reguły: każdy dostęp przez indeks i każde dzielenie wymagają ochrony. Nawet jeśli „wydaje się”, że zawsze będzie poprawnie — praktyka pokaże, że tak bywa rzadko.

> vs >=: „włącznie z granicą” to osobna decyzja

Porównaj dwa sprawdzenia:

#include <iostream>

int main() {
    int age = 18;

    std::cout << (age > 18)  << '\n';  // 0
    std::cout << (age >= 18) << '\n';  // 1
}

Logika „pełnoletni od 18” wymaga >=, a nie >. To nie styl, to sens.

Trochę bardziej życiowy fragment:

#include <iostream>

int main() {
    int age = 0;
    std::cin >> age;

    if (age >= 18) {
        std::cout << "adult\n";
    } else {
        std::cout << "minor\n";
    }
}

Jeśli pomylisz znak, program będzie niepoprawny dokładnie dla jednej wartości. I dlatego takie błędy żyją długo: testujesz 20, 30, 40 — wszystko działa. Zwyczaj, który pomaga, brzmi dziecinnie, ale działa mocno: najpierw powiedz warunek słowami („włącznie z 18”), potem wybierz znak.

Zakres „od L do R włącznie”: piszemy przez &&

Jeśli warunek brzmi „x od 1 do 10”, to niemal zawsze oznacza „nie mniej niż 1 i nie więcej niż 10”.

#include <iostream>

int main() {
    int x = 0;
    std::cin >> x;

    if (x >= 1 && x <= 10) {
        std::cout << "in range\n";
    } else {
        std::cout << "out of range\n";
    }
}

Granica jest częścią zadania. Nie chodzi o „napisać if”, ale o zdecydować, co robić dla 1 i 10.

Pusty łańcuch: zanim sięgniesz po s[0], upewnij się, że łańcuch nie jest pusty

Łańcuch nie gwarantuje, że jest w nim choć jeden znak. Jeśli przeczytałeś łańcuch przez std::getline, użytkownik mógł po prostu nacisnąć Enter.

#include <iostream>
#include <string>

int main() {
    std::string s;
    std::getline(std::cin, s);

    if (s.size() == 0) {
        std::cout << "empty\n";
    } else {
        std::cout << s[0] << '\n';      // wypisujemy pierwszy znak
    }
}

Ochrona „pusty/nie pusty” to nie drobiazg. To to, co oddziela program od wyjścia poza zakres.

Ostatni znak: s[s.size() - 1] wymaga jeszcze większej ostrożności

Ponieważ jeśli s.size() == 0, wyrażenie s.size() - 1 staje się „minus jeden”, a indeksy tego nie lubią.

#include <iostream>
#include <string>

int main() {
    std::string s;
    std::getline(std::cin, s);

    if (s.size() > 0) {
        char last = s[s.size() - 1];
        std::cout << last << '\n';
    } else {
        std::cout << "empty\n";
    }
}

Zwróć uwagę: sprawdzenie tutaj to > 0, a nie >= 0 (druga opcja jest bezsensowna). Tak, size() dla pustego łańcucha wynosi 0.

4. Kontrole ochronne: dzielenie i krótkie spięcie

Dzielenie przez zero to klasyczny problem. Szczególnie frustrujące jest to, że często pojawia się nie dlatego, że chcesz dzielić przez zero, ale dlatego, że nie pomyślałeś, że użytkownik może wpisać 0. A użytkownik może. Nawet jeśli poprosisz „podaj dzielnik” — i tak może wpisać 0.

Krótkie spięcie w && to rzadki przypadek, gdy „leniwość” operatora czyni program bezpieczniejszym: jeśli lewa część jest fałszywa, prawa w ogóle nie jest obliczana. Zatem jeśli po lewej masz sprawdzenie bezpieczeństwa, po prawej możesz umieścić niebezpieczną operację.

Prawidłowa kolejność części w &&

#include <iostream>

int main() {
    int a = 0;
    int b = 0;
    std::cin >> a >> b;

    if (b != 0 && (a / b > 2)) {
        std::cout << "a/b > 2\n";
    } else {
        std::cout << "condition is false\n";
    }
}

Jeśli b == 0, lewa część b != 0 jest fałszywa i dzielenie nie nastąpi. To właśnie ochrona.

Niewłaściwa kolejność (niebezpiecznie)

#include <iostream>

int main() {
    int a = 0;
    int b = 0;
    std::cin >> a >> b;

    if ((a / b > 2) && b != 0) {        // NIEBEZPIECZNE: dzielenie już zaszło
        std::cout << "a/b > 2\n";
    }
}

Nawet jeśli „sprawdzasz b”, robisz to za późno. To jak zakładanie kasku po upadku z roweru: kask jest dobry, ale timing zawiódł.

5. Praktyczny mini-przykład: jedna komenda — jedna akcja

Złożymy teraz wszystko w małą aplikację pokazującą styl „pewnych warunków”. Nie używamy pętli i funkcji (poza main), bo jeszcze ich nie przerabialiśmy. Ale możemy już zrobić program, który nie zawiesi się przy każdym złym wejściu i nie wykona niebezpiecznej operacji bez sprawdzenia.

Pomysł: użytkownik wpisuje komendę (divide, first, last), a potem potrzebne dane.

Szkielet rozgałęzienia po komendzie

#include <iostream>
#include <string>

int main() {
    std::string cmd;
    std::cin >> cmd;

    if (cmd == "divide") {
        std::cout << "mode: divide\n";
    } else if (cmd == "first") {
        std::cout << "mode: first\n";
    } else {
        std::cout << "unknown command\n";
    }
}

To podstawowe „rozwidlenie”: komenda to łańcuch, porównanie łańcuchów daje bool, if/else if/else wybiera gałąź.

Komenda divide: sprawdzamy wejście i chronimy dzielenie

#include <iostream>
#include <string>

int main() {
    std::string cmd;
    std::cin >> cmd;

    if (cmd == "divide") {
        int a = 0, b = 0;
        if (std::cin >> a >> b) {
            if (b != 0) std::cout << (a / b) << '\n';
            else std::cout << "division by zero\n";
        } else {
            std::cout << "input error\n";
        }
    }
}

Tu są dwie linie ochrony: najpierw upewniamy się, że liczby w ogóle się odczytały, potem upewniamy się, że dzielnik nie jest zerem.

Komenda first: czytamy łańcuch i sprawdzamy pustkę

Dla first jest niuans: po std::cin >> cmd w buforze może zostać znak nowej linii i getline odczyta pusty łańcuch. Omówiliśmy to w temacie o getline, więc tu stosujemy sprytny zabieg: czytać łańcuch przez std::getline(std::cin >> std::ws, s) — std::ws „zje” spacje i nowe linie przed właściwym tekstem.

#include <iostream>
#include <string>

int main() {
    std::string cmd;
    std::cin >> cmd;

    if (cmd == "first") {
        std::string s;
        std::getline(std::cin >> std::ws, s);

        if (s.size() == 0) std::cout << "empty\n";
        else std::cout << s[0] << '\n';
    }
}

Tak, wygląda to trochę „magicznie” przez std::ws, ale już znasz problem „pusty łańcuch po >>” i to jedno z normalnych rozwiązań.

Komenda last: ostatni znak bez wyjścia poza zakres

#include <iostream>
#include <string>

int main() {
    std::string cmd;
    std::cin >> cmd;

    if (cmd == "last") {
        std::string s;
        std::getline(std::cin >> std::ws, s);

        if (s.size() > 0) std::cout << s[s.size() - 1] << '\n';
        else std::cout << "empty\n";
    }
}

Sens ten sam: najpierw sprawdzenie, potem indeksowanie.

Schemat „jak myśleć” o niebezpiecznych miejscach

Kiedy piszesz gałąź if, warto mentalnie przejść taką ścieżkę:

flowchart TD
    A[Trzeba wykonać akcję] --> B{Potrzebne dane?}
    B -->|Tak| C{Odczyt się powiódł?}
    C -->|Nie| X[Komunikat o błędzie]
    C -->|Tak| D{Jest niebezpieczna operacja?}
    D -->|Dzielenie/indeks| E{Czy wykonano ochronę?}
    E -->|Nie| Y[Komunikat + nie wykonujemy akcji]
    E -->|Tak| F[Wykonujemy akcję]
    B -->|Nie| F

Ta schemat blokowy to nie „formalizm”, a po prostu sposób, by nie zapominać o kontrolach, szczególnie gdy kod zaczyna się rozgałęziać.

6. Typowe błędy

Błąd nr 1: = zamiast == w warunku.
To jeden z najbardziej uciążliwych błędów, ponieważ często nie blokuje kompilacji. = zmienia zmienną i zwraca przypisaną wartość, dlatego warunek może stać się „fałszywy” lub „prawdziwy” nie na skutek porównania, lecz dlatego, że właśnie coś nadpisałeś. Leczy się to nawykiem uważnego patrzenia na „centralny operator” warunku i pisaniem porównania jawnie: x == 0, cmd == "exit".

Błąd nr 2: pomylona granica (> zamiast >=, < zamiast <=).
Takie błędy prawie zawsze objawiają się „dokładnie dla jednej wartości”: 18, 0, „ostatni indeks”. Dlatego żyją długo: testujesz 20, 30, 40 — wszystko działa. Zwyczaj, który pomaga: najpierw wypowiedz warunek słowami („włącznie z 18”), potem wybierz znak.

Błąd nr 3: indeksowanie łańcucha bez sprawdzenia długości.
std::string nie musi być niepusty. Jeśli robisz s[0] lub s[s.size() - 1], najpierw powinno być warunek s.size() > 0. Szczególnie podstępny jest „ostatni znak”, bo wyrażenie s.size() - 1 wygląda niewinnie, ale dla pustego łańcucha staje się złym indeksem. Ochrona to osobna gałąź if lub lewy argument w &&.

Błąd nr 4: dzielenie bez sprawdzenia zera (lub sprawdzenie jest „po prawej”).
Sprawdzenie b != 0 musi być przed dzieleniem. Najwygodniejszy wzorzec to b != 0 && (a / b > ...), gdzie krótkie spięcie gwarantuje, że dzielenie nie nastąpi przy b == 0. Jeśli zamienisz kolejność, otrzymasz niebezpieczne zachowanie nawet przy obecnym „sprawdzeniu”.

Błąd nr 5: brak minimalnej weryfikacji wejścia.
Jeśli robisz std::cin >> x; i od razu używasz x, zakładasz poprawne wejście. W praktyce prowadzi to do dziwnych „zawieszeń” i błędnych wyników, gdy użytkownik wpisze nie liczbę. Nawet bez zagłębiania się w mechanikę strumieni można pisać bezpieczniej: if (std::cin >> x) { ... } else { ... }. To dodaje kilka linii, ale oszczędza godziny zdziwienia.

1
Zadanie
C++ SELF, poziom 3, lekcja 5
Niedostępne
Wskaźnik zera
Wskaźnik zera
1
Zadanie
C++ SELF, poziom 3, lekcja 5
Niedostępne
Etykieta wartości
Etykieta wartości
1
Zadanie
C++ SELF, poziom 3, lekcja 5
Niedostępne
Suma z weryfikacją
Suma z weryfikacją
1
Zadanie
C++ SELF, poziom 3, lekcja 5
Niedostępne
Trzy polecenia
Trzy polecenia
1
Ankieta/quiz
Instrukcje warunkowe i logika, poziom 3, lekcja 5
Niedostępny
Instrukcje warunkowe i logika
Instrukcje warunkowe i logika
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION