CodeGym /Kursy /C++ SELF /return i wczesne zakończenie

return i wczesne zakończenie

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

1. Wprowadzenie

Kiedy piszesz pierwsze sto linii kodu, wydaje się, że sterowanie przepływem to if, else i pętle. return wygląda jak formalność: „no trzeba coś zwrócić z funkcji int”. Jednak wkrótce okazuje się, że połowa „makaronu” z zagnieżdżonych if/else znika, jeśli nauczysz się wcześniej wychodzić z funkcji, gdy kontynuowanie nie ma sensu.

Ważne, by zapamiętać prostą myśl: return natychmiast kończy bieżącą funkcję. Nie „po pętli”, nie „po if”, nie „kiedyś później”. Teraz. Dlatego return to narzędzie nie tylko „do wyniku”, ale też „do struktury” kodu: pomaga uczynić czytanie liniowym, jak instrukcję, a nie labirynt.

Wyobraź sobie, że funkcja to mały scenariusz. Jeśli w scenariuszu coś pójdzie nie tak (niepoprawne dane, pusta lista, błędna komenda), normalna reakcja to nie ciągnąć całej funkcji przez dziesięć else, lecz spokojnie powiedzieć: „OK, na tym kończymy”.

2. Dwa rodzaje return: z wyrażeniem i bez

Teraz starannie rozłożymy na półki formy return. To jak drzwi: jedne to „wyjście z bagażem”, drugie — „wyjście bez bagażu”. Jeśli pomylić — będzie albo błąd kompilacji, albo dziwne próby przeciągnięcia walizki przez wąskie drzwi. W C++ mamy return expr; i return;, a wybór zależy od typu zwracanego przez funkcję.

Mini-tabela, którą warto mieć w głowie:

Typ funkcji Dozwolony return Co oznacza
int, bool, std::string, ...
return expr;
Zwrócić wartość i zakończyć funkcję
void
return;
(albo po prostu dojść do końca)
Po prostu zakończyć funkcję bez wartości

return expr; — zwracamy wartość

#include <iostream>

int square(int x) {
    return x * x;
}

int main() {
    std::cout << square(7) << '\n'; // 49
}

Tu wszystko proste: funkcja obiecała int, więc musi zwrócić int.

return; — wczesne wyjście z funkcji void

#include <iostream>

void print_positive(int x) {
    if (x <= 0) return;      // wczesne wyjście
    std::cout << x << '\n';  // drukujemy tylko jeśli x > 0
}

int main() {
    print_positive(-5);
    print_positive(10);      // 10
}

Zwróć uwagę: return; tutaj to nie „zwrócić pustkę jako wartość”, lecz po prostu „wyjść i koniec”.

3. „Wszystkie ścieżki muszą zwracać wartość”

Następny problem pojawia się u początkujących bardzo często: funkcja wydaje się zwracać int, i gdzieś wewnątrz już napisałeś return, ale kompilator dalej narzeka. Dlaczego? Bo kompilator (i ty razem z nim) próbuje zrozumieć: co się stanie na każdej możliwej ścieżce wykonania? Jeśli istnieje ścieżka, gdzie funkcja może dojść do końca bez return, to jest to problem.

Intuicyjna analogia: podpisałeś kontrakt „zawsze dostarczam paczkę”, a w jednym scenariuszu po prostu wychodzisz z magazynu. Klient (kompilator) się denerwuje — i słusznie.

Oto zły przykład:

#include <string>

int score_to_grade(int score) {
    if (score >= 90) return 5;
    if (score >= 70) return 4;
    if (score >= 50) return 3;
    // a jeśli score = 10?..
}

Prawidłowa wersja — jawnie zamknąć pozostały przypadek:

int score_to_grade(int score) {
    if (score >= 90) return 5;
    if (score >= 70) return 4;
    if (score >= 50) return 3;
    return 2; // wszystko, co poniżej 50
}

