Как сделать указатель на объект класса с
Перейти к содержимому

Как сделать указатель на объект класса с

  • автор:

Указатели на объекты

При первом знакомстве с указателями в C++ (см. Указатели в C++. Часть 1 ) может сложиться упрощённое представление, что указатели могут указывать только на отдельные переменные встроенных (скалярных) типов C++, и что это просто ещё одна, альтернативная форма доступа к таким переменным. В таком применении указатели были бы приятным дополнением языка, но с весьма ограниченными возможностями.

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

Рассмотрим такие варианты подробнее. Самым простым вариантом будет использование указателей на составные объекты (объекты классов и структуры). Но уже такое использование указателей открывает широкие перспективы в C++, как мы сейчас увидим.

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

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

Урок №162. Указатели, Ссылки и Наследование

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

Указатели, ссылки и дочерние классы

Из урока №155 мы знаем, что при создании объекта дочернего класса выполняется построение двух частей, из которых этот объект и состоит: родительская и дочерняя. Например:

При создании объекта класса Child сначала выполняется построение части Parent, а затем уже части Child. Помните, что тип отношений в наследовании — «является». Поскольку Child «является» Parent, то логично, что Child содержит часть Parent.

Мы можем дать команду указателям и ссылкам класса Child указывать на другие объекты класса Child:

child is a Child and has value 7
rChild is a Child and has value 7
pChild is a Child and has value 7

Интересно, поскольку Child имеет часть Parent, то можем ли мы дать команду указателю или ссылке класса Parent указывать на объект класса Child? Оказывается, можем!

child is a Child and has value 7
rParent is a Parent and has value 7
pParent is a Parent and has value 7

Но это может быть не совсем то, что вы ожидали увидеть!

Поскольку rParent и pParent являются ссылкой и указателем класса Parent, то они могут видеть только члены класса Parent (и члены любых других классов, которые наследует Parent). Таким образом, указатель/ссылка класса Parent не может увидеть Child::getName(). Следовательно, вызывается Parent::getName(), а rParent и pParent сообщают, что они относятся к классу Parent, а не к классу Child.

Обратите внимание, это также означает, что невозможно вызвать Child::getValueDoubled() через rParent или pParent . Они не могут видеть что-либо в классе Child.

Вот еще один более сложный пример:

Результат выполнения программы:

cat is named Matros, and it says Meow
dog is named Barsik, and it says Woof
pAnimal is named Matros, and it says .
pAnimal is named Barsik, and it says .

Мы видим здесь ту же проблему. Поскольку pAnimal является указателем типа Animal, то он может видеть только часть Animal. Следовательно, pAnimal->speak() вызывает Animal::speak(), а не Dog::Speak() или Cat::speak().

Указатели, ссылки и родительские классы

Теперь вы можете сказать: «Примеры, приведенные выше, кажутся глупыми. Почему я должен использовать указатель или ссылку родительского класса на объект дочернего класса, если я могу просто использовать дочерний объект?». Оказывается, на это есть несколько веских причин.

Во-первых, предположим, что вы хотите написать функцию, которая выводит имя и звук животного. Без использования указателя на родительский класс, вам придется реализовать это через перегрузку функций. Например:

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

Однако, поскольку Cat и Dog наследуют Animal, Cat и Dog имеют часть Animal, поэтому мы можем сделать следующее:

Это позволит нам передавать любой класс, который является дочерним классу Animal! Вместо отдельного метода на каждый дочерний класс мы записали один метод, который работает сразу со всеми дочерними классами!

Проблема, конечно, в том, что, поскольку rAnimal является ссылкой класса Animal, то rAnimal.speak() вызовет Animal::speak() вместо метода speak() дочернего класса.

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

Теперь представьте, что у нас 30 разных типов животных. Нам пришлось бы создать 30 массивов — по одному на каждый тип животного!

Однако, поскольку Cat и Dog наследуют Animal, то можно сделать следующее:

Хотя это скомпилируется и выполнится, но, к сожалению, тот факт, что каждый элемент массива animals является указателем на Animal, означает, что animals[iii]->speak() будет вызывать Animal::speak(), вместо методов speak() дочерних классов.

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

Угадайте теперь, зачем нужны виртуальные функции? 🙂

