malloc和放置new与new的区别

37

我最近几天一直在研究这个问题,到目前为止,除了教条主义的争论或者传统的说辞(比如“这是C++的方式!”)之外,我还没有找到什么有说服力的理由。

如果我要创建一个对象数组,除了方便之外,还有什么其他令人信服的理由可以使用:

#define MY_ARRAY_SIZE 10

//  ...

my_object * my_array=new my_object [MY_ARRAY_SIZE];

for (int i=0;i<MY_ARRAY_SIZE;++i) my_array[i]=my_object(i);

结束

#define MEMORY_ERROR -1
#define MY_ARRAY_SIZE 10

//  ...

my_object * my_array=(my_object *)malloc(sizeof(my_object)*MY_ARRAY_SIZE);
if (my_object==NULL) throw MEMORY_ERROR;

for (int i=0;i<MY_ARRAY_SIZE;++i) new (my_array+i) my_object (i);

据我所知,后者比前者更有效率(因为你不需要将内存初始化为某些非随机值/不必要地调用默认构造函数),而实际上唯一的区别就是一个需要使用下面这个清理代码:

delete [] my_array;

另外一种清理方式是:

for (int i=0;i<MY_ARRAY_SIZE;++i) my_array[i].~T();

free(my_array);

我有一个重要的原因需要说明。仅仅因为使用C++(而不是C)这个理由,就声称不能使用mallocfree并不具有说服力,反而显得教条主义。我想知道是否有遗漏的信息使得new []malloc更优秀?
在我看来,你甚至无法使用new []创建没有默认构造函数的数组,而malloc方法则可以用于此类情况。

60
这也不符合 C++ 的惯用写法。惯用的 C++ 会使用 std::vector 或者 std::array - David Brown
3
有些情况下这样做可能是合理的。但这难道不是过度工程化吗(很多类都有无参数构造函数,并且经常你知道你只关心少数几个具有此功能的类),在95%的情况下这不是过早优化吗? - user395760
5
重点是,你的效率是以什么代价换来的?通过完全放弃newdelete,你在实际上获得了多少性能提升,相比于你失去了多少可读性和犯下了多少冗余错误?从来没有一个我或我认识的任何人工作过的程序中,不必要的初始化曾经成为瓶颈。只有在极少数情况下,不必要的默认构造函数调用才会成为问题。在这些罕见的情况下,已经为这些情况处理并安全地抽象化了。 - Sion Sheevok
5
@RobertAllanHenniganLeahy: 我不理解某件事。你显然已经决定要做什么了。你考虑过并拒绝了完全合法和合理的反对论点。那么...你到底有什么问题?你是试图在问题的幌子下进行辩论吗?你是在寻求对你已经建立的观点的确认吗?因为你不太可能找到它。 - Nicol Bolas
3
@RobertAllanHenniganLeahy 做得很好,坚定自己的立场。C++程序员可能会对习惯用法过于抱怨,但事实是这些只是观点。这不一定是“C++的方式”;如果有什么问题,C++让你可以深入了解C的内部,这是有原因的。如果人们想要这么多,他们可以将这个功能封装在一个惯用的包中; 不应该因为别人获得了他们太害怕管理的性能而哭泣。在我看来,C++内存管理库之所以处于当前状态,是因为教条主义阻碍了进步。 - That Realty Programmer Guy
显示剩余19条评论
11个回答

64
我有一个很充分的理由离开。
这取决于你如何定义“充分”。到目前为止,你已经拒绝了许多论点,但对大多数C ++程序员来说,它们肯定是有说服力的,因为你的建议不是在C ++中分配裸数组的标准方式。
事实很简单:是的,你绝对可以按照你描述的方式做事。没有理由说明你的方法不会正常工作。
但是同样,你也可以在C中使用虚函数。如果你付出时间和精力,你可以在纯C中实现类和继承。它们同样完全可用。
因此,重要的不是某件事是否可行。而是它的成本。在C ++中实现继承和虚函数比在C中更容易出错。在C中有多种实现方法,导致不兼容的实现。而在C++中,它们是一流的语言特性,人们高度不可能手动实现语言提供的内容。因此,每个人的继承和虚函数都可以遵守C ++的规则。
这也适用于这个问题。那么手动malloc / free数组管理的收益和损失是什么?
我不能说我即将说的任何内容对你来说都是“有说服力的原因”。我相信不会,因为你似乎已经下定了决心。但是为了记录:
性能
你声称以下内容:
所以,这个说法表明效率的增益主要在于涉及对象的构造。也就是说,调用哪些构造函数。该语句预设你不想调用默认构造函数;你使用默认构造函数只是为了创建数组,然后使用真实的初始化函数将实际数据放入对象中。
嗯...如果这不是你想要做的呢?如果你想要创建一个空数组(即默认构造函数),这种优势完全消失了。
易碎性
假设数组中的每个对象都需要调用特殊的构造函数之类的东西,以便初始化数组需要这种东西。但请考虑您的销毁代码:
for (int i=0;i<MY_ARRAY_SIZE;++i) my_array[i].~T();

