Jak zezwalasz na wprowadzanie spacji za pomocą scanf?


137

Używając poniższego kodu:

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

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

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

Użytkownik może wpisać swoje imię, ale kiedy wpisuje nazwę ze spacją Lucas Aardvark, scanf()po prostu odcina wszystko Lucas. Jak zrobić scanf()zezwalanie na spacje


9
Zauważ, że bardziej idiomatyczne to „malloc (sizeof (char) * 256 + 1)” lub „malloc (256 + 1)”, a nawet lepiej (zakładając, że „name” będzie używane wyłącznie lokalnie) „char name [256 + 1] ] ”. „+1” może działać jako mneumonik dla terminatora zerowego, który należy uwzględnić w przydziale.
Barry Kelly

@Barry - podejrzewam, że sizeof(char) + 256była to literówka.
Chris Lutz,

Odpowiedzi:


199

Ludzie (a zwłaszcza początkujący) nigdy nie powinni używać scanf("%s")ani gets()żadnych innych funkcji, które nie mają ochrony przed przepełnieniem bufora, chyba że wiesz na pewno, że dane wejściowe zawsze będą w określonym formacie (a może nawet wtedy).

Pamiętaj, że scanfoznacza to „skan sformatowany” i jest niewiele mniej sformatowanych niż dane wprowadzone przez użytkownika. Jest to idealne rozwiązanie, jeśli masz całkowitą kontrolę nad formatem danych wejściowych, ale generalnie nie nadaje się do wprowadzania danych przez użytkownika.

Użyj fgets()(który ma ochronę przed przepełnieniem buforu), aby uzyskać dane wejściowe w łańcuchu i sscanf()ocenić je. Ponieważ chcesz tylko tego, co wprowadził użytkownik bez przetwarzania, tak naprawdę nie potrzebujesz sscanf()w tym przypadku:

#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
Nie wiedziałem o fgets(). W rzeczywistości wygląda to na łatwiejsze w użyciu scanf(). +1
Kredns

7
Jeśli chcesz tylko uzyskać linię od użytkownika, jest to łatwiejsze. Jest to również bezpieczniejsze, ponieważ można uniknąć przepełnienia bufora. Rodzina scanf jest naprawdę przydatna do przekształcania łańcucha znaków w różne rzeczy (takie jak cztery znaki i int, na przykład z "% c% c% c% c% d"), ale nawet wtedy powinieneś używać fgets i sscanf, a nie scanf, aby uniknąć możliwości przepełnienia bufora.
paxdiablo

4
Możesz ustawić maksymalny rozmiar bufora w formacie scanf, po prostu nie możesz umieścić tego obliczonego w czasie wykonywania bez budowania formatu w czasie wykonywania (nie ma odpowiednika * dla printf, * jest poprawnym modyfikatorem dla scanf z innym zachowaniem: pomijaniem przypisania ).
AProgrammer

Zauważ również, że scanfma niezdefiniowane zachowanie, jeśli przepełnienie konwersji numerycznej ( N1570 7.21.6.2p10 , ostatnie zdanie, sformułowanie niezmienione od C89), co oznacza, że żadna z scanffunkcji nie może być bezpiecznie używana do konwersji numerycznej niezaufanych danych wejściowych.
zwol

@JonathanKomar i wszyscy inni, którzy czytają to w przyszłości: jeśli twój profesor powiedział ci, że musisz użyć scanfw zadaniu, zrobili to w błędzie i możesz im powiedzieć, że tak powiedziałem, i jeśli chcą się ze mną spierać o to mój adres e-mail można łatwo znaleźć w moim profilu.
zwol

126

Próbować

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

Mam nadzieję, że to pomoże.


11
(1) Oczywiście, aby zaakceptować spacje, musisz umieścić spację w klasie znaków. (2) Zauważ, że 10 to maksymalna liczba znaków, które zostaną odczytane, więc str musi wskazywać przynajmniej na bufor o rozmiarze 11. (3) Końcowe s tutaj nie są dyrektywą formatu, ale scanf spróbuje tutaj dokładnie dopasować. Efekt będzie widoczny na wpisie, takim jak 1234567890s, gdzie s zostaną zużyte, ale nie zostaną umieszczone. Drugi list nie zostanie zużyty. Jeśli umieścisz inny format po s, zostanie on odczytany tylko wtedy, gdy istnieje s do dopasowania.
AProgrammer

Innym potencjalnym problemem, wykorzystania - w innym miejscu niż pierwsze lub ostatnie, jest zdefiniowanie implementacji. Zwykle jest używany dla zakresów, ale to, co wyznacza zakres, zależy od zestawu znaków. EBCDIC ma luki w zakresach liter i nawet zakładając zestawy znaków pochodzące z ASCII, naiwnością jest myślenie, że wszystkie małe litery należą do zakresu az ...
AProgrammer

