使用 copy 构造函数时,是否在复制构造函数之前初始化类数据成员?

Are class data members initialized before the copy constructor when using the copy constructor?

提问人:diagoot 提问时间:10/27/2020 更新时间:10/27/2020 访问量:99

问:

例如,如果我有这个类:

class Counter {
public:
    int* j = new int[5];
}

指针变量初始化为数据成员。如果在我的复制构造函数中,我有类似的东西

int* j = new int[7]int* j = new int[5]()

因此,初始化数据成员,是否会为第一个成员造成内存泄漏,因为它没有事先删除?还是原始数据成员甚至不会初始化?

C++ 指针 memory-leaks copy-constructor

评论

0赞 Sam Varshavchik 10/27/2020
在这方面,复制构造函数没有什么特别之处。你试着做一个心理实验怎么样:在你的默认或显式构造函数中,如果你做同样的事情,你认为你会泄漏内存吗?
0赞 diagoot 10/27/2020
@SamVarshavchik我认为除非在构造函数初始值设定项列表中初始化,否则是的,它还会产生内存泄漏。
1赞 Sam Varshavchik 10/27/2020
你去吧,你自己想通了!

答:

2赞 dfrib 10/27/2020 #1

非静态数据成员的默认成员初始值设定项将在成员初始值设定项列表中不存在相同数据成员的构造函数中使用

[...]它会造成内存泄漏吗?

是的。

示例中使用的默认成员初始值设定项 (DMI):

class Counter {
public:
    int* j = new int[5];  // default member initializer for data member 'j'
}

当对于给定构造函数时,数据成员 here 未在该给定构造函数的成员初始值设定项列表中初始化。j

因此,如果将复制构造函数添加到没有成员初始值设定项列表,则将使用数据成员的默认成员初始值设定项,因此将发生内存泄漏。Counterj

我们可以通过将数据成员的 DMI 更改为立即调用的 lambda 来研究这种行为,以允许我们跟踪何时使用 DMI,以及一个虚拟复制 ctor,它只是通过不同的方式复制复制 copy-in 参数的指针(这只是针对这个虚拟示例;请参阅关于生命周期管理以及深度复制与浅复制的最后一段):j

#include <iostream>

struct Counter {
    int* j = []() { 
        std::cout << "Calling DMI for j.\n";
        auto p = new int[5];
        return p; }();

    // Uses the DMI for member 'j'.
    Counter() {}

    // Uses the DMI for member 'j'.
    Counter(const Counter& c) { j = c.j; }  // Memory leak.
};

int main() {
    Counter c1;       // Calling DMI for j.
    Counter c2 = c1;  // Calling DMI for j.

    // Delete resource common for c1 and c2.
    delete c2.p;      // A rogue resource from c2 construction was leaked.
}

如果任一执行复制复制构造函数的成员初始值设定项列表中的数据成员:j

#include <iostream>

class Counter {
public:
    int* j = []() { 
        std::cout << "Calling DMI for j.\n";
        auto p = new int[5];
        return p; }();

    // Uses the DMI for member 'j'.
    Counter() {}

    // Does not use the DMI for data member 'j'.
    Counter(const Counter& c) : j(c.j) { }
};

int main() {
    Counter c1;       // Calling DMI for j.
    Counter c2 = c1;

    // Delete resource common for c1 and c2.
    delete c2.p;  // OK, no resources leaked.
}

或者干脆将数据成员显式设置为复制 CTOR 中成员初始值设定项列表的一部分:jnullptr

#include <iostream>

class Counter {
public:
    int* j = []() { 
        std::cout << "Calling DMI for j.\n";
        auto p = new int[5];
        return p; }();

    // Uses the DMI for member 'j'.
    Counter() {}

    // Does not use the DMI for data member 'j'.
    Counter(const Counter& c) : j(nullptr) { j = c.j; }
};

int main() {
    Counter c1;       // Calling DMI for j.
    Counter c2 = c1;

    // Delete resource common for c1 and c2.
    delete c2.p;  // OK, no resources leaked.
}

您将覆盖数据成员的 DMI。j

请注意,在使用原始 C 样式指针实现手动内存管理时,需要格外小心,这是生存期问题的常见原因。如果可能,请改用智能指针,例如 或 以避免生存期问题;然而,这超出了这个问题的范围。另请注意,在上面的人为示例中,复制构造函数将浅层复制复制参数的数据成员指针指向(并且可能拥有的)资源。若要实现实际案例复制构造函数,可能需要深度复制此资源。std::unique_pointerstd::shared_pointerintj

1赞 ALX23z 10/27/2020 #2

如果没有覆盖,将触发构造实例化。int* j = new int[5];

让我举个例子:

class Counter {
public:
   Counter(int x) {}; // j will be initialized as int* j = new int[5];
   Counter(double y) : j(nullptr) {}; // the line  j = new int[5]; won't be invoked. instead j = nullptr;

   int* j = new int[5];
 }

默认情况下,copy 构造函数通过复制 来覆盖它。j

因此,如果您显式编写复制构造函数,例如

Counter(const Counter& c) : j(c.j) {};

它将正常工作。但是如果你把它写成

Counter(const Counter& c) {j=c.j;};

这将导致内存泄漏。