คุณอนุญาตให้เว้นวรรคโดยใช้ 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)' หรือดีกว่า (สมมติว่า 'ชื่อ' จะถูกใช้ในท้องถิ่นอย่างเคร่งครัด) 'ชื่อ char [256 + 1 ] '+1' สามารถทำหน้าที่เป็น mneumonic สำหรับ null terminator ซึ่งจะต้องรวมอยู่ในการจัดสรร
Barry Kelly

@ Barry - ฉันสงสัยว่าsizeof(char) + 256เป็นตัวพิมพ์ผิด
Chris Lutz

คำตอบ:


186

ผู้คน (และโดยเฉพาะผู้เริ่มต้น) ไม่ควรใช้scanf("%s")หรือgets()หรือฟังก์ชั่นอื่น ๆ ที่ไม่มีการป้องกันการล้นของบัฟเฟอร์เว้นแต่คุณจะทราบอย่างชัดเจนว่าอินพุตจะอยู่ในรูปแบบเฉพาะเสมอ

จำไว้ว่าscanfย่อมาจาก "scan formatted" และมีรูปแบบที่มีค่าน้อยกว่าข้อมูลที่ผู้ใช้ป้อน เหมาะอย่างยิ่งหากคุณมีการควบคุมทั้งหมดของรูปแบบข้อมูลอินพุต แต่โดยทั่วไปไม่เหมาะสมสำหรับอินพุตของผู้ใช้

ใช้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
Kredns

7
หากคุณต้องการรับสายจากผู้ใช้มันจะง่ายขึ้น นอกจากนี้ยังปลอดภัยกว่าเนื่องจากคุณสามารถหลีกเลี่ยงบัฟเฟอร์ล้นได้ ตระกูล scanf มีประโยชน์จริง ๆ สำหรับการเปลี่ยนสตริงให้เป็นสิ่งต่าง ๆ (เช่นสี่ตัวอักษรและ int ตัวอย่างเช่น "% c% c% c% c% c% d") แต่ถึงอย่างนั้นคุณก็ควรใช้ fgets และ sscanf ไม่ใช่ scanf เพื่อหลีกเลี่ยงความเป็นไปได้ของบัฟเฟอร์ล้น
paxdiablo

4
คุณสามารถใส่ขนาดบัฟเฟอร์สูงสุดในรูปแบบ scanf แต่คุณไม่สามารถใส่ runtime ที่คำนวณได้โดยไม่ต้องสร้างรูปแบบที่ runtime (ไม่เท่ากับ * สำหรับ 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 จะสามารถอ่านได้เฉพาะในกรณีที่มีการจับคู่
AProgrammer

ปัญหาที่อาจเกิดขึ้นอีกประการหนึ่งคือการใช้งาน - ที่อื่นที่ไม่ใช่ที่หนึ่งหรือสุดท้ายคือการใช้งานที่กำหนดไว้ โดยปกติจะใช้สำหรับช่วง แต่สิ่งที่ช่วงที่กำหนดจะขึ้นอยู่กับชุดอักขระ EBCDIC มีช่องว่างในช่วงตัวอักษรและแม้ว่าจะสมมติว่าชุดอักขระที่ได้มาจาก ASCII เป็นเรื่องไร้สาระที่จะคิดว่าตัวอักษรตัวพิมพ์เล็กทั้งหมดอยู่ในช่วง az ...
AProgrammer

1
"% [^ \ n]" มีปัญหาเช่นเดียวกับ get (), บัฟเฟอร์ล้น ด้วยการจับเพิ่มเติมที่ \ n ขั้นสุดท้ายไม่ได้ถูกอ่าน สิ่งนี้จะถูกซ่อนไว้โดยข้อเท็จจริงที่ว่ารูปแบบส่วนใหญ่เริ่มต้นด้วยการข้ามช่องว่างสีขาว แต่ [ไม่ใช่หนึ่งในนั้น ฉันไม่เข้าใจอินสแตนซ์ที่ใช้ 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เพื่อป้องกันการโอเวอร์
โฟลว์

45 คะแนนและไม่มีใครชี้ให้เห็นถึงการฝึกฝนการขนส่งสินค้าที่ชัดเจนs!
Antti Haapala

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

การสาธิต


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);เพื่อให้การอ่านสตริงจะสิ้นสุดลงเมื่อพบอักขระขึ้นบรรทัดใหม่


โปรดระวังว่าสิ่งนี้จะไม่ป้องกันการล้นของบัฟเฟอร์
brunoais

sไม่ได้อยู่ที่นั่น
Antti Haapala

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ไม่ได้อยู่ที่นั่นหลังจากชุดสแกน
Antti Haapala

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

มันทำงานอย่างไร เมื่อผู้ใช้ป้อนอักขระจากอินพุตมาตรฐานผู้ใช้จะถูกเก็บไว้ในตัวแปรสตริงจนกระทั่งมีที่ว่างแรก หลังจากนั้นรายการที่เหลือจะยังคงอยู่ในอินพุตสตรีมและรอ scanf ถัดไป ต่อไปเรามีforลูปที่ใช้ถ่านโดยถ่านจากอินพุตสตรีม (จนถึง\n) และ apends ให้ปลายสตริงตัวแปรดังนั้นการสร้างสตริงที่สมบูรณ์เช่นเดียวกับการป้อนข้อมูลของผู้ใช้จากแป้นพิมพ์

หวังว่านี่จะช่วยใครซักคน!


อาจมีบัฟเฟอร์ล้น
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
คุณช่วยอธิบายได้ไหมว่าทำไมนี่จึงเป็นคำตอบที่ถูกต้อง? กรุณาอย่าโพสต์คำตอบรหัสเท่านั้น
Theo

sไม่ได้อยู่ที่นั่นหลังจากชุดสแกน
Antti Haapala
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.