C ++ успешно копирует динамически размещенный объект без копирования ctor? - PullRequest
1 голос
/ 09 января 2020

У меня есть класс с указателем на символ. Мне интересно, как это возможно, что создание копии возможно без явного определения конструктора копирования?

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

#include <iostream>
#include <cstring>

class Data {
public:
    explicit Data(const char* newData)
        :size{strlen(newData)},
         data{size ? new char[size] : nullptr}
    {
        std::memcpy(data, newData, size);
    }

    friend std::ostream& operator<<(std::ostream& ost, const Data& rhs) {
        for (std::size_t i = 0; i < rhs.size; i++) {
            std::cout << rhs.data[i];
        }
        ost << "\n";
        return ost;
    }

~Data() {
    delete[] data;
}

private:
    std::size_t size;
    char* data;
};

int main() {
    Data data1{"data1"};
    Data data2{data1}; // copy constructor
    std::cout << data1;
    std::cout << data2;

    return 0;
}

Вывод:

data1
data1

Разве конструктор копирования не должен выглядеть так? Я часто вижу такие примеры. Но так как конструктор по умолчанию уже сделал это, то когда мне на самом деле нужно определить copy ctor?

Data(const Data& other) {
    if (this != &other) {
        size = other.size;
        data = new char[size];
        std::memcpy(data, other.data, size);
    }
}

Кстати, я понимаю, что в коде вводятся некоторые практические приемы (например, использование new вместо smart ptrs, в первую очередь не использовать строку, и т. д. c. - но это всего лишь упражнение).

1 Ответ

3 голосов
/ 09 января 2020

Начиная с C ++ 11, генерация неявно определенного конструктора не рекомендуется (по уважительной причине), если в классе есть пользовательский деструктор, но он все еще генерируется. Этот конструктор копирования просто скопирует size и data, создавая неглубокую копию, а не глубокую. Это дорога к катастрофе, потому что data будет удаляться несколько раз, когда исходный объект и его копии будут уничтожены.

Например, если вы запустите исходный код под Valgrind , вы увидите следующий отчет:

==9908== HEAP SUMMARY:
==9908==     in use at exit: 0 bytes in 0 blocks
==9908==   total heap usage: 3 allocs, 4 frees, 73,733 bytes allocated
==9908== 
==9908== All heap blocks were freed -- no leaks are possible
==9908== 
==9908== For counts of detected and suppressed errors, rerun with: -v
==9908== ERROR SUMMARY: 18 errors from 9 contexts (suppressed: 0 from 0)

Если вы хотите, чтобы Data был копируемым Вы должны предоставить конструктор глубокого копирования, который выделяет новое хранилище и копирует в него данные. Тот, что в вашем вопросе выглядит почти (*) нормально:

==9993== HEAP SUMMARY:
==9993==     in use at exit: 0 bytes in 0 blocks
==9993==   total heap usage: 4 allocs, 4 frees, 73,738 bytes allocated
==9993== 
==9993== All heap blocks were freed -- no leaks are possible
==9993== 
==9993== For counts of detected and suppressed errors, rerun with: -v
==9993== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

Если вам вообще не нужна конструкция копирования, вы можете отключить ее неявную генерацию, явно удалив ее:

Data(const Data&) = delete;

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

(*) Обратите внимание, что other.data может быть nullptr, поэтому вам необходимо проверить его перед копированием:

void* memcpy(void* dest, const void* src, std::size_t count);

Если либо dest, либо src является нулевым указателем, поведение не определено, даже если count равно нулю.

...