Làm thế nào để bạn cho phép không gian được nhập bằng scanf?


129

Sử dụng mã sau:

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

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

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

Một người dùng có thể nhập tên của họ nhưng khi họ nhập tên có khoảng trắng như thế Lucas Aardvark, scanf()chỉ cần cắt bỏ mọi thứ sau đó Lucas. Làm cách nào để tạo scanf()khoảng trống


9
Lưu ý rằng thành ngữ nhiều hơn là 'malloc (sizeof (char) * 256 + 1)' hoặc 'malloc (256 + 1)' hoặc thậm chí tốt hơn (giả sử 'tên' sẽ được sử dụng nghiêm ngặt tại địa phương) 'tên char [256 + 1 ] '. '+1' có thể hoạt động như một máy tạo âm cho bộ kết thúc null, cần được đưa vào phân bổ.
Barry Kelly

@Barry - Tôi nghi ngờ sizeof(char) + 256là một lỗi đánh máy.
Chris Lutz

Câu trả lời:


186

Mọi người (và đặc biệt là người mới bắt đầu) không bao giờ nên sử dụng scanf("%s")hoặc gets()bất kỳ chức năng nào khác không có bảo vệ chống tràn bộ đệm, trừ khi bạn biết chắc chắn rằng đầu vào sẽ luôn có định dạng cụ thể (và có lẽ thậm chí là không).

Ghi nhớ hơn scanflà viết tắt của "quét được định dạng" và có ít định dạng ít hơn so với dữ liệu do người dùng nhập. Thật lý tưởng nếu bạn có toàn quyền kiểm soát định dạng dữ liệu đầu vào nhưng thường không phù hợp với đầu vào của người dùng.

Sử dụng fgets()( bảo vệ chống tràn bộ đệm) để đưa đầu vào của bạn vào một chuỗi và sscanf()để đánh giá nó. Vì bạn chỉ muốn những gì người dùng đã nhập mà không cần phân tích cú pháp, nên bạn không thực sự cần sscanf()trong trường hợp này:

#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
Tôi không biết về fgets(). Nó thực sự trông dễ sử dụng hơn sau đó scanf(). +1
Kredns

7
Nếu bạn chỉ muốn nhận được một dòng từ người dùng, nó sẽ dễ dàng hơn. Nó cũng an toàn hơn vì bạn có thể tránh tràn bộ đệm. Họ scanf thực sự hữu ích để biến một chuỗi thành những thứ khác nhau (như bốn ký tự và một ví dụ với "% c% c% c% c% d"), nhưng ngay cả khi đó, bạn nên sử dụng fgets và sscanf, không phải quét, để tránh khả năng tràn bộ đệm.
paxdiablo

4
Bạn có thể đặt kích thước bộ đệm tối đa ở định dạng scanf, bạn không thể đặt thời gian chạy được tính toán mà không xây dựng định dạng khi chạy (không tương đương với * cho printf, * là một công cụ sửa đổi hợp lệ cho scanf với một hành vi khác: triệt tiêu việc gán ).
AProgrammer

Cũng lưu ý rằng scanfcó hành vi undefined nếu chuyển đổi số tràn ( N1570 7.21.6.2p10 , câu cuối cùng, từ ngữ không thay đổi kể từ C89) có nghĩa là không có các scanfchức năng một cách an toàn có thể được sử dụng để chuyển đổi số của đầu vào không tin cậy.
zwol

@JonathanKomar và bất cứ ai khác đọc điều này trong tương lai: nếu giáo sư của bạn nói với bạn rằng bạn phải sử dụng scanftrong một bài tập, họ đã sai khi làm như vậy, và bạn có thể nói với họ rằng tôi đã nói như vậy, và nếu họ muốn tranh luận với tôi về điều đó , địa chỉ email của tôi dễ dàng được tìm thấy từ hồ sơ của tôi.
zwol

124

Thử

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

Mong rằng sẽ giúp.


10
(1) Rõ ràng để chấp nhận khoảng trắng, bạn cần đặt một khoảng trắng trong lớp nhân vật. (2) Lưu ý rằng 10 là số ký tự tối đa sẽ được đọc, do đó str phải trỏ đến bộ đệm có kích thước 11 ít nhất. (3) Các cuối cùng ở đây không phải là một chỉ thị định dạng nhưng scanf sẽ thử ở đây để khớp chính xác. Hiệu ứng sẽ hiển thị trên một mục như 1234567890 trong đó s sẽ được tiêu thụ nhưng không đặt ở đâu. Một lá thư khác sẽ không được tiêu thụ. Nếu bạn đặt một định dạng khác sau s, nó sẽ chỉ được đọc nếu có một s được khớp.
Lập trình viên

Một vấn đề tiềm năng khác, việc sử dụng - ở nơi khác ngoài nơi đầu tiên hoặc cuối cùng là việc thực hiện được xác định. Thông thường, nó được sử dụng cho các phạm vi, nhưng những gì chỉ định phạm vi phụ thuộc vào bộ ký tự. EBCDIC có lỗ hổng trong phạm vi chữ cái và ngay cả khi giả sử bộ ký tự có nguồn gốc ASCII, thật ngây thơ khi nghĩ rằng tất cả các chữ cái viết thường đều nằm trong phạm vi az ...
AProgrammer

