使用 std::vector,为啥 &vec[0] 是未定义的行为,但 vec.data() 是安全的?
Posted
技术标签:
【中文标题】使用 std::vector,为啥 &vec[0] 是未定义的行为,但 vec.data() 是安全的?【英文标题】:With std::vector, why is &vec[0] undefined behavior, but vec.data() safe?使用 std::vector,为什么 &vec[0] 是未定义的行为,但 vec.data() 是安全的? 【发布时间】:2018-06-16 16:30:57 【问题描述】:我一直在通过"Link here" 的 isocpp.org 阅读常见问题解答,并注意到std::vector
的警告:
std::vector<int> v;
auto a = &v[0]; // Is undefined behaviour but
auto a = v.data(); // Is safe
来自实际网站:
void g()
std::vector<Foo> v;
// ...
f(v.begin(), v.size()); // Error, not guaranteed to be the same as &v[0]
↑↑↑↑↑↑↑↑↑ // Cough, choke, gag; use v.data() instead
此外,如果
std::vector
或std::array
为空,而使用.data()
始终是安全的 功能。
我不确定我是否完全理解这一点。 ::data()
返回指向数组开头的指针,&[0]
返回开头的地址。我没有看到这里的区别,我不认为 &[0]
正在取消引用任何东西(即,没有读取元素 0 处的内存)。在调试版本中的 Visual Studio 上,访问下标 [0]
会导致断言失败,但在发布模式下它什么也没说。对于默认构造的向量,两种情况下的地址也是 0。
我也不明白关于::begin()
的评论不能保证与::operator[0]
相同。我假设对于向量,begin()
迭代器、::data()
和 &[0]
中的原始指针都是相同的值。
【问题讨论】:
如果它是空的,那么就没有零索引,并且数据数组可能已经或可能没有为向量创建 重点是 v[0],即没有括号,是 UB...,因为您明确要求一个不存在的元素。 @Andre 所以换句话说,你可以指向一个无效的地址,但不能用 &。 @Zebrafish。确切地说,您可以拥有一个地址无效的指针,并且取消引用是 UB,而不是它自己的指针。在 &v[0] 的情况下,您要求的是一个不存在的元素(使用 v[0]),即 UB。然后,您尝试获取它的地址(使用 &),但此时您已经触发了 UB。 @MassimilianoJanes [expr.unary.op]/1 仅定义*
在表达式实际指向对象或函数时的行为。因此,取消引用是遗漏的 UB - 至少在核心问题 232 实际解决之前。
【参考方案1】:
我没有看到这里的区别
&v[0]
与&(v[0])
相同,即从v
的第一个元素获取地址。但是当v
为空时根本没有元素,v[0]
只是导致UB,它试图返回一个不存在的元素;试图从中获取地址是没有意义的。
v.data()
总是安全的。它将直接返回指向底层数组的指针。当v
为空时,指针仍然有效(它可能是空指针也可能不是);但请注意,取消引用它(如 *v.data()
)也会导致 UB,与 v[0]
相同。
另外我不明白关于
::begin()
的评论不能保证与::operator[0]
相同
std::vector::begin
将返回一个类型为std::vector::iterator
的迭代器,它必须满足RandomAccessIterator 的要求。它可能是一个原始指针,但不一定是。可以将其作为一个类来实现。
【讨论】:
我一定是对引用和指针有误解,因为我真的以为&返回了地址,它可以分配给指针。 @Zebrafish 是的operator&
确实返回了地址,问题是它试图返回不存在元素的地址。
@songyuanyao 我删除了我的答案,因为它被否决了(我懒得为它辩护:))但我仍然认为你的答案的第一部分不正确;只要不发生左值到纯右值的转换,您可以取消引用空指针;因此,语言中没有任何内容禁止 potential 实现 vector 以使 &v[0] 始终等于 data() ...因此您的解释不完整或不正确
@MassimilianoJanes 我希望你能在你的回答中更多地解释它(使用引用的标准)。 :) 好吧,我检查了标准,它没有定义vector
为空时的行为。这意味着任何行为都是可以接受的。另一方面,标准保证v.data()
始终有效,即使它为空;我认为从标准方面来看,这是这两种情况之间最大的区别。
@songyuanyao 是的,我同意 &v[0] 是 UB,只要 v.empty() 为真。问题是您的解释,这似乎表明它是如此因为您不能“返回不存在元素的地址”。 AFAIK,这不是真的;见[expr.unary:1,2,3]和[expr.add:4,7]【参考方案2】:
为了让您的示例更易于理解,您的问题中缺少的信息是 void f(Foo* array, unsigned numFoos);
在您的 Foo
向量上调用 .begin()
不能保证是一个指针。但有些实现可能表现得像它足以让它工作。
在空向量情况下,v.data()
返回一个指针,但您不知道它指向什么。它可能是一个 nullptr,但不能保证。
【讨论】:
为了说明关于空向量上的v.data()
的声明:参见this test with g++。注意在一种情况下它如何返回空指针,在另一种情况下它返回非空值。【参考方案3】:
这一切都归结为一件简单的事情:您可以为指针添加或减去一个整数值,但尝试取消引用无效指针是未定义的行为。
比如说,
int a[10];
int* p = a;
int* q = p + 10; // This is fine
int r = *(p + 10) // This is undefined behaviour
在您的示例中:v[0]
与 *(v's internal pointer+0)
相同,如果向量为空,则会出现问题。
【讨论】:
这是我理解的麻烦,我知道 v[0] 读取 v[0] 处的内存,但认为 &v[0] 只是获取该位置的地址。显然我错了。 v[0] 不仅提供了一个值,还提供了对某个内存位置的引用,比如某种窗口。要做到这一点,它必须适当地解释这个位置内容——如果不能,那就是 UB。指针只关心地址——它不关心某个地址下存储了哪些字节。 @Zebrafish 你没有错,v[0]
不读取任何内存,而是取消引用无效指针。取消引用本身不会做任何事情,除非您读取或写入此值,但 C++ 仍然不允许这样做。但它可以允许这样做。从技术上讲,没有什么可以阻止您的代码工作。
只要向量中有元素,&v[0] 就可以工作。我一直使用这种结构,但只有在将矢量调整到我需要的大小之后。调整大小会分配内存,此时 &v[0] 是完全安全的。根据这个讨论,我以后可能会切换到 .data(),但我不会回去更改旧代码...以上是关于使用 std::vector,为啥 &vec[0] 是未定义的行为,但 vec.data() 是安全的?的主要内容,如果未能解决你的问题,请参考以下文章
为啥 std::vector::resize(n, src) 按值传递?
为啥 std::vector max_size() 函数会返回 -1?
为啥使用 STL std::vector 作为 __block 变量会导致内存损坏?
为啥 std::vector::data() 中没有使用指针 typedef?