Interakcja z konsolą

Program często musi pokazać użytkownikowi wynik albo poprosić go o dane. Konsola jest jednym ze sposobów takiej komunikacji. Warto myśleć o niej jak o dwóch kierunkach przepływu informacji: program wysyła tekst na zewnątrz, a dane wpisane przez użytkownika trafiają do programu.

System udostępnia programowi trzy standardowe strumienie:

  • stdin — standardowe wejście; zwykle znaki wpisane przez użytkownika;
  • stdout — standardowe wyjście; zwykłe komunikaty programu;
  • stderr — wyjście przeznaczone na błędy i ostrzeżenia.

Strumienie mogą być powiązane z terminalem, ale mogą też zostać przekierowane, na przykład do pliku. Dzięki temu program nie musi wiedzieć, czy jego wynik ogląda człowiek, czy zapisuje go inne narzędzie.

Wejście i wyjście w C

W C funkcje obsługujące standardowe wejście i wyjście są zadeklarowane w nagłówku <stdio.h>. Najczęściej na początku nauki spotkasz printf do wypisywania i scanf do odczytywania.

Wypisywanie wartości przez printf

printf składa tekst z dwóch części: zwykłych znaków oraz specyfikatorów formatu, takich jak %d. Każdy specyfikator mówi, jak wypisać odpowiadający mu argument.

#include <stdio.h>

int main(void) {
    int liczba = 12;
    double cena = 3.5;
    char znak = 'A';

    printf("Liczba: %d, cena: %.2f, znak: %c\n", liczba, cena, znak);
    return 0;
}

W tym wywołaniu %d zastępuje wartość liczba, %.2f wypisuje cena z dwiema cyframi po kropce, a %c wstawia znak. \n kończy wiersz. Specyfikatory i argumenty muszą do siebie pasować i występować w tej samej kolejności. Niezgodny typ może prowadzić do błędnego wyniku, a w C nawet do niezdefiniowanego działania programu.

Najczęściej używane specyfikatory:

Specyfikator printf — argument scanf — adres zmiennej
%d int int *
%u unsigned int unsigned int *
%ld long long *
%lld long long long long *
%f float jest przekazywany jako double float *
%lf double (w printf działa jak %f) double *
%Lf long double long double *
%c znak przekazany jako int char *
%s tablica znaków zakończona \0 miejsce na tablicę znaków

Istotna różnica dotyczy liczb zmiennoprzecinkowych: do printf zarówno float, jak i double wypisujemy przez %f; do scanf dla float używamy %f, a dla double — %lf.

Odczytywanie wartości przez scanf

scanf próbuje odczytać znaki ze standardowego wejścia i zamienić je na wskazane typy. Żeby funkcja mogła zapisać wynik do zmiennej, dla zwykłych zmiennych przekazujemy jej adres, zapisany operatorem &.

#include <stdio.h>

int main(void) {
    int wiek;
    double wzrost;
    char pierwszaLitera;
    char miasto[50];

    printf("Podaj wiek: ");
    if (scanf("%d", &wiek) != 1) {
        fprintf(stderr, "Nie udało się odczytać wieku.\n");
        return 1;
    }

    printf("Podaj wzrost w metrach: ");
    if (scanf("%lf", &wzrost) != 1) {
        fprintf(stderr, "Nie udało się odczytać wzrostu.\n");
        return 1;
    }

    printf("Podaj pierwszą literę imienia: ");
    if (scanf(" %c", &pierwszaLitera) != 1) {
        fprintf(stderr, "Nie udało się odczytać znaku.\n");
        return 1;
    }

    printf("Podaj nazwę miasta bez spacji: ");
    if (scanf("%49s", miasto) != 1) {
        fprintf(stderr, "Nie udało się odczytać nazwy miasta.\n");
        return 1;
    }

    printf("Wiek: %d, wzrost: %.2f m, litera: %c, miasto: %s\n",
           wiek, wzrost, pierwszaLitera, miasto);
    return 0;
}

