首选的方法来创建指向数组的`new`指针

17
这为什么是一个错误:
typedef int H[4];

H * h = new H;        // error: cannot convert 'int*' to 'int (*)[4]' in initialization

此外,为什么这不是一个错误:

此外,为什么这不是一个错误:

H * h = new H[1];

为什么编译器认为 new H 返回一个 int *,而 new H[1] 返回一个预期的 H *
换句话说:为什么对于一般类型 T,T * t = new T; 是正确的,但当 T 是数组类型时却不正确? 对于这种简单的数组类型,通过 new 分配内存的规范方式是什么?
请注意,这是一个简化的示例,因此例如 new int[4] 不是可接受的解决方法 - 我需要使用前面的 typedef 中的实际类型。
还要注意,我知道通常比 C 风格数组更喜欢使用 std::vectorstd::array 等,但我有一个需要使用上述类型的“现实世界”用例。

1
typedef int* H[4]; - Rahul Tripathi
1
@RahulTripathi:我不明白那有什么帮助 - 这将使 H 成为一个指向 int 的指针数组,对吗? - Paul R
1
@ErikAlapää:请看问题的最后一段。 - Paul R
1
好的,多维数组可能会变得棘手。 - Angew is no longer proud of SO
1
这是一个典型的情况,C++ 让量子力学看起来轻松、直观和简单... - Fabio says Reinstate Monica
显示剩余17条评论
4个回答

16

对于new T的返回类型和值,C++规则如下:

  • 如果 T 不是数组类型,则返回类型为 T *,返回值是指向动态分配的类型为 T 的对象的指针。
  • 如果 T 是类型为 U 的数组,则返回类型为 U *,返回值是指向动态分配的类型为 T 的数组第一个元素(类型为 U)的指针。

因此,由于您的 H 是一个 int 数组,new H 的返回类型是 int *,而不是 H *

按照相同的规则,new H[1] 返回 H *,但请注意,您已经技术上动态分配了一个大小为 1 x 4 的二维 int 数组。

在泛型代码中解决这个问题的最好方法确实是使用 auto

auto h = new H;

或者,如果您更喜欢强调指针事实:
auto *h = new H;

关于规则看似不一致的理由:

C++中指向数组的指针是相当“危险”的,因为它们的行为相当出乎意料(即你必须非常小心地使用它们,以免产生不良影响)。让我们看一下这段代码:

typedef int H[4];
H *h = obtain_pointer_to_H_somehow();
h[2] = h[1] + 6;

乍一看(甚至第二次看),上面的代码似乎是将数组中的第二个int加6,然后存储在第三个int中。但实际上,它并不是这样做的。
就像对于int *pp[1]是一个int(在地址sizeof(int)字节偏移量处于p),所以对于H *hh[1]是一个H,位于地址4 * sizeof(int)字节偏移处于h。因此,代码被解释为:取h中的地址,加上4 * sizeof(int)字节,然后再加上6,最后将结果地址存储在h8 * sizeof(int)偏移处。当然,这将失败,因为h[2]会退化为rvalue。
好的,那么你可以这样修复它:
*h[2] = *h[1] + 6;

现在情况更糟了。[]* 更紧密地绑定,所以这将会抵达 h 之后的第五个 int 对象(请注意,那里只有 4 个!),加上 6,并将其写入 h 之后的第九个 int。随机写入内存以获得胜利。
要实际执行代码的预期操作,它应该被拼写为:
(*h)[2] = (*h)[1] + 6;

鉴于上述情况,由于通常对动态分配数组的操作是访问其元素,因此new T[]更有意义地返回T *

@PaulR,int *p = new int[5]; 合法,这很“奇怪”吗? - M.M
3
@M.M:可能会让人感到奇怪的是,对于某些类型(如数组、引用),T* p = new T; 并不能起作用。 - Jarod42
@PaulR 我添加了我认为的好理由的讨论。 - Angew is no longer proud of SO
@Angew:谢谢 - 很好的答案 - 我相信它将帮助未来可能会为此问题苦恼的其他人。 - Paul R
有人尝试过查看 auto *h = new H; 中的 "h" 是什么类型吗?请参见 https://www.godbolt.org/z/non5bvTY6typedef int H[4]; auto *h = new H; static_assert(is_same_v<H*, int(*)[4]>); static_assert(is_same_v<decltype(h), int*>); - Nick Huang
显示剩余4条评论

4

主要问题

如何首选使用 new 指向数组的方法?

限制条件:

请注意,我知道使用 std::vector、std::array 等通常优于 C 风格的数组,但我有一个“真实世界”的用例,需要使用上述类型。

答案:

对于非数组情况:

#include <memory>

auto h = std::make_unique<H>();

// h is now a std::unique_ptr<H> which behaves to all intents and purposes
// like an H* but will safely release resources when it goes out of
// scope
// it is pointing to a default-constructed H

// access the underlying object like this:
h.get(); // yields H*
*h;      // yields H&

对于数组情况:

#include <memory>

auto h = std::make_unique<H[]>(4);

// h is now a std::unique_ptr<H[]> which behaves to all intents and purposes
// like an H* but will safely destruct and deallocate the array 
// when it goes out of scope
// it is pointing to an array of 4 default-constructed Hs

// access the underlying object like this:
h[1];     // yields H& - a reference to the 2nd H
h.get();  //yields H* - as if &h[0]

2
在C++标准中,newnew[]是特别分开的,因为它们可以是不同的分配器,用于提高性能和效率;分配数组与单个对象具有不同的使用模式,分配器实现进行了优化。
语法可能不同,因为在标准化时可用的编译时类型内省技术并不像今天那样可靠。
为了编写合理的代码,我认为首选的方法应该是:
struct H { int values[4]; }

H * h = new H;

这样,你的H类型在逻辑上“包含”了四个int值的数组 - 但内存结构仍应兼容(assert(sizeof(H) == 4 * sizeof(int))); 而且你可以使用对象风格分配固定大小的数组。更加符合C++语言的特性。


1
@Jarod42 OP在问题中承认了这一点-但它是否必须是指针兼容的呢? - rvalue

0

如果你基本上手动执行typedef替换,你就可以看到原因:

H * h = new H;

简化为:

int[4]* h = new int[4];

H * h = new H[1];

简化为:

(int*)[4] h = new int[1][4];

因为数组到指针衰减规则的缘故,这是合法的。
如果你想要一个真正的解决方案,并且你知道H的类型,你可以做类似于以下的操作:
typedef int* J;
typedef int H[4];

J j = new H; // This will work

3
int[4]* h 不是合法的语法,正确的写法应该是 int (*h)[4]; 而 new int[1][4] 表示申请了一个包含一个长度为 4 的整型数组的二维数组。 - M.M
@Yuushi:实际的数组类型比问题中的示例复杂得多(它是一个三维数组)-看起来使用auto可能是一个好的解决方案。 - Paul R
你给了一个无用的答案。他所问的是如何通过“new”方法获得“int()[4]”,而不仅仅是“int”。 - Nick Huang

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