差一错误

差一错误（，缩写OBOE）是在计数时由于边界条件判断失误导致结果多了一或少了一的错误，通常指计算机编程中循环多了一次或者少了一次的程序错误，属于逻辑错误的一种。比如，程序员在循环中进行比较的时候，本该使用「小于等于」，但却使用了「小于」，或者是程序员没有考虑到一个序列是从0而不是1开始（许多程序语言的数组下标都是这样）。在数学领域，此错误也时有发生。

遍历数组
假设现在有一堆物品，按m到n（含）依次编号。那么这堆物品的总数是多少？我们可能会直觉地认为有(n - m)个物品，但这就和正确答案差了一，犯了栅栏错误。正确答案应该是(n - m + 1)个物品。

因此，计算机领域中，涉及范围的时候通常用半开区间来表示，从m到n（含）的范围就表示成从m到n + 1（不含），以避免栅栏错误。例如，一个迭代五次的循环可以写成0到5的半开区间：

for (i = 0; i < 5; i++) 
{
    /* 循环体 */
    /* 可以在循环体內執行其他程序 */
}

循环体首次执行时，i等于0，接着i依次变为1、2、3、4。最后，i会变为5，因此i < 5为假（不成立），循环结束。然而，如果在比较中用的是<=（小于等于），循环体则会执行六次：i依次为0、1、2、3、4、5。同样，如果i的初值是1而不是0，那么循环体只会执行四次：i依次为1、2、3、4。这些情况都能产生差一错误。

还有一种情况就是该用while循环的地方却用了do-while循环（反之亦然）。Do-while的循环体至少会执行一次。

程序语言间的差异也会产生混淆。从0开始计数是程序语言最为常见的做法，而有些语言中却以1起始。Pascal语言中可以自定义数组下标的起始值。

栅栏错误
栅栏错误（有时也称为电线杆错误或者灯柱错误）是差一错误的一种。如以下问题：

最容易想到的答案10是错的。这个栅栏有10个间隔，11条栅栏柱。

反过来，柱子数给定时，我们也很容易认为间隔数也是这么多。实际上间隔数要比柱子数少一个。

广义上这个问题可以这么表述：

如果这一列电线杆不组成一个圈，那-{么}-正确答案是n-1，如果在此前提之下，首尾两端也计入间隔，那-{么}-正确答案是n+1，如果电线杆围成一圈，那么答案则是n。思考之前必须先明确问题的定义，一个情况下的答案可能并不适用于其他情况。栅栏错误往往就来源于在数物体还是数间隔的选择上出了差错。

除了计算长度，栅栏错误还会发生在其他单位中。例如，时间金字塔是一个每10年放置1块石块，总共要放置120块的公共艺术作品，完成这个作品需要花费1190年，而不是1200年。早期的栅栏错误也与时间有关，儒略历最开始计算闰年的方式不正确，导致每三年会有一次闰年（正常情况应该是每四年，即每隔三年）。

大数的差一错误一般都不会引起大问题。正是在小数-{}-字上，尤其是精确度要求很高的时候，差一错误可能造成灾难。如果负责计算的人员「重蹈覆辙」，差一错误甚至可以累积。

 安全隐患 
不当使用C标准库中的strncat()函数常常会导致差一错误和安全问题。程序员经常认为strncat()在写入字符串结束符时不会超过最大长度。事实上strncat()会在指定的最大长度之后一字节的位置写入字符串结束符。如下代码：

void foo (char *s) 
{
    char buf[15];
    memset(buf, 0, sizeof(buf));
    strncat(buf, s, sizeof(buf)); // 最后一个参数应为 sizeof(buf)-1
}

差一错误之所以经常在使用C标准库的时候出现，是因为C标准库在需不需要减去一字节这个问题上标准不统一。fgets()和strncpy()这些函数在写入的时候不会超过最大长度（fgets()会自行把长度减一，只取回(长度 - 1)字节），而像strncat()这些函数则会越过最大长度。所以程序员必须牢记哪些函数需要减去一。

在某些系统上（小端序架构），差一错误可导致帧指针的最低字节被覆盖，从而使攻击者能够劫持调用例程的局部变量，给漏洞攻击敞开大门。

要避免这类问题，可以使用这些函数的其他改进版本，如strlcat()和strlcpy()，这些改进版在写入时会考虑缓冲区能容纳的大小，因此更加「安全」。（上面代码的相应部分改成strlcat(buf, s, sizeof(buf))就能解决问题）

 参见 

 鸽巢原理

 從零開始的編號

参考来源

Category:程式錯誤
Category:保全漏洞