对于简单的情况,这样做是可以的。你有一个宏或常量变量,它指示你有多少个对象。然后你循环遍历每个元素来销毁数据。这对于简单的例子来说很好。

现在考虑一个真正的应用程序,而不是一个例子。你会在多少不同的地方创建数组?数十个?数百个?每个都需要有自己的for循环来初始化数组。每个都需要有自己的for循环来销毁数组。

只要输错一次,就可能破坏内存。或者没有删除某些东西。或者其他任何可怕的事情。

这里有一个重要的问题:对于给定的数组,你在哪里保存大小?你知道为每个创建的数组分配了多少项吗?每个数组可能都有自己的方式来知道它存储了多少项。因此,每个析构循环都需要正确获取这些数据。如果弄错了...炸掉了。

然后我们有异常安全,这是一个全新的难题。如果其中一个构造函数抛出异常,之前构造的对象需要被销毁。你的代码没有做到这一点;它不是异常安全的。

现在,考虑另一种选择:

delete[] my_array;

这个方法不可能失败。它将销毁每一个元素,并跟踪数组的大小,而且它还具有异常安全性。所以它是保证能够工作的,只要你使用new[]进行了分配。

当然,你可以说你可以将数组封装在一个对象中。这很有道理。你甚至可以将该对象模板化为数组类型的元素。这样,所有的析构函数代码都是相同的,大小包含在对象中。也许,你意识到用户应该对内存分配的特定方式有一些控制,这样它就不只是malloc/free

恭喜你:你刚刚重新发明了std::vector

这就是为什么许多C++程序员甚至不再输入new[]

灵活性

你的代码使用malloc/free。但是假设我正在进行一些分析,我意识到对于某些经常创建的类型,malloc/free太昂贵了。我为它们创建了一个特殊的内存管理器。但如何将所有的数组分配连接到它们上?

好吧,我必须搜索代码库,找到任何创建/销毁这些类型数组的位置。然后我必须相应地更改它们的内存分配器。然后我必须不断地监视代码库,以防其他人将那些分配器改回来或者引入使用不同分配器的新的数组代码。

如果我使用new[]/delete[]代替,我可以使用运算符重载。我只需为这些类型提供new[]delete[]的重载。没有代码需要改变。要规避这些重载更困难; 它们必须主动尝试才行。等等。

因此,我获得了更大的灵活性,并合理保证我的分配器将在应该使用它们的地方使用。

可读性

考虑下面这个例子:

my_object *my_array = new my_object[10];
for (int i=0; i<MY_ARRAY_SIZE; ++i)
  my_array[i]=my_object(i);

//... Do stuff with the array

delete [] my_array;

与此相比:

my_object *my_array = (my_object *)malloc(sizeof(my_object) * MY_ARRAY_SIZE);
if(my_object==NULL)
  throw MEMORY_ERROR;

int i;
try
{
    for(i=0; i<MY_ARRAY_SIZE; ++i)
      new(my_array+i) my_object(i);
}
catch(...)  //Exception safety.
{
    for(i; i>0; --i)  //The i-th object was not successfully constructed
        my_array[i-1].~T();
    throw;
}

//... Do stuff with the array

for(int i=MY_ARRAY_SIZE; i>=0; --i)
  my_array[i].~T();
free(my_array);

