范围检查错误的原因( Delphi )

djp7away  于 2023-03-01  发布在  其他
关注(0)|答案(2)|浏览(221)

下面是一些代码的压缩版本,这些代码会导致一个范围检查错误和一个溢出错误,我应该打开这些编译器检查指令吗,我理解为什么这会导致一个溢出,在C1的乘法上,它似乎超过了数据类型的最大值。但是为什么这也会触发一个范围检查错误呢? Delphi 的文档和其他关于堆栈溢出的文章使它听起来像范围-检查错误通常是因为数组访问超出了界限,但是我没有访问一个数组,它说这行导致了范围检查错误,也许是因为param 1的赋值,但是如果是这样,为什么这是一个范围检查而不是溢出错误呢?

const
  C1 = 44001;
  C2 = 17999;

function fxnName(..other params...; param1: Word): String;
var
  someByte: byte;
begin
  // some code
  // by now we're in a loop. the following line is where it breaks to in the debugger: 
  param1 := (someByte + param1) * C1 + C2;
  // more code
end;

如果它是相关的,那么当它在调试器中的该行中断时,所有的值看起来都和预期的一样,除了param 1,它显示“未声明的标识符:'param 1'“当我让 Delphi 评估它时。

smdncfj3

smdncfj31#

有关范围检查的文档指出:
$R指令启用或禁用范围检查代码的生成。在{$R +}状态下,所有数组和字符串索引表达式都被验证为在定义的界限内,并且对标量和子范围变量的所有赋值都被检查为在范围内。如果范围检查失败,则引发ERangeError异常(如果未启用异常处理,则终止程序)。
所以这里的原因是给标量赋值,它被传递了一个超过上限的值。
有关简单类型和子范围类型的范围检查错误,请参见docwiki Simple Types
示例:

{$R+} // Range check on
var
  w1,w2 : word;
begin
  w1 := High(word);
  w1 := w1 + 10; // causes range-check error on assignment to w1 (upper range passed)
  w2 := 0;
  w2 := w2 - 10; // causes range-check error on assignment to w2 (lower range passed)
end;

对所有平台无关整数类型的$R和$Q的所有组合的汇总测试:

R+Q+  R+Q-  R-Q+
 ShortInt    R     R     x
 SmallInt    R     R     x
 Integer     O     x     O
 LongInt     O     x     O
 Int64       O     x     O
 Byte        R     R     x
 Word        R     R     x
 LongWord    O     x     O
 Cardinal    O     x     O
 UInt64      O     x     O

R =距离误差;O =溢出误差;x =无
测试是在32位模式下使用XE2进行的(伪代码):

number := High(TNumber);
number := number + 1;
bbmckpt7

bbmckpt72#

我见过太多的Delphi程序员在编写相当大的程序时从来没有激活过范围、溢出和Assert检查。当然,如果你愿意,你可以这样做,但是你的代码会有更多的bug。
所以,如果你允许我让我插入一个平行(bug仍然相关)和你的问题,希望说服更多的程序员启用这3检查现在.一个字的警告也包括在最后.

    • 溢出检查**

这将检查某些整数算术运算(+、-、*、Abs、Sqr、Succ、Pred、Inc和Dec)是否溢出。例如,在+(加法)运算之后,编译器将插入附加的二进制代码,以验证运算结果是否在支持的范围内。
当对整数变量的运算产生的结果超出该变量的范围时,就会发生"整数溢出"。例如,如果将整数变量声明为16位有符号整数,则其值的范围可以是-32768到32767。如果对该变量的运算产生的结果大于32767或小于-32768,发生整数溢出。
发生整数溢出时,操作的结果是未定义的,可能导致程序中出现未定义的行为:·Wrap-around结果可能会产生一个wrap-around值,这意味着数字32768实际上会存储为1,因为它比我们能存储的最大值高1个单位(32767)。·截断结果可能会被截断或修改,以适合整数类型的范围。例如,数字32768实际上将被存储为32767,因为这是我们可以存储的最高值。
未定义的程序行为是最严重的错误之一,因为它不是一个容易重现的错误,所以很难跟踪和修复。
如果您激活此选项,需要付出很小的代价:程序的速度将稍微降低。

    • IO检查**

检查I/O操作的结果。如果I/O操作失败,则会引发异常。如果关闭此开关,则必须手动检查I/O错误。如果激活此开关,则需要付出较小的代价:程序的速度将降低,但并不显著,因为与I/O操作本身所需的毫秒级时间相比,此检查引入的几微秒根本算不上什么(硬盘驱动器很慢)。

    • 范围检查**

