push_back 调用析构函数时使对象中的指针无效

push_back invalidates pointers in objects when calling the destructor

我有一个 struct 包含这样的指针:

struct S {
  S();
  ~S();

  int i;
  std::vector<int>* j;
};

S::S() {
  i = 0;
  j = 0;
}

S::~S() {
  if (j != 0) {
    cout << "Delete " << j << std::endl;
    delete j;
  }
}

我想使用 push_back() 将未知数量的 S 放入 std::vector 中。但是,当 vector 重新分配其内存以增长时,它会调用 S 的析构函数并使指针 j.

无效

我明白为什么下面的例子会出现段错误,但我想知道是否有好的方法来处理这种情况。

实际上,我想我可以通过在 vector 在其范围的末尾。毕竟内存是在main()中分配的,但我感觉S应该在其析构函数中处理它的内存。

int main()
{
    cout << "Insert the first one" << std::endl;
        
    std::vector<S> v;
    v.push_back(S());
    v[0].j = new std::vector<int> {1,2,3};
    
    cout << "Insert a new one" << std::endl;
    
    v.push_back(S());
    v[1].j = new std::vector<int> {2,3,4};

    std::cout << (*(v[0].j))[1] << std::endl; // segfault
    std::cout << (*(v[1].j))[1] << std::endl;
    return 0;
}

S 没有定义任何 copy/move 构造函数或 copy/move 赋值运算符来 copy/move j 。当 main() 中的 std::vector 重新分配其内部数组时,其中任何现有的 S 对象都会在内存中得到 copied/moved。

j 在您的示例中无效,因为 S 根本没有尝试管理 j。对于 main()std::vector 中的每个 S 对象,当它是 copied/moved 时,其 j 指针将 shallow-copied 放入新对象中,并且当旧对象被销毁时,j 指向的 std::vector 也被销毁,留下新对象中的 j 悬空,这就是为什么在访问 [=17 时出现段错误的原因=] 稍后.

classstruct 拥有指向资源的指针时,当它本身是 copied/moved.简而言之,该规则指出,如果 class/struct 需要实现析构函数、copy/move 构造函数或 copy/move 赋值运算符,它可能需要实施所有这些。在你的例子中,因为你有一个销毁 j 的析构函数,你需要添加其他的以确保每个 S 对象都有一个有效的 j 来销毁。

在您的示例中,我建议根本不要从 S 外部分配 std::vector。让 S 处理该分配,外部代码应该只根据需要用值填充 j

试试像这样的东西:

struct S {
  S();
  S(const S &);
  S(S &&);
  ~S();
  S& operator=(S);

  int get_i() const;
  void set_i(int);

  std::vector<int>& get_j();
  const std::vector<int>& get_j() const;
  void set_j(std::vector<int>);

private:
  int i;
  std::vector<int>* j;
};

S::S() {
  i = 0;
  j = new std::vector<int>;
}

S::S(const S &src) {
  i = src.i;
  j = new std::vector<int>(*(src.j));
}

S::S(S &&src) {
  i = src.i; src.i = 0;
  j = src.j; src.j = nullptr;
}

S::~S() {
  cout << "Delete " << j << std::endl;
  delete j;
}

S& S::operator=(S rhs) {
  S temp(std::move(rhs));
  std::swap(i, temp.i);
  std::swap(j, temp.j);
  return *this;
}

int S::get_i() const {
  return i;
}

void S::set_i(int new_value) {
  i = new_value;
}

std::vector<int>& S::get_j() {
  return *j;
}

const std::vector<int>& S::get_j() const {
  return *j;
}

void S::set_j(std::vector<int> new_value) {
  *j = std::move(new_value);
}
int main()
{
    cout << "Insert the first one" << std::endl;
        
    std::vector<S> v;
    v.emplace_back();
    v[0].set_j({1,2,3});
    
    cout << "Insert a new one" << std::endl;
    
    v.emplace_back();
    v[1].set_j({2,3,4});

    std::cout << v[0].get_j()[1] << std::endl;
    std::cout << v[1].get_j()[1] << std::endl;
    return 0;
}

或者,根本不要手动使用 new/delete,而是使用 std::unique_ptr,然后你可以完全删除析构函数(但不是 copy/move构造函数和 copy/move 赋值运算符),例如:

struct S {
  S();
  S(const S &);
  S(S &&);
  S& operator=(S);

  int get_i() const;
  void set_i(int);

  std::vector<int>& get_j();
  const std::vector<int>& get_j() const;
  void set_j(std::vector<int>);

private:
  int i;
  std::unique_ptr<std::vector<int>> j;
};

S::S() {
  i = 0;
  j = std::make_unique<std::vector<int>>();
  // or, prior to C++14:
  // j.reset(new std::vector);
}

S::S(const S &src) {
  i = src.i;
  j = std::make_unique<std::vector<int>>(*(src.j));
  // or, prior to C++14:
  // j.reset(new std::vector<int>(*(src.j)));
}

S::S(S &&src) {
  i = src.i; src.i = 0;
  j = std::move(src.j);
}

S& S::operator=(S rhs) {
  S temp(std::move(rhs));
  std::swap(i, temp.i);
  std::swap(j, temp.j);
  return *this;
}

int S::get_i() const {
  return i;
}

void S::set_i(int new_value) {
  i = new_value;
}

std::vector<int>& S::get_j() {
  return *j;
}

const std::vector<int>& S::get_j() const {
  return *j;
}

void S::set_j(std::vector<int> new_value) {
  *j = std::move(new_value);
}
int main()
{
    cout << "Insert the first one" << std::endl;
        
    std::vector<S> v;
    v.emplace_back();
    v[0].set_j({1,2,3});
    
    cout << "Insert a new one" << std::endl;
    
    v.emplace_back();
    v[1].set_j({2,3,4});

    std::cout << v[0].get_j()[1] << std::endl;
    std::cout << v[1].get_j()[1] << std::endl;
    return 0;
}

然而,话虽如此,实际上根本没有充分的理由动态分配 jstd::vector 为自己实现了 3/5/0 规则,所以你可以摆脱 S 中的动态指针,让编译器为你管理 std::vector 对象,就像它一样为 main() 中的 std::vector 对象执行操作,例如:

struct S {
  int i = 0;
  std::vector<int> j;
};
int main()
{
    cout << "Insert the first one" << std::endl;
        
    std::vector<S> v;
    v.emplace_back();
    v[0].j = {1,2,3};
    
    cout << "Insert a new one" << std::endl;
    
    v.emplace_back();
    v[1].j = {2,3,4};

    std::cout << v[0].j[1] << std::endl;
    std::cout << v[1].j[1] << std::endl;
    return 0;
}