客观来讲,哪一个更容易阅读和理解呢?
看看这个声明语句:(my_object *)malloc(sizeof(my_object) * MY_ARRAY_SIZE)。 这是一种非常低级别的东西。 你不是在分配任何数组;你正在分配一块内存。 你必须手动计算内存块的大小,以匹配对象*所占字节数乘以想要的对象数量。 它甚至包含了一个转换。
相比之下,new my_object [10]则很简单。 new是c++中创建类型实例的关键字。 my_object [10]my_object类型的10个元素数组。 这很简单、明显、直观。 没有转换,没有计算字节大小,没有别的。
使用malloc需要学习如何惯用地使用malloc。而使用new则只需要了解new的工作原理。它少得多,并且更容易理解发生了什么。
此外,在malloc语句之后,您实际上并没有一个对象数组。malloc只是返回一块内存,您已经告诉C++编译器通过转换将其假装成一个指向对象的指针。它不是一个对象数组,因为C++中的对象有生命期,只有在构造函数被调用后才开始。这个内存中什么都没有构造,因此其中没有任何活动对象。
此时my_array并不是数组;它只是一块内存。直到下一步构建对象,它才成为一个my_object数组。这对于新的程序员来说非常难以理解,需要有经验的c++程序员(可能是从C语言学习而来)来知道它们不是实际的对象,应该小心处理。指针还没有像正确的my_object*那样行为良好,因为它还没有指向任何my_object
相比之下,使用new[]就能创建出对象数组。对象已经被构造了,它们是活着、完整的。您可以像使用任何其他my_object*一样使用此指针。
Fin
以上内容并不意味着这种机制在适当的情况下不会有用。 但认可某些情况下的效用,和说它应该成为默认做事方式是完全不同的。

4
鉴于这里的陈述已经很好了,我更愿意在你的回答中添加内容,而不是写一个新的答案。您错过了一个非常好的理由:异常安全。如果调用第三个构造函数失败,则应该为前两个构造对象调用析构函数。new[]自动执行此操作,OP的解决方案存在缺陷。 - Matthieu M.
1
@MatthieuM:我在“脆弱性”部分添加了一些关于异常安全的内容。感谢提醒。 - Nicol Bolas
虽然 newnew[] 的缺点是人们可以在它们的构造函数中抛出异常。这似乎一开始还好,直到你尝试分配 new obj[10] 并且在第9个对象时 obj() 构造函数抛出异常。你将会非常有趣地尝试避免泄漏前面成功创建的9个对象。 - user268396
5
你在说什么?C++规范非常明确:如果一个对象在其构造函数中抛出异常,所有已经构造的对象都将被销毁。这是自动的并且根据规范必须这样执行。如果你看到这种情况没有发生,那就是编译器的错误。 - Nicol Bolas
在现代C++应用程序中,为什么要使用newmallocvector+reserve在可维护性、安全性和性能方面都比两者更好。 - Inverse
1
@Inverse:任何愿意使用malloc而不是new[]的人可能太过于投入于C语言的方式,无法考虑使用vector。此外,我提到了所有需要采取的步骤来使malloc/free实际工作(存储长度以便调用正确数量的析构函数、异常安全等)实际上会给你带来vector - Nicol Bolas

38
如果您不想通过隐式构造函数调用来初始化内存,而只需要为placement new保证内存分配,那么使用malloc和free代替new[]和delete[]完全没有问题。
使用new而不是malloc的强制性原因在于,new通过构造函数调用提供隐式初始化,可以节省额外的memset或相关函数调用,在malloc之后不需要检查每次分配后是否为NULL, 只需封装异常处理程序即可完成工作,避免了像malloc一样冗余的错误检查。但这两个强制性原因都不适用于您的用法。
哪种方法更高效只能通过性能分析来确定,您现在采取的方法没有问题。顺便说一句,我也看不到使用malloc而不是new[]的强制性原因。

