push_back аннулирует указатели в объектах при вызове деструктора

#c #pointers #segmentation-fault

#c #указатели #ошибка сегментации

Вопрос:

У меня есть 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;
  }
}
  

Я хочу ввести неизвестное число S в std::vector using push_back() . Однако, когда vector перераспределяется его память для увеличения, он вызывает деструктор S и аннулирует указатель j .

Я понимаю, почему приведенный ниже пример segfaults , но я хотел бы знать, есть ли хороший способ, которого я не знаю, для управления этим случаем.

На практике, я думаю, что я могу исправить свою проблему, удалив деструктор ~S и delete ‘ing main() перед уничтожением 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;
}
  

Комментарии:

1. Любая причина, по которой вы не используете std::unique_ptr ?

2. Вы знаете, что такое глубокое копирование? Вы знаете, как написать конструктор копирования?

3. Почти никогда нет причин иметь указатель std::vector -владелец .

4. Ваш класс не соблюдает правило 3/5/0 , что вызывает проблемы, с которыми вы сталкиваетесь.

5. но у меня такое чувство, что S должен иметь дело со своей памятью в своем деструкторе — не вините деструктор — S класс имеет неправильную семантику копирования, уже отмеченную классом, не соблюдающим правило 3. Но зачем искать эту проблему, когда в этом нет необходимости? Просто объявите a std::vector<int> j; .

Ответ №1:

S не определяет никаких конструкторов копирования / перемещения или операторов присваивания копирования / перемещения, с помощью которых можно копировать / j перемещать. Когда std::vector in main() перераспределяет свой внутренний массив, все существующие S в нем объекты копируются / перемещаются в памяти.

j становится недействительным в вашем примере, потому S j что вообще не пытается управлять. Для каждого S объекта в main() ‘s std::vector , когда он копируется / перемещается, его j указатель копируется неглубоко в новый объект, а когда старый объект уничтожается, то std::vector указываемый на j него также уничтожается, оставляя j новый объект зависшим, вот почему вы получаете ошибку segfault при j последующем доступе.

Когда class or struct владеет указателем на ресурс, он ДОЛЖЕН правильно реализовать правило 3/5/0 для копирования / перемещения этого ресурса при копировании / перемещении самого ресурса. В двух словах, правило гласит, что если a class / struct необходимо реализовать либо деструктор, конструктор копирования / перемещения, либо оператор присваивания копирования / перемещения, ему, вероятно, необходимо реализовать их все. В вашем случае, поскольку у вас есть деструктор, который уничтожает j , вам нужно добавить другие, чтобы убедиться, что у каждого S объекта есть действительный j для уничтожения.

В вашем примере я бы предложил ВООБЩЕ НЕ выделять std::vector извне S . Пусть S обрабатывает это распределение, внешний код должен просто j заполняться значениями по мере необходимости.

Попробуйте что-то более похожее на это:

 struct S {
  S();
  S(const S amp;);
  S(S amp;amp;);
  ~S();
  Samp; operator=(S);

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

  std::vector<int>amp; get_j();
  const std::vector<int>amp; 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 amp;src) {
  i = src.i;
  j = new std::vector<int>(*(src.j));
}

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

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

Samp; 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>amp; S::get_j() {
  return *j;
}

const std::vector<int>amp; 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 вместо этого, тогда вы можете полностью удалить деструктор (но не конструкторы копирования / перемещения и операторы присваивания копирования / перемещения), например:

 struct S {
  S();
  S(const S amp;);
  S(S amp;amp;);
  Samp; operator=(S);

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

  std::vector<int>amp; get_j();
  const std::vector<int>amp; 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 amp;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 amp;amp;src) {
  i = src.i; src.i = 0;
  j = std::move(src.j);
}

Samp; 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>amp; S::get_j() {
  return *j;
}

const std::vector<int>amp; 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;
}
  

Однако, с учетом сказанного, на самом деле нет веских причин для j динамического выделения вообще. std::vector реализует правило 3/5/0 для самого себя, поэтому вы можете избавиться от динамического указателя в S и позволить компилятору управлять std::vector объектом за вас, точно так же, как это делается для std::vector объекта в main() , например:

 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;
}
  

Комментарии:

1. Спасибо за объяснение. Это проясняет ситуацию.