Bagaimana Anda mengizinkan ruang dimasukkan menggunakan scanf?


129

Menggunakan kode berikut:

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

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

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

Seorang pengguna dapat memasukkan nama mereka tetapi ketika mereka memasukkan nama dengan spasi seperti Lucas Aardvark, scanf()potong saja semuanya Lucas. Bagaimana cara saya membuat scanf()ruang kosong


9
Perhatikan bahwa yang lebih idiomatis adalah 'malloc (sizeof (char) * 256 +1)', atau 'malloc (256 +1)', atau bahkan lebih baik (dengan asumsi 'nama' akan digunakan secara ketat secara lokal) 'char name [256 + 1] ] '. '+1' dapat bertindak sebagai mneumonic untuk terminator nol, yang perlu dimasukkan dalam alokasi.
Barry Kelly

@ Barry - Saya curiga sizeof(char) + 256kesalahan ketik.
Chris Lutz

Jawaban:


186

Orang (dan terutama pemula) tidak boleh menggunakan scanf("%s")ataugets() atau fungsi lain yang tidak memiliki perlindungan buffer overflow, kecuali Anda tahu pasti bahwa input akan selalu dari format tertentu (dan mungkin bahkan tidak kemudian).

Ingat daripada scanfsingkatan "scan diformat" dan ada yang berharga sedikit kurang diformat daripada data yang dimasukkan pengguna. Ini ideal jika Anda memiliki kontrol total format data input tetapi umumnya tidak cocok untuk input pengguna.

Gunakan fgets()(yang memiliki perlindungan buffer overflow) untuk memasukkan input Anda ke string dan sscanf()untuk mengevaluasinya. Karena Anda hanya ingin apa yang dimasukkan pengguna tanpa penguraian, Anda sebenarnya tidak perlu sscanf()dalam hal ini:

#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
Saya tidak tahu fgets(). Ini sebenarnya terlihat lebih mudah digunakan scanf(). +1
Kredns

7
Jika Anda hanya ingin mendapatkan garis dari pengguna, itu lebih mudah. Ini juga lebih aman karena Anda dapat menghindari buffer overflows. Keluarga scanf benar-benar berguna untuk mengubah string menjadi hal-hal yang berbeda (seperti empat karakter dan int misalnya dengan "% c% c% c% c% d"), tetapi, meskipun demikian, Anda harus menggunakan gadget dan sscanf, bukan scanf, untuk menghindari kemungkinan buffer overflow.
paxdiablo

4
Anda dapat menempatkan ukuran buffer maksimum dalam format scanf, Anda tidak bisa menempatkan runtime yang dihitung tanpa membangun format saat runtime (tidak ada yang setara dengan * untuk printf, * adalah pengubah yang valid untuk scanf dengan perilaku lain: menekan penugasan ).
Pemrogram

Perhatikan juga bahwa scanfmemiliki perilaku undefined jika konversi numerik meluap ( N1570 7.21.6.2p10 , kalimat terakhir, kata-kata tidak berubah sejak C89) yang berarti tidak ada satu scanffungsi dapat dengan aman digunakan untuk konversi numerik input tidak dipercaya.
zwol

@JonathanKomar dan siapa pun yang membaca ini di masa depan: jika profesor Anda mengatakan kepada Anda bahwa Anda harus menggunakan scanftugas, mereka salah melakukannya, dan Anda dapat memberi tahu mereka bahwa saya mengatakannya, dan jika mereka ingin berdebat dengan saya tentang hal itu , alamat email saya mudah ditemukan dari profil saya.
zwol

124

Mencoba

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

Semoga itu bisa membantu.


10
(1) Jelas untuk menerima spasi, Anda harus meletakkan spasi di kelas karakter. (2) Perhatikan bahwa 10 adalah jumlah karakter maksimum yang akan dibaca, jadi str harus menunjuk ke penyangga ukuran 11 setidaknya. (3) Final di sini bukan format arahan tetapi scanf akan mencoba di sini untuk mencocokkannya dengan tepat. Efeknya akan terlihat pada entri seperti 1234567890 di mana s akan dikonsumsi tetapi tidak ada tempat. Surat lain tidak akan dikonsumsi. Jika Anda meletakkan format lain setelah huruf s, itu akan dibaca hanya jika ada huruf s yang cocok.
Pemrogram

Masalah potensial lainnya, penggunaan - di tempat selain yang pertama atau terakhir adalah implementasi yang ditentukan. Biasanya, ini digunakan untuk rentang, tetapi rentang yang ditentukan tergantung pada rangkaian karakter. EBCDIC memiliki lubang dalam rentang surat dan bahkan ketika mengasumsikan charset ASCII yang diturunkan itu naif untuk berpikir bahwa semua huruf kecil berada di kisaran az ...
Pemrogram