4
使用new[]相比于OP的解决方案的一个强有力的理由是异常安全性。在构造过程中抛出异常时,OP的代码会崩溃或者泄漏,而new[]将在清理之后允许异常传播。 - Matthieu M.
@MatthieuM:我相信OP已经通过检查malloc的返回值来进行错误处理,这无疑是繁琐的,但是可能的。只是因为我注意到这个帖子上有这么多的流量,让我明确一点。从OP提出问题的方式来看,我的印象仍然是OP不是一个新手,他采取了一种不正确的方法,而是一个相当有经验的用户,他理解语义并期望直接回答。我在这里提供了答案,是的,有更好的方法,但是替代方法实际上不是OP所问的。 - Alok Save
6
@Als:我相信OP至少在C语言方面有经验,然而他的解决方案在面对异常时是不正确的。如果第三个对象的初始化抛出异常会发生什么?谁来清理先前构建的两个对象?他如何确保未初始化的原始内存上不会调用析构函数?new[]可以处理它,但OP没有,这不是完整的替代,只是一个不太稳定的替代。 - Matthieu M.
1
@Robert Allan Hennigan Leahy:回答问题的一个重要部分是知道何时一个人提出了错误的问题。如果有人说“我需要一些钱:我应该去抢银行还是去找高利贷?”,我会正确地建议两个都不要,并建议他们从银行贷款或向朋友借款-或帮助他们意识到他们根本不需要这笔钱。99.9%的人提出像你这样的问题属于那些类别;在两个糟糕的选择之间进行选择,甚至询问他们不应该担心的话题的问题。 - user1084944

19

我会说都不是最好的选择。

最好的方式是:

std::vector<my_object>   my_array;
my_array.reserve(MY_ARRAY_SIZE);

for (int i=0;i<MY_ARRAY_SIZE;++i)
{    my_array.push_back(my_object(i));
}

这是因为在内部,vector 可能已经为您执行了放置 new 操作。它也管理着所有与内存管理相关的其他问题,而您没有考虑到这些问题。


3
使用 emplace_back 可以避免复制构造函数,或者在没有复制构造函数的情况下使用。 - Neil G
2
@NeilG my_object(i) 是一个右值,所以如果可能的话,对象将被移动。但是,emplace_back 将会更加高效 :) - fredoverflow
@FredOverflow:如果my_object不可复制,这段代码是否能编译? - Neil G
@NeilG 如果my_object可移动或可复制,它就可以编译。 - fredoverflow

10

你在这里重新实现了new[]/delete[],并且你所写的内容在开发专用分配器时很常见。

与分配相比,调用简单构造函数的开销很小。它不一定“更加高效”-这取决于默认构造函数和operator=的复杂性。

尚未提到的一个好处是new[]/delete[]已知数组的大小。delete[]只需要在需要时正确地销毁所有元素即可。如果你要拖着另外一个变量(或三个)来确切地知道如何销毁数组,那将是很痛苦的。但是,一个专用的集合类型也是一个不错的选择。

new[]/delete[]很方便。它们引入的开销很小,并且可以避免许多愚蠢的错误。你是否足够强制自己去带走这个功能,并在每个地方使用集合/容器来支持自定义构造?我已经实现了这个分配器-真正的麻烦是为你在实践中需要的所有构造变体创建函数对象。无论如何,你通常会以更精确的执行代价换取一个比大家都知道的惯用语更难维护的程序。


7
我认为两者都不太好看,最好使用向量。只需提前分配空间以提高性能即可。
任选一种方式:
std::vector<my_object> my_array(MY_ARRAY_SIZE);

如果您希望为所有条目初始化一个默认值。

my_object basic;
std::vector<my_object> my_array(MY_ARRAY_SIZE, basic);

如果您不想构建对象,但又想保留空间:

std::vector<my_object> my_array;
my_array.reserve(MY_ARRAY_SIZE);

如果您需要将其作为C风格指针数组访问,只需(确保在保留旧指针的同时不添加内容,但是使用常规的C风格数组也无法实现这一点。)

my_object* carray = &my_array[0];      
my_object* carray = &my_array.front(); // Or the C++ way

访问单个元素:

my_object value = my_array[i];    // The non-safe c-like faster way
my_object value = my_array.at(i); // With bounds checking, throws range exception

漂亮的类型定义:

typedef std::vector<my_object> object_vect;

使用引用将它们传递给函数:

void some_function(const object_vect& my_array);

编辑: 在C++11中,还有std :: array。但问题在于它的大小是通过模板完成的,因此您无法在运行时制作不同大小的数组,并且除非函数期望具有完全相同的大小(或者是模板函数本身),否则无法将其传递到函数中。但是,它可以用于诸如缓冲区之类的东西。

std::array<int, 1024> my_array;

编辑2: 在C++11中,除了push_back之外,还有一个新的emplace_back函数。它基本上允许你“移动”你的对象(或直接在向量中构造你的对象),从而节省了复制操作。

