【问题标题】:sprintf() precision .16 bugsprintf() 精度 .16 错误
【发布时间】:2015-05-30 06:33:27
【问题描述】:

我必须为大学做一个项目,所以我选择编写一个计算器/方程解析器。

整个程序(没有(语法)错误处理): click here

我遇到了一个错误,该错误仅在我将 sprintf() 调用中的精度设置为 16 时发生。在任何高于或低于任何精度的情况下都可以正常工作,并且 16 在调试模式下也可以正常工作。

导致我的程序崩溃的表达式:

12/(-9)+3 ,但 12/(-9) 工作正常

(1)+(2)+(3) 和类似的东西


下面是代码的作用:

-它在用户输入的字符串中查找“上部”括号对

-> j: 括号内第一个字符的位置

-> i: 括号后第一个字符的位置

-用相同的函数递归计算括号的内部,直到没有更多的括号对

-调用另一个函数将这个计算转换成双精度

-函数的返回值是递归调用string[0 to j-2]##result_str[j to i-2]##string[i to end]


这是代码(我上面描述的重要部分一直在最后):

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <math.h>
#define LENGTH(x)  (sizeof(x) / sizeof(x[0]))

int hasPar (char *); //Überprüft, ob Klammern im String sind und gibt Position der ersten Klammer zurück
char* subStr (char* , char* , int , int ); //Funktion, die Substring zurückgibt
double calcStr(char *); //Wertet Terme ohne Klammern aus
double calc (char *); //Rekursive Berechnung des Terms


int main(/*int argc, char *argv[]*/) {
    char *s/*, c*/;
    double ergebnis;

    do {
        puts("Taschenrechner. Ignoriert alles, au\xe1""er 0-9,.,+,-,*,/,^,(,)");

        fflush(stdin);
        s= calloc(100,1);
        scanf("%99[^\n]", s); //[^\n] bedeutet, dass alle Zeichen außer Zeilenumbruch eingelesen werden sollen

        printf("Erkannter Ausdruck:\n%s\n", s);

        ergebnis= calc(s);  
        printf("Berechnetes Ergebnis:\n%f\n", ergebnis);


        puts("Erfolg!");;
        free(s);
        fflush(stdin);
        //if (getc(stdin) == 'c') {break;}
    } while (0);

    return 0;
}




int hasPar (char *s) {
    for (unsigned int k=0; k<strlen(s); k++) { //k verwendet, da mit i Interferenzen mit calc() aufgetreten sind
        if ( s[k] == '(' ) {
            return k;
        }
    }

    return (-1);
}



char* subStr (char* dest, char* src, int offset, int len) {
    int input_len = strlen (src);

    if ( offset+len > input_len ) { //Wenn Substring größer sein sollte als Usprungsstring oder Substring Null Zeichen enthalten soll
        return NULL;
    } else if (len <= 0) {
        dest[0]= '\0';
    }

    strncpy(dest, src + offset, len); //len Zeichen werden aus s ab offset in t kopiert
    dest[len]= '\0';
    return dest;
}



