С++: рождение родительского класса в дочерний класс

Я новичок в С++, и это проблема, которая у меня есть:  У меня есть два класса: Client и Host. И когда все загружено, вы можете нажать две кнопки, если вы нажмете кнопку 1 Client, и если вы нажмете кнопку 2 Host, она будет загружена.

Теперь оба Client и Host являются довольно большими классами, и я не хочу помещать их в память. Поэтому моя идея заключалась в создании класса Base, а затем оба Client и Host должны расширять базовый класс, и тогда единственное, что я должен был сделать, это следующее:

Base connection;

//If button 1 is pressed:
connection = Client();

//If button 2 is pressed:
connection = Host();

Хорошо, это звучало почти слишком хорошо, чтобы быть правдой, и когда я попробовал, у меня не было ошибок. Теперь возникает проблема, Base имеет функцию с именем A, а Client имеет функцию, называемую B. Таким образом, функция B уникальна для класса Client.

Когда я пытаюсь вызвать функцию B, я получаю эту ошибку: 'class Base' has no member named 'B'. Как я могу позволить С++ знать, что я говорю с классом Client или Host вместо Base? Я также открыт для совершенно нового подхода к этой проблеме. Возможно, это просто ошибка в моем процессе мышления.

Спасибо заранее!

Ответ 1

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

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

Чтобы избежать этого, у вас есть несколько параметров.

В классическом подходе используются указатели:

Base * connection;
connection = new YourConcreteType();

Затем, чтобы использовать этот объект, вы должны его устранить, используя оператор звездочки (*):

(*connection).function();
connection->function();    // syntactic sugar, semantically equivalent

Не забыть: вы должны удалить объект после использования:

delete connection;

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

std::shared_ptr<Base> connection;

connection = make_shared<YourConcreteType>(); // construction via 'make_shared'

// ... use as if it were just a pointer ...

connection->function();

// no delete!

Существуют также другие типы "умного указателя", такие как unique_ptr, которые можно использовать, если вы не собираетесь передавать указатель (если он остается в области видимости).

Теперь вы можете реализовать функции в обоих классах отдельно. Чтобы использовать полиморфизм, это означает, что во время выполнения вызывается либо функция одного подкласса, либо другого подкласса, в зависимости от того, какой из них был создан, вы должны объявить функции в базовом классе как virtual, иначе, определение функции в Base будет вызываться независимо от конкретного типа, который вы создали.

В вашем случае вы хотите вызвать функцию, которая должна делать что-то другое, в зависимости от типа. В то время как ваш подход состоял в том, чтобы ввести две разные функции, а именно A и B, вы можете просто объявить одну функцию, называть ее handleEvent как чистую виртуальную (= абстрактную) функцию в базовом классе, что означает "эта функция должна быть реализована в подклассах" и определить ее в двух подклассах самостоятельно:

Base {
    ....
    virtual void handleEvent(...) = 0; // "= 0" means "pure"
};

// Don't provide an implementation

Client {
    void handleEvent(...); // override it
};

// define it for Client:
void Client::handleEvent(...) {
    ...
}

Host {
    void handleEvent(...); // override it
};

// define it for Host:
void Host::handleEvent(...) {
    ...
}

Ответ 2

Когда вы это сделаете:

Base connection;

Вы создаете объект connection, который имеет точно память, необходимую для типа Base.

Когда вы это сделаете:

connection = Client();

вы не расширяете выделенную или преобразованную память connection в экземпляр Client. Вместо этого вы создаете более крупный объект, который "нарезается" на меньший.

То, что вы хотите сделать, - это указатели на использование (или ссылки или интеллектуальные указатели), так что то, что вы храните, - это просто адрес объекта, который может быть одним типом или другим типом. Вот так:

Base *connection;
...
connection = new Client();

Первый оператор создает указатель на Base типизированный объект, а второй выделяет память для Client типизированного, инициализирует его и присваивает свой адрес connection.

Ответ 3

Если вы хотите использовать Base по-прежнему, вам нужно будет сделать это, чтобы использовать эти функции:

if (Button1) {
    dynamic_cast<Client*>(connection)->A();
} else {
    dynamic_cast<Host*>(connection)->B();   
}  

И вам нужно будет установить соединение с указателем. Base * connection.

Это не идеальный вариант. Вы должны исследовать другой способ сделать это, как в других ответах.

Ответ 4

Во-первых, эта строка

connection = Client();

использует оператор присваивания для установки состояния connection, a Base из временного объекта Client. connection все еще является объектом Base. Вы можете сделать следующее:

std::unique_ptr<Base> makeBase(someFlagType flag)
{
  if (flag) {
    return std::unique_ptr<Base>(new Cient);
  } else {
    return std::unique_ptr<Base>(new Host);
  }
}

Тогда

std::unique_ptr<Base> b = makeBase(myFlag);
b->someBaseMethod();

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