Załóżmy, że użytkownik wpisze kolejno 20, 1.75, A i Krakow. Wywołanie scanf("%d", &wiek) odczyta pierwszą liczbę i zapisze ją pod adresem zmiennej wiek. Kolejne wywołania wypełnią pozostałe zmienne. Funkcja zwraca liczbę poprawnie odczytanych wartości — dlatego sprawdzenie != 1 wykrywa sytuację, w której konwersja jednej wartości się nie udała.

W nazwie tablicy miasto nie ma &: w tym wywołaniu nazwa tablicy przekazuje adres jej pierwszego elementu. Szerokość %49s ogranicza zapis do 49 znaków, zostawiając miejsce na końcowy znak \0 w tablicy o rozmiarze 50. %s kończy odczyt na pierwszym białym znaku, więc ta wersja nie wczyta nazwy zawierającej spacje.

Spacja przed %c jest celowa. Po wcześniejszym wpisaniu liczby w strumieniu zwykle zostaje znak końca wiersza. Samo %c odczytałoby ten znak, a format " %c" najpierw pomija białe znaki, a potem pobiera właściwy znak.

Odczytywanie całego wiersza w C

Jeśli tekst może zawierać spacje, samo %s nie wystarczy. Do wczytania wiersza można użyć fgets. Funkcja zapisuje w buforze najwyżej rozmiar - 1 znaków i dodaje końcowe \0.

#include <stdio.h>
#include <string.h>

int main(void) {
    char imie[100];

    printf("Podaj imię i nazwisko: ");
    if (fgets(imie, sizeof imie, stdin) == NULL) {
        fprintf(stderr, "Nie udało się odczytać tekstu.\n");
        return 1;
    }

    imie[strcspn(imie, "\n")] = '\0';
    printf("Witaj, %s!\n", imie);
    return 0;
}

fgets może zapisać również znak nowego wiersza, jeśli zmieści się w buforze. strcspn znajduje jego pozycję, a przypisanie \0 usuwa go z tekstu. Jeśli wiersz jest dłuższy niż bufor, odczytany zostanie tylko jego początek; większy lub dowolnie długi tekst wymaga dodatkowej obsługi.

Dlaczego warto sprawdzać wynik odczytu

Zmienne lokalne, którym nie przypisano wartości, nie zawierają bezpiecznej wartości domyślnej. Jeżeli odczyt się nie powiedzie, a program mimo to użyje takiej zmiennej, wynik może być błędny. scanf zwraca liczbę poprawnych konwersji, a fgets zwraca NULL, gdy nie udało się odczytać wiersza. Sprawdź te wyniki przed dalszym użyciem danych.

Gdy scanf nie potrafi zamienić tekstu na oczekiwany typ, błędny fragment zwykle pozostaje w strumieniu. Samo ponowne wywołanie scanf może wtedy natrafić na ten sam fragment. Powyższe krótkie programy kończą działanie po błędzie; program, który ma ponawiać pytanie, musi dodatkowo usunąć błędny wiersz i dopiero wtedy poprosić o dane ponownie.

Wejście i wyjście w C++

W C++ do standardowej obsługi wejścia i wyjścia służy nagłówek <iostream>. Wypisujemy przez std::cout, odczytujemy przez std::cin, a komunikaty o błędach kierujemy zwykle do std::cerr. Przed nazwami umieszczamy std::, ponieważ te obiekty należą do przestrzeni nazw std.

Operator << przekazuje wartość do strumienia wyjściowego. Operator >> pobiera wartość ze strumienia wejściowego i próbuje zapisać ją w zmiennej. Typ zmiennej określa, jaką wartość ma odczytać.

#include <iostream>
#include <string>

int main() {
    std::string imie;
    int wiek;
    double wzrost;

    std::cout << "Podaj imię i nazwisko: ";
    if (!std::getline(std::cin, imie)) {
        std::cerr << "Nie udało się odczytać imienia.\n";
        return 1;
    }

    std::cout << "Podaj wiek: ";
    if (!(std::cin >> wiek)) {
        std::cerr << "Wiek musi być liczbą całkowitą.\n";
        return 1;
    }

    std::cout << "Podaj wzrost w metrach: ";
    if (!(std::cin >> wzrost)) {
        std::cerr << "Wzrost musi być liczbą.\n";
        return 1;
    }

    std::cout << imie << ", wiek: " << wiek
              << ", wzrost: " << wzrost << " m\n";
    return 0;
}