Tu nie ma żadnej magii: po prostu uczciwie mówimy, co robić w pozostałych przypadkach.

Czasami student próbuje „naprawić” to tak: dodaje else-gałęzie i pogłębia strukturę. Ale często dużo prościej zostawić liniowy ciąg i postawić finalny return na dole. To jak grzecznie zamknąć drzwi, zamiast budować kolejny korytarz.

4. Wczesne wyjście: guard clauses

Teraz dochodzimy do głównego triku wykładu: wczesne wyjście, które często nazywa się guard clause (sprawdzenie ochronne). To technika, gdzie najpierw odrzucasz „złe” przypadki, a dopiero potem piszesz główną logikę. Dla początkującego to bardzo przyjemny styl: zamienia kod w sekwencję sprawdzeń, a nie w matrioszkę z if/else.

Porównaj dwa style. Pierwszy — „las if”:

#include <iostream>

void print_discounted_price(int price, int discount) {
    if (price > 0) {
        if (discount >= 0 && discount <= 100) {
            int result = price - price * discount / 100;
            std::cout << result << '\n';
        } else {
            std::cout << "Bad discount\n";
        }
    } else {
        std::cout << "Bad price\n";
    }
}

Tu wszystko wydaje się poprawne, ale czyta się ciężko: żeby dotrzeć do sedna, trzeba przejść dwa poziomy warunków.

Teraz ten sam sens, ale z wczesnymi wyjściami:

#include <iostream>

void print_discounted_price(int price, int discount) {
    if (price <= 0) {
        std::cout << "Bad price\n";
        return;
    }
    if (discount < 0 || discount > 100) {
        std::cout << "Bad discount\n";
        return;
    }
    int result = price - price * discount / 100;
    std::cout << result << '\n';
}

Sens stał się zauważalnie jaśniejszy: najpierw „ochrona przy wejściu” (guards), potem normalny scenariusz.

Niewielka schemat-blokowy tego, co się dzieje w stylu guard:

flowchart TD
    A[Start funkcji] --> B{price > 0?}
    B -- nie --> X[Komunikat o bledzie] --> R[return]
    B -- tak --> C{discount w 0..100?}
    C -- nie --> Y[Komunikat o bledzie] --> R
    C -- tak --> D[Obliczamy wynik] --> E[Druk] --> F[Koniec]

Zauważ ważny szczegół: guard clauses szczególnie dobrze działają w funkcjach, które walidują dane wejściowe. A dane wejściowe w realnym życiu niemal nigdy nie są „idealnie ładne”.

5. return w pętlach i podczas wyszukiwania

Jeszcze jedno miejsce, gdzie return upraszcza kod, to wyszukiwanie wewnątrz pętli. Początkujący często piszą: „ustawię flagę found, potem po pętli sprawdzę flagę”. To działa, ale często wydłuża kod i dodaje zbędne zmienne.

Jeśli zadanie brzmi „znajdź i zgłoś”, to naturalne rozwiązanie: jak tylko znajdziesz — od razu return.

Przykład: czy w stringu jest cyfra

#include <string>

bool contains_digit(std::string s) {
    for (char c : s) {
        if (c >= '0' && c <= '9') return true;
    }
    return false;
}

Jeśli cyfra pojawi się na początku stringu, nie musimy przebiegać całego stringu „dla porządku”. Już znamy odpowiedź.

Przykład: znaleźć notatkę po tytule

Ustalmy, że tworzymy proste konsolowe narzędzie NoteKeeper: przechowuje notatki w std::vector<std::string>, potrafi dodać, wypisać listę i usunąć po nazwie. To nie „produkt idealny”, ale świetne pole do ćwiczeń.

Funkcja wyszukująca indeks:

#include <string>
#include <vector>

int find_note_index(const std::vector<std::string>& notes, std::string title) {
    for (int i = 0; i < static_cast<int>(notes.size()); ++i) {
        if (notes[i] == title) return i;
    }
    return -1;
}

