【问题标题】:Why is the copy constructor not called when objects are allocated on heap?为什么在堆上分配对象时不调用复制构造函数?
【发布时间】:2021-02-26 17:54:31
【问题描述】:
class Guitars
{
private:
    int serialNumber{0};
    float price{0.0};
    // GuitarSpecs spec{};

public:
    Guitars(int serNum, float price)
    {
        this->serialNumber = serNum;
        this->price = price;
    };
    Guitars(const Guitars &s)
        : serialNumber{s.serialNumber}, price{s.price}
    {
        std::cout << "Copy" << std::endl;
    };

    Guitars(Guitars &&source) noexcept : serialNumber{source.serialNumber}, price{source.price}
    {
        source.serialNumber = NULL;
        source.price = NULL;
        std::cout << "Move" << std::endl;
    };

    int GetSerial() const { return serialNumber; };
    float GetPrice() const { return price; };
    void SetPrice(float x) { this->price = x; }
};

class Inventory
{
private:
    list<Guitars *> *guitarList;

public:
    Inventory()
    {
        guitarList = new list<Guitars *>;
    }

    void AddGuitar(int serNum, float price)
    {
        Guitars *x = new Guitars(serNum, price);
        // Guitars x(serNum,price);
        guitarList->push_back(x);
    }

    void Display()
    {
        for (auto &&i : *guitarList)
        {
            std::cout << i->GetPrice() << "   " << i->GetSerial() << endl;
        }
    }

    ~Inventory()
    {
        for (auto &&i : *guitarList)
        {
            std::cout << i->GetSerial() << "  "
                      << "deleted " << std::endl;
            delete i;
        }
        std::cout << "List is deleted" << std::endl;
        delete guitarList;
    }
};

int main()
{
    Inventory I;
    I.AddGuitar(12050, 50.23);
    I.AddGuitar(10000, 20.00);
    I.Display();

    return 0;
}

有人能解释一下为什么上面的代码中没有调用复制构造函数吗?

当我在堆上创建 Guitar 指针列表以及堆上的 Guitar 对象以及指向它们的指针并将这些指针保存在 Inventory 列表中时,不会调用复制构造函数。为什么会发生这种情况?这是否更有效,因为程序不需要创建对象的副本,它在堆上创建一次,我们将指针保存在我们身边。

【问题讨论】:

  • 复制指针不会复制底层对象。
  • 坦率地说,list&lt;Guitars *&gt; *guitarList; 是可憎的,list&lt;Guitars&gt; guitarList; 是你应该使用的。
  • 这些都不是性能最好的。我不确定您为什么要如此糟糕地使用指针。使用std::list::emblace_back,或者如果由于某种原因不能,请移动操作。
  • @MohammadHussein 在您发布的代码中根本不需要指针。让编译器生成的复制构造函数和赋值运算符来完成工作,因为编译器知道在这种情况下如何优化代码。通过使用指针和new/delete,您正在做的是取消编译器优化代码的手段——因此代码可能会运行更慢,而不是更快。 “使用指针优化编译器”的技巧可能在 20 年前就奏效了,但在当今优化编译器的时代,它并不总是奏效。
  • 在堆上创建 List 并使用复制和移动构造函数使程序更高效 -- 这是不好的信息。在堆上创建对象不会提高程序的性能。

标签: c++ c++11 pointers c++14 copy-constructor


【解决方案1】:

一些详细的答案,基于您谈论的优化:

您有以下代码:

list<Guitars *> *guitarList;

void AddGuitar(int serNum, float price)
{
    Guitars *x = new Guitars(serNum, price);
    // Guitars x(serNum,price);
    guitarList->push_back(x);
}

我认为您使用所有这些指针的原因是为了更快。如果Guitars 对象像往常一样在堆栈上创建然后被推回,那将创建一个副本,真的。
您可以做的是为Guitars 定义一个移动操作,并将堆栈对象移动到您在列表中创建的内容中,例如调用move constructor
但更好的是使用std::list::emplace_back,像这样:

list<Guitars> guitarList;

void AddGuitar(int serNum, float price)
{
    guitarList.emplace_back(serNum, price);
}

无论如何,如果你谈论最优性,那些指针并不好。指针需要额外的空间,并且每次访问数据时,都必须取消引用指针。此外,正如@PaulMcKenzie 在 cmets 中所写,这可能会阻止编译器为您优化。

此外,将列表成员本身设为指针,与list&lt;Guitars*&gt;* guitarList;list&lt;Guitars&gt;* guitarList; 一起使用,也不是一个好主意。我看到的唯一原因是如果您想交换两个 Inventory 对象的列表,但在这种情况下,只需在列表上调用 std::swap

如果您放弃指针,请注意您的所有其他代码如何立即变得容易得多。你甚至根本不需要定义你的析构函数。

(至于您提出的实际问题,就像@Jarod42 已经写的那样,复制指针不会复制对象。)

(顺便说一句,如果类 Guitars 表示单吉他,那么我会选择单数,Guitar。)

编辑:

我用不同的方式创建了一个小系列的测试来填充列表,使用Guitars 大部分未修改。 (不过,我删除了指向 NULL 的非指针的分配。)无论如何,我做了以下测试设置:

#include <iostream>
#include <list>

class Guitar
{
private:
    int serialNumber{0};
    float price{0.0};

public:
    Guitar(int serNum, float price)
    {
        std::cout << "Base" << std::endl;
        this->serialNumber = serNum;
        this->price = price;
    }
    Guitar(const Guitar& s)
        : serialNumber{s.serialNumber}, price{s.price}
    {
        std::cout << "Copy" << std::endl;
    }

    Guitar(Guitar&& source) noexcept : serialNumber{source.serialNumber}, price{source.price}
    {
        std::cout << "Move" << std::endl;
    }
};

void test_1()
{        
    std::cout << "test 1" << std::endl;
    std::list<Guitar*> guitarList;
    Guitar* x = new Guitar(1, 2.);
    guitarList.push_back(x);
    std::cout << std::endl;
}

void test_2()
{ 
    std::cout << "test 2" << std::endl;
    std::list<Guitar> guitarList;
    Guitar x(1, 2.);
    guitarList.push_back(x);
    std::cout << std::endl;
}

void test_3()
{ 
    std::cout << "test 3" << std::endl;
    std::list<Guitar> guitarList;
    guitarList.push_back(Guitar(1, 2.));
    std::cout << std::endl;
}

void test_4()
{
    std::cout << "test 4" << std::endl;
    std::list<Guitar> guitarList;
    guitarList.emplace_back(1, 2.);
    std::cout << std::endl;
}

int main()
{
    test_1();
    test_2();
    test_3();
    test_4();
}

这个的输出是:

test 1
Base

test 2
Base
Copy

test 3
Base
Move

test 4
Base

我希望这有助于进一步了解这里的工作原理。
测试可以在http://www.cpp.sh/35ld6下找到

另外,我想提一下,如果我们谈论优化,我们必须谈论我们优化的什么。现在,我们有一些几乎没有内容的小对象列表。在这种情况下,根本不会优化,因为我们谈论的是纳秒的差异。
需要考虑的案例有:

  1. 一小部分易于移动的大型对象。在这种情况下,我们需要确保没有调用复制构造函数,但 move 就可以了。
  2. 一小部分难以移动的大物体。在这种情况下,我们只想使用基本运算符,可能像您最初所做的那样通过指针 - 但emplace_back 也可以工作并使事情变得更容易。请注意,难以移动的对象会暗示该类的设计不佳。
  3. 一大堆小物件。在这里,我们希望使用尽可能少的构造函数,包括移动构造函数。我们也不想使用指针列表,因为这会给我们每个对象额外的 64 位,以及稍后的大量取消引用。在这种情况下,emplace_back 真的很出色。

换句话说,emplace_back 不会出错。

【讨论】:

  • 通过引用传递ints 和floats 肯定不如通过值传递它们最佳。不要这样做。
  • @G.Sliepen 你能详细说明一下吗?现在我只看到两个副本而不是没有副本。 (并不是我不信任你,只是想在我编辑答案之前听听原因。)
  • @Aziuth 谢谢,这对我帮助很大:) 你能在上面的 Guitar 类中添加一个复制和移动构造函数吗?
  • 这是一个讨论passing integers as references vs. copying的链接。请注意,编译器非常擅长优化事物,特别是如果它们可以进行过程间优化,因此如果所有内容都在同一个源文件中,或者如果使用链接时间优化,编译器可能会优化掉引用。但是在某些情况下,这是不可能的,而且需要更多的打字:)
  • @G.Sliepen 非常感谢您的努力。今天学到了一些东西。
猜你喜欢
  • 1970-01-01
  • 2012-03-16
  • 2021-02-18
  • 2014-01-14
  • 1970-01-01
  • 2010-11-24
  • 2013-05-20
  • 2013-12-08
相关资源
最近更新 更多