将二维数组视为一维数组。

16

假设我们有一个二维的int数组:

int a[3][4] = { { 1,3,2,4 }, { 2,1,5,3 }, { 0,8,2,3 } };

将其地址引用并重新解释为指向一维int数组的指针合法吗?基本上是:

int *p = reinterpret_cast<int *>(&a);

这样我就可以做一些类似于以下的事情(大致上):

template<typename T, size_t X, size_t Y>
void sort2(T(&arr)[X][Y])
{
    T *p = reinterpret_cast<T *>(&arr);
    std::sort(p, p + X*Y);
}

示例:https://ideone.com/tlm190

据我所知,标准保证2D数组的对齐方式在内存中是连续的,虽然 p + X*Y 技术上超出了范围,但由于从未访问过,因此不应导致未定义行为。

当需要时,我是否可以将2D数组绝对视为1D数组?


3
有两种观点……一些人认为它是合法的,而另一些人则认为它不是。在C和C++中已经争论了30年,但没有得出结论。 - M.M
不确定它是否是 https://dev59.com/e1YM5IYBdhLWcg3wvCD- 的重复。它们本质上是相同的。 - xskxzr
@xskxzr 相关,但不完全重复。 - Killzone Kid
无论如何,T.C.的答案也适用于这个问题。 - xskxzr
那么一个简单的 int *p = a[0]; 怎么样? - chux - Reinstate Monica
显示剩余2条评论
4个回答

5
感谢您的回复和评论,但我认为正确答案是 - 目前代码表现出技术 UB,虽然可以纠正。我查看了 @xskxzr 提供的一些问题 [1, 2],并引用了标准中的 this quote from the standard

如果两个对象可以相互转换指针,则它们具有相同的地址,并且可以通过 reinterpret_­cast 从一个指针获得另一个指针。[注:即使数组对象和其第一个元素具有相同的地址,它们也不能相互转换指针。—end note]

然后在reinterpret_cast页面上,有以下带有示例的备注:

假设满足对齐要求,reinterpret_cast 在处理涉及指针可互换对象的少数局限情况外,不会改变指针的值:

int arr[2];
int* p5 = reinterpret_cast<int*>(&arr); // value of p5 is unchanged by reinterpret_cast and
                                        // is "pointer to arr"

尽管这编译时没有警告并运行, 但从技术上讲,这是未定义的行为,因为p5从技术上讲仍然是指向arr而不是arr[0]。因此,基本上使用我使用的reinterpret_cast会导致UB。考虑到上述情况,如果我直��创建指向第一个intint *(根据@codekaizer的答案,这是可以的),那么这应该是有效的,对吗?
template<typename T, size_t X, size_t Y>
void sort2(T(&arr)[X][Y])
{
    T *p = &arr[0][0]; // or T *p = arr[0];
    std::sort(p, p + X * Y);
}

但是由于指针p指向第一个包含Y个元素的T数组的第一个T,因此它可能也是UB。 因此,p + X * Y将超出这个T数组的第一个数组范围,因此UB(再次感谢@xskxzr提供的链接和评论)。
如果表达式P指向具有n个元素的数组对象x的元素x [i],则具有值j的表达式P + J和J + P分别指向(可能是假想的)元素x [i+j],如果0≤i+j≤n; 否则,行为未定义。 因此,在我放弃之前,这是我的最后一次尝试:
template<typename T, size_t X, size_t Y>
void sort2(T(&arr)[X][Y])
{
    T(&a)[X * Y] = reinterpret_cast<T(&)[X * Y]>(arr);
    std::sort(a, a + X * Y);
}

在这里,T arr[X][Y]首先会通过reinterpret_cast转换为T a[X*Y],我认为现在是有效的。重新解释的数组a可以愉快地衰减为指向数组a[X*Y]第一个元素的指针(a + X * Y也在范围内),并在std::sort中被转换为迭代器。 简短版本 由于不正确使用reinterpret_cast,OP的行为是未定义的。将二维数组转换为一维数组的正确方法是:
//-- T arr2d[X][Y]
T(&arr1d)[X*Y] = reinterpret_cast<T(&)[X*Y]>(arr2d);

类型为T1的lvalue表达式可以转换为另一种类型T2的引用。结果是一个lvalue或xvalue,引用与原始lvalue相同的对象,但具有不同的类型。不创建临时副本,不调用构造函数或转换函数。只有在类型别名规则允许的情况下,才能安全地访问所得到的引用。 类型别名规则
每当尝试通过类型为AliasedType的glvalue读取或修改DynamicType类型的对象的存储值时,除非以下条件之一成立,否则行为未定义:
- AliasedType和DynamicType是相似的。 类型相似性
非正式地说,如果忽略顶层cv限定符,两种类型是相似的,它们都是相同大小的数组或未知边界的数组,并且数组元素类型是相似的。

数组元素类型

在声明 T D 中,其中 D 的形式为

D1 [ 常量表达式 opt ] 属性说明符序列 opt