1
"% [^ \ n]" có cùng vấn đề với get (), lỗi tràn bộ đệm. Với phần bổ sung mà phần cuối cùng không đọc được; điều này sẽ bị che giấu bởi thực tế là hầu hết các định dạng bắt đầu bằng cách bỏ qua các khoảng trắng, nhưng [không phải là một trong số chúng. Tôi không hiểu ví dụ sử dụng scanf để đọc chuỗi.
Lập trình viên

1
Đã xóa phần scuối của chuỗi đầu vào vì nó không cần thiết và không chính xác trong một số trường hợp nhất định (như đã nêu trong các nhận xét trước đó). [là định dạng riêng của nó chứ không phải là một số biến thể của định dạng s.
paxdiablo

53

Ví dụ này sử dụng một bộ quét đảo ngược, vì vậy scanf tiếp tục lấy các giá trị cho đến khi nó gặp '\ n' - dòng mới, do đó, không gian cũng được lưu lại

#include <stdio.h>

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

1
Cẩn thận với tràn bộ đệm. Nếu người dùng viết "tên" với 50 ký tự, chương trình có thể sẽ bị sập.
brunoais

3
Như bạn đã biết kích thước bộ đệm, bạn có thể sử dụng %20[^\n]sđể ngăn chặn lỗi tràn bộ đệm
osvein

45 điểm và không ai chỉ ra sự sùng bái hàng hóa rõ ràng khi scó ở đó!
Antti Haapala

22

Bạn có thể sử dụng cái này

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

Hoặc cái này

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

BẢN GIỚI THIỆU


1
Tôi đã không kiểm tra, nhưng dựa trên các câu trả lời khác trong chính trang này, tôi tin rằng kích thước bộ đệm chính xác cho scanf trong ví dụ của bạn sẽ là: scanf("%19[^\n]", name);(vẫn +1 cho câu trả lời ngắn gọn)
Dr Beco

1
Cũng như một ghi chú bên lề, sizeof(char)theo định nghĩa luôn là 1, vì vậy không cần phải nhân nó.
paxdiablo

8

Không sử dụng scanf()để đọc các chuỗi mà không chỉ định độ rộng trường. Bạn cũng nên kiểm tra các giá trị trả về cho các lỗi:

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

Ngoài ra, sử dụng 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

Bạn có thể sử dụng fgets()chức năng để đọc một chuỗi hoặc sử dụng scanf("%[^\n]s",name);để việc đọc chuỗi sẽ chấm dứt khi gặp một ký tự dòng mới.


Cẩn thận rằng điều này không ngăn chặn tràn bộ đệm
brunoais

skhông thuộc về nơi đó
Antti Haapala

5

getline()

Bây giờ là một phần của POSIX, không hơn không kém.

Nó cũng quan tâm đến vấn đề cấp phát bộ đệm mà bạn đã hỏi trước đó, mặc dù bạn phải chăm sóc freebộ nhớ.


Tiêu chuẩn? Trong tài liệu tham khảo bạn trích dẫn: "Cả getline () và getd006 () đều là các phần mở rộng GNU."
AProgrammer

1
POSIX 2008 bổ sung getline. Vì vậy, GNU đã đi trước và thay đổi tiêu đề của họ cho glibc xung quanh phiên bản 2.9 và điều này gây ra rắc rối cho nhiều dự án. Không phải là một liên kết dứt khoát, nhưng hãy nhìn vào đây: bugzilla.redhat.com/show_orms.cgi?id=493941 . Đối với trang người dùng trực tuyến, tôi đã chộp lấy trang đầu tiên được tìm thấy trên google.
dmckee --- ex-moderator mèo con

3

Nếu ai đó vẫn đang tìm kiếm, đây là những gì làm việc cho tôi - để đọc độ dài chuỗi tùy ý bao gồm cả khoảng trắng.

Cảm ơn nhiều áp phích trên web đã chia sẻ giải pháp đơn giản và thanh lịch này. Nếu nó hoạt động, tín dụng thuộc về họ nhưng bất kỳ lỗi nào là của tôi.

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

2
Điều đáng chú ý là đây là phần mở rộng POSIX và không tồn tại trong tiêu chuẩn ISO. Để hoàn thiện, có lẽ bạn cũng nên kiểm tra errnovà dọn dẹp bộ nhớ được phân bổ.
paxdiablo

skhông thuộc về nơi đó sau khi quét
Antti Haapala

1

Bạn có thể sử dụng scanfcho mục đích này với một mẹo nhỏ. Trên thực tế, bạn nên cho phép người dùng nhập liệu cho đến khi người dùng nhấn Enter ( \n). Điều này sẽ xem xét mọi nhân vật, bao gồm cả không gian . Đây là ví dụ:

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

Làm thế nào điều này hoạt động? Khi người dùng nhập các ký tự từ đầu vào tiêu chuẩn, chúng sẽ được lưu trữ trong biến chuỗi cho đến khoảng trống đầu tiên. Sau đó, phần còn lại của mục nhập sẽ vẫn ở trong luồng đầu vào và đợi lần quét tiếp theo. Tiếp theo, chúng ta có một forvòng lặp lấy char bằng char từ luồng đầu vào (đến \n) và xin lỗi chúng đến cuối chuỗi biến , do đó tạo thành một chuỗi hoàn chỉnh giống như đầu vào của người dùng từ bàn phím.

Hy vọng điều này sẽ giúp được ai đó!


Đối tượng tràn bộ đệm.
paxdiablo

0

Mặc dù bạn thực sự không nên sử dụng scanf()cho loại điều này, bởi vì có nhiều cuộc gọi tốt hơn như gets(), hoặc getline()nó có thể được thực hiện:

#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
Có một lý do tại sao getskhông được chấp nhận và bị xóa ( stackoverflow.com/questions/30890696/why-gets-is-deprecated ) khỏi tiêu chuẩn. Nó thậm chí còn tồi tệ hơnscanfbởi vì ít nhất sau này có cách để làm cho nó an toàn.
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
Bạn có thể giải thích tại sao đây là câu trả lời đúng? Xin vui lòng không đăng câu trả lời chỉ mã.
Theo

skhông thuộc về nơi đó sau khi quét
Antti Haapala
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.