Tu return użyte jest dokładnie tak, jak trzeba: znalazłeś — zwracasz indeks; nie znalazłeś — zwracasz -1 na końcu. Tak, tutaj jest static_cast<int>, żeby nie narzekać na typy (różnicę między int a size_t omawialiśmy wcześniej i jeszcze zobaczymy w praktyce).

6. NoteKeeper: składamy aplikację z wczesnymi return

Teraz zrobimy mały krok w stronę całości: połączymy idee return i wczesnego wyjścia w jedno miniaplikacyjne ćwiczenie. Naszym celem jest, żeby kod czytał się jak scenariusz: „pokaż menu → przeczytaj komendę → wykonaj → powtórz”. I żeby każda funkcja była maksymalnie prosta: sprawdziła dane wejściowe, jeśli źle — wyszła, jeśli dobrze — wykonała pracę.

main() i kod zakończenia programu

main() to też funkcja, tylko „specjalna”: jej rezultat to kod zakończenia programu. Nieformalnie można myśleć tak: 0 oznacza „wszystko OK”, a wartość niezerowa — „coś poszło nie tak”.

W zadaniach edukacyjnych prawie zawsze zwraca się 0. Czasami zobaczysz, że return 0; w ogóle się nie pisze — w C++ dozwolone jest „dojść do końca main”, i to będzie równoważne z sukcesem.

Ja jednak polecam na początku zapisywać return 0; jawnie: dzięki temu lepiej czujesz, że main jest zwykłą funkcją, a zakończenie programu to świadomy punkt.

Przykład z różnymi wynikami:

#include <iostream>

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

    if (x < 0) {
        std::cout << "Negative is not allowed\n";
        return 1; // sygnał: błąd/niepowodzenie
    }

    std::cout << "OK\n";
    return 0; // sukces
}

Prototypy funkcji

#include <string>
#include <vector>

void print_menu();
int read_command();
void list_notes(const std::vector<std::string>& notes);
void add_note(std::vector<std::string>& notes);
void remove_note(std::vector<std::string>& notes);
int find_note_index(const std::vector<std::string>& notes, std::string title);

Tu nie omawiamy organizacji po plikach. Na razie wszystko żyje w jednym pliku .cpp.

Menu i odczyt komendy

#include <iostream>

void print_menu() {
    std::cout << "1) List notes\n";
    std::cout << "2) Add note\n";
    std::cout << "3) Remove note\n";
    std::cout << "0) Exit\n";
}

int read_command() {
    int cmd = -1;
    if (!(std::cin >> cmd)) return -1; // jeśli wejście się zepsuło — wychodzimy
    return cmd;
}

Zwróć uwagę na styl: jeśli odczyt się nie powiódł, nie udajemy dalej, że jest komenda. Uczciwie zwracamy „złą wartość” (-1). Tak, później uczynimy wejście bardziej niezawodnym, ale teraz ważne jest, by zobaczyć moc wczesnego wyjścia.

Wypisanie listy notatek

#include <iostream>
#include <vector>
#include <string>

void list_notes(const std::vector<std::string>& notes) {
    if (notes.empty()) {
        std::cout << "(empty)\n";
        return;
    }
    for (const std::string& n : notes) std::cout << n << '\n';
}

Jeśli lista jest pusta, główny scenariusz (pętla drukująca) po prostu nie jest potrzebny. Wczesny return; to uwydatnia.

Dodawanie notatki

#include <iostream>
#include <string>
#include <vector>

void add_note(std::vector<std::string>& notes) {
    std::string title;
    std::cout << "Title: ";
    std::cin >> title;

    if (title.empty()) return; // w praktyce rzadko tu trafimy przez >>
    notes.push_back(title);
}

Tutaj widzimy, że guard clauses to nawyk. Nawet jeśli konkretnie tu empty() prawie się nie zdarzy, taki sposób myślenia jest użyteczny.

