Как разрешить ввод пробелов с помощью scanf?


129

Используя следующий код:

char *name = malloc(sizeof(char) + 256); 

printf("What is your name? ");
scanf("%s", name);

printf("Hello %s. Nice to meet you.\n", name);

Пользователь может ввести свое имя, но когда он вводит имя с пробелом, как Lucas Aardvark, то scanf()просто обрезает все после Lucas. Как сделать scanf()разрешенные пробелы


9
Обратите внимание, что более идиоматическим является 'malloc (sizeof (char) * 256 + 1)', 'malloc (256 + 1)' или даже лучше (при условии, что 'name' будет использоваться строго локально) 'char name [256 + 1 ]». «+1» может действовать как мнемоника для нулевого терминатора, который необходимо включить в распределение.
Барри Келли

@Barry - я подозреваю, что sizeof(char) + 256это опечатка.
Крис Лутц,

Ответы:


186

Людям (и особенно новичкам) никогда не следует использовать scanf("%s")или gets()или любые другие функции, которые не имеют защиты от переполнения буфера, если вы не знаете наверняка, что ввод всегда будет иметь определенный формат (и, возможно, даже тогда).

Помните, что чем scanfозначает «отформатировано отсканированное изображение», и есть гораздо менее отформатированные данные, чем данные, введенные пользователем. Это идеально, если вы полностью контролируете формат входных данных, но обычно не подходит для ввода пользователем.

Используйте fgets()(который имеет защиту от переполнения буфера), чтобы получить ввод в строку и sscanf()оценить ее. Поскольку вам просто нужно то, что пользователь ввел без синтаксического анализа, sscanf()в этом случае вам все равно не нужно :

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

/* Maximum name size + 1. */

#define MAX_NAME_SZ 256

int main(int argC, char *argV[]) {
    /* Allocate memory and check if okay. */

    char *name = malloc(MAX_NAME_SZ);
    if (name == NULL) {
        printf("No memory\n");
        return 1;
    }

    /* Ask user for name. */

    printf("What is your name? ");

    /* Get the name, with size limit. */

    fgets(name, MAX_NAME_SZ, stdin);

    /* Remove trailing newline, if there. */

    if ((strlen(name) > 0) && (name[strlen (name) - 1] == '\n'))
        name[strlen (name) - 1] = '\0';

    /* Say hello. */

    printf("Hello %s. Nice to meet you.\n", name);

    /* Free memory and exit. */

    free (name);
    return 0;
}

1
Я не знал об этом fgets(). Тогда это действительно выглядит проще в использовании scanf(). +1
Креднс 08

7
Если вы просто хотите получить строку от пользователя, это проще. Это также безопаснее, так как вы можете избежать переполнения буфера. Семейство scanf действительно полезно для преобразования строки в разные вещи (например, четыре символа и int, например, с "% c% c% c% c% d"), но даже тогда вы должны использовать fgets и sscanf, а не scanf, чтобы избежать переполнения буфера.
paxdiablo

4
Вы можете указать максимальный размер буфера в формате scanf, вы просто не можете поместить его, вычисляемый во время выполнения, без построения формата во время выполнения (нет эквивалента * для printf, * является допустимым модификатором для scanf с другим поведением: подавление присвоения ).
AProgrammer

Также обратите внимание, что scanfимеет неопределенное поведение при переполнении числового преобразования ( N1570 7.21.6.2p10 , последнее предложение, формулировка не изменилась с C89), что означает, что ни одна из scanfфункций не может безопасно использоваться для числового преобразования ненадежного ввода.
zwol

@JonathanKomar и все, кто будет это читать в будущем: если ваш профессор сказал вам, что вы должны использовать scanfэто в задании, они поступили неправильно, и вы можете сказать им, что я так сказал, и если они захотят поспорить со мной по этому поводу , мой адрес электронной почты легко найти в моем профиле.
zwol

124

Пытаться

char str[11];
scanf("%10[0-9a-zA-Z ]", str);

Надеюсь, это поможет.