Наш пример с Animal/Cat/Dog не работает так, как мы хотим, потому что ссылка/указатель класса Animal не может получить доступ к методам speak() дочерних классов. Один из способов обойти эту проблему — сделать так, чтобы данные, возвращаемые методом speak(), стали доступными в виде родительской части класса Animal (так же, как name класса Animal доступен через член m_name ).

Обновите классы Animal, Cat и Dog в коде, приведенном выше, добавив новый член m_speak в класс Animal. Инициализируйте его соответствующим образом. Следующая программа должна работать корректно:

Pointer declaration

Объявляет переменную типа указатель или указатель на член.

Syntax

Объявление указателя любое простое заявление которого описатель имеет форму.

* attr(optional)cv(optional)declarator (1)
nested-name-specifier * attr(optional)cv(optional)declarator (2)
nested-name-specifier последовательность имен и операторов разрешения области ::
attr (начиная с C++11) список атрибутов
cv квалификация const/volatile,которая применяется к декларируемому указателю (а не к указанному типу,квалификация которого является частью decl-specifier-seq)
declarator любой декларатор, кроме ссылочного (нет указателей на ссылки). Это может быть другой указатель указателя (указатель на указатель разрешен)

Нет указателей на ссылки и нет указателей на битовые поля . Как правило, упоминания «указателей» без уточнения не включают указатели на (нестатические) члены.

Pointers

Каждое значение типа указателя является одним из следующих:

  • a указатель на объект или функцию (в этом случае указатель point to объект или функцию),или
  • a указатель на конец объекта , or
  • the нулевое значение указателя для такого типа,или
  • an недопустимое значение указателя .

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

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

https://amdy.su/wp-admin/options-general.php?page=ad-inserter.php#tab-8

Перенаправление через недопустимое значение указателя и передача недопустимого значения указателя в функцию деаллокации имеют неопределенное поведение.Любое другое использование значения недействительного указателя имеет поведение,определяемое реализацией.Некоторые реализации могут определить,что копирование недопустимого значения указателя вызывает системный сбой во время выполнения.

Указатели на объекты

Указатель на объект может быть инициализирован возвращаемым значением оператора address-of, примененным к любому выражению типа объекта, включая другой тип указателя:

Указатели могут появляться как операнды встроенного оператора косвенности (унарный operator* ), который возвращает выражение lvalue , идентифицирующее объект, на который указывает указатель:

Указатели на объекты класса также могут отображаться как левые операнды операторов доступа к членам operator-> и operator->* .

Из -за неявного преобразования массива в указатель указатель на первый элемент массива может быть инициализирован выражением типа массива:

Из -за неявного преобразования производных в базу для указателей указатель на базовый класс может быть инициализирован с адресом производного класса:

Если Derived является полиморфным , такой указатель может использоваться для вызовов виртуальных функций .

Определенные операторы сложения, вычитания , увеличения и уменьшения определены для указателей на элементы массивов: такие указатели удовлетворяют требованиям LegacyRandomAccessIterator и позволяют алгоритмам библиотеки C ++ работать с необработанными массивами.

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

Многие реализации также обеспечивают строгое полное упорядочение указателей случайного происхождения, например, если они реализованы как адреса в непрерывном виртуальном адресном пространстве. Те реализации, которые не (например, где не все биты указателя являются частью адреса памяти и должны игнорироваться для сравнения, или требуется дополнительное вычисление, или иначе указатель и целое число не являются отношением 1 к 1), обеспечивают специализация std::less для указателей с такой гарантией. Это позволяет использовать все указатели случайного происхождения в качестве ключей в стандартных ассоциативных контейнерах, таких как std::set или std::map .

Указатели на аннулирование

Указатель на объект любого типа может быть неявно преобразован в указатель на void (необязательно cv-qualified ); значение указателя не изменяется. Обратное преобразование, для которого требуется static_cast или явное приведение , дает исходное значение указателя:

Если исходный указатель указывает на подобъект базового класса в объекте некоторого полиморфного типа, dynamic_cast может использоваться для получения void* , указывающего на полный объект самого производного типа.

Указатели на void имеют тот же размер, представление и выравнивание, что и указатели на char .

Указатели на void используются для передачи объектов неизвестного типа, что часто встречается в интерфейсах C: std::malloc возвращает void* , std::qsort ожидает предоставленный пользователем обратный вызов, который принимает два аргумента const void* . pthread_create ожидает предоставленный пользователем обратный вызов, который принимает и возвращает void* . Во всех случаях вызывающая сторона несет ответственность за приведение указателя к правильному типу перед использованием.

