Trudno wyobrazić sobie aby każdy program mógł kontaktować się z użytkownikiem tylko wypisując komunikaty, ale nie umożliwiając pobrania żadnych danych od niego. Urządzenie terminalowe składa się z dwu elementów – ekranu oraz klawiatury – dlatego naturalnym procesem jest obsługa tejże w celu pobrania danych od użytkownika.

W języku C i C++ przewidziano różną filozofię obsługi terminala, w C++ obsługę stanowi dedykowany strumień w kanale cin, w C są to procedury i funkcje w różnym stopniu złożoności (czy też “głębokości” sięgania) obsługują klawiaturę.

Najprostszym podejściem (aczkolwiek mocno niewłaściwym, co zostanie udowodnione w dalszej treści materiału) jest skorzystanie z naturalnego dla C++ strumienia cin. Przykładem jest poniższy kod:

#include<iostream>
#include<iomanip>

int main(){
	int i;
	std::cout << "Podaj wartość liczbową typu int: ";
	std::cin >> i;
	std::cout << "Wartość ciągu odczytanego z klawiatury to: " << std::setw(6) << std::right << i <<std::endl;
	return 0;
    }

Jak widać po uruchomieniu programu, czeka on na podanie wartości liczbowej typu int, a następnie próbuje przypisać ją zmiennejtego samego typu, co w przypadku podania właściwych wartości udaje się skutecznie przeprowadzić.

Uwaga: Dla uproszczenia w celach dydaktycznych kodu programu, nie została w nim użyta metoda ustawiająca parametry konsoli ekranowej do wyświetlenia polskich znaków, jak to uczynić aby polskie znaki były poprawnie wyświetlane omówione jest w dodatku.

Z powyżej omówioną techniką wiąże się szereg problemów związanych z odczytem wartości – wystarczy dla wyżej zaprezentowanego kodu zamiast oczekiwanej liczby całkowitej spróbować wpisać ciąg znakowy lub liczbę zmiennoprzecinkową. Dla testów potwierdzających te problemy wystarczy przetestować poniższy kod, gdzie sekwencyjnie (jedna po drugiej) odczytywane są dwie wartości z klawiatury.

#include<iostream>
#include<iomanip>
using namespace std;
int main(){
		int i, j;
		std::cout << "Podaj wartość liczbową \"i\": ";
		std::cin >> i;
		std::cout << "Podaj wartość liczbową: \"j\" ";
		std::cin >> j;
		std::cout << "Wartość ciągu odczytanego z klawiatury to: " << std::setw(6) << std::right << i <<std::endl;
		std::cout << "Wartość ciągu odczytanego z klawiatury to: " << std::setw(6) << std::right << j <<std::endl;
		return 0;
    }

Wystarczy uruchomić ten program i spróbować wpisać niepoprawne wartości np. przy pierwszym pytaniu o wartość wprowadzić (od razu – w jednym wpisie) takie dane jak:

  • Ala ma kota
  • 10 7
  • 6,456
  • 87.764764
  • true
  • g 4

Jak zachował się program? Zgodnie z oczekiwaniami? Dane są poprawnie wprowadzone?

Tak właśnie działa obsługa strumienia wejściowego, z jednej strony jest bardzo łatwa do oprogramowania, z drugiej bardzo podatna na nieprawidłowe działanie – dlatego więc nie należy używać tej formy pobierania danych, o ile nie jest zamysłem programisty wykorzystanie dokładnie takiej formy.

Jak poprawić program, aby był odporny na takie działanie? Należy odczytać wejście w zgodnym typie z jakim jest ono skojarzone, a więc za pomocą typu string. Spowoduje to oczyszczenie bufora strumienia, dzięki czemu program przestaje “przeskakiwać” kolejne cin‘y. Otrzymany w ten sposób ciąg znakowy należy spróbować przekształcić w oczekiwany typ wartości za pomocą jednej z dostępnych metod. Przykładowo można dokonać to zgodnie z poniższym kodem:

#include<iostream>
#include<iomanip> // formatowanie wyjścia poprzez cin
#include<string> // biblioteka wymagana do obsługi konwersji string'ów
using namespace std;
int main(){		
	int i, j;
	string zd;	
	do {
		cout << "Podaj wartość liczbową \"i\": ";
		getline(cin,zd);
		i=atoi(zd.c_str());
	} while (i==0);
	do {
		cout << "Podaj wartość liczbową \"j\": ";
		getline(cin,zd);
		j=atoi(zd.c_str());
	} while (j==0);		
cout << "Wartość ciągu odczytanego z klawiatury to: " << setw(6) << right << i <<endl;
cout << "Wartość ciągu odczytanego z klawiatury to: " << setw(6) << right << j <<endl;
return 0;
    }

Jak teraz zachowuje się w testach powyższy program? Jak widać wymusza wprowadzenie poprawnych wartości, co najwyżej przyjmuje liczby zmiennoprzecinkowe jako całkowite odcinając ich część od przecinka. Wadą tego rozwiązania jest fakt, że metoda std::atoi (wymaga biblioteki string) nie zwraca błędu w postaci pozwalającej na osobne obsłużenie go. Błąd sygnalizowany jest wartością 0, wobec czego w powyższej formie kodu programu nie jest możliwe wprowadzenie wartości 0, gdyż jest to traktowane jako błąd i program żąda podania innej wartości.

Innym sposobem zastosowania konwersji jest użycie metody std::stoi (wymaga biblioteki string), która to zgłasza błąd w postaci tzw. wyjątku (ang: throw exception). Daje to możliwość sukcesu w celu określenia prawidłowej konwersji poprzez obsłużenie wyjątków za pomocą klauzuli try-catch, co zostało przedstawione w poniższym przykładzie:

#include<iostream>
#include <stdexcept>  //biblioteka wymagana dla obsługi wyjątków
#include<iomanip>
#include<string>  // biblioteka wymagana do obsługi konwersji string'ów
using namespace std;
int main(){
	int i;
	string zd;	
	bool inBlad;

do {
	inBlad=false;
	cout << "Podaj wartość liczbową \"i\": ";
	try
	{
	  getline(cin,zd);
	  i=stoi(zd.c_str());
	}
	catch (invalid_argument const &e)
	{
	  std::cerr << "Nieprawidłowa wartość!!! \n";
	  inBlad=true;
	}
	catch (std::out_of_range const &e)
	{
	  cerr << "Przekroczenie dopuszczalnej wartości!!! \n";
	  inBlad=true;
	}
		} while (inBlad);	
		
std::cout << "Wartość ciągu odczytanego z klawiatury to: " << std::setw(6) << std::right << i <<std::endl;
return 0;
    }

Powyższy kod także ma pewną wadę, pomimo znaczącej poprawy co do kontroli nad wprowadzanymi danymi, próba wprowadzenia ciągu znakowego np: ” 463ab.9esd” nie spowoduje wygenerowania błędu. Dzieje się to z powodu takiego, iż początkowe “białe znaki” są usuwane, a następujące po nich znaki są cyframi i metoda je interpretuje jako poprawne aż do momentu wystąpienia pierwszego błędu, gdzie kończy się konwersja, a pozostałe znaki są odrzucane. Możliwa jest akceptacja takiego rozwiązania, ale nadal cechuje się ona niską elegancją.

Kolejną metodą jest wykorzystanie podejścia strumieniowego string stream (wymaga biblioteki sstream), która działa podobnie do poprzednich przykładów, jest też w pełni zgodna z podejściem strumieniowym tak mocno akcentowanym w C++. W przypadku błędu zwraca ona wartości: INT_MAX, INT_MIN, lub jeśli łańcuch znakowy nie może być zinterpretowany jako liczba zwraca wartość 0.

do {
  std::cout << "Podaj wartość liczbową: \"j\" ";
  getline(cin,zd);
   } while (0==std::istringstream(zd) >> j);
std::cout << "Wartość ciągu odczytanego z klawiatury to: " << std::setw(6) << std::right << j <<std::endl;

Następna metoda to wywodząca się z C procedura sscanf(), która dokonuje interpretacji tablicy znaków celem przekształcenia ich w oczekiwany typ wynikowy (patrz parametry komendy printf()), zgodnie z poniższym przykładem:

#include <cstdio>
#include <cstdlib>
#include <string>

int main() {
	std::string zd;
	int i;
	char ch[250];
	
	do {
		puts("Podaj wartość liczbową \"i\"");
		gets(ch);	
	} while (sscanf(ch,"%d",&i)!=1);	
	
	printf("Wartość ciągu odczytanego z klawiatury to: %6d \n\r",i);
	
	return 0;
 }

