Ответ в теме: Указатели и ссылки в С++

      Комментарии к записи Ответ в теме: Указатели и ссылки в С++ отключены
#2944

На самом деле, если в вашем коде подойдут ссылки – всегда пользуйтесь ими вместо указателей, т.к. они более безопасны чем сырые указатели и более эффективны чем “умные” указатели. Однако, не во всех случаях это возможно.

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

Наконец, если один объект вложен в другой, но их время жизни различно – может быть целесообразно использовать указатели. Так, например, если игрок может обладать мечем и щитом, но в какой-то момент может и продать/потерять их, то мы могли бы обнулить указатели. Однако, ссылка всегда связана с существующим объектом, поэтому при их использовании нам пришлось бы породить дополнительные классы-наследники от каждого типа вооружения, отражающие отсутствие соответствующей амуниции.

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

int main() {
  Engineer engineer(1);
  Programmer programmer(10);
  
  Staff &staff = engineer;
    
  cout << pay(staff) << endl;
  
  staff = programmer;
  
  cout << pay(staff) << endl;
}

references_using_example

На снимке экрана приведены сначала результаты работа программы из вашего сообщения (с указателями), а затем – приведенного кода со ссылками. При инициализации ссылки задается динамический тип объекта: Staff &staff = engineer;, а при последущем присваивании ссылке нового значения он не изменяется: staff = programmer;. В результате, при вызове по ссылке виртуальной функции в обоих случаях будет использована Engineer::salary(), хотя данные (m_seniority), будут выбираться из того, объекта, на который мы ссылаемся (Programmer programmer(10)).

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

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

class_example

#include <iostream>
using namespace std;

class Helmet { 
public:
  Helmet(const int defence) : m_defence(defence) {
    cout << "create Helmet" << endl;
  }
  virtual void type() = 0;
protected:
  int m_defence;
};

class WoodenHelmet : public Helmet { 
public:
  WoodenHelmet() : Helmet(5) {
    cout << "\tcreate WoodenHelmet" << endl;
  }
  virtual void type() {
    cout << "\tWoodenHelmet: " << m_defence << endl;
  }
}; 
class MetalHelmet : public Helmet { 
public:
  MetalHelmet() : Helmet (12) {
    cout << "\tcreate MetalHelmet" << endl;
  }
  virtual void type() {
    cout << "\tMetalHelmet: " << m_defence << endl;
  }
}; 

class Character {
  Helmet &m_helmet;
public:
  Character(Helmet &helmet) : m_helmet(helmet) {
  }
  void use_helmet() {
    cout << "use ";
    m_helmet.type();
  }
  void set_helmet(Helmet &helmet) {
    cout << "set helmet" << endl;
    m_helmet = helmet;
  }
};

int main() {
  WoodenHelmet woodenHelmet;
  MetalHelmet metalHelmet;
  Character character(woodenHelmet);
  character.use_helmet();
  character.set_helmet(metalHelmet);
  character.use_helmet();
}

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

references_error_example

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

Таким образом, по возможности стоит использовать ссылки, однако в ряде случаев нельзя обойтись без указателей. Избежать возможных утечек памяти при использовании указателей можно с использованием объектов управления ресурсами (идиома RAII) и, в частности, умных указателей.