10
(1) Очевидно, чтобы принимать пробелы, вам нужно поместить пробел в класс символов. (2) Обратите внимание, что 10 - это максимальное количество символов, которые будут прочитаны, поэтому str должна указывать на буфер размером как минимум 11. (3) Последний s здесь не является директивой формата, но здесь scanf попытается точно сопоставить его. Эффект будет виден в записи типа 1234567890s, где s будет потребляться, но не где. Другое письмо не будет использовано. Если вы поставите другой формат после s, он будет прочитан только в том случае, если есть s, для которого нужно сопоставить.
AProgrammer

Другая потенциальная проблема, использование - в другом месте, кроме первого или последнего, определяется реализацией. Обычно он используется для диапазонов, но то, что обозначает диапазон, зависит от кодировки. EBCDIC имеет дыры в диапазонах букв, и даже если предположить, что кодировки, производные от ASCII, наивно думать, что все строчные буквы находятся в диапазоне az ...
AProgrammer

1
"% [^ \ n]" имеет ту же проблему, что и gets (), переполнение буфера. С дополнительной уловкой, что \ n final не читается; это будет скрыто тем фактом, что большинство форматов начинаются с пропуска пробелов, но [не входит в их число. Я не понимаю, как использовать scanf для чтения строк.
AProgrammer,

1
Удален символ sиз конца входной строки, поскольку в некоторых случаях он является избыточным и неверным (как указано в предыдущих комментариях). [это собственный спецификатор формата, а не его разновидность s.
paxdiablo

54

В этом примере используется инвертированный набор сканирования, поэтому scanf продолжает принимать значения, пока не встретит символ новой строки '\ n', поэтому пробелы также сохраняются.

#include <stdio.h>

int main (int argc, char const *argv[])
{
    char name[20];
    scanf("%[^\n]s",name);
    printf("%s\n", name);
    return 0;
}

1
Осторожно с переполнением буфера. Если пользователь напишет «имя» из 50 символов, возможно, произойдет сбой программы.
brunoais

3
Как вы знаете размер буфера, вы можете использовать его %20[^\n]sдля предотвращения переполнения буфера
Osvein

45 баллов, и никто не указал на очевидное наличие груза s!
Антти Хаапала

22

Вы можете использовать это

char name[20];
scanf("%20[^\n]", name);

Или это

void getText(char *message, char *variable, int size){
    printf("\n %s: ", message);
    fgets(variable, sizeof(char) * size, stdin);
    sscanf(variable, "%[^\n]", variable);
}

char name[20];
getText("Your name", name, 20);

DEMO


1
Я не тестировал, но, основываясь на других ответах на этой самой странице, я считаю, что правильный размер буфера для scanf в вашем примере будет: scanf("%19[^\n]", name);(все еще +1 для краткого ответа)
Dr Beco

1
В качестве примечания, sizeof(char)по определению всегда равно 1, поэтому нет необходимости умножать на него.
paxdiablo

8

Не используйте scanf()для чтения строк без указания ширины поля. Вам также следует проверить возвращаемые значения на наличие ошибок:

#include <stdio.h>

#define NAME_MAX    80
#define NAME_MAX_S "80"

int main(void)
{
    static char name[NAME_MAX + 1]; // + 1 because of null
    if(scanf("%" NAME_MAX_S "[^\n]", name) != 1)
    {
        fputs("io error or premature end of line\n", stderr);
        return 1;
    }

    printf("Hello %s. Nice to meet you.\n", name);
}

В качестве альтернативы используйте fgets():

#include <stdio.h>

#define NAME_MAX 80

int main(void)
{
    static char name[NAME_MAX + 2]; // + 2 because of newline and null
    if(!fgets(name, sizeof(name), stdin))
    {
        fputs("io error\n", stderr);
        return 1;
    }

    // don't print newline
    printf("Hello %.*s. Nice to meet you.\n", strlen(name) - 1, name);
}

6

Вы можете использовать эту fgets()функцию для чтения строки или использовать ее scanf("%[^\n]s",name);так, чтобы чтение строки прекращалось при обнаружении символа новой строки.



sздесь не место
Антти Хаапала

5

getline()

Тем не менее, теперь часть POSIX.

Он также решает проблему выделения буфера, о которой вы спрашивали ранее, хотя вы должны позаботиться о freeпамяти.


Стандартный? В цитируемой вами ссылке: «И getline (), и getdelim () являются расширениями GNU».
AProgrammer

1
POSIX 2008 добавляет getline. Итак, GNU пошла вперед и изменила свои заголовки для glibc около версии 2.9, и это вызывает проблемы для многих проектов. Не окончательная ссылка, но посмотрите здесь: bugzilla.redhat.com/show_bug.cgi?id=493941 . Что касается онлайн-страницы руководства, я взял первую, найденную Google.
dmckee --- котенок экс-модератора

3

Если кто-то все еще ищет, вот что у меня сработало - прочитать строку произвольной длины, включая пробелы.

Спасибо множеству постеров в Интернете за то, что они поделились этим простым и элегантным решением. Если это сработает, то заслуга принадлежит им, но все ошибки - мои.

char *name;
scanf ("%m[^\n]s",&name);
printf ("%s\n",name);

2
Стоит отметить, что это расширение POSIX, которого нет в стандарте ISO. Для полноты картины вам, вероятно, также следует проверить errnoи очистить выделенную память.
paxdiablo

sне место там после сканирования
Антти Хаапала

1

Вы можете использовать scanfдля этого небольшую хитрость. Фактически, вы должны разрешить ввод данных, пока пользователь не нажмет Enter ( \n). Это будет учитывать каждый символ, включая пробел . Вот пример:

int main()
{
  char string[100], c;
  int i;
  printf("Enter the string: ");
  scanf("%s", string);
  i = strlen(string);      // length of user input till first space
  do
  {
    scanf("%c", &c);
    string[i++] = c;       // reading characters after first space (including it)
  } while (c != '\n');     // until user hits Enter
  string[i - 1] = 0;       // string terminating
return 0;
}

Как это работает? Когда пользователь вводит символы из стандартного ввода, они будут храниться в строковой переменной до первого пробела. После этого остальная часть записи останется во входном потоке и будет ждать следующего сканирования. Далее у нас естьfor цикл, который берет char за char из входного потока (до \n) и добавляет их к концу строки переменной, таким образом формируя полную строку, аналогичную вводу пользователя с клавиатуры.

Надеюсь, это кому-то поможет!


Возможен переполнение буфера.
paxdiablo

0

Хотя вам действительно не следует использовать scanf()для такого рода вещей, потому что есть гораздо лучшие вызовы, такие как gets()или getline(), это можно сделать:

#include <stdio.h>

char* scan_line(char* buffer, int buffer_size);

char* scan_line(char* buffer, int buffer_size) {
   char* p = buffer;
   int count = 0;
   do {
       char c;
       scanf("%c", &c); // scan a single character
       // break on end of line, string terminating NUL, or end of file
       if (c == '\r' || c == '\n' || c == 0 || c == EOF) {
           *p = 0;
           break;
       }
       *p++ = c; // add the valid character into the buffer
   } while (count < buffer_size - 1);  // don't overrun the buffer
   // ensure the string is null terminated
   buffer[buffer_size - 1] = 0;
   return buffer;
}

#define MAX_SCAN_LENGTH 1024

int main()
{
   char s[MAX_SCAN_LENGTH];
   printf("Enter a string: ");
   scan_line(s, MAX_SCAN_LENGTH);
   printf("got: \"%s\"\n\n", s);
   return 0;
}

2
Есть причина, по которой getsон устарел и был удален ( stackoverflow.com/questions/30890696/why-gets-is-deprecated ) из стандарта. Это даже хуже , scanfпотому что, по крайней мере, у последнего есть способы сделать это безопасным.
paxdiablo

-1
/*reading string which contains spaces*/
#include<stdio.h>
int main()
{
   char *c,*p;
   scanf("%[^\n]s",c);
   p=c;                /*since after reading then pointer points to another 
                       location iam using a second pointer to store the base 
                       address*/ 
   printf("%s",p);
   return 0;
 }

4
Вы можете объяснить, почему это правильный ответ? Пожалуйста, не отправляйте ответы только с кодом.
Тео

sне место там после сканирования
Антти Хаапала
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.