1
"% [^ \ n]" memiliki masalah yang sama dengan mendapat (), buffer overflow. Dengan tambahan tangkapan yang final tidak dibaca; ini akan disembunyikan oleh fakta bahwa sebagian besar format mulai dengan melewatkan spasi putih, tetapi [bukan salah satunya. Saya tidak mengerti contoh saat menggunakan scanf untuk membaca string.
Pemrogram

1
Dihapus sdari akhir string input karena itu berlebihan dan salah dalam kasus-kasus tertentu (seperti yang ditunjukkan dalam komentar sebelumnya). [apakah itu format specifier sendiri daripada beberapa variasi yang ssatu.
paxdiablo

54

Contoh ini menggunakan scanset terbalik, jadi scanf terus menerima nilai sampai menemukan '\ n' - baris baru, jadi spasi juga bisa disimpan

#include <stdio.h>

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

1
Hati-hati dengan buffer overflows. Jika pengguna menulis "nama" dengan 50 karakter, program mungkin akan macet.
brunoais

3
Seperti yang Anda ketahui ukuran buffer, Anda dapat menggunakan %20[^\n]suntuk mencegah buffer overflows
osvein

Skor 45 dan tidak ada yang menunjukkan pemujaan kargo yang jelas karena berada di ssana!
Antti Haapala

22

Anda bisa menggunakan ini

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

Atau ini

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
Saya tidak menguji, tetapi berdasarkan jawaban lain di halaman ini, saya percaya ukuran buffer yang tepat untuk scanf dalam contoh Anda adalah: scanf("%19[^\n]", name);(masih +1 untuk jawaban singkat)
Dr Beco

1
Sama seperti catatan tambahan, sizeof(char)menurut definisi selalu 1, jadi tidak perlu dikalikan dengan itu.
paxdiablo

8

Jangan gunakan scanf()untuk membaca string tanpa menentukan lebar bidang. Anda juga harus memeriksa nilai kembali untuk kesalahan:

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

Atau, gunakan 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

Anda dapat menggunakan fgets()fungsi ini untuk membaca string atau menggunakannya scanf("%[^\n]s",name);agar pembacaan string akan berakhir setelah bertemu dengan karakter baris baru.


Hati-hati bahwa ini tidak mencegah buffer overflows
brunoais

sbukan milik di sana
Antti Haapala

5

getline()

Sekarang bagian dari POSIX, tidak ada yang kurang.

Ini juga menangani masalah alokasi buffer yang Anda tanyakan sebelumnya, meskipun Anda harus menjaga freememori.


Standar? Dalam referensi Anda mengutip: "Keduanya getline () dan getdelim () adalah ekstensi GNU."
Pemrogram

1
POSIX 2008 menambahkan getline. Jadi GNU maju dan mengubah header mereka untuk glibc sekitar versi 2.9, dan itu menyebabkan masalah bagi banyak proyek. Bukan tautan yang pasti, tetapi lihat di sini: bugzilla.redhat.com/show_bug.cgi?id=493941 . Adapun halaman manual on-line, saya meraih yang pertama ditemukan google.
dmckee --- ex-moderator kitten

3

Jika seseorang masih mencari, inilah yang bekerja untuk saya - untuk membaca panjang string acak termasuk spasi.

Terima kasih banyak poster di web untuk berbagi solusi sederhana & elegan ini. Jika berhasil, kredit diberikan kepada mereka tetapi kesalahan ada pada saya.

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

2
Perlu dicatat bahwa ini adalah ekstensi POSIX dan tidak ada dalam standar ISO. Untuk kelengkapan, Anda mungkin juga harus memeriksa errnodan membersihkan memori yang dialokasikan juga.
paxdiablo

sbukan milik di sana setelah scanset
Antti Haapala

1

Anda dapat menggunakan scanfuntuk tujuan ini dengan sedikit trik. Sebenarnya, Anda harus mengizinkan input pengguna hingga pengguna menekan Enter ( \n). Ini akan mempertimbangkan setiap karakter, termasuk ruang . Berikut ini contohnya:

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

Bagaimana ini bekerja? Ketika pengguna memasukkan karakter dari input standar, mereka akan disimpan dalam variabel string hingga ruang kosong pertama. Setelah itu, sisa entri akan tetap dalam aliran input, dan menunggu pemindaian berikutnya. Selanjutnya, kami memiliki forloop yang mengambil char by char dari input stream (sampai \n) dan menyarankan mereka untuk mengakhiri string variabel , sehingga membentuk string lengkap sama seperti input pengguna dari keyboard.

Semoga ini bisa membantu seseorang!


Tunduk pada buffer overflow.
paxdiablo

0

Meskipun Anda benar-benar tidak boleh menggunakannya scanf()untuk hal semacam ini, karena ada banyak panggilan yang lebih baik seperti gets()atau getline(), hal itu dapat dilakukan:

#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
Ada alasan mengapa getstidak digunakan lagi dan dihapus ( stackoverflow.com/questions/30890696/why-gets-is-deprecated ) dari standar. Itu bahkan lebih buruk bahwa scanfkarena setidaknya yang terakhir memiliki cara untuk membuatnya aman.
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
Bisakah Anda jelaskan mengapa ini adalah jawaban yang benar? Tolong jangan memposting jawaban hanya kode.
Theo

sbukan milik di sana setelah scanset
Antti Haapala
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.