UWAGA: Analizując powyższy przykład należy zauważyć, że wczytywane znaki umieszczane są w tablicy ch, której wielkość została ustawiona na 250 znaków. Procedura gets() pobierając znaki z standardowego wejścia nie kontroluje pojemności bufora, co spowoduje, że przy pobraniu większej ilości znaków niż zadeklarowana ilość nastąpi przepełnienie bufora i nadpisanie pamięci z nieprzewidywalnym skutkiem. Dlatego też jest to metoda niebezpieczna dla programu i została usunięta z C++ od standardu C++11!!!

Wszystkie powyżej wymienione metody cechują się tym samym szczegółem – jeśli początek ciągu znakowego da się zinterpretować jako liczbę, to tak zostanie uczynione. Konwersje przerywa pierwszy napotkany znak nie dający się przekształcić na notację liczbową.

Wszystkie przykłady oparte były o konwersję z typu string do typu int. Należy pamiętać, że w bibliotece stdlib znajdują się funkcje umożliwiające konwersje do innych typów danych.

Innym sposobem dokonania poprawnej konwersji ciągu znakowego do zadanego typu jest leksykograficzna analiza ciągu zgodnie z poniższym przykładem:

#include <iostream>

int main()
{
	std::string s;
	bool inBlad;
	int liczba = 0;
do {
	inBlad=false;
	std::cout << "Podaj wartość liczbową \"i\": ";
	getline(std::cin,s);
	for (char c: s)
	{
		if (c >= '0' && c <= '9') {
			liczba = liczba * 10 + (c - '0');
		}
		else {
			inBlad=true;
			std::cerr << "Nieprawidłowa wartość!!! \n";
			liczba=0;
			break;
		}
	} 
} while (inBlad || s=="");

std::cout << liczba << std::endl;
return 0;
}

Co ciekawe, przykład powyższy jest jedynym do tej pory poznanym i działającym sposobem, aby w pełni wymusić wpisywanie poprawnych wartości. Próba wpisania jakiegokolwiek niedozwolonego znaku na którejkolwiek pozycji ciągu wymusi ponowne żądanie uzupełnienia danych. Pomijając fakt prostoty rozwiązania na tzw. “chłopski rozum”, to program działa bardzo skutecznie i niezawodnie. Podobnym rozwiązaniem cechuje się użycie boost::lexical_cast dostępnej dla C++ od wersji C++11.

Możliwie przydatną funkcją jest pobranie znaku z konsoli, tak jak poniżej – wymaga naciśnięcia Enter, po wprowadzeniu znaku. Takim sposobem jest metoda getchar() z biblioteki cstdio.

#include <iostream>
#include <cstdio>

int main()
{
	char zn;
	do
	{ 
		zn=getchar();;
		system("cls");		
		printf("Klawisz o kodzie: %d",zn);
	} while (zn!=27);   //Klawisz Esc kończy działanie programu.
	return 0;
}

Wszystkie powyższe metody operują na pobieraniu ciągu znakowego zakończonego klawiszem Enter. Często jednakże przydatną funkcją byłaby możliwość reakcji na wciśnięty klawisz (np. dla wyboru opcji). Taką funkcjonalność posiada np. funkcja getch() z biblioteki conio.h, a używa się jej analogicznie z poniższym przykładem.

#include <iostream>
#include <conio.h>

int main()
{
	char zn;
	do
	{ 
		zn=getch();
		system("cls");		
		printf("Klawisz o kodzie: %d",zn);
	} while (zn!=27);   //Klawisz Esc kończy działanie programu.
	return 0;
}

Podsumowując, zostało powyżej przedstawionych kilka sposobów odczytu danych z klawiatury z omówieniem ich możliwości oraz niedoskonałości, a także ze wskazaniem wynikających z nich zagrożeń. Opracowanie nie wyczerpuje wszystkich sposobów, ale stanowi podstawę do wielu standardowych podejść.

Zadanie do samodzielnego wykonania: Przećwicz zagadnienia związane z odczytem wartości z klawiatury. Przygotuj programy, które będą dokonywać bezpiecznego odczytu danych o różnych typach wprowadzanych z klawiatury w postaci ciągów znakowych. Zapoznaj się z kontrolą zakresów, obsługą wyjątków. Przemyśl podane rozwiązania i wybierz sposób, który jest najbliższy Tobie.