并且声明中标识符的类型为“派生声明符类型列表 T”,则声明 D 的标识符类型是数组类型;如果声明 D 的标识符类型包含 auto 类型说明符,则程序是非法的。 T 被称为数组的 元素类型


解决方案取决于一个二维数组可以被视为一个一维数组。您可以查看我的帖子获取详细信息。 - xskxzr
1
1D数组 a 在现实中并不存在,因此将数组转换为指针会导致未定义的行为。T.C. 的回答在这里也适用。 - xskxzr
虽然二维数组和一维数组在内存中的位置相同,但你不能将它们视为相同的数组。你可以阅读cppreference上的reinterpret_cast页面:“类型为T1的左值表达式可以转换为另一种类型T2的引用。结果是一个左值或xvalue,引用与原始左值相同的对象,但具有不同的类型。不创建临时副本,不调用构造函数或转换函数。如果允许类型别名规则(见下文),则只能安全地访问所得到的引用”。 - xskxzr
2
我看到你的编辑了。T[X*Y]T[X][Y]并不相似。T[X*Y]的长度为X*Y,而T[X][Y]的长度为XT[X*Y]的元素类型为T,而T[X][Y]的元素类型为T[Y] - xskxzr
@xskxzr 大小相同,相似元素为T,我认为你误解了文档(或者是我误解了:)) - Killzone Kid
显示剩余4条评论

2
http://www.cplusplus.com/doc/tutorial/arrays/
int jimmy [3][5];   // is equivalent to
int jimmy [15];     // (3 * 5 = 15)  

创建一个数组(无论是几维的)时,数组内存是一个固定大小的内存块,其大小为 size = sizeof(type) * dim0 * dim1 * ....; 所以针对你的问题,是的,你可以安全地将该数组重新转换为一维数组。

1
值得注意的是,这不适用于动态创建的数组。 - Piotr Siupa
1
它们是“等价的”,但你不能将T[X][Y]强制转换为T[X*Y]。一个是int **类型,另一个是int *类型。 - Vivick
1
@Vivick 实际上,一个是类型为 int(*)[15],另一个是类型为 int(*)[3][5] - Killzone Kid
3
在我看来,[3][5][15] 相同并不意味着你可以安全地在它们之间进行转换,它们仍然属于不同的类型。 - 463035818_is_not_a_number

1
一个二维数组不能被当作一个一维数组处理 正如@KillzoneKid所指出的那样,即使它们共享相同的地址值,数组的地址也不能与第一个元素的地址互相转换。 constexpr提供了一种方便的方法来评估未定义行为。当评估constexpr时,编译器需要检测UB。因此,可以进行简单的测试。这个函数在编译时评估时将检测访问超出数组范围的值。
// sum "n" sequential ints
constexpr int sum(const int* pi, int n) {
    int sum = 0;
    while (n-- > 0)
        sum += *pi++;
    return sum;
};

当在运行时使用指向1D或2D数组的第一个元素的指针调用此函数时,它将对元素求和,但如果“n”超出数组边界,则会出现UB。对于2D数组,这将是最低维度的范围。而不是整个数组的大小。
通过检查指向int的不同实例,我们可以看到一旦尝试访问超出数组维度的值就会发生UB。
int main()
{
    constexpr int i0{1};
    constexpr int i1[2]{ 1,2 };
    constexpr int i2[2][2] = { { 1,2}, {3,4} };

    constexpr int sum0 = sum(&i0, 1);       // fails for n>1
    constexpr int sum1 = sum(&i1[0], 2);    // fails for n>2
    constexpr int sum2 = sum(&i2[0][0], 2); // fails for n>2
    const int sum3 = sum(&i2[0][0], 4);     // runtime calc, UB. fails if constexpr

    return sum0 + sum1 + sum2 + sum3;       // 17
}

对于超出现有数据范围的访问,例如在sum0和sum1中,UB是清晰的。但是对于n=[0:3)的现有数据,sum2指向它,然而constexpr评估显示对于n=4,它是UB。
当我得知这一点时,我有些惊讶。我使用过的每个编译器在执行诸如将矩阵的所有系数按固定量缩放等操作时都能正常工作。但是我可以看到基于优化假设的理由,即矩阵的某些部分不会从另一个不在同一数组序列中的部分的函数调用的结果中改变。

0

是的,它是合法有效的。

根据dcl.array:

如果 E 是 i×j×⋯×k 级别为 n 的 n 维数组,则出现在受到"数组到指针"转换影响的表达式中的 E 将被转换为一个级别为 j×⋯×k 的(n−1)维数组的指针。如果对此指针应用了"*"运算符(无论是显式地还是隐式地作为下标引用的结果),则结果是指向(n−1)维数组的指针,这个指针本身会立即被转换为指针。


1
这段文字与问题有什么关联? - xskxzr
1
所以 int a[3][4] 可以转换为 int (*)[4]。OP 想要一直到 int*... - Jarod42

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