早些时候,我们对大型数学程序包(例如Scilab和Octave)进行了代码审查,而计算器作为小型实用程序而被搁置一旁,由于它们的小型代码而难以出错。 我们误以为没有注意他们。 Windows计算器的源代码发布的案例表明,每个人都对讨论隐藏在其中的错误感兴趣,并且那里有足够的错误来撰写有关该错误的文章。 我和我的同事决定检查一些流行的计算器的代码,结果发现Windows计算器的代码还不错(破坏器)。
引言
夸口! -通用的跨平台计算器。 它易于使用,但提供了复杂数学软件包中通常具有的功能和多功能性,以及满足日常需求(例如货币换算和利息计算)的有用工具。 该项目包含两个组件:
libqalculate (库和CLI)和
qalculate-gtk (GTK + UI)。 该研究仅涉及libqalculate代码。
为了更方便地将项目与我们最近检查过的Windows计算器进行比较,我给出了libqalculate的Cloc实用程序的输出:
从主观上讲,存在更多错误,并且比Windows计算器的代码中的错误更严重。 但我建议您通过阅读此代码回顾自己得出结论。
顺便说一下,这里是指向有关检查Microsoft计算器的文章的链接:“
计算Windows计算器中的错误。 ”
PVS-Studio被用作静态分析工具。 这是一组用于代码质量控制,搜索错误和潜在漏洞的解决方案。 支持的语言包括:C,C ++,C#和Java。 该分析仪可以在Windows,Linux和macOS上启动。
再次复制粘贴和错别字!
V523'then '语句等效于'else'语句。 编号.cc 4018
bool Number::square() { .... if(mpfr_cmpabs(i_value->internalLowerFloat(), i_value->internalUpperFloat()) > 0) { mpfr_sqr(f_tmp, i_value->internalLowerFloat(), MPFR_RNDU); mpfr_sub(f_rl, f_rl, f_tmp, MPFR_RNDD); } else { mpfr_sqr(f_tmp, i_value->internalLowerFloat(), MPFR_RNDU); mpfr_sub(f_rl, f_rl, f_tmp, MPFR_RNDD); } .... }
if和
else语句中的代码完全相同。 相邻的代码片段与此非常相似,但是它们使用不同的功能:
internalLowerFloat()和
internalUpperFloat() 。 可以安全地假设程序员在此处复制了代码,却忘记了更正函数名称。
V501在“ ||”的左侧和右侧有相同的子表达式“!Mtr2.number()。IsReal()”。 操作员。 BuiltinFunctions.cc 6274
int IntegrateFunction::calculate(....) { .... if(!mtr2.isNumber() || !mtr2.number().isReal() || !mtr.isNumber() || !mtr2.number().isReal()) b_unknown_precision = true; .... }
这里出现了重复的表达式,因为它们在一个地方而不是名称
mtr编写了
mtr2 。 因此,在这种情况下,不会调用
mtr.number()。IsReal()函数。
V501在“ ||”的左侧和右侧有相同的子表达式“ vargs [1] .representsNonPositive()”。 操作员。 BuiltinFunctions.cc 5785