std::vector<SomeClass> v;
SomeClass bob {"Bob", "Ross", 10.34f};
v.emplace_back(bob);
v.emplace_back("Another", "One", 111.0f); // <- Note this doesn't work with initialization lists ☹

1
+1,但是您可以为接受数组的函数使用函数模板,例如在此“swap”函数中。 - Felix Dombek
6
我想给这个答案点赞,但是&my_array[0]&my_array.front()完全相同。在两种情况下,如果向量为空,则行为是未定义的。此外,在超出范围时抛出异常并不能使不正确的程序变得更加正确。 - Mankarse
1
@David:我尝试编辑了一下,但是由于修改的字符数不够,所以被拒绝了。无论如何,在第8行末尾缺少回车符,这导致代码块呈段落文本而不是代码呈现。 - Todd Lehman
即使调用了reserve,直接访问空向量中的元素仍然是未定义行为。 - rubenvb
这使得它不能作为动态缓冲区(用于调用C函数),尽管我们有用于此的unique_ptr数组,但您必须显式调用new[]... - rubenvb
显示剩余2条评论

5

哦,好吧,我原本认为由于回答的数量不多,所以没有必要介入...但我想我会像其他人一样被吸引进来。让我们开始吧

  1. 为什么你的解决方案是有问题的
  2. C++11处理原始内存的新设施
  3. 更简单的方法完成这项工作
  4. 建议

1. 为什么你的解决方案是有问题的

首先,你提供的两个代码片段并不相等。 new[] 可以正常工作,而你的解决方案在存在异常时失败得非常严重。

在幕后,new[] 跟踪构造的对象数,因此如果发生异常(例如,在第三个构造函数调用期间),它将正确调用前两个已构造对象的析构函数。

然而,你的解决方案失败得非常严重:

  • 要么你根本不处理异常(并且会大量泄漏)
  • 要么你尝试调用整个数组的析构函数,即使只构造了一半(可能会崩溃,但是由于未定义的行为,谁知道呢)

因此,这两者显然不相等。 你的解决方案有问题

2. C++11处理原始内存的新设施

在C++11中,委员会成员们意识到我们喜欢玩弄原始内存,并引入了更有效、更安全的帮助工具。

请查看cppreference的<memory>简介。以下示例展示了新的好东西(*):

#include <iostream>
#include <string>
#include <memory>
#include <algorithm>

int main()
{
    const std::string s[] = {"This", "is", "a", "test", "."};
    std::string* p = std::get_temporary_buffer<std::string>(5).first;

    std::copy(std::begin(s), std::end(s),
              std::raw_storage_iterator<std::string*, std::string>(p));

    for(std::string* i = p; i!=p+5; ++i) {
        std::cout << *i << '\n';
        i->~basic_string<char>();
    }
    std::return_temporary_buffer(p);
}

请注意,get_temporary_buffer是无异常的,它返回已实际分配内存元素数量作为pair的第二个成员(因此使用.first获取指针)。
(*) 或者可能并不像MooingDuck所说的那样新。 3. 更简单的方法完成此操作 就我而言,您似乎真正需要的是一种类型化的内存池,其中某些嵌入可能尚未初始化。
您了解 boost::optional 吗?
它基本上是一个原始内存区域,可以容纳给定类型(模板参数)的一个项目,但默认情况下没有任何内容。它具有类似指针的接口,可以查询内存是否实际被占用。最后,使用In-Place Factories,如果这是一个问题,你可以安全地使用它而不复制对象。
好吧,你的用例看起来真的像是std::vector< boost::optional<T> >(或者可能是deque?)。
4.建议
最后,如果您真的想自己做,无论是为了学习还是因为没有STL容器真正适合您,我建议您将其封装在一个对象中,以避免代码分散到各个地方。
别忘了:不要重复自己!
使用一个(模板化的)对象,您可以在一个地方捕捉您设计的精髓,然后在任何地方重用它。
当然,为什么不在这样做的同时利用新的C++11设施呢 :) ?

