Що я можу використовувати для перетворення вводу замість scanf?


125

Я дуже часто бачив, як люди відштовхують інших від використання scanfта кажуть, що є кращі альтернативи. Однак я бачу або "не використовувати scanf", або "ось правильний рядок формату" , і ніколи не згадуються приклади "кращих альтернатив" .

Наприклад, візьмемо цей фрагмент коду:

scanf("%c", &c);

Це зчитує пробіл, який залишився у вхідному потоці після останнього перетворення. Зазвичай запропонованим рішенням цього є:

scanf(" %c", &c);

або не використовувати scanf.

Оскільки scanfце погано, які варіанти ANSI C для перетворення вхідних форматів, які scanfзазвичай можуть обробляти (такі як цілі числа, числа з плаваючою комою та рядки) без використання scanf?

Відповіді:


87

Найпоширеніші способи читання введення:

  • використання fgetsз фіксованим розміром, що зазвичай пропонується, і

  • використання fgetc, що може бути корисно, якщо ви читаєте лише одну char.

Для перетворення вхідних даних, ви можете використовувати різні функції:

  • strtoll, для перетворення рядка в ціле число

  • strtof// d/ ld, щоб перетворити рядок у число з плаваючою комою

  • sscanf, що не так вже й погано, як просто використання scanf, хоча в ньому є більшість падінь, згаданих нижче

  • Немає хороших способів проаналізувати розділений роздільником вхід в простому ANSI C. Будь-яке використання strtok_rз POSIX або strtok, що не є безпечним для потоків. Ви також можете згорнути свій власний варіант, захищений від потоку, використовуючи strcspnта strspn, оскільки strtok_rце не передбачає жодної спеціальної підтримки ОС.

  • Це може бути зайвим, але ви можете використовувати лексери та аналізатори ( flexі bisonце найпоширеніші приклади).

  • Перетворення немає, просто використовуйте рядок


Оскільки я не вдавався в точні питання, чому scanf це погано в моєму питанні, я докладно поясню :

  • З специфікаторами перетворення %[...]і %c, scanfне з'їдає пробіл. Це, мабуть, не широко відомо, про що свідчать багато дублів цього питання .

  • Існує деяка плутанина щодо того, коли використовувати унарний &оператор при посиланні на scanfаргументи 's (зокрема з рядками).

  • Ігнорувати повернене значення від дуже просто scanf. Це може легко викликати невизначену поведінку від читання неініціалізованої змінної.

  • Забути це дуже просто, щоб запобігти переповненню буфера scanf. scanf("%s", str)так само погано, як, якщо не гірше, ніж gets.

  • Ви не можете виявити переповнення при перетворенні цілих чисел у scanf. Насправді переповнення викликає невизначеність поведінки в цих функціях.



56

Чому scanfпогано?

Основна проблема полягає в тому, що scanfніколи не мали на меті мати справу з введенням користувача. Він призначений для використання з "ідеально" відформатованими даними. Я цитував слово "ідеально", тому що це не зовсім правда. Але він не призначений для аналізу даних, настільки ж ненадійних, як і введення користувача. За своєю природою введення користувачів не передбачувано. Користувачі неправильно розуміють інструкції, роблять помилки, випадково натискають клавішу введення, перш ніж вони будуть виконані тощо. Можна з розумом запитати, чому з нього читається функція, яку не слід використовувати для введення користувача stdin. Якщо ви досвідчений користувач * nix, пояснення не стане несподіванкою, але може заплутати користувачів Windows. У * nix системах дуже часто побудувати програми, які працюють через трубопроводи,stdoutstdinдругого. Таким чином, ви можете переконатися, що вихід і вхід передбачувані. За цих обставин scanfнасправді працює добре. Але працюючи з непередбачуваним вкладом, ви ризикуєте різними проблемами.

То чому б не було простих у використанні стандартних функцій для введення користувачем? Тут можна лише здогадуватися, але я припускаю, що старі хардкорні хакери C просто вважали, що існуючі функції досить хороші, хоча вони дуже незграбні. Крім того, коли ви дивитесь на типові термінальні програми, вони дуже рідко читають введення користувачів з stdin. Найчастіше ви передаєте весь вхід користувача як аргументи командного рядка. Звичайно, є винятки, але для більшості програм введення користувача - дуже незначна річ.