Delphi极客称之为"最重要的Delphi设置",我完全同意。它检查所有数组和字符串索引表达式是否在定义的范围内。它还检查所有标量和子范围变量的赋值是否在范围内。
下面是一个示例代码,如果范围检查不可用,它会毁了我们的生活:

Type 
    Pasword= array [1..10] of byte; // we define an array of 10 elements
…
x:= Pasword[20];       // Range Checking will prevent the program from accessing element 20 (ERangecheckError exception is raised). Security breach avoided. Error log automatically sent to the programmer. Bruce Willis saves everyone.
    • 启用运行时错误检查**

要激活运行时错误检查,请转到"项目选项"并选中以下三个框:
在"项目选项"

中启用运行时错误检查

    • Assert**

一个好的程序员必须在代码中使用Assert来提高程序的质量和稳定性。说真的,伙计!你真的需要使用Assert。
Assert用于检查在程序的某个点上应该始终为真的条件,并在不满足条件时引发异常。在SysUtils单元中定义的Assert过程通常用于执行Assert。
你可以把Assert看作是穷人的单元测试。我强烈建议你深入研究Assert。它们非常有用,而且不需要像单元测试那样多的工作。
典型示例:

SysUtils.Assert(Input <> Nil, ‘The input should not be nil!’);

但是为了让程序检查我们的Assert,我们需要在项目设置-〉编译器选项中激活这个特性,否则它们将被忽略,就像它们不在我们的代码中一样。确保你理解我刚才所说的含义!例如,如果我们在Assert中调用有副作用的例程,我们会搞砸。在下面的例子中,在调试过程中,当Assert打开时,Test()函数将被执行,并且"This was executed"将出现在备忘录中。2然而,在发布过程中,该文本将不会出现在备忘录中,因为现在Assert被简单地忽略了。3恭喜,我们刚刚使程序在调试/发布模式下有了不同的行为。

function TMainForm.Test: Boolean;
begin
 Result:= FALSE;
 mmo.Lines.Add('This was executed');
end;

procedure TMainForm.Start;
VAR x: Integer;
begin
 x:= 0;
 if x= 0
 then Assert(Test(), 'nope');
end;

下面是一些如何使用它的示例:
1要检查输入参数是否在0..100范围内:

procedure DoSomething(value: Integer);
begin
  Assert((value >= 0) and (value <= 100), 'Value out of range');
  …
end;

2在使用指针之前检查它是否不为空:

Var p: Pointer;
Begin
  p := GetPointer;
  Assert(Assigned(p), 'Pointer is nil');
   …
End;

3在继续之前检查变量是否具有特定值:

var i: Integer;
begin
   i := GetValue;
   Assert(i = 42, 'Incorrect response to “What is the answer to life”!');
  …
end;

Assert也可以通过在项目选项中定义NDEBUG符号或使用{$D -}编译器指令来禁用。
在某些情况下,我们还可以使用Assert作为处理错误和异常的更优雅的方式,因为它更具可读性,而且它还包括一个自定义消息,这将帮助开发人员了解哪里出错了。
就我个人而言,我经常在例程的顶部使用它来检查参数是否有效。

激活这个特性会(自然地)使你的程序变慢,因为...嗯,有一行额外的代码要执行。
"没有什么是免费的"
一切美好的事物都是有代价的(幸运的是,我们的代价很小):启用运行时错误检查和Assert会降低程序的速度,并使程序变得更大。
现在的电脑有很多内存,所以内存的微小增加是无关紧要的,所以我们先把它放在一边,但让我们看看速度,因为这不是我们可以轻易忽视的:

Type                 Disabled   Enabled
Range checking       73ms         120ms
Overflow checking    580ms        680ms
I/O checking         Not tested   Not tested

正如我们所看到的,程序的速度受到这些运行时检查的强烈影响。如果我们有一个程序,速度是关键,我们最好只在调试期间激活“运行时错误检查”。我所做的,我也让它在第一个版本激活,并等待几个星期。如果没有错误报告,然后我发布了一个更新,其中“运行时错误检查”关闭。
就我个人而言,我让“IO检查”始终处于活动状态。由于这种检查而对性能的影响是微观的。

重要警告:

如果你有一个已经存在的项目写得不太好,并且你激活了下面的任何一个运行时错误检查,你的程序可能会比平常更频繁地崩溃。运行时错误检查例程并没有破坏你的程序。它总是被破坏--你只是不知道。运行时检查例程现在正在寻找所有那些代码可疑、低劣和发臭的地方。运行时检查的唯一目的是查找程序中的错误。

相关问题