double calcStr (char *s) {
    char *t, *t_first;
    int len_s= strlen(s), len_first=0;
    t= calloc(len_s, sizeof(char)); //Kopie von s zum Arbeiten erstellen, Schritt 1
    strcpy(t,s); //Kopie von s zum Arbeiten erstellen, Schritt 2

    //ACHTUNG: REIHENFOLGE WICHTIG FÜR KORREKTE ANWENDUNG VON RECHENREGELN

    if(t[0] != '+') { //+ als unärer Operator
        t_first= strtok(t,"+"); //String auf + prüfen, Nach Ausführung von strtok: t_first: String bis exklusiv +
        len_first=strlen(t_first); //Länge des Ergebnisstrings berechnen zum Vergleich mit Länge des Ursprungsstrings
        if (len_first != len_s) { //Wenn Länge gleich, dann ist hier auch der Inhalt gleich, also kein Plus enthalten
            switch ( t_first[len_first-1] ) { //Wenn + unär, also vor dem + ein anderes OpSym
                case '+':
                    t[len_first-1]= '\0';
                    return calcStr(t) + (calcStr(s+len_first));
                case '-':
                    t[len_first-1]= '\0';
                    return calcStr(t) - (calcStr(s+len_first));
                case '*':
                    t[len_first-1]= '\0';
                    return calcStr(t) * (calcStr(s+len_first));
                case '/':
                    t[len_first-1]= '\0';
                    return calcStr(t) / (calcStr(s+len_first));
                case '^':
                    t[len_first-1]= '\0';
                    return pow(calcStr(t), (calcStr(s+len_first)));
                default:
                    return calcStr(t_first) + (calcStr(s+len_first+1)); //Rekursives Aufrufen der Strings links und rechts des Operationszeichens
            }
        }
    }

    strcpy(t,s); //da t bei Überprüfung auf + verändert wurde, Wiederherstellung der Arbeitskopie aus Ursprungsstring
    if(t[0] != '-') { //- als unärer Operator
        t_first= strtok(t,"-"); //analog oben
        len_first=strlen(t_first); 
        if (len_first != len_s) {
            switch ( t_first[len_first-1] ) {
                case '+':
                    t[len_first-1]= '\0';
                    return calcStr(t) + (calcStr(s+len_first));
                case '-':
                    t[len_first-1]= '\0';
                    return calcStr(t) - (calcStr(s+len_first));
                case '*':
                    t[len_first-1]= '\0';
                    return calcStr(t) * (calcStr(s+len_first));
                case '/':
                    t[len_first-1]= '\0';
                    return calcStr(t) / (calcStr(s+len_first));
                case '^':
                    t[len_first-1]= '\0';
                    return pow(calcStr(t), (calcStr(s+len_first)));
                default:
                    return calcStr(t_first) + (calcStr(s+len_first+1)); //Rekursives Aufrufen der Strings links und rechts des Operationszeichens
            }
        }
    }

    strcpy(t,s);
    t_first= strtok(t,"*");
    len_first=strlen(t_first);
    if (len_first != len_s) {
        return calcStr(t_first) * (calcStr(s+len_first+1));
    }

    strcpy(t,s); //analog
    t_first= strtok(t,"/");
    len_first=strlen(t_first);
    if (len_first != len_s) {
        return calcStr(t_first) * (1 / calcStr(s+len_first+1));
    }

    strcpy(t,s);
    t_first= strtok(t,"^");
    len_first=strlen(t_first);
    if (len_first != len_s) {
        if (t_first[len_first-1]=='e') {
            if (t_first[0]=='-') {
                return (-1)*exp(calcStr(s+len_first+1));
            } else if (t_first[0]=='+') {
                return exp(calcStr(s+len_first+1));
            } else {
                return pow(calcStr(t_first), (calcStr(s+len_first+1)));
            }
        }
    }

    return atof(s); //String ist bei keinem Operationszeichen zerfallen => String ist Zahl ; atof castet string zu double (aus stdlib.h)

}


double calc (char *s) {
    double result_d=0.;
    char *t, *result_str;
    int check=1, i=hasPar(s), j=0;
    if ( i == (-1) ) {
        return calcStr(s);
    } else {
        j= ++i; //j=++i ist Position des Chars nach der ersten öffnenden Klammer
        while (check > 0) {
            if (s[i] == '(') {
                check++;
            } else if (s[i] == ')') {
                check--;
            }
            i++;
        }   //Bestimmen der Länge der "obersten" Klammer, i ist Position des ersten Zeichens nach der Klammer

        t= calloc (strlen(s), sizeof(char)); //string to store substring in
result_str= calloc (strlen(s), sizeof(char)); //string to store result of parantheses-calculation in
result_d= calc (subStr (result_str, s, j, i-j-1)); //call the function we're already in
sprintf (result_str, "%-.16f", result_d); //cast result back to string, this is where I think the crash is caused
return calc (strcat (subStr (t, s, 0, j-1), strcat (result_str, s+i))); //recursive call of concatenated string described as above
    }
}

-subStr(char *dest, char *src, int offset, int len) 只是一个使用strncpy() 进行错误处理的函数 calc() 是我们所在的函数

-s是用户给的*char

-result_d是括号内子串的计算结果(双变量)

-result_str 是存储result_d 类型转换的*char


我希望我没有忘记任何事情。如果需要更多代码或信息,请发表评论。我也可以提供 *.exe 来试用。

请记住:sprintf (result_str, "%.17f", result_d); 一切正常,所以我猜它不可能是数组越界(我认为)

P.S.:如果有人知道如何避免将双精度转换为字符串,请说出来。

【问题讨论】:

  • 注意:你不需要使用sizeof(char),使用1 就可以了,因为sizeof(char) 的定义总是1。
  • 没有足够的代码来猜测问题可能是什么,但请注意,在分配字符串时,您通常需要strlen(s) + 1 而不是strlen(s),因为您需要一个额外的字符作为终止符。
  • 请正确格式化您的代码。使其更具人类可读性。 :-)
  • 你确定缓冲区result_str足够大吗?
  • 您对分配的内存的缓冲区大小有很多错误计算。如果你想使用strcpy,你应该分配strlen(s) + 1来满足终止空字符。如果你想strcat,bussfe 必须能够同时保存两个字符串和一个终止符。如果要打印精度为 16 的浮点数,则需要 16 位数字,加上一个点,再加上一个可能的指数加上终止符。考虑使用内存检查运行您的代码,例如Valgrind,修复非法内存访问。

标签: c string printf double


【解决方案1】:

你在这里callocing 很多内存,有时不注意正确的尺寸。此外,如果您在堆栈上分配字符串 s,所有早期返回 à la return calc(s) 都会引入内存泄漏。

您使用 C99,那么为什么不使用可变长度数组 (VLA)?您分配的金额很小,很容易放入堆栈。您甚至可以尝试不分配任何东西,而是使用指向原始(只读)数组的开始和结束指针进行操作。对于将输入读入缓冲区,分配固定大小的内存与使用本地固定大小的缓冲区(如 s[100])相比没有优势。

无论如何,主要问题是您的calc,您在其中构造了一个新字符串,该字符串具有括号之前的部分、子表达式的结果和括号之后的部分。您对传递给calcStr 的子字符串使用相同的缓冲区,并以16 的精度打印结果。前者没问题,因为您分配了字符串长度。后者不行,因为缓冲区可能只是太短而无法打印 16 位数字,但该缓冲区还必须保存连接的结果,即原始字符串长度减去子字符串长度加上大约 20 个字符16位加点加零结束符。

您可以使用单个 snprintf 表达式来代替连接。使用可变字符串精度格式%.*s,您可以编写子字符串。

这是一个更好的calc,它可以慷慨地猜测尺寸并注意不泄漏临时缓冲区t

double calc(char *s)
{
    double result_d;
    char *t;
    int check = 1;
    int i = hasPar(s);
    int j = 0;

    if (i == -1) return calcStr(s);

    j = ++i;
    while (check > 0) {
        if (s[i] == '(') {
            check++;
        } else if (s[i] == ')') {
            check--;
        }
        i++;
    }

    t = calloc(strlen(s) + 32, sizeof(*t));
    result_d = calc(subStr(t, s, j, i - j - 1));

    snprintf(t, strlen(s) + 32, "%.*s%.16f%s",
        j - 1, s, result_d, s + i);

    result_d = calc(t);    
    free(t);

    return result_d;
}

您的代码中仍然存在一个内存错误:您忘记为calcStr 中的t 的空终止符分配内存,并且您还泄漏了该缓冲区。我建议一个 VLA:

double calcStr(char *s)
{
    char *t_first;
    int len_s = strlen(s), len_first = 0;
    char t[len_s + 1]; //
    strcpy(t, s);

    ...
}

最后,使用字符串连接和子字符串求值的计算器的实现很笨拙。 使用shunting-yard algorithm 可以轻松解析表达式,这将轻松解析运算符优先级和括号。

【讨论】:

  • 谢谢。这和您上面的最后一条评论是这里的第一个有用的东西。我没有尽我所能来这里。我现在试图了解Dr. Memory 的输出。当我理解您所写的所有内容时,我将接受答案。并感谢有关该算法的说明。我不知道。 (主要是因为我想自己做一些事情,所以我没有提前做研究)。该算法看起来更像是由专业人士完成的,而不是我的“拼图”实现 :-)而且没有人告诉我有关 VLA 的事情......再次,谢谢!
  • 好的,我从你的回答中学到了很多东西。我没有想到使用 snprintf 进行连接是一种耻辱。不幸的是,您重写的代码中仍然存在错误:它应该是result_d = calc(t),否则您将无法识别任何相同级别的括号。但是:我仍然不知道为什么这个错误只发生在精度 16 而不是更高(我尝试到 20 并且一切正常)。它不应该造成相同的泄漏/溢出吗?
  • 嗯。我可以尝试解释为什么它适用于较短的表达式12/(-9),但不适用于12/(-9) + 3。尽管您希望它甚至应该是短字符串的单词,您分配的地方更少。实际上,系统不会分配确切的内存量。它通常为您提供 2^n 或 2^n 减去一些家庭区域的内存块,因此这两种情况都会分配 16 个字符。用 16 个字符的字符串替换 (-9) 可能刚好适合第一个字符串的分配内存,但不适合第二个。
  • ... 即使您写超出正式分配的内存,这也可能是真的。这仍然不能解释为什么崩溃发生在精度为 16 时,而不是精度为 17 时。毕竟分配是一样的,只是输出可能更长。没有调试器很难找出答案。它也依赖于编译器和平台。 (这就是人们一直在谈论的臭名昭著的“未定义行为”。理论上,任何事情都可能发生。在实践中,通常有一个很好的解释为什么会发生。)
  • ... 对calc/calcStr 的失误感到抱歉。我只检查了你的(和更简单的)输入并专注于记忆的东西。我知道你想自己做这个项目——它让你思考问题,也给人一种成就感。你的方法也不全是坏的,只是字符串在 C 中使用起来很痛苦。(我曾经在 Pascal 中实现了一个类似于你的算法,但它有一个额外的标记化步骤,因此字符数组是一个标记数组。这使得更容易识别括号和占主导地位的运算符。)
猜你喜欢
  • 1970-01-01
  • 2015-09-08
  • 2019-01-24
  • 1970-01-01
  • 2023-03-27
  • 2014-01-30
  • 2020-04-27
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多