跟随计算器的脚步:Qalculate


以前,我们对大型数学程序包(例如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); } .... } 

ifelse块中的代码绝对相同 相邻的代码片段与此非常相似,但是其中使用了不同的功能: 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; .... } 

在这种情况下,由于在一个地方写入了mtr2而不是mtr ,所以出现了重复的表达式 因此,该条件中不存在对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; .... } 

3年前,当我注意到此类代码后,我为我和其他开发人员写了一个备忘单:“ 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(); // <= str += "\""; DataPropertyIter it; DataProperty *o = NULL; if(o_data) { // <= o = o_data->getFirstProperty(&it); } .... } 

在一个函数中,无论是否进行检查,都将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]; // <= mpz_init_set(*remcopy, remainder); } mpz_mul_si(remainder, remainder, base); mpz_tdiv_qr(remainder, remainder2, remainder, d); exact = (mpz_sgn(remainder2) == 0); if(!started) { started = (mpz_sgn(remainder) != 0); } if(started) { mpz_mul_si(num, num, base); mpz_add(num, num, remainder); } if(try_infinite_series) { if(started && first_rem_check == 0) { remainders.push_back(remcopy); } else { if(started) first_rem_check--; mpz_clear(*remcopy); free(remcopy); // <= } } .... } .... } 

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; // <= return mstruct_new; } delete cu; // <= if(eo.approximation == APPROXIMATION_EXACT && cu->hasApproximateRelationTo(u, true)) { // <= if(!u->isRegistered()) delete u; return mstruct_new; } .... } 

分析器警告说,代码在释放内存后立即调用cu对象的方法。 但是当试图解决它时,代码却变得更加奇怪。 首先,调用delete cu总是会发生-在此情况下及之后都发生。 其次,该条件之后的代码表示指针ucu不相等,这意味着在删除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)); // <= .... #else return NULL; #endif } 

当使用realloc()函数时,建议使用中间缓冲区,以防万一无法分配内存,指向旧内存区域的指针将不可避免地丢失。

结论


Qalculate! 项目在最佳免费计算器的列表中名列前茅,而其中包含许多严重错误。 另一方面,我们尚未检查其竞争对手。 我们将尝试遍历所有流行的计算器。

至于与Windows世界中计算器的质量进行比较,到目前为止,Microsoft提供的实用程序看起来更加可靠且运行良好。

检查您自己的“计算器”-下载PVS-Studio并为您的项目尝试。 :-)

Source: https://habr.com/ru/post/zh-CN443656/


All Articles