std::getline(std::cin, imie) odczytuje cały wiersz, więc imię i nazwisko mogą zawierać spację. Z kolei std::cin >> wiek pomija początkowe białe znaki i odczytuje wartość pasującą do typu int. W wyrażeniu if (!(std::cin >> wiek)) program najpierw próbuje odczytać liczbę, a następnie sprawdza, czy operacja się powiodła. Jeśli użytkownik wpisze na przykład słowo zamiast liczby, program wypisze błąd na std::cerr i zakończy działanie.

Różnica między >> a getline

Operator >> jest wygodny dla pojedynczych wartości i słów, ale zatrzymuje odczyt tekstu na białym znaku. std::getline pobiera cały wiersz aż do znaku nowego wiersza. Połączenie tych metod wymaga uwagi: po std::cin >> wiek znak końca wiersza pozostaje w strumieniu. Bez jego pominięcia następujące getline może od razu odczytać pusty tekst.

#include <iostream>
#include <limits>
#include <string>

int main() {
    int wiek;
    std::string imie;

    std::cout << "Podaj wiek: ";
    if (!(std::cin >> wiek)) {
        std::cerr << "Wiek musi być liczbą całkowitą.\n";
        return 1;
    }

    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');

    std::cout << "Podaj imię i nazwisko: ";
    std::getline(std::cin, imie);

    std::cout << "Witaj, " << imie << "!\n";
    return 0;
}

ignore odrzuca znaki aż do końca bieżącego wiersza. <limits> jest potrzebny do użycia std::numeric_limits<std::streamsize>::max(), czyli maksymalnej liczby znaków, które chcemy pominąć. Nie jest to ogólna metoda „czyszczenia bufora” po każdym wejściu — przydaje się konkretnie wtedy, gdy po odczycie wartości przez >> chcemy przejść do odczytu kolejnego wiersza.

Formatowanie liczb

Nagłówek <iomanip> udostępnia manipulatory formatujące. std::fixed wybiera zapis dziesiętny o stałej liczbie cyfr po kropce, a std::setprecision(2) w tym trybie ustawia dwie cyfry po kropce.

#include <iostream>
#include <iomanip>

int main() {
    double cena = 12.5;

    std::cout << "Domyślnie: " << cena << '\n';
    std::cout << "Do dwóch miejsc: "
              << std::fixed << std::setprecision(2) << cena << '\n';
    return 0;
}

Manipulatory takie jak std::fixed pozostają aktywne dla kolejnych wartości wypisywanych do tego samego strumienia. Jeśli później potrzebujesz innego formatu, ustaw go jawnie.

Co jest wspólne, a co różni C i C++?

Oba języki korzystają ze standardowych strumieni wejścia i wyjścia, ale udostępniają inne podstawowe interfejsy. W C typ i sposób zapisu danych określamy specyfikatorem formatu funkcji printf lub scanf. W C++ operatory strumieniowe dobierają obsługę do typu wartości, a tekst wygodnie przechowujemy w std::string. Nie mieszaj tych interfejsów bez konkretnego powodu — łatwiej wtedy zrozumieć, skąd pochodzą dane i jak są formatowane.

Kolorowanie tekstu — temat dodatkowy

Niektóre terminale rozpoznają kody ANSI. Poniższy kod prosi terminal o czerwony tekst, a następnie przywraca domyślny styl:

#include <iostream>

int main() {
    std::cout << "\033[1;31mCzerwony tekst\033[0m\n";
    return 0;
}

Obsługa kodów zależy od terminala i systemu. Na Windows istnieje też systemowe API (nagłówek <windows.h>), ale jest ono specyficzne dla tej platformy. Kolor nie należy do podstawowej obsługi wejścia i wyjścia — program powinien pozostać zrozumiały również wtedy, gdy terminal nie obsługuje kolorów.