Usuwanie po nazwie

#include <iostream>
#include <string>
#include <vector>

void remove_note(std::vector<std::string>& notes) {
    std::string title;
    std::cout << "Title to remove: ";
    std::cin >> title;

    int idx = find_note_index(notes, title);
    if (idx == -1) {
        std::cout << "Not found\n";
        return;
    }
    notes.erase(notes.begin() + idx);
}

Tu wczesny return; usuwa zbędne else i sprawia, że główny scenariusz jest liniowy: znalazłeś — usunąłeś.

main: scenariusz łatwy do czytania

#include <iostream>
#include <vector>
#include <string>

int main() {
    std::vector<std::string> notes;

    while (true) {
        print_menu();
        int cmd = read_command();

        if (cmd == 0) return 0;
        if (cmd == 1) list_notes(notes);
        else if (cmd == 2) add_note(notes);
        else if (cmd == 3) remove_note(notes);
        else std::cout << "Unknown command\n";
    }
}

Zwróć uwagę: return 0; wewnątrz pętli to konstrukcja całkowicie normalna. Nie musimy robić break, potem wychodzić z pętli, a potem jeszcze gdzieś zwracać. Jeśli „wyjście z aplikacji” to rzeczywisty koniec main, można to po prostu zapisać wprost.

7. Typowe błędy przy pracy z return i wczesnym wyjściem

Błąd nr 1: zapomnieć return na jednej ze ścieżek w funkcji, która powinna zwracać wartość.
Zwykle zdarza się to, gdy napisałeś if (warunek) return ...;, a dla pozostałych przypadków nic nie przewidziałeś. Kompilator może dać ostrzeżenie, a czasem zachowanie staje się nieprzewidywalne. Prosta zasada: po łańcuchu warunków zawsze daj „przypadek domyślny” jako finalny return.

Błąd nr 2: pisać return; w funkcji zwracającej int/bool/string.
Czasem wydaje się: „chcę po prostu wcześniej wyjść”. Ale kontrakt funkcji to zwrócić wartość. Jeśli musisz wyjść wcześniej, zwróć sensowną wartość (np. false, -1, pusty string) i ustal to w projekcie funkcji.

Błąd nr 3: próbować zwrócić wartość z funkcji void.
To sytuacja lustrzana: void znaczy „brak wartości”. Jeśli łapiesz się na chęci napisania return result; — może funkcja powinna zwracać wynik i warto zmienić jej typ (albo podzielić na dwie funkcje: „oblicz” i „wypisz”).

Błąd nr 4: budować głębokie if/else, chociaż można było użyć kilku guard clause i kontynuować.
Najczęściej wygląda to jak „piramida w prawo”: kod przesuwa się o 4 spacje na każdym poziomie i po chwili czytasz nie logikę, lecz schody. Wczesne wyjścia to uczciwy sposób powiedzieć: „jeśli zły przypadek — kończymy” i nie taszczyć go przez całą funkcję.

Błąd nr 5: pisać kod po return i dziwić się, że on nie działa.
Po return funkcja się zakończyła. Wszystko poniżej staje się nieosiągalne. Czasem kompilator nawet ostrzega o unreachable code, ale nie zawsze. Dobra praktyka: jeśli postawiłeś return, wyobraź sobie tam kropkę i nie planuj „jeszcze jednej linijki”.

1
Zadanie
C++ SELF, poziom 14, lekcja 3
Niedostępne
Wskaźnik znaku
Wskaźnik znaku
1
Zadanie
C++ SELF, poziom 14, lekcja 3
Niedostępne
Sprawdzenie rabatu
Sprawdzenie rabatu
1
Zadanie
C++ SELF, poziom 14, lekcja 3
Niedostępne
Pierwszy minus
Pierwszy minus
1
Zadanie
C++ SELF, poziom 14, lekcja 3
Niedostępne
Ocena testu
Ocena testu
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION