【问题标题】:Very strange behavior with extremely simple plain C non-negative integer parser非常简单的普通 C 非负整数解析器的非常奇怪的行为
【发布时间】:2020-01-10 14:55:30
【问题描述】:

作为一段更大的代码的一部分,我编写了一组函数来扫描文件、跳过空格并将任何非负整数读取到数组中。问题是,当调用空格跳过代码时,如果我不使用 any 参数调用 printf(),则代码无法产生正确的输出。我已将问题隔离到eat_whitespace() 函数。

在我的代码中,我调用了 fseek(),并且我尝试了与该函数调用等效的各种变体,但是它们都没有帮助。我可以用 printf(NULL) 编译我的代码,但是,这似乎是一个巨大的问题。此外,我遇到了奇怪的行为,其中使用 kludge 的工作代码产生不正确的输出,但是当再次执行时,输出又恢复正确。这似乎表明我的代码没有正确关闭文件句柄,但确实如此,而且我已经验证它确实如此。真不知道是什么问题。

//#define KLUDGE1
//#define KLUDGE2

int is_ws_char (char c) {
    char ws_chars[4] = {'\n', '\t', ' ', '\r'};
    int detected_ws_char = 0;
    for (int i = 0; i < 4; i++) {
        if (c == ws_chars[i]) {
            detected_ws_char = 1;
            break;
        }
    }
    return(detected_ws_char);
}

int eat_whitespace(FILE *data) {
    char c;

    while (fread(&c, sizeof(c), 1, data) == 1) {
        //Problem lies with the following code...
        //Seems that you need, for some strange reason, to issue a 
        //printf for the code to work... 

#ifdef KLUDGE1
        printf(NULL);
#endif

        if (!is_ws_char(c)) {
#ifdef KLUDGE2
            printf(NULL);
#endif
            fseek(data, -1L, SEEK_CUR);
            return(NOT_WHITESPACE);
        }
    }
    return(END);
}

预期结果将是返回正确结果的数字解析代码。例如,this 输入应该产生以下输出,而不必使用 printf(NULL) "fix":

42
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
100
100
1001
2222
2002
3333
1
2
3
4
6
5
7
9
8
10
11
1
2
3
4
5
6
7
8
9
10
11

当不使用 printf(NULL) kludge 时,会生成以下错误输出:

42
12
22
32
42
52
62
72
82
92
10
11
12
13
14
15
16
17
18
19
20
100
1000
1001
2222
2002
3333
13
23
33
43
63
53
73
93
83
10
11
1
20
30
40
54
6
7
80
9
10
11

编辑:我已经将数字解析函数分解成他们自己的小测试程序,我将在下面粘贴

#include <stdio.h>
#include <stdlib.h>

#define END -1
#define NOT_WHITESPACE -2
#define NOT_NUMBER -2
#define NUM_SIZE_EXCEEDED -3
#define MAX_NUM_SIZE 257
//#define DEBUG1
//#define DEBUG2
//#define KLUDGE1
#define KLUDGE2

int is_ws_char (char c) {
    char ws_chars[4] = {'\n', '\t', ' ', '\r'};
    int detected_ws_char = 0;
    for (int i = 0; i < 4; i++) {
        if (c == ws_chars[i]) {
            detected_ws_char = 1;
            break;
        }
    }
    return(detected_ws_char);
}

int eat_whitespace(FILE *data) {
    char c;

    while (fread(&c, sizeof(c), 1, data) == 1) {

        //Problem lies with the following code...
        //Seems that you need, for some strange reason, to issue a printf to stdout, with *any* argument eg. NULL for
        //the code to work...   
#ifdef KLUDGE1
        printf(NULL);
#endif

        if (!is_ws_char(c)) {
#ifdef KLUDGE2
            printf(NULL);
#endif
            fseek(data, -1L, SEEK_CUR);
            return(NOT_WHITESPACE);
        }
    }
    return(END);
}

int eat_number(FILE* data) {
    char c;
    char number_string[MAX_NUM_SIZE];
    int num_size = 0;
    int number;
    int chars_read;
    int token_type; 
    token_type = eat_whitespace(data);
    int digit_detected = 0;
    char digits[10] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
    if (token_type == END) {
        return(END);
    }
    while(1) {
        if (num_size >= MAX_NUM_SIZE) {
            return(NUM_SIZE_EXCEEDED);
        }
        chars_read = (int) fread((void *) &c, sizeof(char), 1, data);
        if (!chars_read) {
            return(END);
        }
        else {
            for (int i = 0; i < 10; i++) {
                if (digits[i] == c) {
                    digit_detected = 1;
                    number_string[num_size] = c;
                    num_size++;
                    break;
                }
            }
            if(digit_detected) {
                digit_detected = 0;
                continue;
            }
            else {
                if (is_ws_char(c)) {
                    fseek(data, -1L, SEEK_CUR);
                    number_string[(num_size+1)] = (char) 0x0;
                    number = atoi(number_string);
                    return(number);
                }
                else {
                    fprintf(stderr, "Invalid character : %c", c);
                    return(NOT_NUMBER);
                }
            }
        }
    }
}


int main(int argc, char **argv) {

    int token_type = 0;

    int n1 = 0;

    char *input_file_name = (*(argv + 1));

    FILE *input_file = fopen(input_file_name, "rb");

    if (input_file == NULL) {
        fprintf(stderr, "Can't open input file : %s\n", input_file_name);
        return(1);
    }

    while (1) {
        token_type = eat_whitespace(input_file);
        if (token_type == END) {
            fclose(input_file);
            return(0);
        } 
        else if (token_type == NOT_WHITESPACE ) {
            n1 = eat_number(input_file);
            switch(n1) {
                case NOT_NUMBER : {
                    fclose(input_file);
                    fprintf(stderr, "Invalid number...\n");
                    return(1);
                }
                case NUM_SIZE_EXCEEDED : {
                    fprintf(stderr, "Exceeded maximum size of number string, which is 255 characters...\n");
                    fclose(input_file);
                    return(1);
                }
                default : {
                    if (n1 >= 0) {
                        printf("%i\n", n1);
                        break;
                    }
                    else {
                        fprintf(stderr, "Unknown error parsing number...\n");
                        fclose(input_file);
                        return(1);
                    }
                }
            }
        }
        else {
            fclose(input_file);
            fprintf(stderr, "Uknown error skipping whitespace...\n");
            return(1);
        }       

    }

    fclose(input_file);

    return(0);
}

它可能不漂亮,但你可以用例如编译它。 gcc -g -Wall -Wpedantic -O0 number_parser_test.c -o number_parser_test

编辑#2:我已将输入粘贴到下面的小测试程序中。我不知道标签是否会被保留,为什么我链接到 pastebin 上的相同文本

42   1  2 3    4 5 6 7 8 9 10 11 12 13 14 15 16  17 18      19  20
     00100 100  1001

    2222 2002
            3333




 1  2  3  4  6 5 7 9    8 10 11

00000001
02
0003
04
5
06
00007
8
09
010
000011

编辑#3:我可以用来演示问题的最小输入是上述文本的第一行,包含数字 42,然后是数字 1 到 20,包括空格和制表符。

【问题讨论】:

标签: c parsing null printf stdio


【解决方案1】:

问题中代码的主要问题(截至revision 4)是null终止字符串的代码在字符串中执行了一个字符太远,因此在数字之后有垃圾要转换孤立。在函数eat_number()中,一行:

number_string[(num_size+1)] = (char) 0x0;

太迟写一个字节;你应该有:

number_string[num_size] = '\0';

如果你考虑一下当你有num_size == 1 时几乎退化的情况,你需要将空字节放在number_string[0] 中的数字之后,即number_string[1]number_string[num_size]

这段代码展示了我是如何调试它的——使用打印语句(用// cmets 注释掉)。他们为我指明了正确的方向,尤其是//fprintf(stderr, "cvt=[%s]", number_string);。在尝试调试代码之前,我删除了您的 cmets 和标记为 kludges 的部分。

#include <stdio.h>
#include <stdlib.h>

#define END -1
#define NOT_WHITESPACE -2
#define NOT_NUMBER -2
#define NUM_SIZE_EXCEEDED -3
#define MAX_NUM_SIZE 257

static int is_ws_char(char c)
{
    char ws_chars[4] = {'\n', '\t', ' ', '\r'};
    int detected_ws_char = 0;
    for (int i = 0; i < 4; i++)
    {
        if (c == ws_chars[i])
        {
            detected_ws_char = 1;
            break;
        }
    }
    return(detected_ws_char);
}

static int eat_whitespace(FILE *data)
{
    char c;

    while (fread(&c, sizeof(c), 1, data) == 1)
    {
        //fprintf(stderr, "W(%d=%c)", c, c);
        if (!is_ws_char(c))
        {
            fseek(data, -1L, SEEK_CUR);
            return(NOT_WHITESPACE);
        }
    }
    return(END);
}

static int eat_number(FILE *data)
{
    char c;
    char number_string[MAX_NUM_SIZE];
    int num_size = 0;
    int number;
    int chars_read;
    int token_type;
    int digit_detected = 0;
    token_type = eat_whitespace(data);
    char digits[10] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
    if (token_type == END)
    {
        return(END);
    }
    while (1)
    {
        if (num_size >= MAX_NUM_SIZE)
        {
            return(NUM_SIZE_EXCEEDED);
        }
        chars_read = (int) fread((void *) &c, sizeof(char), 1, data);
        //fprintf(stderr, "N(%d=%c)", c, c);
        if (!chars_read)
        {
            return(END);
        }
        else
        {
            for (int i = 0; i < 10; i++)
            {
                if (digits[i] == c)
                {
                    digit_detected = 1;
                    number_string[num_size] = c;
                    num_size++;
                    break;
                }
            }
            if (digit_detected)
            {
                digit_detected = 0;
                continue;
            }
            else
            {
                if (is_ws_char(c))
                {
                    fseek(data, -1L, SEEK_CUR);
                    number_string[num_size] = '\0';
                    //number_string[(num_size + 1)] = (char) 0x0;
                    //fprintf(stderr, "cvt=[%s]", number_string);
                    number = atoi(number_string);
                    return(number);
                }
                else
                {
                    fprintf(stderr, "Invalid character : %c", c);
                    return(NOT_NUMBER);
                }
            }
        }
    }
}

int main(int argc, char **argv)
{
    int token_type = 0;

    int n1 = 0;

    if (argc != 2)
    {
        fprintf(stderr, "Usage: %s filename\n", argv[0]);
        return 1;
    }
    char *input_file_name = (*(argv + 1));

    FILE *input_file = fopen(input_file_name, "rb");

    if (input_file == NULL)
    {
        fprintf(stderr, "Can't open input file : %s\n", input_file_name);
        return(1);
    }

    while (1)
    {
        token_type = eat_whitespace(input_file);
        if (token_type == END)
        {
            fclose(input_file);
            return(0);
        }
        else if (token_type == NOT_WHITESPACE)
        {
            n1 = eat_number(input_file);
            switch (n1)
            {
            case NOT_NUMBER:
                fclose(input_file);
                fprintf(stderr, "Invalid number...\n");
                return(1);

            case NUM_SIZE_EXCEEDED:
                fprintf(stderr, "Exceeded maximum size of number string, which is 255 characters...\n");
                fclose(input_file);
                return(1);

            default:
                if (n1 >= 0)
                {
                    printf("%i\n", n1);
                    break;
                }
                else
                {
                    fprintf(stderr, "Unknown error parsing number...\n");
                    fclose(input_file);
                    return(1);
                }
            }
        }
        else
        {
            fclose(input_file);
            fprintf(stderr, "Uknown error skipping whitespace...\n");
            return(1);
        }
    }

    fclose(input_file);

    return(0);
}

代码还有大量其他问题,但您似乎在某些规则下工作,这些规则不允许您使用 fscanf()&lt;ctype.h&gt; 或 @987654336 中的函数完成工作@。在大多数情况下,我不打算进一步剖析代码,因为似乎有这些要求。

我确实注意到检测字符是否为数字的循环是不寻常的。 C 标准要求代码集支持10 sequential code points for the digits '0' to '9',因此无论代码集如何,您都可以安全地使用这样的符号:

char c = …;
int value;

if (c >= '0' && c <= '9')
    value = c - '0';

c 中的字符转换为value 中的整数0..9。或者,如果你可以使用&lt;ctype.h&gt;中的函数,那么:

if (isdigit((unsigned char)c))
    value = c - '0';

如果编译器将纯 char 实现为带符号类型,则需要强制转换,这通常是使用 Intel 处理器(x86 等)完成的方式。 (有关投射需要的更多信息,请参阅 C11 §7.4 Character handling &lt;ctype.h&gt; ¶1。)

给定数据文件(无制表符;单行,末尾有换行符):

42   1  2 3    4 5 6 7 8 9 10 11 12 13 14 15 16  17 18      19  20

上面代码的输出(没有调试打印)是:

42
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

这是预期的。有了错误和调试代码,输出(来自程序io37 编译自io37.c)看起来像:

$ io37 data
W(52=4)W(52=4)N(52=4)N(50=2)N(32= )cvt=[42?]42
W(32= )W(32= )W(32= )W(49=1)W(49=1)N(49=1)N(32= )cvt=[1?]1
W(32= )W(32= )W(50=2)W(50=2)N(50=2)N(32= )cvt=[2?]2
W(32= )W(51=3)W(51=3)N(51=3)N(32= )cvt=[3?]3
W(32= )W(32= )W(32= )W(32= )W(52=4)W(52=4)N(52=4)N(32= )cvt=[4?]4
W(32= )W(53=5)W(53=5)N(53=5)N(32= )cvt=[5?]5
W(32= )W(54=6)W(54=6)N(54=6)N(32= )cvt=[6?]6
W(32= )W(55=7)W(55=7)N(55=7)N(32= )cvt=[7?]7
W(32= )W(56=8)W(56=8)N(56=8)N(32= )cvt=[8?]8
W(32= )W(57=9)W(57=9)N(57=9)N(32= )cvt=[9?]9
W(32= )W(49=1)W(49=1)N(49=1)N(48=0)N(32= )cvt=[10?]10
W(32= )W(49=1)W(49=1)N(49=1)N(49=1)N(32= )cvt=[11?]11
W(32= )W(49=1)W(49=1)N(49=1)N(50=2)N(32= )cvt=[12?]12
W(32= )W(49=1)W(49=1)N(49=1)N(51=3)N(32= )cvt=[13?]13
W(32= )W(49=1)W(49=1)N(49=1)N(52=4)N(32= )cvt=[14?]14
W(32= )W(49=1)W(49=1)N(49=1)N(53=5)N(32= )cvt=[15?]15
W(32= )W(49=1)W(49=1)N(49=1)N(54=6)N(32= )cvt=[16?]16
W(32= )W(32= )W(49=1)W(49=1)N(49=1)N(55=7)N(32= )cvt=[17?]17
W(32= )W(49=1)W(49=1)N(49=1)N(56=8)N(32= )cvt=[18?]18
W(32= )W(32= )W(32= )W(32= )W(32= )W(32= )W(49=1)W(49=1)N(49=1)N(57=9)N(32= )cvt=[19?]19
W(32= )W(32= )W(50=2)W(50=2)N(50=2)N(48=0)N(10=
)cvt=[20?]20
W(10=
)$

这将虚假字符显示为?,尽管我在不同版本的代码中遇到了各种奇怪的字符。 ? 允许代码出现以正确转换数字。最后一个输出没有被换行符终止,所以我的提示符(显示为$)出现在)之后。