Отже, що ти можеш зробити?

Моя улюблена fgetsв поєднанні з sscanf. Я колись написав відповідь на це, але повний код повторно опублікую. Ось приклад із гідною (але не досконалою) перевіркою помилок та їх розбором. Це досить добре для налагодження.

Примітка

Мені особливо не подобається просити користувача ввести дві різні речі в один рядок. Я роблю це лише тоді, коли вони природним чином належать один одному. Як, наприклад, printf("Enter the price in the format <dollars>.<cent>: ")а потім використовувати sscanf(buffer "%d.%d", &dollar, &cent). Я б ніколи не робив чогось подібного printf("Enter height and base of the triangle: "). Основним моментом використання fgetsнижче є інкапсуляція входів, щоб переконатися, що один вхід не впливає на наступний.

#define bsize 100

void error_function(const char *buffer, int no_conversions) {
        fprintf(stderr, "An error occurred. You entered:\n%s\n", buffer);
        fprintf(stderr, "%d successful conversions", no_conversions);
        exit(EXIT_FAILURE);
}

char c, buffer[bsize];
int x,y;
float f, g;
int r;

printf("Enter two integers: ");
fflush(stdout); // Make sure that the printf is executed before reading
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) error_function(buffer, r);

// Unless the input buffer was to small we can be sure that stdin is empty
// when we come here.
printf("Enter two floats: ");
fflush(stdout);
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) error_function(buffer, r);

// Reading single characters can be especially tricky if the input buffer
// is not emptied before. But since we're using fgets, we're safe.
printf("Enter a char: ");
fflush(stdout);
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%c", &c)) != 1) error_function(buffer, r);

printf("You entered %d %d %f %c\n", x, y, f, c);

Якщо ви робите багато з них, я можу порекомендувати створити обгортку, яка завжди розмивається:

int printfflush (const char *format, ...)
{
   va_list arg;
   int done;
   va_start (arg, format);
   done = vfprintf (stdout, format, arg);
   fflush(stdout);
   va_end (arg);
   return done;
}```

Якщо це зробити, ви усунете загальну проблему, яка полягає у зворотному новому рядку, який може зіпсуватись із введенням гнізда. Але у нього є ще одне питання, яке полягає в тому випадку, якщо лінія довша за bsize. Ви можете перевірити це за допомогою if(buffer[strlen(buffer)-1] != '\n'). Якщо ви хочете видалити новий рядок, ви можете зробити це за допомогою buffer[strcspn(buffer, "\n")] = 0.

Загалом, я б радив не сподіватися, що користувач введе введення в якомусь дивному форматі, який слід розібрати в різних змінних. Якщо ви хочете призначити змінні heightі widthне вимагайте обох одночасно. Дозвольте користувачеві натиснути клавішу введення між ними. Також такий підхід в одному сенсі дуже природний. Ви ніколи не отримаєте вхід, stdinпоки не натиснете Enter, то чому б не завжди прочитати весь рядок? Звичайно, це все ще може спричинити проблеми, якщо рядок довший за буфер. Чи пам’ятав я згадати, що введення користувача невміло в С? :)

Щоб уникнути проблем з рядками, що перевищують буфер, ви можете використовувати функцію, яка автоматично виділяє буфер відповідного розміру getline(). Недолік полягає в тому, що згодом вам буде потрібно freeрезультат.

Посилення гри

Якщо ви серйозно ставитесь до створення програм на С із введенням користувача, рекомендую ознайомитись із бібліотекою ncurses. Тому що, ймовірно, ви також хочете створити програми з деякою термінальною графікою. На жаль, ви втратите деяку портативність, якщо це зробите, але це дає набагато кращий контроль над введенням користувача. Наприклад, це дає можливість миттєво читати натискання клавіші, а не чекати, коли користувач натисне клавішу Enter.


Зверніть увагу, що (r = sscanf("1 2 junk", "%d%d", &x, &y)) != 2не визначає, як поганий текст, що не вводиться цифрами.
chux

1
@chux Виправлено% f% f. Що ви маєте на увазі під першим?
клотт

З fgets()про "1 2 junk", if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) {не повідомляє нічого поганого введення , навіть якщо у нього є «сміття».
chux

@chux Ах, тепер я бачу. Добре, що це було навмисно.
клотт

1
scanfпризначений для використання з ідеально відформатованими даними, але навіть це неправда. Окрім проблеми із "мотлохом", про яку згадував @chux, є також той факт, що такий формат, як із "%d %d %d"задоволенням, читає введення з одного, двох або трьох рядків (або навіть більше, якщо є втручаються порожні рядки), що немає спосіб форсувати (скажімо) дворядковий ввід, роблячи щось на зразок "%d\n%d %d"тощо, scanfможе бути доречним для форматованого введення потоку , але це зовсім не добре для будь-якого рядкового набору.
Стів Саміт

18

scanfце дивовижно, коли ви знаєте, що ваш внесок завжди добре структурований і добре поводиться. Інакше ...

IMO, ось найбільші проблеми з scanf:

  • Ризик переповнення буфера - якщо ви не вказали ширину поля для специфікаторів перетворення %sта %[конверсії, ви ризикуєте переповнювати буфер (намагаючись прочитати більше вхідних даних, ніж розмір буфера для утримання). На жаль, немає жодного хорошого способу вказати це як аргумент (як з printf) - вам доведеться або твердо кодувати його як частину специфікатора перетворення, або робити якісь макросигнали.

  • Приймає вхідні дані, які слід відхилити. Якщо ви читаєте дані зі %dспецифікатором конверсії і ви вводите щось на кшталт 12w4, ви очікуєте scanf відхилити цей вхід, але це не так - він успішно перетворює та призначає 12, залишаючи w4в потоці введення щоб порушити наступне прочитане.

Отже, що слід використовувати замість цього?

Зазвичай я рекомендую читати весь інтерактивний ввід у вигляді тексту fgets- це дозволяє вказати максимальну кількість символів для читання за один раз, щоб ви могли легко запобігти переповненню буфера:

char input[100];
if ( !fgets( input, sizeof input, stdin ) )
{
  // error reading from input stream, handle as appropriate
}
else
{
  // process input buffer
}

Один придумок fgetsполягає в тому, що він буде зберігати останній рядок у буфері, якщо є місце, тож ви можете легко перевірити, чи хтось набрав більше вводу, ніж ви очікували:

char *newline = strchr( input, '\n' );
if ( !newline )
{
  // input longer than we expected
}

Як ви вирішите це з вами, ви вирішите - ви можете відхилити весь вхід з-під руки, і перервати будь-який інший вхід за допомогою getchar:

while ( getchar() != '\n' ) 
  ; // empty loop

Або ви можете обробити отриманий вами вклад і прочитати ще раз. Це залежить від проблеми, яку ви намагаєтеся вирішити.

Щоб токенізувати вхід (розділити його на основі одного або декількох роздільників), ви можете використовувати strtok, але будьте обережні - strtokзмінює його вхід (він замінює роздільники на строковий термінатор), і ви не можете зберегти його стан (тобто ви можете ' t частково токенізуйте одну струну, потім почніть маркувати іншу, а потім виберіть місце, де ви зупинилися в початковій рядку). Існує варіант, strtok_sякий зберігає стан токенізатора, але реалізація AFAIK не є обов'язковою (вам потрібно перевірити, що __STDC_LIB_EXT1__визначено, щоб побачити, чи він доступний).

Після того, як ви зробили токенізовану інформацію, якщо вам потрібно перетворити рядки в числа (тобто "1234"=> 1234), у вас є варіанти. strtolі strtodперетворить рядкові подання цілих чисел та дійсних чисел у їхні типи. Вони також дозволяють зрозуміти 12w4проблему, про яку я згадував вище - один з їх аргументів - це вказівник на перший символ, не перетворений у рядку:

char *text = "12w4";
char *chk;
long val;
long tmp = strtol( text, &chk, 10 );
if ( !isspace( *chk ) && *chk != 0 )
  // input is not a valid integer string, reject the entire input
else
  val = tmp;

Якщо ви не вказали ширину поля ... - або придушення перетворення (наприклад %*[%\n], що корисно для роботи з довгими рядками пізніше у відповіді).
Toby Speight

Існує спосіб отримати специфікацію ширини поля під час виконання, але це не приємно. Вам нарешті потрібно побудувати рядок формату у вашому коді (можливо, використовуючи snprintf()),.
Toby Speight

5
Ви зробили найпоширенішу помилку isspace()там - він приймає непідписані символи, представлені як int, тому вам потрібно подати заявку, unsigned charщоб уникнути UB на платформах, де charпідписано.
Toby Speight

9

У цій відповіді я припускаю, що ви читаєте та інтерпретуєте рядки тексту . Можливо, ви спонукаєте користувача, який щось набирає та натискає кнопку ВІДРАТИ. Або, можливо, ви читаєте рядки структурованого тексту з якогось файлу даних.

Оскільки ви читаєте рядки тексту, має сенс організувати свій код навколо функції бібліотеки, яка читає, ну, рядок тексту. Функція Standard є fgets(), хоча є й інші (в тому числі getline). А далі наступний крок - якось інтерпретувати цей рядок тексту.

Ось основний рецепт заклику fgetsпрочитати рядок тексту:

char line[512];
printf("type something:\n");
fgets(line, 512, stdin);
printf("you typed: %s", line);

Це просто читається в одному рядку тексту і виводить його назад. Як написано, у нього є кілька обмежень, до яких ми дістанемося за хвилину. Він також має дуже чудову особливість: це число 512, яке ми передали як другий аргумент, fgets- це розмір масиву, який lineми просимо fgetsпрочитати. Цей факт - що ми можемо сказати, fgetsскільки дозволено читати - означає, що ми можемо бути впевнені, що fgetsвін не переповнить масив, прочитавши в нього занадто багато.

Отже, тепер ми знаємо, як читати рядок тексту, але що робити, якщо ми дійсно хотіли прочитати ціле число, або число з плаваючою комою, або один символ, або одне слово? (Тобто, що робити , якщо scanfвиклик ми намагаємося поліпшити був використанням специфікатора формату , як %d, %f, %cабо %s?)

Легко переосмислити рядок тексту - рядок - як будь-який із цих речей. Для перетворення рядка в ціле число найпростіший (хоча і недосконалий) спосіб це зробити - зателефонувати atoi(). Для того, щоб перетворити в число з плаваючою точкою, є atof(). (І є й кращі способи, як ми побачимо через хвилину.) Ось дуже простий приклад:

printf("type an integer:\n");
fgets(line, 512, stdin);
int i = atoi(line);
printf("type a floating-point number:\n");
fgets(line, 512, stdin);
float f = atof(line);
printf("you typed %d and %f\n", i, f);

Якщо ви хотіли, щоб користувач ввів один символ (можливо, yабо nяк відповідь "так / ні"), ви можете буквально просто схопити перший символ рядка, наприклад:

printf("type a character:\n");
fgets(line, 512, stdin);
char c = line[0];
printf("you typed %c\n", c);

(Це, звичайно, ігнорує можливість того, що користувач набрав відповідь з кількома символами; він спокійно ігнорує будь-які додаткові символи, які були введені.)

Нарешті, якщо ви хотіли, щоб користувач вводив рядок, який точно не містить пробілів, якщо ви хочете обробити рядок введення

hello world!

як рядок, що "hello"слідує за чимось іншим (а це був би scanfформат, який %sби зробив), ну, в такому випадку я трохи перерізав, все-таки не так просто переосмислити рядок, зрештою, тому відповідь на це частина питання доведеться трохи почекати.

Але спершу я хочу повернутися до трьох речей, які я пропустив.

(1) Ми телефонували

fgets(line, 512, stdin);

читати в масив line, і де 512 є розмір масиву lineтак fgetsне знає переповнення його. Але для того, щоб переконатися, що 512 - це правильне число (особливо, щоб перевірити, чи можливо хтось налаштував програму, щоб змінити розмір), вам доведеться прочитати туди, де lineбуло оголошено. Це неприємність, тому є два набагато кращі способи синхронізації розмірів. Ви можете (a) використовувати препроцесор, щоб створити ім'я для розміру:

#define MAXLINE 512
char line[MAXLINE];
fgets(line, MAXLINE, stdin);

Або (b) використовувати sizeofоператор C :

fgets(line, sizeof(line), stdin);

(2) Друга проблема полягає в тому, що ми не перевіряли наявність помилок. Читаючи дані, ви завжди повинні перевірити можливість помилки. Якщо з будь-якої причини fgetsне вдається прочитати рядок тексту, про який ви його просили, це вказує, повертаючи нульовий покажчик. Тож ми повинні були робити такі речі

printf("type something:\n");
if(fgets(line, 512, stdin) == NULL) {
    printf("Well, never mind, then.\n");
    exit(1);
}

Нарешті, проблема полягає в тому, що для того, щоб прочитати рядок тексту, він fgetsзчитує символи та заповнює їх у свій масив, поки не знайде \nсимвол, який закінчує рядок, і він також заповнить \nсимвол у своєму масиві . Ви можете побачити це, якщо трохи змінити наш попередній приклад:

printf("you typed: \"%s\"\n", line);

Якщо я запускаю це і набираю "Стів", коли це підказує, воно виводиться на друк

you typed: "Steve
"

Це "на другому рядку - це те, що рядок, яку він читав і друкував назад, був насправді "Steve\n".

Іноді цей додатковий рядок не має значення (наприклад, коли ми телефонували atoiабо atof, оскільки вони обидва ігнорують будь-який додатковий нечисловий ввід після числа), але іноді це має велике значення. Тому часто ми хочемо зняти цей новий рядок. Є кілька способів зробити це, до якого я дістанусь за хвилину. (Я знаю, що я говорив це багато. Але я повернусь до всіх тих речей, обіцяю.)

У цей момент ви можете задуматися: "Я думав, що ви сказали, що scanf це не добре, а цей інший спосіб буде набагато кращим. Але fgetsпочинає виглядати як неприємність. Виклик scanfбув таким простим ! Чи не можу я продовжувати його використовувати? "

Звичайно, ви можете продовжувати використовувати scanf, якщо хочете. (А для справді простих речей, певним чином, це простіше.) Але, будь ласка, не плачте до мене, коли він не дає вам ладу через одну з 17 його примх і байок, або переходить у нескінченну петлю через введення вашої не сподівався, або коли ви не можете зрозуміти, як за допомогою нього зробити щось складніше. А давайте подивимось на fgetsфактичні неприємності:

  1. Ви завжди повинні вказати розмір масиву. Ну, звичайно, це зовсім неприємно - це особливість, адже переповнення буфера - це справді погана річ.

  2. Ви повинні перевірити повернене значення. Насправді це миття, тому що для scanfправильного використання ви також повинні перевірити його повернене значення.

  3. Ви повинні зняти \nспинку. Це, я визнаю, справжня неприємність. Мені б хотілося, щоб існувала стандартна функція, я можу вас вказати на те, що не мала цієї малої проблеми. (Будь ласка, ніхто не виховує gets.) Але порівняно з scanf's17 різними неприємностями, я прийму цю одну неприємність у fgetsбудь-який день.

Так як же ви видалите цей рядок? Три способи:

(a) Очевидний спосіб:

char *p = strchr(line, '\n');
if(p != NULL) *p = '\0';

(b) Хитрий та компактний спосіб:

strtok(line, "\n");

На жаль, це не завжди працює.

(c) Ще один компактний і м'яко незрозумілий спосіб:

line[strcspn(line, "\n")] = '\0';

І тепер, коли це не в змозі, ми можемо повернутися до іншої речі, яку я пропустив: недосконалості atoi()та atof(). Проблема в тому, що вони не дають вам жодної корисної вказівки на успіх чи невдачу: вони спокійно ігнорують проміжок нечислового введення, і вони спокійно повертають 0, якщо числового введення взагалі немає. Кращі альтернативи - які також мають певні інші переваги - є strtolі strtod. strtolтакож дозволяє використовувати базу, відмінну від 10, тобто ви можете отримати ефект (серед іншого) %oабо за %xдопомогоюscanf. Але показати, як правильно використовувати ці функції, - це історія сама по собі, і це було б занадто відволіканням від того, що вже перетворюється на досить фрагментарну розповідь, тому я нічого більше про них зараз не збираюся говорити.

Решта основних розповідей стосується введення, яке ви, можливо, намагаєтеся розібрати, що є складнішим, ніж просто одне число чи символ. Що робити, якщо ви хочете прочитати рядок, що містить два числа, або кілька розділених пробілами слів, або конкретні обрамлювальні знаки? Ось де речі стають цікавими, і де, ймовірно, ускладнюються речі, якщо ви намагалися робити речі за допомогою scanf, і де набагато більше варіантів тепер, коли ви чисто читали один рядок тексту, використовуючи fgets, хоча повний розповідь про всі ці варіанти Можливо, можна було б заповнити книгу, тож ми лише зможемо тут подряпати поверхню.

  1. Моя улюблена техніка - розбити рядок на "слова", розділені пробілом, а потім зробити щось далі з кожним "словом". Однією з основних стандартних функцій для цього є strtok(яка також має свої проблеми, і яка також оцінює цілу окрему дискусію). Моє власне уподобання - це виділена функція для побудови масиву покажчиків до кожного розбитого "слова", функції, яку я описую в цих конспектах курсу . У будь-якому випадку, щойно ви отримаєте "слова", ви можете додатково обробити кожне з них, можливо, з тими ж atoi/ atof/ strtol/ strtod функціями, які ми вже розглянули.

  2. Як не парадоксально, хоча ми витратили тут чимало часу і зусиль, з'ясовуючи, як відійти від цього scanf, ще один прекрасний спосіб поводження з рядком тексту, який ми щойно прочитали, fgets- це передати його sscanf. Таким чином ви отримуєте більшість переваг scanf, але без більшості недоліків.

  3. Якщо ваш синтаксис введення особливо складний, може бути доцільним використовувати бібліотеку "regexp" для його розбору.

  4. Нарешті, ви можете використовувати будь-які спеціальні рішення для аналізу. Ви можете переміщати рядок символу одночасно за допомогою char *вказівника, перевіряючи очікувані символи. Або ви можете здійснити пошук конкретних символів , використовуючи функції , такі як strchrабо strrchr, або , strspnабо strcspn, або strpbrk. Або ви можете проаналізувати / перетворити та пропустити групи знакових символів за допомогою функцій strtolабо, strtodякі ми пропустили раніше.

Очевидно, що можна сказати набагато більше, але, сподіваємось, це вступ вас почне.


Чи є вагомі причини писати, sizeof (line)а не просто sizeof line? Перший робить це схожим lineна ім'я типу!
Toby Speight

@TobySpeight Важлива причина? Ні, я сумніваюся. Дужки в дужках - моя звичка, тому що я не можу турбуватись, щоб запам'ятати, чи потрібні вони іменам об'єктів чи типів, але багато програмістів залишають їх, коли можуть. (Для мене це питання особистих уподобань та стилю, і досить незначний у цьому.)
Стів Самміт

+1 для використання sscanfв якості механізму перетворення, але збирання (і, можливо, масаж) вхідних даних за допомогою іншого інструменту. Але, можливо, варто згадати getlineв цьому контексті.
dmckee --- кошеня колишнього модератора

Коли ви говорите про " fscanfфактичні неприємності", ви маєте на увазі fgets? І неприємність №3 насправді дратує, особливо з огляду на те, що scanfповертає непотрібний вказівник на буфер, а не повертає кількість введених символів (що зробило б зняття з нового рядка набагато чистішим).
Supercat

1
Дякуємо за пояснення вашого sizeofстилю. Для мене запам’ятати, коли ти маєш паренів, дуже просто: я думаю (type), що це як акторський склад без значення (адже нас цікавить лише тип). Ще одне: ви говорите, що strtok(line, "\n")це не завжди працює, але це не очевидно, коли це не може. Я здогадуюсь, ви думаєте про випадок, коли рядок був довшим за буфер, тому у нас немає нового рядка, а strtok()повертається нульовим? Дуже шкода fgets()не повертає кориснішого значення, тому ми можемо знати, чи існує новий рядок чи ні.
Toby Speight

7

Що я можу використовувати для розбору вводу замість scanf?

Замість того scanf(some_format, ...), розгляньте fgets()сsscanf(buffer, some_format_and %n, ...)

Використовуючи " %n"код, можна просто визначити, чи вдало було проскановано весь формат і чи не було в кінці зайвих непотрібних проміжків.

// scanf("%d %f fred", &some_int, &some_float);
#define EXPECTED_LINE_MAX 100
char buffer[EXPECTED_LINE_MAX * 2];  // Suggest 2x, no real need to be stingy.

if (fgets(buffer, sizeof buffer, stdin)) {
  int n = 0;
  // add ------------->    " %n" 
  sscanf(buffer, "%d %f fred %n", &some_int, &some_float, &n);
  // Did scan complete, and to the end?
  if (n > 0 && buffer[n] == '\0') {
    // success, use `some_int, some_float`
  } else {
    ; // Report bad input and handle desired.
  }

6

Зазначимо вимоги розбору як:

  • дійсний вхід повинен бути прийнятий (і перетворений в іншу форму)

  • неправильний ввід повинен бути відхилений

  • коли будь-який вхід відхилений, необхідно надати користувачеві описове повідомлення, яке пояснює (чітко "легко зрозуміла нормальним людям, яка не є програмістами"), чому його було відхилено (щоб люди могли зрозуміти, як виправити виправлення проблема)

Щоб зробити речі дуже простими, давайте розглянемо розбір одного простого десяткового цілого числа (яке було введено користувачем) та нічого іншого. Можливими причинами відхилення введення користувача є:

  • вхід містив неприйнятні символи
  • вхід представляє число, яке нижче прийнятого мінімуму
  • вхід представляє число, яке перевищує прийнятий максимум
  • вхід представляє число, яке має ненульову дробову частину

Давайте також визначимо належним чином "вхідні дані, що містять неприйнятні символи"; і скажіть, що:

  • провідна пробіл та пробіли пробілів будуть ігноровані (наприклад, "
    5" буде розглядатися як "5")
  • нульовий або один десятковий знак дозволено (наприклад, "1234." та "1234.000" обидва трактуються так само, як "1234")
  • має бути принаймні одна цифра (наприклад, "." відхилено)
  • не більше однієї десяткової коми дозволено (наприклад, "1.2.3" відхилено)
  • коми, які не знаходяться між цифрами, будуть відхилені (наприклад, "1234" відхилено)
  • коми, що знаходяться після десяткової крапки, будуть відхилені (наприклад, "1234 000 000" відхилено)
  • коми, що знаходяться після іншої коми, відхиляються (наприклад, "1,, 234" відхиляється)
  • всі інші коми будуть ігноровані (наприклад, "1,234" буде трактуватися як "1234")
  • знак мінус, який не є першим символом, який не є пробілом, відхиляється
  • позитивний знак, який є не першим символом, який не є пробілом, відхиляється

З цього можна визначити, що потрібні наступні повідомлення про помилки:

  • "Невідомий символ на початку введення"
  • "Невідомий символ в кінці введення"
  • "Невідомий символ посеред вводу"
  • "Кількість занизька (мінімум - ....)"
  • "Кількість зависока (максимум ...)"
  • "Число не ціле число"
  • "Занадто багато десяткових знаків"
  • "Без десяткових цифр"
  • "Неправильна кома на початку номера"
  • "Погана кома в кінці числа"
  • "Погана кома посеред числа"
  • "Неправильна кома після десяткової коми"

З цього моменту ми бачимо, що підходяща функція для перетворення рядка в ціле число повинна відрізняти дуже різні типи помилок; і що щось на кшталт " scanf()" або " atoi()" або " strtoll()" є абсолютно і абсолютно нікчемним, оскільки вони не дають тобі жодної вказівки на те, що було не так з введенням (і використовують абсолютно нерелевантне та невідповідне визначення того, що є / що не є "дійсним вхід ").

Натомість давайте почнемо писати щось не марне:

char *convertStringToInteger(int *outValue, char *string, int minValue, int maxValue) {
    return "Code not implemented yet!";
}

int main(int argc, char *argv[]) {
    char *errorString;
    int value;

    if(argc < 2) {
        printf("ERROR: No command line argument.\n");
        return EXIT_FAILURE;
    }
    errorString = convertStringToInteger(&value, argv[1], -10, 2000);
    if(errorString != NULL) {
        printf("ERROR: %s\n", errorString);
        return EXIT_FAILURE;
    }
    printf("SUCCESS: Your number is %d\n", value);
    return EXIT_SUCCESS;
}

Для задоволення заявлених вимог; ця convertStringToInteger()функція, ймовірно, може скластися кілька сотень рядків коду сама по собі.

Тепер це було просто "розбір єдиного простого десяткового цілого числа". Уявіть, якби ви хотіли розібрати щось складне; як список структур "ім'я, адреса, номер телефону, адреса електронної пошти"; а може, як мова програмування. У цих випадках вам може знадобитися написати тисячі рядків коду, щоб створити синтаксичний аналіз, який не є калікою.

Іншими словами...

Що я можу використовувати для розбору вводу замість scanf?

Напишіть (потенційно тисячі рядків) коду самостійно, щоб відповідати вашим вимогам.


5

Ось приклад використання flexдля сканування простого вводу, у цьому випадку файлу чисел з плаваючою точкою ASCII, які можуть бути або в американському ( n,nnn.dd), або в європейському ( n.nnn,dd) форматах. Це просто скопійовано із значно більшої програми, тому можуть бути деякі невирішені посилання:

/* This scanner reads a file of numbers, expecting one number per line.  It  */
/* allows for the use of European-style comma as decimal point.              */

%{
  #include <stdlib.h>
  #include <stdio.h>
  #include <string.h>
  #ifdef WINDOWS
    #include <io.h>
  #endif
  #include "Point.h"

  #define YY_NO_UNPUT
  #define YY_DECL int f_lex (double *val)

  double atofEuro (char *);
%}

%option prefix="f_"
%option nounput
%option noinput

EURONUM [-+]?[0-9]*[,]?[0-9]+([eE][+-]?[0-9]+)?
NUMBER  [-+]?[0-9]*[\.]?[0-9]+([eE][+-]?[0-9]+)?
WS      [ \t\x0d]

%%

[!@#%&*/].*\n

^{WS}*{EURONUM}{WS}*  { *val = atofEuro (yytext); return (1); }
^{WS}*{NUMBER}{WS}*   { *val = atof (yytext); return (1); }

[\n]
.


%%

/*------------------------------------------------------------------------*/

int scan_f (FILE *in, double *vals, int max)
{
  double *val;
  int npts, rc;

  f_in = in;
  val  = vals;
  npts = 0;
  while (npts < max)
  {
    rc = f_lex (val);

    if (rc == 0)
      break;
    npts++;
    val++;
  }

  return (npts);
}

/*------------------------------------------------------------------------*/

int f_wrap ()
{
  return (1);
}

-5

Інші відповіді дають правильні деталі низького рівня, тому я обмежуся вищим рівнем: Спочатку проаналізуйте, як ви очікуєте, щоб виглядав кожен рядок введення. Спробуйте описати введення формальним синтаксисом - якщо пощастить, ви виявите, що його можна описати, використовуючи звичайну граматику або хоча б без контекстну граматику . Якщо звичайної граматики достатньо, то ви можете кодувати машину з кінцевим станомяка розпізнає та інтерпретує кожен командний рядок по одному символу. Потім ваш код прочитає рядок (як пояснено в інших відповідях), а потім сканує символи в буфері через стан-машину. У певних станах ви зупиняєте та перетворюєте відскановану до цього часу підрядку в число чи будь-яке інше. Ви, ймовірно, можете "закатати своє", якщо це просто; якщо вам здається, що вам потрібна повна без контексту граматика, вам краще розібратися, як використовувати існуючі інструменти розбору (re: lexта yaccабо їх варіанти).


Машина з кінцевим станом може бути надмірна; можливі простіші способи виявлення переповнення в конверсіях (наприклад, перевірка того, чи є errno == EOVERFLOWпісля використання strtoll).
СС Енн

1
Чому б ви кодували свою власну машину з кінцевим станом, коли flex робить їх написання тривіально простим?
jamesqf
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.