C, PHP, VB, .NET

Дневникът на Филип Петров


* Проблеми свързани с използването на scanf

Публикувано на 25 октомври 2008 в раздел С/С++.

Както вече се запознахте scanf е удобна функция за четене на различни типове данни от клавиатурата. Всеки, който я е използвал активно обаче се е сблъсквал с един много неприятен проблем – невалидно подадени данни от клавиатурата. Например:

	int number;
	printf("I will check if your numbers are odd or even. Type 0 to exit.\n\n");
	do{
		printf("type a number");
		scanf("%d", &number);

		if (number != 0){
			if (number%2 == 0) printf("Even!\n");
			else printf("Odd!\n");
		}

	} while (number != 0);

На пръв поглед програмата върви нормално. Въвеждаме числа от клавиатурата и програмата проверява дали са четни или нечетни. Когато въведем 0 програмата ще спре. Какво ще се случи обаче ако не въведем число, а например буквата ‘a’? Отговорът, е че програмата ще „зацикли“ и ще започне да изкарва текст на екрана до безкрайност (т.е. до натискане на CTRL+C). Определено не е това, което сме очаквали.

За да обясним защо се получава така ще трябва да преминем стъпка по стъпка. Когато се стартира програмата влизаме в цикъла и чакаме потребителят да въведе число от стандартния вход. Програмата спира и чака потребителят да въведе нещо от клавиатурата и да натисне Enter. След като сме въвели буквата ‘a’, scanf я прочита като първи символ от входа и я сравнява с това, което му е зададено като маска (%d, т.е. digit). Като резултат вместо да върне грешка програмата ще продължи. Ще се направят сравненията с if, въпреки че в променливата няма записана смислена стойност, след което отново ще се върнем на мястото, където от потребителя се иска да въведе число, т.е. извикваме scanf втори път. Тук scanf няма да блокира програмата и да чака вие да въведете число, а ще вземе стария вход (буквата ‘a’), защото тя не е „почистена“ от буфера. Така се получава безкраен цикъл. Оказва се, че при невалидни входни данни входния буфер никога не се изтрива.

Остава отворен въпроса как да се справим с този проблем. Много хора препоръчват да не се използва scanf въобще, а вместо това да четем символни низове и след това сами да ги превръщаме в числа и да правим съответните проверки. Този метод не е много ефикасен откъм бързодействие и заемана памет. Правилният подход е да почистваме буфера преди четене на числа. За целта ще напишем функция, към която подаваме указател към променливата от тип int и която почиства буфера и използва scanf, за да прочете числото. Функцията ще връща 1 ако сме въвели число, ще връща 0 и ще почиства буфера ако не е число и ще върне 2 в специален случай, в който имаме „неочакван край“ на входните данни:

	int getdigit(int *input){
		fflush(stdout);
		int n = scanf("%d%*[^\n]", input);

		if (n == 1) return 1;
		else if (n != EOF) {
			clearerr(stdin);
			scanf("%*[^\n]");
			return 0;
		}
		else {
			return 2;
		}
	}

Сега можем да модифицираме нашата програма по следния начит:

	int number;
	printf("I will check if your numbers are odd or even. Type 0 to exit.\n\n");
	do{
		printf("type a number");
		int n = getdigit(&number);
		switch(n){
		case 1: if (number != 0){
				if (number%2 == 0) printf("Even!\n");
				else printf("Odd!\n");
			}
			break;
		case 0: printf("Invalid input. Please enter NUMBER!\n");
			break;
		default:printf("Very wrong input! I will stop main!\n");
			return 0;
		}
	} while (number != 0);

Абсолютно същата „тактика“ може да се използва при четене на други типове данни. Опитайте се да реализирате такива функции.

П.П. Scanf има други, много по-сериозни проблеми свързани с buffer overflow, но те са извън обхвата на текущата статия.

 



Добави коментар

Адресът на електронната поща няма да се публикува


*