稍有不同的代码版本有如下几行:

(32= )(49=1)(49=1)(49=1)(49=1)(32= )cvt=[11H]11
(32= )(49=1)(49=1)(49=1)(50=2)(32= )cvt=[12H]12
(32= )(49=1)(49=1)(49=1)(51=3)(32= )cvt=[13H]13

它也转换为 OK(H 不是数字),但更清楚地显示了不正确的空终止问题。未定义行为的缺点之一是无法预测额外位置中的内容。

修复已激活但调试代码已到位,输出的相应部分如下所示:

W(32= )W(49=1)W(49=1)N(49=1)N(49=1)N(32= )cvt=[11]11
W(32= )W(49=1)W(49=1)N(49=1)N(50=2)N(32= )cvt=[12]12
W(32= )W(49=1)W(49=1)N(49=1)N(51=3)N(32= )cvt=[13]13

cvt 字符串现在显然是纯数字的,不受编译器一时兴起的影响。

【讨论】:

    【解决方案2】:

    我发现了问题,这是由以下原因产生的一个错误:

    number_string[(num_size+1)] = (char) 0x0;
    

    应该是:

    number_string[num_size] = (char) 0x0;
    

    jdb2

    【讨论】:

      猜你喜欢
      • 2019-04-26
      • 1970-01-01
      • 2018-04-02
      • 1970-01-01
      • 1970-01-01
      • 2016-03-13
      • 2011-03-01
      • 1970-01-01
      • 2012-11-18
      相关资源
      最近更新 更多