Я работаю над системой, в которой класс имеет частный член, который является указателем базового класса. Указатель может указывать на унаследованный объект класса. Я хотел бы, чтобы параметризованные конструкторы и конструкторы копирования могли брать указатель типа базового класса и глубоко копировать его, не стирая унаследованный тип. Вот некоторый код, который я сделал, чтобы продемонстрировать эту проблему; Я хотел бы, чтобы c2 и c3 вызывали B.print вместо A.print, но не могу понять, как это сделать.
#include <iostream>
class A
{
protected:
double a;
double b;
public:
A()
{
a = 0.0;
b = 0.0;
}
A(double a, double b)
{
this->a = a;
this->b = b;
}
A(const A& copy)
{
a = copy.a;
b = copy.b;
}
virtual void print()
{
std::cout << "print A" << std::endl;
std::cout << "a: " << a << ", b: " << b << std::endl;
}
};
class B : public A
{
private:
double c;
public:
B()
{
c = 0.0;
}
B(double a, double b, double c) : A(a, b)
{
this->c = c;
}
B(const B& copy) : A(copy)
{
c = copy.c;
}
virtual void print()
{
std::cout << "print B" << std::endl;
std::cout << "a: " << a << ", b: " << b << ", c: " << c << std::endl;
}
};
class C
{
private:
A* a;
public:
C()
{
a = nullptr;
}
C(A* a)
{
this->a = new A(*a);
}
C(const C& copy)
{
a = nullptr;
if (copy.a != nullptr)
a = new A(*copy.a);
}
~C()
{
delete a;
a = nullptr;
}
void print()
{
if (a != nullptr)
a->print();
}
};
int main()
{
A* a = new A(3.5, 4.5);
A* b = new B(3.5, 4.5, 5.5);
C c1(a), c2(b), c3(c2);
delete a, b;
c1.print();
c2.print();
c3.print();
}
Я попытался включить перечисление доступных типов и сохранить это значение отдельно, но эта программа была нерасширяемой, так как требовала изменения переключателя и перечисления для каждого добавленного унаследованного типа.
90% это отвлекающий маневр. Проблема в этой строке this->a = new A(*a);
. Это не может работать таким образом. Вы хотите прочитать о создании clone
виртуального метода. `
ot: a = nullptr;
в деструкторе бессмысленно. Имхо надо удалить. Я бы не позволил ему пройти код-ревью.
delete a, b;
делает не то, что вы думаете: он эквивалентен не delete a; delete b;
, а delete a; b;
(или даже проще, просто delete a;
), поскольку delete
— унарный оператор с более высоким приоритетом, чем оператор ,
. Кстати: я рекомендую использовать std::unique_ptr
вместо того, чтобы использовать new
+ delete
вручную...
Ваша настоящая проблема связана с этими строками:
C(A* a) {
this->a = new A(*a);
}
C(const C& copy) {
a = nullptr;
if (copy.a != nullptr)
a = new A(*copy.a);
}
которые явно создают объект с динамическим типом A
, игнорируя динамические типы a
и copy.a
.
Обычный шаблон для копирования реального динамического типа чего-либо:
struct Base
{
// copy ctor etc. as usual
virtual Base* clone() const { return new Base(*this); }
};
struct Derived: Base
{
virtual Base* clone() const { return new Derived(*this); }
};
Другие, разные примечания:
Вам не нужно прожигать 6 строк в конструкторе по умолчанию, который больше ничего не делает. Просто пиши
class A
{
protected:
double a{};
double b{};
и пусть компилятор по умолчанию. Вы также можете по умолчанию использовать большинство ваших конструкторов копирования.
Если вы контролируете время жизни указателя (как C
), просто используйте std::unique_ptr
. Это там, это бесплатно, это гораздо более явно о вашей семантике.
Когда вы владеете полиморфным объектом (т. е. контролируете его время жизни) с помощью указателя базового класса, этот базовый класс должен иметь виртуальный деструктор (если только вы не делаете что-то экзотическое с помощью средства удаления стирания типов, которое отсутствует). области применения здесь).
Если вы решили использовать this->a
для элементов данных, используйте его последовательно. Если вы используете его только для устранения неоднозначности, просто измените имя параметра/локальной переменной, чтобы оно не конфликтовало.
Существуют соглашения об именах членов, такие как m_a
или просто a_
, которые позволяют легко увидеть, какие переменные являются членами данных, устраняют двусмысленность и по-прежнему меньше печатают/визуального шума, чем случайное разбрызгивание this->
.
Рабочий код только с описанными выше изменениями:
#include <iostream>
#include <memory>
class A
{
protected:
double a_{};
double b_{};
public:
A() = default;
A(double a, double b) : a_(a), b_(b) {}
A(const A&) = default;
virtual ~A() = default;
virtual A* clone() const { return new A(*this); }
virtual void print()
{
std::cout << "print A" << '\n'
<< "a: " << a_ << ", b: " << b_ << '\n';
}
};
class B : public A
{
double c_{};
public:
B() = default;
B(double a, double b, double c) : A(a, b), c_(c) {}
B(const B&) = default;
virtual B* clone() const override { return new B(*this); }
virtual void print() override
{
std::cout << "print B" << '\n'
<< "a: " << a_ << ", b: " << b_ << ", c: " << c_ << '\n';
}
};
// owns and deep-copies an A.
// class invariant: ptr_ is non-null
//
class C
{
private:
std::unique_ptr<A> ptr_;
public:
explicit C(A* a) : ptr_(a->clone()) {}
C(const C& copy) : ptr_(copy.ptr_->clone()) {}
void print()
{
ptr_->print();
}
};
int main()
{
A a{3.5, 4.5};
B b{3.5, 4.5, 5.5};
C c1(&a), c2(&b), c3(c2);
c1.print();
c2.print();
c3.print();
}
NB. Динамическое выделение верхнего уровня было ненужным, даже если вы хотите C
выполнить глубокое копирование, поэтому я просто полностью удалил его вместо того, чтобы либо изменить его, чтобы также использовать unique_ptr
, либо исправить это неправильное выражение delete
.
Кроме того, классы с методами virtual
практически всегда должны иметь деструктор virtual
.
Я знал, что если начать исправлять очевидные проблемы, это откроет банку с червями. Хорошая точка зрения!
это создает
A
, а не что-то ещеthis->a = new A(*a);