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

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

В этой теме 1 ответ, 2 участника, последнее обновление  Васильев Владимир Сергеевич 1 год, 1 месяц назад.

  • Автор
    Сообщения
  • #2941

    questioner
    Участник

    Я языке С++ есть указатели и ссылки. Указатель хранит адрес объекта, а ссылка задает альтернативное имя объекта. И те, и другие могут использоваться, например, при передаче объекта в функцию с целью избежать копирование:

    int str_len(String s) {
      return s.length();
    }
    int str_len(const String& s) {
      return s.length();
    }
    int str_len(const String* s) {
      return s->length();
    }

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

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

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

    Type tmp(a[i]);
    a[i] = a[j];
    a[j] = tmp;

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

    Однако, в С++11 появились особые ссылки на временные объекты и возможность задания перемещающего конструктора и оператора присваивания, которые позволяют выполнять перестановку объектов в контейнере также эффективно, как и перестановку указателей.

    Нередко, я также встречал мнение, что через указатели в С++ реализуется полиморфизм. Однако, тоже самое я могу сделать при помощь ссылок.

    polymorphism

    #include <iostream>
    using namespace std;
    
    const double SeniorityIndex = 1.1;
    
    class Staff {
    public:
      Staff(int seniority) 
        : m_seniority(seniority) {
      }
      
      int seniority() { 
        return m_seniority;
      }
      virtual double salary() = 0;
    protected:
      int m_seniority;
    };
    
    class Engineer : public Staff {
    public:
      Engineer(int seniority) 
        : Staff(seniority) {
      }
      virtual double salary() { 
        return 30000;
      }
    };
    
    class Programmer: public Staff {
    public:
      Programmer(int seniority) 
        : Staff(seniority) {
      }
      virtual double salary() {
        return 32000;
      }
    };
    
    double pay(Staff* staff) {
      return staff->salary() * staff->seniority()*SeniorityIndex;
    }
    
    int main() {
      Staff 
        *engineer = new Engineer(1),
        *programmer = new Programmer(10);
        
      cout << pay(engineer) << endl << pay(programmer) << endl;
    }

    В этом примере функция pay принимает объект по указателю на базовый класс, за счет этого в качестве аргумента может быть передан экземпляр любого из дочерних классов. В момент компиляции известно лишь то, что аргумент является указателем на Staff (статический тип объекта), точный (динамический) тип объект становится известным лишь на этапе выполнения (в зависимости от того, какой объект будет передан фактически). Для своего аргумента функция вызывает виртуальную функцию salary() — при этом за счет механизма позднего связывания будет вызвана функция в соответствии с динамическим типом объекта.

    Однако, тоже самое может быть выполнено при помощи ссылок:

    double pay(Staff& staff) {
      return staff.salary() * staff.seniority()*SeniorityIndex;
    }

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

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

  • #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) и, в частности, умных указателей.

Для ответа в этой теме необходимо авторизоваться.