Подтвердить что ты не робот

Оператор присваивания и конструктор копирования С++

У меня есть следующий код, чтобы проверить мое понимание основных указателей в С++:

// Integer.cpp
#include "Integer.h"
Integer::Integer()
{
  value = new int;
  *value = 0;
}

Integer::Integer( int intVal )
{
  value = new int;
  *value = intVal;
} 

Integer::~Integer()
{
  delete value;
}

Integer::Integer(const Integer &rhInt)
{
  value = new int;
  *value = *rhInt.value;
}

int Integer::getInteger() const
{
  return *value;
}

void Integer::setInteger( int newInteger )
{
  *value = newInteger;
}

Integer& Integer::operator=( const Integer& rhInt )
{   
  *value = *rhInt.value;
  return *this;
}

// IntegerTest.cpp
#include <iostream>
#include <cstdlib>
#include "Integer.h"

using namespace std;

void displayInteger( char* str, Integer intObj )
{
  cout << str << " is " << intObj.getInteger() << endl;
}

int main( int argc, char* argv[] )
{
 Integer intVal1;
 Integer intVal2(10);

 displayInteger( "intVal1", intVal1 );
 displayInteger( "intVal2", intVal2 );

 intVal1 = intVal2;

 displayInteger( "intVal1", intVal1 );

 return EXIT_SUCCESS;
}

Этот код работает точно так, как ожидалось, он печатает:

intVal1 is 0

intVal2 is 10

intVal1 is 10

Однако, если я удалю конструктор копирования, он выведет что-то вроде:

intVal1 is 0

intVal2 is 10

intVal1 is 6705152

Я не понимаю, почему это так. Я понимаю, что конструктор копирования используется, когда присваивание относится к объекту, который не существует. Здесь intVal1 существует, поэтому почему оператор присваивания не называется?

4b9b3361

Ответ 1

Конструктор копирования не используется во время назначения. Конструктор копирования в вашем случае используется при передаче аргументов функции displayInteger. Второй параметр передается по значению, что означает, что он инициализируется директором копирования.

Ваша версия конструктора копирования выполняет глубокое копирование данных, принадлежащих классу (как и ваш оператор присваивания). Итак, все работает правильно с вашей версией конструктора копирования.

Если вы удалите свой собственный конструктор копирования, компилятор будет генерировать один для вас неявно. Создатель, созданный компилятором, выполнит мелкое копирование объекта. Это нарушит "Правило трех" и уничтожит функциональность вашего класса, что и есть то, что вы наблюдаете в своем эксперименте. В основном, первый вызов displayInteger повреждает ваш объект intVal1, а второй вызов displayInteger повреждает ваш объект intVal2. После этого оба ваших объекта сломаны, поэтому третий вызов displayInteger отображает мусор.

Если вы измените объявление displayInteger на

void displayInteger( char* str, const Integer &intObj )

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

Ответ 2

Проблема, с которой вы столкнулись, вызвана конструктором копии по умолчанию, который копирует указатель, но не связывает его с недавно выделенной памятью (как это делает реализация конструктора копирования). Когда вы передаете объект по значению, создается копия, и когда выполнение выходит за рамки, эта копия разрушается. delete из деструктора делает недействительным указатель value объекта intVal1, делая его висящим указателем, разыменование которого вызывает поведение undefined.

Отладочные выходы могут использоваться для понимания поведения вашего кода:

class Integer {
public:

    Integer() {
      cout << "ctor" << endl;
      value = new int;
      *value = 0;
    }

    ~Integer() {
        cout << "destructor" << endl;
        delete value;
    }

    Integer(int intVal) {
      cout << "ctor(int)" << endl;
      value = new int;
      *value = intVal;
    } 

    Integer(const Integer &rhInt) {
      cout << "copy ctor" << endl;
      value = new int;
      *value = *rhInt.value;
    }

    Integer& operator=(const Integer& rhInt){   
      cout << "assignment" << endl;
      *value = *rhInt.value;
      return *this;
    }

    int *value;
};

void foo(Integer intObj) {
    cout << intObj.value << " " << *(intObj.value) << endl;
}

Теперь вывод этого кода:

Integer intVal1;
Integer intVal2(10);

foo( intVal1 );
foo( intVal2 );

intVal1 = intVal2;

foo( intVal1 );

является:

т е р
т е р (целое)
копия ctor
0x9ed4028 0
деструктор
копия ctor
0x9ed4038 10
деструктор
назначение
копия ctor
0x9ed4048 10
деструктор
деструктор
деструктор

который показывает, что конструктор копирования используется при передаче объектов по значению. Однако важно заметить, что деструктор призван вернуться из вашей функции. И если вы удалите реализацию конструктора копирования, то вывод будет следующим:

т е р
т е р (целое)
0x8134008 0
деструктор
0x8134018 10
деструктор
назначение
0x8134008 135479296
деструктор
деструктор
деструктор

показывающий, что первая копия называется delete на том же указателе (указывая на 0x8134008), как это было использовано третьей копией позже, где была использована память, указанная этим оборванным указателем.

Ответ 3

Подумайте об этом вызове:

displayInteger( "intVal1", intVal1 );

Вы создаете копию intVal1 в параметре intObj displayInteger:

void displayInteger( char* str, Integer intObj )
{
  cout << str << " is " << intObj.getInteger() << endl;
}

Эта копия будет указывать на тот же int, что intVal1. Когда displayInteger возвращается, intObj уничтожается, что приведет к уничтожению int, а указатель в intVal1 указывает на недопустимый объект. В этот момент все ставки отключены (поведение A.K.A. undefined), если вы попытаетесь получить доступ к значению. Аналогичная ситуация наблюдается и для intVal2.

На более общем уровне, удалив конструктор копирования, вы нарушаете правило из трех, что обычно приводит к таким проблемам.