sprintf() 精度 .16 错误
sprintf() precision .16 bug
我必须为大学做一个项目,所以我选择编写一个 calculator/equation 解析器。
整个程序(没有(语法)错误处理):
click here
我遇到了一个错误,该错误仅在我将 sprintf() 调用中的精度设置为 16 时出现。任何精度高于或低于 16 时一切正常,在调试模式下也可以正常工作。
导致我的程序崩溃的表达式:
12/(-9)+3 ,但 12/(-9) 工作正常
(1)+(2)+(3) 和类似的东西
代码的作用如下:
-它在用户输入的字符串
中查找 "upper" 括号对
-> j: 括号内第一个字符的位置
-> i: 括号后第一个字符的位置
-用相同的函数递归计算括号的内部,直到没有更多的一对括号
-调用另一个函数将此计算转换为 double
-return函数的值是递归调用字符串[0到j-2]##result_str[j到i-2]##string[i到结束]的串联
这是代码(我上面描述的重要部分一直在最后):
#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]= '[=10=]';
}
strncpy(dest, src + offset, len); //len Zeichen werden aus s ab offset in t kopiert
dest[len]= '[=10=]';
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]= '[=10=]';
return calcStr(t) + (calcStr(s+len_first));
case '-':
t[len_first-1]= '[=10=]';
return calcStr(t) - (calcStr(s+len_first));
case '*':
t[len_first-1]= '[=10=]';
return calcStr(t) * (calcStr(s+len_first));
case '/':
t[len_first-1]= '[=10=]';
return calcStr(t) / (calcStr(s+len_first));
case '^':
t[len_first-1]= '[=10=]';
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]= '[=10=]';
return calcStr(t) + (calcStr(s+len_first));
case '-':
t[len_first-1]= '[=10=]';
return calcStr(t) - (calcStr(s+len_first));
case '*':
t[len_first-1]= '[=10=]';
return calcStr(t) * (calcStr(s+len_first));
case '/':
t[len_first-1]= '[=10=]';
return calcStr(t) / (calcStr(s+len_first));
case '^':
t[len_first-1]= '[=10=]';
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.: 如果有人知道如何避免将 double 重新转换为字符串,请说出来。
您 calloc
在这里占用了大量内存,有时没有注意正确的尺寸。此外,如果您在堆栈上分配一个字符串 s
,所有早期 returns à 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 可以轻松解析表达式,这将轻松解析运算符优先级和括号。
我必须为大学做一个项目,所以我选择编写一个 calculator/equation 解析器。
整个程序(没有(语法)错误处理): click here
我遇到了一个错误,该错误仅在我将 sprintf() 调用中的精度设置为 16 时出现。任何精度高于或低于 16 时一切正常,在调试模式下也可以正常工作。
导致我的程序崩溃的表达式:
12/(-9)+3 ,但 12/(-9) 工作正常
(1)+(2)+(3) 和类似的东西
代码的作用如下:
-它在用户输入的字符串
中查找 "upper" 括号对-> j: 括号内第一个字符的位置
-> i: 括号后第一个字符的位置
-用相同的函数递归计算括号的内部,直到没有更多的一对括号
-调用另一个函数将此计算转换为 double
-return函数的值是递归调用字符串[0到j-2]##result_str[j到i-2]##string[i到结束]的串联
这是代码(我上面描述的重要部分一直在最后):
#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]= '[=10=]';
}
strncpy(dest, src + offset, len); //len Zeichen werden aus s ab offset in t kopiert
dest[len]= '[=10=]';
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]= '[=10=]';
return calcStr(t) + (calcStr(s+len_first));
case '-':
t[len_first-1]= '[=10=]';
return calcStr(t) - (calcStr(s+len_first));
case '*':
t[len_first-1]= '[=10=]';
return calcStr(t) * (calcStr(s+len_first));
case '/':
t[len_first-1]= '[=10=]';
return calcStr(t) / (calcStr(s+len_first));
case '^':
t[len_first-1]= '[=10=]';
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]= '[=10=]';
return calcStr(t) + (calcStr(s+len_first));
case '-':
t[len_first-1]= '[=10=]';
return calcStr(t) - (calcStr(s+len_first));
case '*':
t[len_first-1]= '[=10=]';
return calcStr(t) * (calcStr(s+len_first));
case '/':
t[len_first-1]= '[=10=]';
return calcStr(t) / (calcStr(s+len_first));
case '^':
t[len_first-1]= '[=10=]';
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.: 如果有人知道如何避免将 double 重新转换为字符串,请说出来。
您 calloc
在这里占用了大量内存,有时没有注意正确的尺寸。此外,如果您在堆栈上分配一个字符串 s
,所有早期 returns à 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 可以轻松解析表达式,这将轻松解析运算符优先级和括号。