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

C++ 使用复制构造函数时,类数据成员是否在复制构造函数之前初始化?,c++,class,pointers,memory-leaks,copy-constructor,C++,Class,Pointers,Memory Leaks,Copy Constructor,例如,如果我有这个类: class Counter { public: int* j = new int[5]; } 指针变量初始化为数据成员。如果在我的复制构造函数中,我有 int*j=new int[7]或int*j=new int[5] 因此,在初始化数据成员时,会不会因为第一个成员事先没有被删除而导致内存泄漏?或者原始数据成员甚至不会初始化?在成员初始值设定项列表中不存在相同数据成员的构造函数中,将使用非静态数据成员的默认成员初始值设定项 […]是否会造成内存泄漏 对 示例中使

例如,如果我有这个类:

class Counter {
public:
    int* j = new int[5];
}
指针变量初始化为数据成员。如果在我的复制构造函数中,我有

int*j=new int[7]或int*j=new int[5]

因此,在初始化数据成员时,会不会因为第一个成员事先没有被删除而导致内存泄漏?或者原始数据成员甚至不会初始化?

在成员初始值设定项列表中不存在相同数据成员的构造函数中,将使用非静态数据成员的默认成员初始值设定项 […]是否会造成内存泄漏

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

仅当给定构造函数的成员初始值设定项列表中未初始化数据成员(此处为j)时,才会使用

因此,如果您添加一个复制构造函数来计数器,而没有成员初始值设定项列表,那么将使用数据成员j的默认成员初始值设定项,因此您将出现内存泄漏

我们可以通过将数据成员j的DMI更改为立即调用的lambda来研究这种行为,以允许我们跟踪何时使用DMI或不使用DMI,以及通过不同方式简单复制copy in参数指针的伪复制ctor,这仅适用于此伪示例;请参阅关于生存期管理以及深度复制与浅复制的最后一段:

#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显式设置为nullptr,作为复制ctor中成员初始值设定项列表的一部分:

#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.
}
您将覆盖数据成员j的DMI

请注意,在使用原始C风格指针实现手动内存管理时,您需要格外小心,这是解决生命周期问题的常见方法。如果可能,可以使用智能指针,例如std::unique_指针或std::shared_指针,以避免生命周期问题;然而,这超出了这个问题的范围。请注意,在上面的人为示例中,copy构造函数将浅层复制参数点中copy的j data成员指针可能拥有的int资源。为了实现一个实际的复制构造函数,您可能需要深度复制此资源。

在成员初始值设定项列表中不存在相同数据成员的构造函数中,将使用非静态数据成员的默认成员初始值设定项 […]是否会造成内存泄漏

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

仅当给定构造函数的成员初始值设定项列表中未初始化数据成员(此处为j)时,才会使用

因此,如果您添加一个复制构造函数来计数器,而没有成员初始值设定项列表,那么将使用数据成员j的默认成员初始值设定项,因此您将出现内存泄漏

我们可以通过将数据成员j的DMI更改为立即调用的lambda来研究这种行为,以允许我们跟踪何时使用DMI或不使用DMI,以及通过不同方式简单复制copy in参数指针的伪复制ctor,这仅适用于此伪示例;请参阅关于生存期管理以及深度复制与浅复制的最后一段:

#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显式设置为nullptr,作为复制ctor中成员初始值设定项列表的一部分:

#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.
}
您将覆盖数据成员j的DMI


请注意,在使用原始C风格指针实现手动内存管理时,您需要格外小心,这是解决生命周期问题的常见方法。如果可能,可以使用智能指针,例如std::unique_指针或std::shared_指针,以避免生命周期问题;然而,这超出了这个问题的范围。请注意,在上面的人为示例中,copy构造函数将浅层复制参数点中copy的j data成员指针可能拥有的int资源。为了实现一个实际的复制构造函数,您可能需要深度复制这个资源。

构造实例化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];
 }
默认情况下,复制构造函数通过复制j来覆盖它

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

Counter(const Counter& c) : j(c.j) {};
它会正常工作。但是如果你像这样写的话

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

它将导致内存泄漏。

构造实例化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];
 }
默认情况下,复制构造函数通过复制j来覆盖它

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

Counter(const Counter& c) : j(c.j) {};
它会正常工作。但是如果你像这样写的话

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

这将导致内存泄漏。

在这方面,复制构造函数没有什么特别之处。你试试心理实验怎么样:默认还是e
xplicit构造函数,如果你做同样的事情,你认为你会泄漏内存吗?@SamVarshavchik我认为除非在构造函数初始值设定项列表中初始化,否则它也会造成内存泄漏。好了,你自己解决了这个问题!在这方面,复制构造函数没有什么特别之处。你试试心理实验怎么样:在你的默认或显式构造函数中,如果你做同样的事情,你认为你会泄漏内存吗?@SamVarshavchik我认为除非在构造函数初始值设定项列表中初始化,否则它也会产生内存泄漏。好了,你自己解决了这个问题!