使用运算符 [] 引用 std::vector 上最后一个元素时出现问题<>

Problems referencing one past last element on std::vector<> using operator[]

本文关键字:gt 元素 问题 lt 运算符 引用 vector std 最后一个      更新时间:2023-10-16

给定此有效的 C 或 C++ 代码

int x() {
int numbers[3]; // Lets suppose numbers are filled in with values
int sum = 0;
for (const int* p = &numbers[0]; p != &numbers[3]; ++p)
sum += *p;
return sum;
}

这段代码使用指针算法,据我所知,有一个指针指向数组中最后一个元素是有效的,引用该指针是未指定的,但我们可以让指针指向该位置。 因此,&p[0]、&p[1]、&p[2] 和 &p[3] 是有效的指针,p[0]、p[1] 和 p[2] 是有效值。

如果我用std::vector<int>替换 int 数组,一切都应该没问题,我们得到这段代码

#include <vector>
int x() {
std::vector<int> numbers(3);
int sum = 0;
for (const int* p = &numbers[0]; p != &numbers[3]; ++p)
sum += *p;
return sum;
}

但是在 DEBUG 模式下的 Visual C++ 2017 下运行,我得到这个异常"矢量下标超出范围",这是从 MS STL 库触发的,因为实现假设使用 operator[] 我们会自动引用底层值,但事实并非如此。 这是执行边界检查的 MS STL 代码...

_NODISCARD _Ty& operator[](const size_type _Pos)
{   // subscript mutable sequence
#if _ITERATOR_DEBUG_LEVEL == 2
if (size() <= _Pos)
{   // report error
_DEBUG_ERROR("vector subscript out of range");
}
#elif _ITERATOR_DEBUG_LEVEL == 1
_SCL_SECURE_VALIDATE_RANGE(_Pos < size());
#endif /* _ITERATOR_DEBUG_LEVEL */
return (this->_Myfirst()[_Pos]);
}

如果我将 &numbers[0] 和 &numbers[3] 替换为 numbers.begin(( 和 numbers.end((,则不会收到错误。

我同意这是非常丑陋的代码,但我简化真正的代码只是为了暴露错误。

原始代码在元素为零的向量上使用 &vec[0]。

所以我的问题是:

这是Microsoft视觉C++ STL 实现上的错误,还是对向量<>的运算符 [] 有一些限制?

我知道用at((替换[]将是一个错误,但我知道&vec[size]应该仍然对std::vector有效<>

是否明确定义了获取指向最后一个&p[n]元素的指针,这一直是一个灰色区域。

但是,指向最后一个元素之后的指针是明确定义的。

您可以通过使用普通指针算法来避免此类错误:

for (const int* p = numbers.data(); p != numbers.data() + 3; ++p)

或者,更一般地说,迭代器:

using std::begin;
using std::end;
for(auto p = begin(v), q = end(v); p != q; ++p)

或者使用循环范围

for(auto const& element : v)

真的没有充分的理由使用v[v.size()]

VisualC++ 中的调试迭代器库正确报告了问题,并且问题并不微妙。

根据标准,[sequence.reqmts] 表 101,a提供operator[]类型的序列容器的表达式a[n](std::vector,就像basic_stringdequearray一样(,具有*(a.begin()+n)的操作语义。但是,在您正在运行的情况下,a.begin()+n将等效于a.end().因此,在应用地址运算符之前*(a.end())结果。

取消引用容器的end()迭代器会调用未定义的行为。视觉C++在报告断言时是正确的,您最好更改枚举策略。

取消引用过去最后一个元素会导致未定义的行为。如果声明std::vector<int> numbers(3)则允许访问的最后一个元素是numbers[2]。原始数组的故事相同。

如果可以,请避免使用原始数组:

int x() {
std::vector<int> numbers(3); 
//...
int sum = 0;
for (auto value : numbers)
sum += value;
return sum;
}

因此,&p[0]、&p[1]、&p[2] 和 &p[3] 是有效的指针

不。数组的下标运算符(p[x](是*(p+x)的语法糖,所以&p[3]实际上是在做&(*(p+3))*(p+3)是未定义的行为!!

如果您想要的只是一个过去的地址,那么p+3是完全有效的。这将使用指针算法,并且不会取消引用任何内容。

如果我用 std::vector 替换 int 数组,一切都应该没问题

再说一遍,不!如果尝试取消引用尚未分配的内存位置,将出现未定义的行为。std::vector没有说明v[v.length()]被分配给你,所以这是未定义的行为。

该实现假设使用 operator[] 我们自动引用底层值,但事实并非如此

是的,它是!!阅读operator[]上的 cppreference 页面:">返回对指定位置位置的元素的引用。 不执行边界检查。就像原始数组一样,这里的下标运算符返回对基础值的引用,这意味着这里涉及取消引用步骤!