Какова вся суета о конструкторах С++ copy?

Возможный дубликат:
Когда нам нужно использовать конструкторы копирования?

Почему именно конструкторы копии С++ так важны? Я только что узнал о них, и я не совсем понимаю, в чем их суета. Кажется, вы всегда должны писать конструктор копирования для своих классов, если используете указатели, но почему?

Спасибо, Бода Сидо.

Ответы

Ответ 1

Конструкторы копирования и операторы присваивания очень важны в С++, потому что язык имеет "семантику копирования", то есть когда вы передаете параметр или сохраняете значение в контейнере, копия объекта передается или сохраняется. Как С++ может сделать копию или выполнить присвоение объекта? Для родных типов он знает сам по себе, но для пользовательских типов вместо этого он автоматически генерирует построение или назначение копии по отдельности.

Более подробно, если вы заявляете, например:

class P2d
{
    public:
        double x, y;
        P2d(double x, double y) : x(x), y(y)
        { }
};

компилятор С++ автоматически завершает ваш код до

class P2d
{
    public:
        double x, y;
        P2d(double x, double y) : x(x), y(y)
        { }

        P2d(const P2d& other) : x(other.x), y(other.y)
        { }

        P2d& operator=(const P2d& other)
        {
            x = other.x;
            y = other.y;
            return *this;
        }
};

Являются ли эти автоматически создаваемые конструкторы экземпляров и операторы присваивания правильными для вашего класса? Во многих случаях да... но, конечно, возможно, эти реализации совершенно неправы. Довольно часто, например, когда у вас есть указатели, содержащиеся внутри ваших объектов, то просто копировать указатель, когда вы хотите сделать копию объекта, не так, как нужно.

Вы должны понимать, что С++ делает много копий объектов, и вы должны понять, какой тип копии он будет делать для определенных вами классов. Если автоматически созданная копия не то, что вам нужно, вы должны либо предоставить свою собственную реализацию, либо вы должны сообщить компилятору, что копирование должно быть запрещено для вашего класса.

Вы можете запретить компилятору создавать копии, объявляя конструктор и оператор присваивания private, а не предоставляя реализацию. Поскольку это частные функции, и любой внешний код, который будет их использовать, получит ошибку компилятора, и поскольку вы их объявили, но вы их не реализовали, вы получите сообщение об ошибке, если по ошибке вы в конечном итоге сделать копии внутри реализации класса.

Например:

class Window
{
    public:
        WindowType t;
        Window *parent,
               *prev_in_parent, *next_in_parent,
               *first_children, *last_children;
        Window(Window *parent, WindowType t);
        ~Window();

    private:
        // TABOO! - declared but not implemented
        Window(const Window&); // = delete in C++11
        Window& operator=(const Window&); // = delete in C++11
};

Если последняя часть кажется абсурдной (как вы можете сделать копии в реализации по ошибке), обратите внимание, что на С++ очень просто сделать лишние копии по ошибке, потому что язык построен вокруг концепции копирования вещей вокруг.

Золотое правило состоит в том, что если ваш класс имеет деструктор (потому что ему нужно выполнить некоторую очистку), то, скорее всего, копия по отдельности не подходит... а также если у вас есть специальная логика чтобы сделать построение копии, аналогичная логика, вероятно, необходима также при назначении (и наоборот). Таким образом, правило, известное как Большая тройка, указывает, что либо ваш класс не имеет настраиваемого деструктора, ни конструктора копирования, ни оператора присваивания, ни вашего класса должны иметь все три из них.

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

Ответ 2

Просто: когда С++ использует конструктор копии по умолчанию, он копирует указатель, но не указывает на данные. Результат: два объекта, указывающие на одни и те же данные. Если оба считают, что они владеют этими данными и удаляют указатель при вызове их деструктора, у вас есть куча проблем...

Ответ 3

Почему именно конструкторы копирования С++ так важно?

Конструкторы копирования не нужны на большинстве других языков, потому что они либо:

  • У вас нет указателей (например, старых версий BASIC), в этом случае копирование объектов всегда безопасно или
  • Не имеют ничего, кроме указателей/ссылок (например, Java, Python), и в этом случае копирование происходит редко, а затем может быть выполнено с помощью метода copy() или clone().

С++ предпочитает семантику значений, но также использует много указателей, что означает, что:

  • Объекты много копируются, а
  • Вы должны указать, хотите ли вы получить мелкую или глубокую копию по причинам, о которых говорили другие.

Ответ 4

Каждый класс в С++ имеет неявный конструктор копирования, который выполняет мелкую копию объекта. Мелкий означает, что он копирует значение членов. Поэтому, если есть указатель, значение указателя копируется, поэтому оба объекта указывают на то же самое.

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

Часто вы даже не хотите создавать копии своего объекта, поэтому хороший стиль должен объявлять конструктор копии, но не определять его. (Пустое объявление в файле заголовка).

Затем, если вы случайно создали копию (например, вернув объект, забыли и когда объявили метод "параметр за ссылкой" и т.д.), вы получите ошибку компоновщика.

Если вы объявите конструктор копии закрытым, вы также получите ошибку компилятора (если используется вне класса).

Подводя итог: всегда хороший стиль для объявления конструктора копирования явно - особенно если вам вообще не нужно: просто напишите

private:
  MyClass(const MyClass&);

в определении вашего класса, чтобы отключить конструктор копирования вашего класса.