Указатели на функции

Указатель на функцию можно инициализировать с помощью адреса функции, не являющейся членом или статической функции-члена. Из -за неявного преобразования функции в указатель оператор address-of является необязательным:

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

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

Указатель на функцию можно использовать в качестве левого операнда оператора вызова функции , это вызывает указанную функцию:

Дереференцирование указателя функции дает значение lvalue,идентифицирующее указанную функцию:

Указатель на функцию может быть инициализирован из набора перегрузок, который может включать функции, специализации шаблонов функций и шаблоны функций, если только одна перегрузка соответствует типу указателя ( более подробно см. Адрес перегруженной функции ):

Операторы сравнения на равенство определены для указателей на функции (они сравнивают равные, если указывают на одну и ту же функцию).

Указатели на членов

Указатели на членов данных

Указатель на нестатический объект-член m , который является членом класса C , может быть инициализирован точно выражением &C::m .Такие выражения, как &(C::m) или &m внутри функции-члена C , не формируют указатели на члены.

Такой указатель может использоваться в качестве правого операнда оператора operator.* доступа указатель на член . * И operator->* :

Указатель на член данных доступного однозначного не виртуального базового класса может быть неявно преобразован в указатель на тот же элемент данных производного класса:

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

Тип указателя на члена может быть указателем на самого члена:указатели на членов могут быть многоуровневыми и могут быть cv-квалифицированы по-разному на каждом уровне.Допускаются также смешанные многоуровневые комбинации указателей и указателей на членов:

Указатели на функции членов

Указатель на нестатическую функцию-член f , которая является членом класса C , может быть инициализирован точно выражением &C::f .Такие выражения, как &(C::f) или &f внутри функции-члена C , не формируют указатели на функции-члены.

Такой указатель может быть использован в качестве правого операнда оператора operator.* доступа к указателю на член . * И operator->* . Полученное выражение может быть использовано только в качестве левого операнда оператора вызова функции:

Указатель на функцию-член базового класса может быть неявно преобразован в указатель на ту же функцию-член производного класса:

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

Указатели на функции-члены могут использоваться как обратные вызовы или как объекты функций, часто после применения std::mem_fn или std::bind :

Null pointers

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

Чтобы инициализировать указатель нулевым значением или присвоить нулевое значение существующему указателю, можно использовать литерал нулевого указателя nullptr , константу нулевого указателя NULL или неявное преобразование из целочисленного литерала со значением ​0 ​0​

Zero — и стоимости инициализации также инициализировать указатели на их значения NULL.

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

Constness

  • Если cv появляется перед * в объявлении указателя, он является частью decl-specifier-seq и применяется к указанному объекту.
  • Если cv появляется после * в объявлении указателя, это часть объявления и применяется к указателю, который объявлен.
Syntax meaning
const T* указатель на постоянный объект
T const* указатель на постоянный объект
T* const постоянный указатель на объект
const T* const постоянный указатель на постоянный объект
T const* const постоянный указатель на постоянный объект

В общем, неявное преобразование из одного многоуровневого указателя в другой следует правилам, описанным в квалификационных преобразованиях и в операторах сравнения указателей .

Defect reports

Следующие отчеты о дефектах,изменяющих поведение,были применены ретроактивно к ранее опубликованным стандартам C++.

18.1 – Указатели и ссылки базового класса на объекты производных классов

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

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

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

Например, вот простой случай:

Когда мы создаем объект Derived , он содержит часть Base (которая создается первой) и часть Derived (которая создается второй). Помните, что наследование подразумевает между двумя классами связь «является чем-либо». Поскольку Derived «является» Base , уместно, чтобы объект Derived содержал часть Base .

Указатели, ссылки и производные классы

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

Это дает следующий результат:

Однако, поскольку Derived содержит часть Base , более интересный вопрос заключается в том, позволит ли C++ установить указатель или ссылку типа Base на объект Derived . Оказывается, мы можем это сделать!

Это дает следующий результат:

Этот результат может быть не совсем таким, как вы ожидали вначале!

