end()
定义为超过实际结尾的位置,而不是在实际结尾处?end()
定义为超过实际结尾的位置,而不是在实际结尾处?最有说服力的观点来自Dijkstra本人提出的:
你希望范围的大小是一个简单的差值end − begin;
包括下界更“自然”,当序列退化为空时,也因为另一种选择(排除下界)需要存在“开始之前”的哨兵值。
你仍然需要证明为什么从零开始计数而不是从一开始计数,但这不是你的问题的一部分。
在处理多个嵌套或迭代调用范围构造的算法时,半开范围约定背后的智慧一次又一次地得到回报,这些算法能够自然地链接。相比之下,使用双重封闭范围会导致偏移量错误和极其不愉快且难以理解的代码。例如,考虑一个分区[n0, n1)[n1, n2)[n2,n3)。另一个例子是标准迭代循环for (it = begin; it != end; ++it)
,它运行了end - begin
次。如果两端都是包含的,相应的代码将变得不太可读-想象一下如何处理空范围。
最后,我们还可以提出一个很好的观点,为什么计数应该从零开始:对于我们刚刚确定的半开区间约定,如果你给出一个N元素的范围(比如枚举数组成员),那么0就是自然的“开始”,这样你就可以将范围写成[0,N),而不需要任何尴尬的偏移或纠正。
简而言之:在基于范围的算法中我们不会看到数字1
,这直接是 [begin, end) 约定的结果和动机。++
自增的迭代器模板 step_by<3>
,然后就具有最初宣传的语义。 - Kerrek SB<
的地方使用 !=
,那么这就是一个错误。顺便说一下,这种类型的错误很容易通过单元测试或断言找到。 - Phil1970实际上,如果您考虑迭代器不是指向序列元素而是在其之间,那么与迭代器相关的许多内容会突然变得更加清晰,通过解除引用可以访问其右侧的下一个元素。然后,“结束后的一个”迭代器突然就很容易理解了:
+---+---+---+---+
| A | B | C | D |
+---+---+---+---+
^ ^
| |
begin end
显然,begin
指向序列的开头,而end
则指向同一序列的结尾。解引用begin
访问元素A
,解引用end
没有意义,因为它右侧没有元素。同时,在中间添加迭代器i
会产生
+---+---+---+---+
| A | B | C | D |
+---+---+---+---+
^ ^ ^
| | |
begin i end
如果你立即查看从begin
到i
范围内的元素,你会发现它包含元素A
和B
,而从i
到end
范围内的元素包含元素C
和D
。解引用i
给出其右侧的元素,也就是第二个序列的第一个元素。
甚至对于反向迭代器的“偏移一”的问题也变得显而易见:翻转该序列将得到:
+---+---+---+---+
| D | C | B | A |
+---+---+---+---+
^ ^ ^
| | |
rbegin ri rend
(end) (i) (begin)
我已经写出了相应的非反向(基)迭代器,其中在括号中。你会发现,属于i
的反向迭代器(我命名为ri
)仍然指向元素B
和C
之间。然而,由于反转序列,现在元素B
位于其右边。
foo[i]
是紧接在位置i
之后的项的简写)。思考一下,我想知道一个语言是否有单独的运算符用于“在位置i之后立即的项”和“在位置i之前立即的项”,因为许多算法使用相邻项对,说“在位置i两侧的项”可能比“在位置i和i + 1处的项”更清晰。 - supercatbegin[0]
(假设是随机访问迭代器)将访问元素1
,因为在我的示例序列中没有元素0
。 - celtschkstart()
来启动特定的进程或其他操作时,如果与已经存在的函数冲突,那将会很烦人)。 - Fareanorend()
定义为超出实际结尾而不是在实际结尾处?begin()
等于end()
。end()
之前继续循环即可。因为那样
size() == end() - begin() // For iterators for whom subtraction is valid
而且你不必做像尴尬这样的事情
// Never mind that this is INVALID for input iterators...
bool empty() { return begin() == end() + 1; }
而且你不会意外地编写错误的代码,比如:
bool empty() { return begin() == end() - 1; } // a typo from the first version
// of this post
// (see, it really is confusing)
bool empty() { return end() - begin() == -1; } // Signed/unsigned mismatch
// Plus the fact that subtracting is also invalid for many iterators
还有: 如果end()
指向有效元素,find()
会返回什么?
你真的想再加一个名为invalid()
的成员函数来返回无效迭代器吗?!
两个迭代器已经够痛苦了...
哦,还有,看一下这篇相关文章.
如果end
在最后一个元素之前,你怎么在真正的结尾处进行insert()
操作呢?!
半开放范围的迭代器惯用语法[begin(), end())
最初是基于普通数组的指针算术操作而来。在那种操作模式下,您会有一个函数,该函数接收一个数组和其大小作为参数。
void func(int* array, size_t size)
当你已经有这些信息时,将区间转换为半开区间[begin, end)
非常简单:
int* begin;
int* end = array + size;
for (int* it = begin; it < end; ++it) { ... }
要使用完全封闭的范围,会更加困难:
int* begin;
int* end = array + size - 1;
for (int* it = begin; it <= end; ++it) { ... }
由于在C++中,指向数组的指针是迭代器(并且语法设计允许这样做),因此调用std::find(array, array + size, some_value)
比调用std::find(array, array + size - 1, some_value)
容易得多。此外,如果您使用半开区间,可以使用!=
运算符检查结束条件,因为(如果您的运算符定义正确),<
意味着!=
。
for (int* it = begin; it != end; ++ it) { ... }
然而,对于完全封闭的范围,没有简单的方法来实现这一点。你只能使用<=
。
C++中唯一支持<
和>
操作的迭代器是随机访问迭代器。如果你必须为每个C++迭代器类编写<=
运算符,那么你必须使所有迭代器都可以进行完全比较,并且如果C++使用完全封闭的范围,则创建不太灵活的迭代器(例如在std::list
上操作的双向迭代器或在iostreams
上操作的输入迭代器)的选择将更少。
使用 end()
函数可以方便地通过 for 循环迭代集合,因为它指向末尾的下一个位置:
for (iterator it = collection.begin(); it != collection.end(); it++)
{
DoStuff(*it);
}
如果使用end()
指向最后一个元素,循环就会变得更加复杂:
iterator it = collection.begin();
while (!collection.empty())
{
DoStuff(*it);
if (it == collection.end())
break;
it++;
}
begin() == end()
。!=
而不是 <
(小于),因此将 end()
指向末尾位置的下一个位置是方便的。