1
"% [^ \ n]" ma ten sam problem co gets (), przepełnienie bufora. Z dodatkowym haczykiem, że \ n finał nie jest czytany; będzie to ukryte przez fakt, że większość formatów zaczyna się od pomijania białych znaków, ale [nie jest jednym z nich. Nie rozumiem przypadku używania scanf do odczytu ciągów.
AProgrammer

1
Usunięto znak sz końca ciągu wejściowego, ponieważ w niektórych przypadkach jest on zarówno zbędny, jak i niepoprawny (jak wskazano we wcześniejszych komentarzach). [jest to własny specyfikator formatu, a nie jakąś odmianę tego s.
paxdiablo

54

W tym przykładzie zastosowano odwrócony zestaw skanowania, więc scanf pobiera wartości, dopóki nie napotka „\ n” - nowej linii, więc spacje również zostaną zapisane

#include <stdio.h>

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

1
Ostrożnie z przepełnieniem bufora. Jeśli użytkownik wpisze „nazwę” składającą się z 50 znaków, program prawdopodobnie ulegnie awarii.
brunoais

3
Jak znasz rozmiar bufora, możesz go użyć, %20[^\n]saby zapobiec przepełnieniu bufora
osvein

45 punktów i nikt nie wskazał na oczywiste kultywowanie ładunku s!
Antti Haapala

22

Możesz tego użyć

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

Albo to

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);

PRÓBNY


1
Nie testowałem, ale na podstawie innych odpowiedzi na tej stronie, uważam, że prawidłowy rozmiar bufora dla scanf w twoim przykładzie to: scanf("%19[^\n]", name);(nadal +1 za zwięzłą odpowiedź)
DrBeco

1
Na marginesie, sizeof(char)z definicji zawsze wynosi 1, więc nie ma potrzeby mnożenia przez to.
paxdiablo

8

Nie używaj scanf()do odczytywania ciągów bez określenia szerokości pola. Powinieneś również sprawdzić zwracane wartości pod kątem błędów:

#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);
}

Alternatywnie użyj 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

Możesz użyć tej fgets()funkcji do odczytania ciągu lub użyć, scanf("%[^\n]s",name);aby odczyt ciągu zakończył się po napotkaniu znaku nowego wiersza.


Uważaj, żeby to nie zapobiegło przepełnieniu bufora
brunoais

snie należy tam
Antti Haapala

5

getline()

Teraz jednak część POSIX.

Zajmuje się również problemem alokacji bufora, o który pytałeś wcześniej, chociaż musisz zadbać o freepamięć.


Standard? W odnośniku cytujesz: "Zarówno getline (), jak i getdelim () są rozszerzeniami GNU."
AProgrammer

1
POSIX 2008 dodaje getline. Więc GNU poszło naprzód i zmieniło swoje nagłówki na glibc wokół wersji 2.9 i powoduje problemy w wielu projektach. Nie jest to ostateczny link, ale spójrz tutaj: bugzilla.redhat.com/show_bug.cgi?id=493941 . Jeśli chodzi o stronę podręcznika on-line, złapałem pierwszą znalezioną przez Google.
dmckee --- kociak ex-moderator

3

Jeśli ktoś nadal szuka, oto co zadziałało dla mnie - odczyt dowolnej długości łańcucha ze spacjami.

Dzięki wielu plakatom w sieci za udostępnienie tego prostego i eleganckiego rozwiązania. Jeśli to zadziała, zasługa ich, ale wszelkie błędy są moje.

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

2
Warto zauważyć, że jest to rozszerzenie POSIX i nie istnieje w standardzie ISO. Aby uzyskać kompletność, prawdopodobnie powinieneś również sprawdzić errnoi wyczyścić przydzieloną pamięć.
paxdiablo

snie pasuje tam po scansecie
Antti Haapala

1

Możesz użyć scanfdo tego celu z małą sztuczką. Właściwie powinieneś pozwolić użytkownikowi na wprowadzanie danych, dopóki użytkownik nie naciśnie Enter ( \n). Uwzględni to każdy znak, w tym spację . Oto przykład:

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;
}

Jak to działa? Gdy użytkownik wprowadza znaki ze standardowego wejścia, będą one przechowywane w zmiennej łańcuchowej do pierwszej spacji. Następnie reszta wpisu pozostanie w strumieniu wejściowym i będzie czekać na następne skanowanie. Następnie mamy forpętlę, która pobiera znak po znaku ze strumienia wejściowego (do \n) i dołącza je do końca łańcucha zmiennej , tworząc w ten sposób pełny ciąg, taki sam jak dane wejściowe użytkownika z klawiatury.

Mam nadzieję, że to komuś pomoże!


Z zastrzeżeniem przepełnienia bufora.
paxdiablo

0

Chociaż naprawdę nie powinieneś używać scanf()tego typu rzeczy, ponieważ istnieją znacznie lepsze wywołania, takie jak gets()lub getline(), można to zrobić:

#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
Jest powód, dla którego getszostał wycofany i usunięty ( stackoverflow.com/questions/30890696/why-gets-is-deprecated ) ze standardu. Jest jeszcze gorzej , scanfbo przynajmniej ta ostatnia ma sposoby, aby to zabezpieczyć.
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
Czy możesz wyjaśnić, dlaczego to jest poprawna odpowiedź? Prosimy nie wysyłać odpowiedzi zawierających tylko kod.
Theo

snie należy tam po scansecie
Antti Haapala
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.