Оказывается, поскольку rBase и pBase являются ссылкой и указателем типа Base , они могут видеть только члены класса Base (или любых классов, от которых Base наследуется). Таким образом, даже если Derived::getName() затеняет (скрывает) Base::getName() для объектов Derived , указатель/ссылка типа Base не может видеть Derived::getName() . Следовательно, они вызывают Base::getName() , поэтому rBase и pBase сообщают, что они являются объектами Base , а не Derived .

Обратите внимание, что это также означает, что с помощью rBase или pBase нельзя вызвать Derived::getValueDoubled() . Они ничего не видят в классе Derived .

Вот пример чуть сложнее, который мы рассмотрим в следующем уроке:

Этот дает следующий результат:

Мы видим здесь ту же проблему. Поскольку pAnimal является указателем Animal , он может видеть только часть, относящуюся к классу Animal . Следовательно, pAnimal->speak() вызывает функцию Animal::speak() , а не Dog::speak() или Cat::speak() .

Использование указателей и ссылок на базовые классы

Теперь вы можете сказать: «Приведенные выше примеры кажутся глупыми. Зачем мне устанавливать указатель или ссылку базового класса на объект производного класса, если я могу просто использовать объект производного класса? » Оказывается, на то есть немало веских причин.

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

Не так уж сложно, но подумайте, что бы произошло, если бы вместо двух у нас было 30 различных типов животных. Вам бы пришлось написать 30 почти идентичных функций! Кроме того, если вы когда-нибудь добавите новый тип животных, вам придется написать новую функцию и для него. Это огромная трата времени, учитывая, что единственная реальная разница – это тип параметра.

Однако, поскольку класс Cat и Dog являются производными от Animal , классы Cat и Dog содержат часть, относящуюся Animal . Следовательно, имеет смысл сделать что-то вроде этого:

Это позволит нам передавать объекты любых классов, производных от Animal , даже тех, которые мы создали после того, как написали эту функцию! Вместо отдельной функции на каждый производный класс мы получаем одну функцию, которая работает со всеми классами, производными от Animal !

Проблема, конечно, в том, что, поскольку rAnimal является ссылкой на Animal , rAnimal.speak() будет вызывать Animal::speak() вместо производной версии speak() .

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

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

Однако, поскольку и Cat , и Dog являются производными от Animal , имеет смысл сделать что-то вроде этого:

Хотя это компилируется и выполняется, но, к сожалению, тот факт, что каждый элемент массива animals является указателем на Animal , означает, что animal->speak() будет вызывать Animal::speak() вместо версии speak() производного класса, которая нам нужна. На выходе мы получаем:

Хотя оба этих метода могут сэкономить нам много времени и энергии, у них одна и та же проблема. Указатель или ссылка на базовый класс вызывает базовую версию функции, а не производную версию. Если бы только был способ заставить эти указатели базового класса вызывать производную версию функции вместо базовой версии.

Можете догадаться, для чего нужны виртуальные функции? 🙂

Небольшой тест

Вопрос 1

Наш приведенный выше пример с Animal / Cat / Dog не работает так, как мы хотим, потому что ссылка или указатель на Animal не может получить доступ к производной версии speak() , необходимой для возврата значения, правильного для Cat или Dog . Один из способов обойти эту проблему – сделать данные, возвращаемые функцией speak() , доступными как часть базового класса Animal (так же, как название животного доступно через член m_name ).

Обновите классы Animal , Cat и Dog из примера выше, добавив в Animal новый член с именем m_speak . Инициализируйте его соответствующим образом. Следующая программа должна работать правильно:

Вопрос 2

Почему это решение неоптимально?

Подсказка: подумайте о будущем состоянии классов Cat и Dog , в котором мы хотим различать кошек и собак большим количеством способов.

Подсказка: подумайте о том, как ограничивает вас наличие члена, который должен быть установлен при инициализации.

Текущее решение не является оптимальным, потому что нам нужно добавить новый член для каждого способа, которым мы хотим различать классы Cat и Dog . Со временем наш класс Animal может стать довольно сложным и большим с точки зрения используемой памяти!

Кроме того, это решение работает только в том случае, если член базового класса может быть определен во время инициализации. Например, если speak() возвращает случайный результат для каждого объекта Animal (например, вызов Dog::speak() может возвращать " woof ", " arf " или " yip "), такого рода решения начинают становиться неудобными и разваливаются.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *