Правило C 5 — ошибка сегментации в пользовательском деструкторе

#c

#c

Вопрос:

Я некоторое время изучал C и начал писать более крупный проект, просто чтобы понять, что все, что я думал о C , неверно.

В принципе, у меня есть вызываемый класс DenseVector , который содержит удвоения. Я хочу переместить, скопировать конструкцию и т. Д. Этот вектор, Чтобы использовать его с векторами и т. Д.

Класс выглядит примерно так:


DenseVector.h:

 
class DenseVector {
    
    private:
        double* values = nullptr;
        int size;
    public:
        DenseVector();
        DenseVector(int size);      
        DenseVector(double x, double y);       
        DenseVector(double x, double y, double z);       
        DenseVector(const DenseVector amp;other);
        DenseVector(DenseVector amp;amp;other);
        virtual ~DenseVector();

        DenseVectoramp; operator=(const DenseVectoramp; other);
        DenseVectoramp; operator=(DenseVectoramp;amp; other);
}
 

DenseVector.cpp

 DenseVector::DenseVector() : DenseVector(3) {}

DenseVector::DenseVector(int size) : size(size) {
    this->values = new double[size   size % 2]{0};
}

DenseVector::DenseVector(double x, double y) {
    this->size = 2;
    this->values = new double[2]{x,y};
}

DenseVector::DenseVector(double x, double y, double z) {
    this->size = 3;
    this->values = new double[4]{x,y,z};
}

DenseVector::DenseVector(const DenseVector amp;other) : size(other.size) {
    this->values = new double[size   size % 2]{0};
    memcpy(values, other.values, size * sizeof(double));
}

DenseVector::DenseVector(DenseVector amp;amp;other) : values(other.values), size(other.size){}


DenseVector::~DenseVector() {
    if(values != nullptr){
        delete values;
        values = nullptr;
    }
}

DenseVector amp;DenseVector::operator=(const DenseVector amp;other) {
    this->size = other.size;
    memcpy(values, other.values, size * sizeof(double));
    return *this;
}

DenseVector amp;DenseVector::operator=(DenseVector amp;amp;other) {
    this->values = other.values;
    this->size = other.size;
    return *this;
}
 

Я предполагаю, что это очень простая реализация для математических векторов в C . Обратите внимание, что внутренний размер массива всегда кратен двум. Это связано с ускорением использования AVX / SSE, которое не является частью этого вопроса.

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

И примером может быть следующий фрагмент:

    std::vector<DenseVector> positions;
   for(int i = 0; i < 10; i  ){
       positions.push_back({1,2,3});
   }
 

Это действительно меня смущает, и я был бы очень рад, если бы кто-нибудь мог помочь мне с этой проблемой, поскольку это случалось со мной много раз в других программах.
Также в чем будет разница между использованием push_back и emplace_back в этом случае? Следует ли предпочесть одно другому? Я не понимаю, в какой момент объекты будут созданы, перемещены, удалены и т.д.

Приветствую, Финн

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

1. Ваш конструктор перемещения выполняет (поверхностное) копирование, а не перемещение.

2. В чем разница?

3. @FinnEggers, при перемещении копии указатели в объекте rvalue устанавливаются в nullptr

Ответ №1:

Я вижу как минимум две проблемы:

 DenseVector amp;DenseVector::operator=(const DenseVector amp;other) {
    this->size = other.size;
    memcpy(values, other.values, size * sizeof(double));
    return *this;
}
 

Вы не проверяете, достаточно ли у вас места this->values . Если this->size меньше other->size , вам нужно перераспределить.

 DenseVector::DenseVector(DenseVector amp;amp;other) : values(other.values),
                                                size(other.size){}

DenseVector amp;DenseVector::operator=(DenseVector amp;amp;other) {
    this->values = other.values;
    this->size = other.size;
    return *this;
}
 

В обоих случаях вы получаете два указателя, указывающих на одну и ту же память. Теперь, когда вы уничтожаете оба вектора, вы получаете double delete . Вам нужно иметь other->values = nullptr; в обеих функциях.

Лучший способ исправить обе проблемы — использовать std::vector и полагаться на правило нуля.

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

1. Это звучит разумно, но я подумал, что при перемещении объекта старый объект не будет удален?

2. @FinnEggers старый объект не будет удален? — В какой-то момент его необходимо удалить. Именно тогда вы столкнетесь с проблемой двойного удаления.

3. Ммм, тогда какой смысл вообще двигаться? Я думал, что можно «просто» передать все данные объекта on другому

4. Но ваш ответ на это. Спасибо за ваш ответ 🙂 Теперь, учитывая, что удаление также будет вызываться для перемещенного объекта, это имеет большой смысл

5. @Finn сам объект никуда не перемещается. Ресурс кучи перемещается — принимается новым объектом. Тогда вам определенно нужен указатель на nullptr ресурс старого объекта, потому что его деструктор не должен удалять ресурс во время неизбежного выхода из области видимости (или отклонения в качестве временного объекта). Вы больше не заботитесь об этом старом объекте, но его указатель ДОЛЖЕН БЫТЬ признан недействительным при перемещении ctor и назначении перемещения.

Ответ №2:

У вас есть несколько проблем.

Тот, который находится в деструкторе, delete должен совпадать с new not new[] .

Вы должны использовать delete[] здесь:

 DenseVector::~DenseVector() {
    delete [] values;
}
 

Примечание: удаление нулевого указателя в порядке, проверка не требуется. установка значений в nullptr значение также бесполезна, так как чтение значения после уничтожения объекта в любом случае является UB.

Ваш конструктор перемещения не перемещается, а выполняет мелкую копию, поэтому у вас будет двойное удаление, оно должно быть

 DenseVector::DenseVector(DenseVector amp;amp;other) /*noexcept*/: values(other.values), size(other.size)
{
    other.vlalues = nullptr;
    other.size = 0;
}
 

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

1. Я видел это раньше, но я видел много случаев, когда люди просто используют delete вместо delete[] . Итак, я предполагаю, что так и должно быть сделано?

2. Они вызывают неопределенное поведение. (как и в случае с тривиальным деструктором (как для double), большинство компиляторов делают то же самое, код, похоже, работает).

Ответ №3:

У вас много проблем:

сначала вы должны переместить объекты в move-ctor и оставить moved-from в допустимом состоянии:

 DenseVector::DenseVector(DenseVector amp;amp;other) : values(std::move(other.values)), size(std::move(other.size))
{
     other.values = nullptr;    
}
 
  • Вы должны использовать ctor-init list для инициализации данных-членов, а не назначать им внутри тела ctor после инициализации по умолчанию в ctor-init-list.
      DenseVector::DenseVector(int size) : size(size),
          values(new double[size]{0})
     {
    
     }
     

Также в деструкторе вы освобождаете массив двойников delete , поведение которого не определено, поэтому используйте его таким образом:

  delete[] values;
 
  • Вам не нужно проверять в dtor, является ли values это nullptr или нет:
       delete[] values;
     

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

1. Есть ли важная разница между использованием init-list и просто использованием тела конструктора?

2. Конечно! использование тела ctor — это просто присвоение, поэтому данные элемента записываются дважды: один раз инициализированный по умолчанию int ctor-init-list (допускается тогда и только тогда, когда они сконструированы по умолчанию). второе в присваивании в теле конструктора.