【问题标题】:Are class data members initialized before the copy constructor when using the copy constructor?使用复制构造函数时,类数据成员是否在复制构造函数之前初始化?
【发布时间】:2021-02-08 22:55:46
【问题描述】:

例如,如果我有这个类:

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

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

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

因此也初始化了数据成员,因为它没有被事先删除,它是否会为第一个成员创建内存泄漏?或者原始数据成员甚至不会初始化?

【问题讨论】:

  • 复制构造函数在这方面没有什么特别之处。你试试心理实验怎么样:在你的默认或显式构造函数中,如果你做同样的事情,你认为你会泄漏内存吗?
  • @SamVarshavchik 我认为除非在构造函数初始化程序列表中初始化,否则它也会造成内存泄漏。
  • 好了,你自己想出来的!

标签: c++ class pointers memory-leaks copy-constructor


【解决方案1】:

非静态数据成员的默认成员初始化器将用于构造函数中,其中相同的数据成员不存在于成员初始化器列表中

[...] 会不会造成内存泄漏...?

是的。

您的示例中使用的 默认成员初始化程序 (DMI):

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

仅在用于给定构造函数的数据成员(此处为j)未在该给定构造函数的成员初始化列表中初始化。

因此,如果您将复制构造函数添加到Counter没有成员初始化列表,则将使用数据成员j 的默认成员初始化器,因此您将拥有内存泄漏。

我们可以通过将数据成员j 的 DMI 更改为立即调用的 lambda 来研究此行为,以便我们跟踪何时使用 DMI,以及简单复制指针的虚拟复制 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 数据成员复制

#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.
}

或者简单地将数据成员 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_pointerstd::shared_pointer 等智能指针来避免生命周期问题;但是,这超出了这个问题的范围。请注意,在上面的人为示例中,复制构造函数将 浅复制 int 资源,其中复制参数的 j 数据成员指针指向(并且可能拥有)。为了实现一个真实的案例复制构造函数,您可能希望深复制此资源。

【讨论】:

    【解决方案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];
     }
    

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

    所以如果你像这样显式地编写复制构造函数

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

    它将正常工作。但是如果你这样写

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

    这会导致内存泄漏。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2023-03-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-07-06
      • 1970-01-01
      • 2018-08-02
      相关资源
      最近更新 更多