什么是“偏移量为一错误”(off-by-one error)以及如何修复它?

45

什么是差一错误(off-by-one error)?如果我有这样的错误,如何修复它?


软件开发中最困难的两件事是缓存一致性、命名事物和偏移一个错误。 - Jim Garrison
6个回答

83

一个一位偏差错误(off-by-one error)的例子是当你想要执行一个循环n次并写下以下代码时:

for (int i = 1; i < n; ++i) { ... }
或:
for (int i = 0; i <= n; ++i) { ... }

在第一种情况下,循环将执行(n-1)次,在第二种情况下将执行(n+1)次,这被称为“错位”。其他变化也可能发生,但通常由于循环变量的初始值或循环结束条件中的错误而导致循环执行次数过多或过少。

循环可以正确编写为:

for (int i = 0; i < n; ++i) { ... }

for循环只是while循环的一个特例。在while循环中可能会出现相同类型的错误。


9
一个off-by-one error是指你期望某个值为N,但实际上却是N-1或N+1。例如,你期望程序执行10次操作,但实际上只执行了9或11次(少一次或多一次)。在编程中,这种错误最常见于处理“for”循环时。
这种错误发生的原因是你没有意识到用来计数的数字可能与你要计数的东西的数量不同。换句话说,你用来计数的数字可能与你要计数的东西的总数不同。没有什么能强制两者相同。试着大声从0数到10,你会说出11个数字,但你说的最后一个数字是10。

预防这个问题的一种方法是认识到我们的大脑有倾向(也许是认知偏差)犯这个错误。记住这一点可能有助于您识别和预防未来的情况。但我想,防止这个错误的最好方法是编写单元测试。测试将帮助您确保代码正常运行。


5
假设您有以下包含数组和for循环的代码:
char exampleArray[] = { 'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd' };

for(int i = 0; i <= 11; i++)
{
  print(exampleArray[i])
}

看到问题了吗?因为我数了一下我的数组有11个字符,所以我设置了循环迭代11次。然而,在大多数编程语言中,数组从零开始,这意味着当我的代码要打印时

exampleArray[11]

我将会因为示例中的数组在索引11处没有值而收到一个越界错误。
在这种情况下,我可以通过简单地告诉我的循环少迭代一次来轻松解决这个问题。
调试此问题的最简单方法是打印出您的上限和下限,并查看哪个值生成了一个索引越界错误,然后在整个迭代中将您的值设置为比它大或小1。
当然,这假定错误是由循环超过或少于数组边界而生成的。还有其他情况会导致索引越界错误,但这是最常见的情况。索引越界总是指尝试访问数据,而数据不存在于不在数据范围内的边界之外。

好的回答!这是指哪种编程语言?对我来说看起来像是C语言,但我不确定。 - S.S. Anne
@JL2210 这通常适用于大多数编程语言(我敢说所有编程语言,但我不会这样说,因为我不能百分之百确定是否有其他编程语言中这个错误可能意味着其他东西)。在我的例子中,我正在使用Java,但是我做了一个通用的打印而不是System.out.print(),因为我变懒了并决定保持它的通用性。就像我说的那样,这个概念应该贯穿你所使用的大多数编程语言。 - Vapidant
C语言中不存在索引越界错误,它们只会导致未定义的行为。 - S.S. Anne
1
@JL2210 我不经常使用C语言,所以我不知道这个问题,但我刚查了一下并进行了一些测试,似乎你是正确的。话虽如此,我认为即使它没有技术上抛出越界错误,这仍然是一个越界错误。在C语言中,当尝试访问超出边界的内容时,似乎会返回一些随机内存,这些内存仍由软件拥有,导致一些随机的意外返回,或者软件可能会尝试检索它不拥有的内存,这将导致崩溃。无论哪种情况,这个概念在我看来仍然适用。 - Vapidant
或者它可能无意中格式化您的硬盘。未定义行为意味着“没有限制”;您的程序可能会导致计算机烧毁,而这种行为仍然是有效的。 - S.S. Anne

2
一个简单的经验法则:
int i = 0; // if i is initiated as 0, always use a < or > in the condition
while (i < 10) 
    System.out.printf("%d ", i++);

int i = 1; // if i is initiated as 1, always use a <= or >= in the condition
while (i <= 10)
    System.out.printf("%d ". i++);

2

Off by one error(有时称为OBOE)会在你试图定位字符串或数组的特定索引(以切片或访问段),或者循环遍历它们的索引时出现。

如果我们以Javascript作为示例语言,索引从开始,而不是,这意味着最后一个索引总是比项目长度少一。如果您尝试访问等于长度的索引,则程序可能会抛出

"index out of range" reference error

打印undefined

当您使用接受索引范围作为参数的字符串或数组方法时,有助于阅读该语言的文档并了解它们是否是包含的(给定索引处的项是返回内容的一部分)或不包含的。以下是一些off by one错误的示例:

let alphabet = "abcdefghijklmnopqrstuvwxyz";
let len = alphabet.length;
for (let i = 0; i <= len; i++) {
  // loops one too many times at the end
  console.log(alphabet[i]);
}
for (let j = 1; j < len; j++) {
  // loops one too few times and misses the first character at index 0
  console.log(alphabet[j]);
}
for (let k = 0; k < len; k++) {
  // Goldilocks approves - this is just right
  console.log(alphabet[k]);
}

2

常见的一个-off-by-one混淆是因为一些语言从零开始枚举向量(例如C),而其他语言从一开始枚举(例如R)。因此,大小为n的向量x在C中的成员从x [0]到x [n-1]运行,但在R中从x [1]到x [n]运行。

当编写用于循环递增的常见习语时,您还将面临off-by-one挑战:

在C中:

i = (i+1)%n

在R中:

i <- (i-1)%%n + 1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接