MSVC一直有get_temporary_buffer,这真的不是C++03的一部分吗?另外,需要提到的是void* memory = operator new(sizeof(my_object)*10); - Mooing Duck
@MooingDuck:我本以为不是,但我没有标准可以检查,所以你可能是对的(看着我的链接,我意识到它没有标记C++11)。 - Matthieu M.
1
请注意,get_temporary_bufferraw_storage_iterator在C++17中已被弃用。我们应该采取类似于以下方式:std::pmr::monotonic_buffer_resource resource(sizeof(std::string)*5); std::pmr::polymorphic_allocator<std::string> pallocator(&resource); std::string* p = pallocator.allocate(5); 尽管目前还没有编译器支持它。这来自于Boost,所以我写了一个演示 - AndyG

3

2
独断还是不独断,这正是所有STL容器分配和初始化的方法。
它们使用分配器来分配未初始化的空间,并通过容器构造函数进行初始化。
如果这(像许多人所说的)“不是C++”,那么标准库怎么可能只是像这样实现的呢?
如果你不想使用malloc/free,你可以只用new char[]来分配“字节”。
myobjet* pvext = reinterpret_cast<myobject*>(new char[sizeof(myobject)*vectsize]);
for(int i=0; i<vectsize; ++i) new(myobject+i)myobject(params);
...
for(int i=vectsize-1; i!=0u-1; --i) (myobject+i)->~myobject();
delete[] reinterpret_cast<char*>(myobject);

这使您可以利用初始化和分配之间的分离,同时利用new分配异常机制。

请注意,将我的第一行和最后一行放入myallocator<myobject>类中,将第二行和倒数第二行放入myvector<myobject>类中,我们就重新实现了std::vector<myobject,std::allocator<myobject>>


2
如果这个(正如许多人所说的)“不是C ++”,那么标准库怎么可能只是像那样实现呢? 因为它被包装在一个具有专用容器的库中,其构造函数和析构函数使其全部变得无形和透明。 此外,这些标准容器实际上使用了这个特性,因为分配器允许替换内存分配部分。 而调用malloc将始终调用malloc; malloc不能扩展或重载。 - Nicol Bolas
@NicolBolas: 确实如此,但是:也许OP处于同样的情况。如果这对STL作者有效,那么对OP也必须有效。也许这不是“方便”,但方便并不等同于道德,道德也不等同于合法性。我的意思是,通常一个“道德”问题被视为“法律”问题。在我看来,这是错误的。 - Emilio Garavaglia

1
你在这里展示的实际上是使用与系统通用分配器不同的内存分配器时的正确方法 - 在这种情况下,你将使用分配器(alloc->malloc(sizeof(my_object))来分配内存,然后使用放置 new 运算符进行初始化。这在高效的内存管理方面有许多优势,并且在标准模板库中非常常见。

1

如果你正在编写一个类,模仿std::vector的功能或需要控制内存分配/对象创建(在数组中插入/删除等)-那就是正确的方法。在这种情况下,问题不在于“不调用默认构造函数”。而是能够“分配原始内存,在那里移动旧对象,然后在旧地址处创建新对象”,能够使用某种形式的realloc等等。毫无疑问,自定义分配+放置new更加灵活...我知道,我有点喝醉了,但std::vector是给懦夫用的...关于效率-人们可以编写自己的std::vector版本,它将至少与大多数使用的80%的std::vector功能一样快(并且很可能更小,以sizeof()为单位),可能不到3个小时。


3
你明显喝醉了 8-) 坦白地说,如果每次编写C++程序都要花费3小时的话,我会浪费很多时间...现在我并不反对偶尔重新编写像std::vector之类的容器可能会有用(比如实时系统),但在大多数情况下,默认的std::vector更加简单易用,并且与boost::shared_ptr<>兼容! - Alexis Wilke
@Azza:当然可以编写更快的向量。那很容易。直到你记住异常安全性。然后要匹配向量就真的很难了。 - Mooing Duck
@AlexisWilke 我已经不再喝醉了,尽管我现在有点宿醉...你花了3个小时编写一个模板化的vector,然后你在所有程序中都使用它。为什么每次都要重新编写呢? - lapk
@MooingDuck 异常是 C++ 的一个重要组成部分。或者说,它们应该被视为:RAII + 异常 = 所有的错误处理,或接近它。我不明白为什么会有什么难以匹配的地方,使用异常在 C++ 中很正常,这并不新奇。 - lapk
实际上,每次工作与不同的客户一起需要3个小时是必要的。除非你在空闲时间做这件事并且许可它以便任何人都可以重复使用... 我对此的主要问题是我的版本可能只能与g++一起使用。 - Alexis Wilke

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