在此代码中手动查找异常是不现实的! 但是他们是。 而且,在原始文件中,这些片段写在一行中。 分析器检测到重复表达式
vargs [1] .representsNonPositive() ,这可能表示输入错误,因此可能表示错误。
这是您几乎找不到的全部可疑地点列表:
- V501在“ ||”的左侧和右侧有相同的子表达式“ vargs [1] .representsNonPositive()”。 操作员。 BuiltinFunctions.cc 5788
- V501在“ &&”运算符的左侧和右侧有相同的子表达式“ append”。 MathStructure.cc 1780
- V501在“ &&”运算符的左侧和右侧有相同的子表达式“ append”。 MathStructure.cc 2043年
- V501在'&&'运算符的左侧和右侧有相同的子表达式'(* * v_subs [v_order [1]]。RepresentsNegative(true)'。 MathStructure.cc 5569
无效的循环
V534可能在“ for”运算符内比较了错误的变量。 考虑查看“ i”。 MathStructure.cc 28741
bool MathStructure::isolate_x_sub(....) { .... for(size_t i = 0; i < mvar->size(); i++) { if((*mvar)[i].contains(x_var)) { mvar2 = &(*mvar)[i]; if(mvar->isMultiplication()) { for(size_t i2 = 0; i < mvar2->size(); i2++) { if((*mvar2)[i2].contains(x_var)) {mvar2 = &(*mvar2)[i2]; break;} } } break; } } .... }
在内部循环中,计数器是变量
i2 ,但是由于输入错误,发生了错误-在停止循环的情况下,使用了来自外部循环的变量
i 。
冗余还是错误?
V590考虑检查此表达式。 表达式过多或打印错误。 编号CC 6564
bool Number::add(const Number &o, MathOperation op) { .... if(i1 >= COMPARISON_RESULT_UNKNOWN && (i2 == COMPARISON_RESULT_UNKNOWN || i2 != COMPARISON_RESULT_LESS)) return false; .... }
在看了这样的代码之后,三年前,我写了一条笔记来帮助自己和其他程序员:“
C / C ++中的逻辑表达式。专业人士怎么错了 。” 遇到这样的代码后,我确信该注释一点也没有变得不那么重要。 您可以在文章中查找,找到与代码相对应的错误模式,并找出所有细微差别。
在此示例的情况下,转到“表达式== ||”部分 !=”并且我们了解到表达式
i2 == COMPARISON_RESULT_UNKNOWN不会影响任何内容。
取消引用未验证的指针
V595在对nullptr进行验证之前,已使用了'o_data'指针。 检查行:1108、1112。DataSet.cc 1108
string DataObjectArgument::subprintlong() const { string str = _("an object from"); str += " \""; str += o_data->title();
一个函数中的
o_data指针被取消引用,而无需检查和检查。 这可能是冗余代码,也可能是潜在错误。 我倾向于最后一种选择。
还有两个类似的地方:
- V595在对nullptr进行验证之前,已使用了'o_assumption'指针。 检查行:229,230。Variable.cc 229
- V595在针对nullptr对其进行验证之前,已使用了'i_value'指针。 检查行:3412,3427。Number.cc 3412
free()或删除[]?
V611使用“ new”运算符分配了内存,但使用“ free”功能释放了内存。 考虑检查“ remcopy”变量后面的操作逻辑。 cc 8123号
string Number::print(....) const { .... while(!exact && precision2 > 0) { if(try_infinite_series) { remcopy = new mpz_t[1];
remcopy阵列的内存以各种方式分配和释放,这是一个严重的错误。
遗失的变更
bool expand_partial_fractions(MathStructure &m, ....) { .... if(b_poly && !mquo.isZero()) { MathStructure m = mquo; if(!mrem.isZero()) { m += mrem; m.last() *= mtest[i]; m.childrenUpdated(); } expand_partial_fractions(m, eo, false); return true; } .... }
变量
m在函数中通过引用
被接受,这意味着对其进行了修改。 但是分析器发现该代码包含同名的局部变量,该局部变量与功能参数的范围重叠,从而导致更改丢失。
奇怪的指针
V774释放内存后使用了“ cu”指针。 计算器.cc 3595
MathStructure Calculator::convertToBestUnit(....) { .... CompositeUnit *cu = new CompositeUnit("", "...."); cu->add(....); Unit *u = getBestUnit(cu, false, eo.local_currency_conversion); if(u == cu) { delete cu;
分析器警告释放内存后,该代码包含对
cu对象的方法的调用。 但是,如果您试图理解代码,那么它将更加奇怪。 首先,
删除cu的调用总是发生-在此情况下及之后。 其次,条件之后的代码假定指针
u和
cu不相等,因此清除
cu对象后,使用
u对象是合乎逻辑的。 很有可能在代码中打了一个错字,并且计划仅使用变量
u 。
使用查找功能
V797 '查找'函数的使用就像返回布尔类型一样。 函数的返回值应该与std :: string :: npos进行比较。 单位cc 404
MathStructure &AliasUnit::convertFromFirstBaseUnit(....) const { if(i_exp != 1) mexp /= i_exp; ParseOptions po; if(isApproximate() && suncertainty.empty() && precision() == -1) { if(sinverse.find(DOT) || svalue.find(DOT)) po.read_precision = READ_PRECISION_WHEN_DECIMALS; else po.read_precision = ALWAYS_READ_PRECISION; } .... }
尽管代码可以成功编译,但是由于
find函数返回许多类型为
std :: string :: size_type的代码,因此看起来很可疑。 如果在线上的任意位置找到该点,则条件为true,除非该点在起点处。 这是一个奇怪的测试。 我不确定,但是也许应该将代码重写如下:
if( sinverse.find(DOT) != std::string::npos || svalue.find(DOT) != std::string::npos) { po.read_precision = READ_PRECISION_WHEN_DECIMALS; }
潜在的内存泄漏
V701 realloc()可能泄漏:当realloc()分配内存失败时,原始指针“缓冲区”丢失。 考虑将realloc()分配给一个临时指针。 实用程序cc 703
char *utf8_strdown(const char *str, int l) { #ifdef HAVE_ICU .... outlength = length + 4; buffer = (char*) realloc(buffer, outlength * sizeof(char));
当使用
realloc()函数时,建议使用中间缓冲区,因为如果不可能分配内存,则指向旧内存的指针将不可避免地丢失。
结论
Qalculate项目! 在最佳的免费计算器列表中排名第一,同时它包含许多严重的错误。 但是我们还没有看到他的竞争对手。 我们将尝试浏览所有流行的计算器。
至于与Windows世界计算器质量的比较,而Microsoft实用程序看起来更可靠,质量更高。
通过下载
PVS-Studio并在您的项目上进行尝试来检查“计算器”。 :-)

如果您想与说英语的读者分享这篇文章,请使用以下链接:Svyatoslav Razmyslov。
跟随计算器的脚步:Qalculate!