Tôi có thể sử dụng gì để chuyển đổi đầu vào thay vì scanf?


125

Tôi đã rất thường xuyên thấy mọi người không khuyến khích người khác sử dụng scanfvà nói rằng có những lựa chọn thay thế tốt hơn. Tuy nhiên, tất cả những gì tôi cuối cùng nhìn thấy là "không sử dụng scanf" hoặc "đây là một chuỗi định dạng đúng" và không bao giờ có bất kỳ ví dụ nào về "các lựa chọn thay thế tốt hơn" được đề cập.

Ví dụ: hãy lấy đoạn mã này:

scanf("%c", &c);

Điều này đọc khoảng trắng còn lại trong luồng đầu vào sau lần chuyển đổi cuối cùng. Giải pháp được đề xuất thông thường cho vấn đề này là sử dụng:

scanf(" %c", &c);

hoặc không sử dụng scanf.

scanflà xấu, một số tùy chọn ANSI C để chuyển đổi các định dạng đầu vào scanfthường có thể xử lý (như số nguyên, số dấu phẩy động và chuỗi) mà không sử dụng là scanfgì?

Câu trả lời:


87

Các cách đọc đầu vào phổ biến nhất là:

  • sử dụng fgetsvới kích thước cố định, đó là những gì thường được đề xuất và

  • sử dụng fgetc, có thể hữu ích nếu bạn chỉ đọc một char.

Để chuyển đổi đầu vào, có nhiều chức năng mà bạn có thể sử dụng:

  • strtoll, để chuyển đổi một chuỗi thành một số nguyên

  • strtof/ d/ ld, để chuyển đổi một chuỗi thành một số dấu phẩy động

  • sscanf, Mà không phải là như xấu như chỉ đơn giản sử dụng scanf, mặc dù nó có hầu hết các downfalls đề cập dưới đây

  • Không có cách nào tốt để phân tích đầu vào được phân tách bằng dấu phân cách trong ANSI C. Hoặc sử dụng strtok_rtừ POSIX hoặc strtokkhông an toàn cho chuỗi. Bạn cũng có thể cuộn biến thể an toàn luồng của riêng mình bằng cách sử dụng strcspnstrspn, strtok_rkhông liên quan đến bất kỳ hỗ trợ hệ điều hành đặc biệt nào.

  • Nó có thể là quá mức cần thiết, nhưng bạn có thể sử dụng từ vựng và trình phân tích cú pháp ( flexbisonlà ví dụ phổ biến nhất).

  • Không có chuyển đổi, chỉ cần sử dụng chuỗi


Vì tôi không đi sâu vào chính xác tại sao scanf câu hỏi của tôi lại tệ, tôi sẽ giải thích:

  • Với các chỉ định chuyển đổi %[...]%c, scanfkhông ăn hết khoảng trắng. Điều này dường như không được biết đến rộng rãi, bằng chứng là nhiều bản sao của câu hỏi này .

  • Có một số nhầm lẫn về thời điểm sử dụng toán tử đơn nguyên &khi đề cập đến scanfcác đối số (cụ thể là với các chuỗi).

  • Rất dễ dàng để bỏ qua giá trị trả về từ scanf. Điều này có thể dễ dàng gây ra hành vi không xác định từ việc đọc một biến chưa được khởi tạo.

  • Rất dễ quên để ngăn chặn tràn bộ đệm scanf. scanf("%s", str)cũng tệ như, nếu không nói là tệ hơn gets.

  • Bạn không thể phát hiện tràn khi chuyển đổi số nguyên với scanf. Trong thực tế, tràn gây ra hành vi không xác định trong các chức năng này.



56

Tại sao là scanfxấu?

Vấn đề chính là scanfkhông bao giờ có ý định đối phó với đầu vào của người dùng. Nó dự định được sử dụng với dữ liệu được định dạng "hoàn hảo". Tôi đã trích dẫn từ "hoàn hảo" bởi vì nó không hoàn toàn đúng. Nhưng nó không được thiết kế để phân tích dữ liệu không đáng tin cậy như đầu vào của người dùng. Theo bản chất, đầu vào của người dùng là không thể dự đoán. Người dùng hiểu sai hướng dẫn, mắc lỗi chính tả, vô tình nhấn enter trước khi hoàn thành, v.v ... Người ta có thể hỏi một cách hợp lý tại sao một chức năng không nên được sử dụng cho đầu vào của người dùng đọc từ đó stdin. Nếu bạn là người dùng * nix có kinh nghiệm, lời giải thích sẽ không gây ngạc nhiên nhưng nó có thể gây nhầm lẫn cho người dùng Windows. Trong các hệ thống * nix, việc xây dựng các chương trình hoạt động thông qua đường ống là rất phổ biến.stdoutstdincủa thứ hai. Bằng cách này, bạn có thể chắc chắn rằng đầu ra và đầu vào có thể dự đoán được. Trong những trường hợp này, scanfthực sự hoạt động tốt. Nhưng khi làm việc với đầu vào không thể đoán trước, bạn có nguy cơ gặp đủ loại rắc rối.

Vậy tại sao không có bất kỳ chức năng tiêu chuẩn dễ sử dụng nào cho đầu vào của người dùng? Người ta chỉ có thể đoán ở đây, nhưng tôi cho rằng các hacker cũ C cứng nhắc chỉ nghĩ rằng các chức năng hiện có là đủ tốt, mặc dù chúng rất cồng kềnh. Ngoài ra, khi bạn nhìn vào các ứng dụng đầu cuối điển hình, chúng rất hiếm khi đọc đầu vào của người dùng stdin. Thông thường bạn chuyển tất cả đầu vào của người dùng dưới dạng đối số dòng lệnh. Chắc chắn, có những trường hợp ngoại lệ, nhưng đối với hầu hết các ứng dụng, đầu vào của người dùng là một điều rất nhỏ.

vậy, bạn có thể làm gì?

Yêu thích của tôi là fgetskết hợp với sscanf. Tôi đã từng viết một câu trả lời về điều đó, nhưng tôi sẽ đăng lại mã hoàn chỉnh. Dưới đây là một ví dụ với kiểm tra lỗi và phân tích lỗi (nhưng không hoàn hảo). Nó đủ tốt cho mục đích gỡ lỗi.

Ghi chú

Tôi đặc biệt không thích yêu cầu người dùng nhập hai thứ khác nhau trên một dòng. Tôi chỉ làm điều đó khi họ thuộc về nhau một cách tự nhiên. Ví dụ như printf("Enter the price in the format <dollars>.<cent>: ")sử dụng sscanf(buffer "%d.%d", &dollar, &cent). Tôi sẽ không bao giờ làm một cái gì đó như printf("Enter height and base of the triangle: "). Điểm chính của việc sử dụng fgetsdưới đây là đóng gói các đầu vào để đảm bảo rằng một đầu vào không ảnh hưởng đến đầu vào tiếp theo.

#define bsize 100

void error_function(const char *buffer, int no_conversions) {
        fprintf(stderr, "An error occurred. You entered:\n%s\n", buffer);
        fprintf(stderr, "%d successful conversions", no_conversions);
        exit(EXIT_FAILURE);
}

char c, buffer[bsize];
int x,y;
float f, g;
int r;

printf("Enter two integers: ");
fflush(stdout); // Make sure that the printf is executed before reading
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) error_function(buffer, r);

// Unless the input buffer was to small we can be sure that stdin is empty
// when we come here.
printf("Enter two floats: ");
fflush(stdout);
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) error_function(buffer, r);

// Reading single characters can be especially tricky if the input buffer
// is not emptied before. But since we're using fgets, we're safe.
printf("Enter a char: ");
fflush(stdout);
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%c", &c)) != 1) error_function(buffer, r);

printf("You entered %d %d %f %c\n", x, y, f, c);

Nếu bạn làm nhiều thứ trong số này, tôi có thể khuyên bạn nên tạo một trình bao bọc luôn tuôn ra:

int printfflush (const char *format, ...)
{
   va_list arg;
   int done;
   va_start (arg, format);
   done = vfprintf (stdout, format, arg);
   fflush(stdout);
   va_end (arg);
   return done;
}```

Làm như thế này sẽ loại bỏ một vấn đề phổ biến, đó là dòng mới có thể gây rối với đầu vào tổ. Nhưng nó có một vấn đề khác, đó là nếu dòng dài hơn bsize. Bạn có thể kiểm tra với if(buffer[strlen(buffer)-1] != '\n'). Nếu bạn muốn xóa dòng mới, bạn có thể làm điều đó với buffer[strcspn(buffer, "\n")] = 0.

Nói chung, tôi khuyên bạn không nên hy vọng người dùng nhập dữ liệu vào một số định dạng kỳ lạ mà bạn nên phân tích thành các biến khác nhau. Nếu bạn muốn gán các biến heightwidth, đừng yêu cầu cả hai cùng một lúc. Cho phép người dùng nhấn enter giữa chúng. Ngoài ra, cách tiếp cận này là rất tự nhiên theo một nghĩa. Bạn sẽ không bao giờ nhận được đầu vào từ stdincho đến khi bạn nhấn enter, vậy tại sao không luôn luôn đọc toàn bộ dòng? Tất nhiên điều này vẫn có thể dẫn đến các vấn đề nếu dòng dài hơn bộ đệm. Tôi có nhớ đề cập đến rằng đầu vào của người dùng bị cồng kềnh trong C không? :)

Để tránh các vấn đề với các dòng dài hơn bộ đệm, bạn có thể sử dụng chức năng tự động phân bổ bộ đệm có kích thước phù hợp, bạn có thể sử dụng getline(). Hạn chế là bạn sẽ cần freekết quả sau đó.

Đẩy mạnh trò chơi

Nếu bạn nghiêm túc về việc tạo chương trình trong C với đầu vào của người dùng, tôi khuyên bạn nên xem thư viện như thế nào ncurses. Bởi vì sau đó bạn có thể cũng muốn tạo các ứng dụng với một số đồ họa đầu cuối. Thật không may, bạn sẽ mất một số tính di động nếu bạn làm điều đó, nhưng nó cho phép bạn kiểm soát đầu vào của người dùng tốt hơn nhiều. Chẳng hạn, nó cung cấp cho bạn khả năng đọc một phím bấm ngay lập tức thay vì chờ người dùng nhấn enter.


Lưu ý rằng (r = sscanf("1 2 junk", "%d%d", &x, &y)) != 2không phát hiện xấu như văn bản không phải là số.
chux - Phục hồi Monica

1
@chux Đã sửa% f% f. Bạn có ý nghĩa gì với người đầu tiên?
klutt

Với fgets()các "1 2 junk", if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) {không báo cáo sai bất cứ điều gì với đầu vào mặc dù nó có "rác".
chux - Phục hồi Monica

@chux À, giờ em hiểu rồi. Vâng đó là cố ý.
klutt

1
scanfđược dự định sẽ được sử dụng với dữ liệu được định dạng hoàn hảo Nhưng ngay cả điều đó không đúng. Bên cạnh vấn đề với "rác" như được đề cập bởi @chux, còn có một định dạng như "%d %d %d"rất vui khi đọc đầu vào từ một, hai hoặc ba dòng (hoặc thậm chí nhiều hơn, nếu có các dòng trống can thiệp), rằng không có cách để buộc (giả sử) một đầu vào hai dòng bằng cách thực hiện một cái gì đó như "%d\n%d %d", v.v. scanfcó thể phù hợp với đầu vào luồng được định dạng , nhưng nó không tốt cho bất kỳ thứ gì dựa trên dòng.
Hội nghị thượng đỉnh Steve

18

scanflà tuyệt vời khi bạn biết đầu vào của bạn luôn có cấu trúc tốt và hành xử tốt. Nếu không thì...

IMO, đây là những vấn đề lớn nhất với scanf:

  • Rủi ro tràn bộ đệm - nếu bạn không chỉ định độ rộng trường cho các chỉ định %s%[chuyển đổi, bạn có nguy cơ tràn bộ đệm (cố gắng đọc nhiều đầu vào hơn so với bộ đệm có kích thước để giữ). Thật không may, không có cách nào tốt để xác định đó là một đối số (như với printf) - bạn phải mã hóa nó như một phần của công cụ xác định chuyển đổi hoặc thực hiện một số shenanigans macro.

  • Chấp nhận đầu vào đó nên bị từ chối - Nếu bạn đang đọc một đầu vào với %dspecifier chuyển đổi và bạn gõ một cái gì đó giống như 12w4, bạn sẽ mong đợi scanf để từ chối đầu vào đó, nhưng nó không - nó cải và chuyển nhượng các thành công 12, để lại w4trong dòng đầu vào để hôi lên đọc tiếp.

Vì vậy, những gì bạn nên sử dụng thay thế?

Tôi thường khuyên bạn nên đọc tất cả đầu vào tương tác dưới dạng văn bản bằng cách sử dụng fgets- nó cho phép bạn chỉ định số lượng ký tự tối đa để đọc tại một thời điểm, do đó bạn có thể dễ dàng ngăn chặn lỗi tràn bộ đệm:

char input[100];
if ( !fgets( input, sizeof input, stdin ) )
{
  // error reading from input stream, handle as appropriate
}
else
{
  // process input buffer
}

Một fgetsđiều khó hiểu là nó sẽ lưu dòng mới trong bộ đệm nếu có chỗ, vì vậy bạn có thể kiểm tra dễ dàng để xem ai đó đã nhập nhiều đầu vào hơn bạn mong đợi:

char *newline = strchr( input, '\n' );
if ( !newline )
{
  // input longer than we expected
}

Cách bạn đối phó với điều đó tùy thuộc vào bạn - bạn có thể từ chối toàn bộ đầu vào và loại bỏ mọi đầu vào còn lại bằng getchar:

while ( getchar() != '\n' ) 
  ; // empty loop

Hoặc bạn có thể xử lý đầu vào bạn nhận được cho đến nay và đọc lại. Nó phụ thuộc vào vấn đề bạn đang cố gắng giải quyết.

Để hóa đầu vào (tách nó dựa trên một hoặc nhiều dấu phân cách), bạn có thể sử dụng strtok, nhưng hãy cẩn thận - strtoksửa đổi đầu vào của nó (nó ghi đè lên dấu phân cách bằng dấu kết thúc chuỗi) và bạn không thể giữ trạng thái của nó (nghĩa là bạn có thể ' t token hóa một phần một chuỗi, sau đó bắt đầu token hóa một chuỗi khác, sau đó chọn nơi bạn rời khỏi chuỗi ban đầu). Có một biến thể, strtok_sduy trì trạng thái của mã thông báo, nhưng việc triển khai AFAIK là tùy chọn (bạn sẽ cần kiểm tra xem nó có __STDC_LIB_EXT1__được xác định để xem nó có khả dụng không).

Khi bạn đã mã hóa đầu vào của mình, nếu bạn cần chuyển đổi chuỗi thành số (nghĩa là "1234"=> 1234), bạn có các tùy chọn. strtolstrtodsẽ chuyển đổi các biểu diễn chuỗi của số nguyên và số thực thành các loại tương ứng của chúng. Chúng cũng cho phép bạn nắm bắt được 12w4vấn đề tôi đã đề cập ở trên - một trong những đối số của chúng là một con trỏ tới ký tự đầu tiên không được chuyển đổi trong chuỗi:

char *text = "12w4";
char *chk;
long val;
long tmp = strtol( text, &chk, 10 );
if ( !isspace( *chk ) && *chk != 0 )
  // input is not a valid integer string, reject the entire input
else
  val = tmp;

Nếu bạn không chỉ định độ rộng trường ... - hoặc triệt tiêu chuyển đổi (ví dụ: %*[%\n]hữu ích cho việc xử lý các dòng quá dài sau này trong câu trả lời).
Toby Speight

Có một cách để có được đặc tả thời gian chạy của độ rộng trường, nhưng nó không hay. Cuối cùng, bạn phải xây dựng chuỗi định dạng trong mã của mình (có thể sử dụng snprintf()) ,.
Toby Speight

5
Bạn đã mắc một lỗi phổ biến nhất isspace()ở đó - nó chấp nhận các tự không dấu được thể hiện dưới dạng int, vì vậy bạn cần bỏ qua unsigned charđể tránh UB trên các nền tảng charđược ký.
Toby Speight

9

Trong câu trả lời này, tôi sẽ giả định rằng bạn đang đọc và giải thích các dòng văn bản . Có lẽ bạn đang nhắc người dùng, người đang gõ một cái gì đó và nhấn RETURN. Hoặc có lẽ bạn đang đọc các dòng văn bản có cấu trúc từ một tệp dữ liệu nào đó.

Vì bạn đang đọc các dòng văn bản, nên tổ chức mã của bạn xung quanh chức năng thư viện là đọc, một dòng văn bản. Hàm Standard là fgets(), mặc dù có các hàm khác (bao gồm getline). Và sau đó bước tiếp theo là diễn giải dòng văn bản đó bằng cách nào đó.

Đây là công thức cơ bản để gọi fgetsđể đọc một dòng văn bản:

char line[512];
printf("type something:\n");
fgets(line, 512, stdin);
printf("you typed: %s", line);

Điều này chỉ đơn giản là đọc trong một dòng văn bản và in lại. Như đã viết, nó có một vài hạn chế, chúng ta sẽ nhận được trong một phút. Nó cũng có một tính năng rất tuyệt vời: số 512 mà chúng tôi đã chuyển qua làm đối số thứ hai fgetslà kích thước của mảng linemà chúng tôi yêu cầu fgetsđọc vào. Thực tế này - rằng chúng ta có thể cho biết fgetsnó được phép đọc bao nhiêu - có nghĩa là chúng ta có thể chắc chắn rằng fgetssẽ không tràn mảng bằng cách đọc quá nhiều vào nó.

Vì vậy, bây giờ chúng ta biết cách đọc một dòng văn bản, nhưng nếu chúng ta thực sự muốn đọc một số nguyên, hoặc một số dấu phẩy động, hoặc một ký tự hoặc một từ đơn thì sao? (Tức là, những gì nếu scanfcuộc gọi chúng tôi đang cố gắng để cải thiện trên đã sử dụng một specifier định dạng như %d, %f, %c, hay %s?)

Thật dễ dàng để diễn giải lại một dòng văn bản - một chuỗi - như bất kỳ thứ gì trong số này. Để chuyển đổi một chuỗi thành một số nguyên, cách đơn giản nhất (mặc dù không hoàn hảo) là thực hiện nó là gọi atoi(). Để chuyển đổi thành một số dấu phẩy động, có atof(). (Và cũng có những cách tốt hơn, như chúng ta sẽ thấy trong một phút.) Đây là một ví dụ rất đơn giản:

printf("type an integer:\n");
fgets(line, 512, stdin);
int i = atoi(line);
printf("type a floating-point number:\n");
fgets(line, 512, stdin);
float f = atof(line);
printf("you typed %d and %f\n", i, f);

Nếu bạn muốn người dùng nhập một ký tự (có thể yhoặc nlà phản hồi có / không), bạn có thể chỉ cần lấy ký tự đầu tiên của dòng, như thế này:

printf("type a character:\n");
fgets(line, 512, stdin);
char c = line[0];
printf("you typed %c\n", c);

(Tất nhiên, điều này bỏ qua khả năng người dùng đã gõ phản hồi đa ký tự; nó lặng lẽ bỏ qua bất kỳ ký tự phụ nào được nhập.)

Cuối cùng, nếu bạn muốn người dùng gõ một chuỗi chắc chắn không chứa khoảng trắng, nếu bạn muốn xử lý dòng đầu vào

hello world!

vì chuỗi được "hello"theo sau bởi một thứ khác (đó là những gì scanfđịnh dạng %ssẽ làm), trong trường hợp đó, tôi đã bị xơ một chút, rốt cuộc, không dễ để diễn giải lại dòng theo cách đó, vì vậy, câu trả lời cho điều đó một phần của câu hỏi sẽ phải chờ một chút.

Nhưng trước tiên tôi muốn quay lại ba điều tôi đã bỏ qua.

(1) Chúng tôi đã gọi

fgets(line, 512, stdin);

để đọc vào mảng linevà trong đó 512 là kích thước của mảng lineđể fgetsbiết không tràn vào nó. Nhưng để chắc chắn rằng 512 là số phù hợp (đặc biệt, để kiểm tra xem có thể ai đó đã điều chỉnh chương trình để thay đổi kích thước không), bạn phải đọc lại bất cứ nơi nào lineđược khai báo. Điều đó gây phiền toái, vì vậy có hai cách tốt hơn để giữ kích thước đồng bộ. Bạn có thể, (a) sử dụng bộ tiền xử lý để đặt tên cho kích thước:

#define MAXLINE 512
char line[MAXLINE];
fgets(line, MAXLINE, stdin);

Hoặc, (b) sử dụng sizeoftoán tử C :

fgets(line, sizeof(line), stdin);

(2) Vấn đề thứ hai là chúng tôi chưa kiểm tra lỗi. Khi bạn đọc đầu vào, bạn phải luôn kiểm tra khả năng xảy ra lỗi. Nếu vì bất kỳ lý do gì fgetskhông thể đọc dòng văn bản bạn yêu cầu, nó chỉ ra điều này bằng cách trả về một con trỏ null. Vì vậy, chúng ta nên làm những việc như

printf("type something:\n");
if(fgets(line, 512, stdin) == NULL) {
    printf("Well, never mind, then.\n");
    exit(1);
}

Cuối cùng, có một vấn đề là để đọc một dòng văn bản, fgetsđọc các ký tự và điền chúng vào mảng của bạn cho đến khi nó tìm thấy \nký tự kết thúc dòng đó và nó cũng điền \nký tự vào mảng của bạn . Bạn có thể thấy điều này nếu bạn sửa đổi ví dụ trước của chúng tôi một chút:

printf("you typed: \"%s\"\n", line);

Nếu tôi chạy cái này và gõ "Steve" khi nó nhắc tôi, nó sẽ in ra

you typed: "Steve
"

Điều đó "trên dòng thứ hai là bởi vì chuỗi nó đọc và in ra thực sự là "Steve\n".

Đôi khi, dòng mới bổ sung đó không thành vấn đề (như khi chúng tôi gọi atoihoặc atof, vì cả hai đều bỏ qua bất kỳ đầu vào không phải là số nào sau số), nhưng đôi khi nó rất quan trọng. Vì vậy, thường chúng tôi sẽ muốn loại bỏ dòng mới đó. Có một số cách để làm điều đó, mà tôi sẽ nhận được trong một phút. (Tôi biết tôi đã nói điều đó rất nhiều. Nhưng tôi sẽ quay lại với tất cả những điều đó, tôi hứa.)

Tại thời điểm này, bạn có thể suy nghĩ: "Tôi nghĩ bạn nói scanf là không tốt, và cách nào khác này sẽ tốt hơn rất nhiều Nhưng. fgetsĐang bắt đầu trông giống như một phiền toái gọi. scanfdễ dàng như vậy tôi không thể tiếp tục sử dụng nó!? "

Chắc chắn, bạn có thể tiếp tục sử dụng scanf , nếu bạn muốn. (Và đối với những điều thực sự đơn giản, theo một cách nào đó thì đơn giản hơn.) Nhưng, xin vui lòng, đừng khóc với tôi khi nó làm bạn thất bại do một trong 17 quirks và foibles của nó, hoặc đi vào một vòng lặp vô hạn vì đầu vào của bạn không mong đợi, hoặc khi bạn không thể tìm ra cách sử dụng nó để làm điều gì đó phức tạp hơn. Và hãy xem fgetsnhững phiền toái thực tế:

  1. Bạn luôn phải xác định kích thước mảng. Chà, tất nhiên, điều đó không gây phiền toái gì cả - đó là một tính năng, bởi vì tràn bộ đệm là một điều thực sự tồi tệ.

  2. Bạn phải kiểm tra giá trị trả lại. Trên thực tế, đó là một rửa, bởi vì để sử dụngscanf chính xác, bạn cũng phải kiểm tra giá trị trả lại của nó.

  3. Bạn phải lột bỏ \nlưng. Đây là, tôi thừa nhận, một phiền toái thực sự. Tôi ước có một chức năng Tiêu chuẩn mà tôi có thể chỉ cho bạn rằng không có vấn đề nhỏ này. (Xin vui lòng không ai đưa lên gets.) Nhưng so vớiscanf's 17 phiền toái khác nhau, tôi sẽ nhận điều này phiền toái fgetsbất cứ ngày nào.

Rồi sao để bạn tước dòng mới đó? Ba cách:

(a) Cách rõ ràng:

char *p = strchr(line, '\n');
if(p != NULL) *p = '\0';

(b) Cách khéo léo & nhỏ gọn:

strtok(line, "\n");

Thật không may, cái này không phải lúc nào cũng hoạt động.

(c) Một cách nhỏ gọn và tối nghĩa khác:

line[strcspn(line, "\n")] = '\0';

Và bây giờ đã hết cách, chúng ta có thể quay lại với một thứ khác mà tôi đã bỏ qua: sự không hoàn hảo của atoi()atof(). Vấn đề với họ là họ không cung cấp cho bạn bất kỳ dấu hiệu thành công hay thất bại nào: họ lặng lẽ bỏ qua đầu vào không có chữ số và họ lặng lẽ trả về 0 nếu không có đầu vào số nào cả. Các lựa chọn thay thế ưa thích - cũng có một số lợi thế nhất định - là strtolstrtod. strtolcũng cho phép bạn sử dụng một cơ sở khác ngoài 10, nghĩa là bạn có thể nhận được hiệu ứng của (trong số những thứ khác) %ohoặc %xvớiscanf. Nhưng chỉ ra cách sử dụng các chức năng này một cách chính xác là một câu chuyện, và sẽ quá mất tập trung từ những gì đã biến thành một câu chuyện khá phân mảnh, vì vậy tôi sẽ không nói gì thêm về chúng bây giờ.

Phần còn lại của câu chuyện chính liên quan đến đầu vào mà bạn có thể đang cố phân tích nó phức tạp hơn chỉ là một số hoặc ký tự. Điều gì sẽ xảy ra nếu bạn muốn đọc một dòng chứa hai số hoặc nhiều từ được phân tách bằng khoảng trắng hoặc dấu chấm câu cụ thể? Đó là nơi mọi thứ trở nên thú vị và nơi mọi thứ có thể trở nên phức tạp nếu bạn đang cố gắng thực hiện mọi thứ bằng cách sử dụng scanfvà bây giờ có nhiều tùy chọn hơn khi bạn đọc sạch một dòng văn bản fgets, mặc dù toàn bộ câu chuyện về tất cả các tùy chọn đó có lẽ có thể lấp đầy một cuốn sách, vì vậy chúng ta sẽ chỉ có thể làm trầy xước bề mặt ở đây.

  1. Kỹ thuật yêu thích của tôi là chia dòng thành các "từ" được phân tách bằng khoảng trắng, sau đó thực hiện thêm một số từ với mỗi "từ". Một chức năng tiêu chuẩn chính để thực hiện điều này là strtok(cũng có vấn đề của nó và cũng đánh giá một cuộc thảo luận hoàn toàn riêng biệt). Sở thích riêng của tôi là một chức năng chuyên dụng để xây dựng một loạt các con trỏ cho mỗi "từ" tách rời, một chức năng tôi mô tả trong các ghi chú khóa học này . Ở bất cứ giá nào, một khi bạn đã có "từ", bạn có thể xử lý thêm từng từ, có lẽ với cùng atoi/ atof/ strtol/ strtod chức năng mà chúng tôi đã xem xét.

  2. Nghịch lý thay, mặc dù chúng ta đã dành một lượng thời gian và nỗ lực khá lớn ở đây để tìm ra cách di chuyển khỏi scanf, một cách tốt khác để đối phó với dòng văn bản chúng ta vừa đọc fgetslà chuyển nó đến sscanf. Theo cách này, bạn kết thúc với hầu hết các lợi thế của scanf, nhưng không có hầu hết các nhược điểm.

  3. Nếu cú ​​pháp đầu vào của bạn đặc biệt phức tạp, có thể phù hợp để sử dụng thư viện "regrec" để phân tích cú pháp.

  4. Cuối cùng, bạn có thể sử dụng bất cứ giải pháp phân tích cú pháp ad hoc nào phù hợp với bạn. Bạn có thể di chuyển qua dòng một ký tự tại một thời điểm bằng một char *con trỏ kiểm tra các ký tự mà bạn mong đợi. Hoặc bạn có thể tìm kiếm các ký tự cụ thể sử dụng chức năng thích strchrhoặc strrchrhoặc strspnhoặc strcspnhoặc strpbrk. Hoặc bạn có thể phân tích / chuyển đổi và bỏ qua các nhóm ký tự chữ số bằng cách sử dụng strtolhoặc các strtodhàm mà chúng ta đã bỏ qua trước đó.

Rõ ràng có nhiều điều có thể nói, nhưng hy vọng phần giới thiệu này sẽ giúp bạn bắt đầu.


Có một lý do tốt để viết sizeof (line)chứ không đơn giản sizeof line? Các cựu làm cho nó trông giống như linelà một loại tên!
Toby Speight

@TobySpeight Một lý do tốt? Không, tôi nghi ngờ nó. Các dấu ngoặc đơn là thói quen của tôi, bởi vì tôi không thể bận tâm để nhớ liệu đó có phải là đối tượng hoặc loại tên mà họ yêu cầu hay không, nhưng nhiều lập trình viên bỏ chúng khi họ có thể. (Đối với tôi, đó là vấn đề sở thích và phong cách cá nhân, và một vấn đề nhỏ ở đó.)
Steve Summit

+1 để sử dụng sscanflàm công cụ chuyển đổi nhưng thu thập (và có thể xoa bóp) đầu vào bằng một công cụ khác. Nhưng có lẽ đáng nói getlinetrong bối cảnh taht.
dmckee --- ex-moderator mèo con

Khi bạn nói về " fscanfphiền toái thực tế", ý bạn là fgetsgì? Và phiền toái # 3 thực sự làm tôi khó chịu, đặc biệt là khi scanftrả về một con trỏ vô dụng cho bộ đệm thay vì trả về số lượng ký tự đầu vào (điều này sẽ giúp loại bỏ dòng mới sạch hơn nhiều).
supercat

1
Cảm ơn đã giải thích về sizeofphong cách của bạn . Đối với tôi, việc ghi nhớ khi bạn nhận được các parens rất dễ: Tôi nghĩ (type)giống như một diễn viên không có giá trị (vì chúng tôi chỉ quan tâm đến loại hình). Một điều khác: bạn nói rằng strtok(line, "\n")không phải lúc nào cũng hoạt động, nhưng nó không rõ ràng khi nó có thể không. Tôi đoán bạn đang nghĩ về trường hợp dòng dài hơn bộ đệm, vì vậy chúng tôi không có dòng mới và strtok()trả về null? Thật đáng tiếc thực sự fgets()không trả lại một giá trị hữu ích hơn để chúng ta có thể biết liệu dòng mới có ở đó hay không.
Toby Speight

7

Tôi có thể sử dụng gì để phân tích cú pháp đầu vào thay vì scanf?

Thay vì scanf(some_format, ...), hãy cân nhắc fgets()vớisscanf(buffer, some_format_and %n, ...)

Bằng cách sử dụng " %n", mã có thể đơn giản phát hiện xem tất cả các định dạng đã được quét thành công hay chưa và cuối cùng không có rác ngoài không gian trắng.

// scanf("%d %f fred", &some_int, &some_float);
#define EXPECTED_LINE_MAX 100
char buffer[EXPECTED_LINE_MAX * 2];  // Suggest 2x, no real need to be stingy.

if (fgets(buffer, sizeof buffer, stdin)) {
  int n = 0;
  // add ------------->    " %n" 
  sscanf(buffer, "%d %f fred %n", &some_int, &some_float, &n);
  // Did scan complete, and to the end?
  if (n > 0 && buffer[n] == '\0') {
    // success, use `some_int, some_float`
  } else {
    ; // Report bad input and handle desired.
  }

6

Hãy nêu các yêu cầu của phân tích cú pháp như:

  • đầu vào hợp lệ phải được chấp nhận (và chuyển đổi thành một số hình thức khác)

  • đầu vào không hợp lệ phải bị từ chối

  • khi bất kỳ đầu vào nào bị từ chối, cần cung cấp cho người dùng một thông điệp mô tả giải thích (rõ ràng "dễ hiểu bởi những người bình thường không phải là lập trình viên") tại sao nó bị từ chối (để mọi người có thể tìm ra cách khắc phục vấn đề)

Để giữ cho mọi thứ rất đơn giản, hãy xem xét phân tích một số nguyên thập phân đơn giản (được người dùng nhập vào) và không có gì khác. Những lý do có thể khiến đầu vào của người dùng bị từ chối là:

  • đầu vào chứa các ký tự không được chấp nhận
  • đầu vào đại diện cho một số thấp hơn mức tối thiểu được chấp nhận
  • đầu vào đại diện cho một số cao hơn mức tối đa được chấp nhận
  • đầu vào đại diện cho một số có phần phân số khác không

Chúng ta cũng xác định đúng "đầu vào chứa các ký tự không được chấp nhận"; và nói rằng:

  • khoảng trắng hàng đầu và khoảng trắng theo sau sẽ bị bỏ qua (ví dụ: "
    5" sẽ được coi là "5")
  • 0 hoặc một dấu thập phân được cho phép (ví dụ: "1234." và "1234.000" đều được xử lý giống như "1234")
  • phải có ít nhất một chữ số (ví dụ: "." bị từ chối)
  • không cho phép nhiều hơn một dấu thập phân (ví dụ: "1.2.3" bị từ chối)
  • dấu phẩy không nằm giữa các chữ số sẽ bị từ chối (ví dụ: ", 1234" bị từ chối)
  • dấu phẩy sau dấu thập phân sẽ bị từ chối (ví dụ: "1234.000.000" bị từ chối)
  • dấu phẩy sau khi dấu phẩy khác bị từ chối (ví dụ: "1, 234" bị từ chối)
  • tất cả các dấu phẩy khác sẽ bị bỏ qua (ví dụ: "1,234" sẽ được coi là "1234")
  • một dấu trừ không phải là ký tự không phải khoảng trắng đầu tiên bị từ chối
  • một dấu hiệu tích cực không phải là ký tự không phải khoảng trắng đầu tiên bị từ chối

Từ đó, chúng ta có thể xác định rằng cần có các thông báo lỗi sau:

  • "Ký tự không xác định khi bắt đầu nhập"
  • "Ký tự không xác định ở cuối đầu vào"
  • "Ký tự không xác định ở giữa đầu vào"
  • "Số quá thấp (tối thiểu là ....)"
  • "Số quá cao (tối đa là ....)"
  • "Số không phải là số nguyên"
  • "Quá nhiều điểm thập phân"
  • "Không có chữ số thập phân"
  • "Dấu phẩy xấu khi bắt đầu số"
  • "Dấu phẩy xấu ở cuối số"
  • "Dấu phẩy xấu ở giữa số"
  • "Dấu phẩy xấu sau dấu thập phân"

Từ thời điểm này, chúng ta có thể thấy rằng một hàm phù hợp để chuyển đổi một chuỗi thành một số nguyên sẽ cần phải phân biệt giữa các loại lỗi rất khác nhau; và rằng một cái gì đó như " scanf()" hoặc " atoi()" hoặc " strtoll()" hoàn toàn vô dụng và hoàn toàn vô giá trị vì chúng không cung cấp cho bạn bất kỳ dấu hiệu nào về lỗi sai với đầu vào (và sử dụng định nghĩa hoàn toàn không liên quan và không phù hợp về những gì không / không hợp lệ đầu vào").

Thay vào đó, hãy bắt đầu viết một cái gì đó không vô dụng:

char *convertStringToInteger(int *outValue, char *string, int minValue, int maxValue) {
    return "Code not implemented yet!";
}

int main(int argc, char *argv[]) {
    char *errorString;
    int value;

    if(argc < 2) {
        printf("ERROR: No command line argument.\n");
        return EXIT_FAILURE;
    }
    errorString = convertStringToInteger(&value, argv[1], -10, 2000);
    if(errorString != NULL) {
        printf("ERROR: %s\n", errorString);
        return EXIT_FAILURE;
    }
    printf("SUCCESS: Your number is %d\n", value);
    return EXIT_SUCCESS;
}

Để đáp ứng các yêu cầu đã nêu; này convertStringToInteger()chức năng là khả năng kết thúc được vài trăm dòng mã tất cả của chính nó.

Bây giờ, đây chỉ là "phân tích một số nguyên thập phân đơn giản". Hãy tưởng tượng nếu bạn muốn phân tích một cái gì đó phức tạp; như một danh sách các cấu trúc "tên, địa chỉ đường phố, số điện thoại, địa chỉ email"; hoặc có thể giống như một ngôn ngữ lập trình. Đối với những trường hợp này, bạn có thể cần phải viết hàng ngàn dòng mã để tạo một phân tích không phải là một trò đùa bị tê liệt.

Nói cách khác...

Tôi có thể sử dụng gì để phân tích cú pháp đầu vào thay vì scanf?

Tự viết (có khả năng hàng ngàn dòng) mã cho phù hợp với yêu cầu của bạn.


5

Dưới đây là một ví dụ về việc sử dụng flexđể quét một đầu vào đơn giản, trong trường hợp này là một tệp gồm các số dấu phẩy động ASCII có thể ở định dạng US ( n,nnn.dd) hoặc European ( n.nnn,dd). Điều này chỉ được sao chép từ một chương trình lớn hơn nhiều, vì vậy có thể có một số tài liệu tham khảo chưa được giải quyết:

/* This scanner reads a file of numbers, expecting one number per line.  It  */
/* allows for the use of European-style comma as decimal point.              */

%{
  #include <stdlib.h>
  #include <stdio.h>
  #include <string.h>
  #ifdef WINDOWS
    #include <io.h>
  #endif
  #include "Point.h"

  #define YY_NO_UNPUT
  #define YY_DECL int f_lex (double *val)

  double atofEuro (char *);
%}

%option prefix="f_"
%option nounput
%option noinput

EURONUM [-+]?[0-9]*[,]?[0-9]+([eE][+-]?[0-9]+)?
NUMBER  [-+]?[0-9]*[\.]?[0-9]+([eE][+-]?[0-9]+)?
WS      [ \t\x0d]

%%

[!@#%&*/].*\n

^{WS}*{EURONUM}{WS}*  { *val = atofEuro (yytext); return (1); }
^{WS}*{NUMBER}{WS}*   { *val = atof (yytext); return (1); }

[\n]
.


%%

/*------------------------------------------------------------------------*/

int scan_f (FILE *in, double *vals, int max)
{
  double *val;
  int npts, rc;

  f_in = in;
  val  = vals;
  npts = 0;
  while (npts < max)
  {
    rc = f_lex (val);

    if (rc == 0)
      break;
    npts++;
    val++;
  }

  return (npts);
}

/*------------------------------------------------------------------------*/

int f_wrap ()
{
  return (1);
}

-5

Các câu trả lời khác cung cấp các chi tiết cấp thấp phù hợp, vì vậy tôi sẽ giới hạn bản thân ở cấp độ cao hơn: Đầu tiên, hãy phân tích xem bạn mong đợi mỗi dòng đầu vào trông như thế nào. Hãy thử mô tả đầu vào bằng một cú pháp chính thức - với may mắn, bạn sẽ thấy nó có thể được mô tả bằng cách sử dụng một ngữ pháp thông thường hoặc ít nhất là một ngữ pháp không ngữ cảnh . Nếu một ngữ pháp thông thường đủ, thì bạn có thể mã hóa một máy trạng thái hữu hạntại đó nhận ra và giải thích từng ký tự một dòng lệnh. Mã của bạn sau đó sẽ đọc một dòng (như được giải thích trong các câu trả lời khác), sau đó quét các ký tự trong bộ đệm thông qua máy trạng thái. Tại một số trạng thái nhất định, bạn dừng và chuyển đổi chuỗi con được quét cho đến nay thành một số hoặc bất cứ điều gì. Bạn có thể có thể 'cuộn của riêng bạn' nếu nó đơn giản; nếu bạn thấy bạn cần một ngữ pháp hoàn toàn không có ngữ cảnh, bạn nên tìm ra cách sử dụng các công cụ phân tích cú pháp hiện có (re: lexyacccác biến thể của chúng).


Một máy trạng thái hữu hạn có thể là quá mức cần thiết; cách dễ dàng hơn để phát hiện tràn trong chuyển đổi (chẳng hạn như kiểm tra nếu errno == EOVERFLOWsau khi sử dụng strtoll) là có thể.
SS Anne

1
Tại sao bạn lại viết mã cho máy trạng thái hữu hạn của riêng bạn, khi flex làm cho việc viết chúng trở nên đơn giản?
jamesqf
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.