Как сделать указатель на класс c

Обновлено: 07.07.2024

Ссылка reference — механизм языка программирования (C++), позволяющий привязать имя к значению. В частности, ссылка позволяет дать дополнительное имя переменной и передавать в функции сами переменные, а не значения переменных.

Синтаксически ссылка оформляется добавлением знака & (амперсанд) после имени типа. Ссылка на ссылку невозможна.

Ссылка требует инициализации. В момент инициализации происходит привязка ссылки к тому, что указано справа от = . После инициализации ссылку нельзя “отвязать” или “перепривязать”.

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

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

  1. Что-то имеет слишком длинное, неудобное название. Привязав к нему ссылку, мы получим более удобное, короткое локальное название. При этом мы можем не указывать тип этого “чего-то”, можно использовать вместо типа ключевое слово auto :
  1. Выбор объекта привязки ссылки может происходить во время исполнения программы и зависеть от некоего условия. Пример:

Впрочем, основным применением ссылок является передача параметров в функции “по ссылке” и возвращение функциями ссылок на некие внешние объекты.

Передача по ссылке by reference напоминает передачу “по имени”. Таким образом, можно сказать, что, используя ссылки, мы передаём не значения, а сами переменные, содержащие эти значения. В реальности “за ширмой” происходит передача адресов этих переменных. Передача ссылки на переменную, время жизни которой заканчивается, например, возврат из функции ссылки на локальную переменную, приводит к неопределённому поведению.

Ранний пример использования ссылок для возврата из функции более одного значения представлен в самостоятельной работе 3.

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

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

Обратите внимание на ключевое слово const . Данное ключевое слово позволяет нам указать, что мы хотим ссылку на константу, т.е. функция char_freq использует s как константу и не пытается её изменять, а ссылка нужна для того, чтобы избежать копирования. Рекомендуется использовать const везде, где достаточно константы. Компилятор проверит, действительно ли мы соблюдаем константность.

Ставить слово const можно перед именем типа и после имени типа, это эквивалентные записи.

Общие сведения

Что такое указатель pointer уже рассказывалось во введении.

В C и C++ указатель определяется с помощью символа * после типа данных, на которые этот указатель будет указывать.

Указатель — старший родственник ссылки. Указатели активно использовались ещё в машинных языках и оттуда были перенесены в C. Ссылки же доступны только в C++.

Указатели — простые переменные. Указатели не “делают вид”, что они — те значения в памяти, к которым они привязаны. Чтобы получить указатель на переменную, нужно явно взять её адрес с помощью оператора & . Чтобы обратиться к переменной, на которую указывает указатель, требуется явно разыменовать его с помощью оператора * .

Так же, как и в случае ссылок, можно использовать ключевое слово const , чтобы создать указатель на константу.

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

Указатели можно передавать в функции и возвращать из функций как и любые “элементарные” значения. Ещё пример с указателями:

Для обращения к полю структуры по указателю на объект структуры предусмотрен специальный оператор -> (“стрелка”).

В отличие от ссылок, указатели не обязательно инициализировать. Указатели можно инициализировать специальным значением нулевой указатель nullptr , которое сигнализирует об отсутствии привязки указателя к чему-либо. Присваивание указателю другого адреса меняет его привязку. Это позволяет использовать указатели там, где семантика ссылок слишком сильно ограничивает наши возможности.

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

Наличие нулевого указателя позволяет, например, возвращать указатель на искомый объект и в том случае, когда ничего не было найдено. Просто в этой ситуации возвращаем нулевой указатель, а принимающая сторона должна быть готова к такому развитию событий. Указатель автоматически преобразуется к булевскому значению: нулевой указатель даёт false , прочие указатели дают true , поэтому, если p — указатель, то

есть то же самое, что

есть то же самое, что

Например, поиск самого левого нуля в массиве чисел с плавающей точкой может быть записан так:

Данный пример использует арифметику указателей и массивы. Данная тема освещена в разделе массивы и ссылки.

Бестиповый указатель

Вместо типа данных при объявлении указателя можно поставить ключевое слово void . Данное ключевое слово означает, что мы описываем указатель “на что угодно”, т. е. просто адрес в памяти. Любой указатель автоматически приводится к типу void* — бестиповому указателю typeless pointer . Прочие указатели, соответственно, называются типизированными или типизованными typed . Приведение от void* к типизованному указателю возможно с помощью оператора явного приведения типа.

В C бестиповые указатели широко применяются для оперирования кусками памяти или реализации обобщённых функций, которые могут работать со значениями разных типов. В последнем случае конкретный тип маскируется с помощью void (“пустышка”). При использовании таких функций обычно приходится где-то явно приводить тип указателей. C++ позволяет отказаться от подобной практики благодаря поддержке полиморфизма и обобщённого программирования (материал 2-го семестра).

О цикле for (int byte: buffer) см. здесь.

Указатель на указатель

Так как указатель — обычная переменная, возможен указатель на указатель. И указатель на указатель на указатель. И указатель (на указатель) n раз для натурального n. Максимальный уровень вложенности задаётся компилятором, но на практике уровни больше 2 практически не используются.

Система ранжирования C-программистов.

Чем выше уровень косвенности ваших указателей (т. е. чем больше “*” перед вашими переменными), тем выше ваша репутация. Беззвёздочных C-программистов практически не бывает, так как практически все нетривиальные программы требуют использования указателей. Большинство являются однозвёздочными программистами. В старые времена (ну хорошо, я молод, поэтому это старые времена на мой взгляд) тот, кто случайно сталкивался с кодом, созданный трёхзвёздочным программистом, приходил в благоговейный трепет.

Некоторые даже утверждали, что видели трёхзвёздочный код, в котором указатели на функции применялись более чем на одном уровне косвенности. Как по мне, так эти рассказы столь же правдивы, сколь рассказы об НЛО.

Просто чтобы было ясно: если вас назвали Трёхзвёздочным Программистом, то обычно это не комплимент."

Условия для проверки себя на “трёхзвёздность” перечислены на другой странице того же сайта.

В случае C указатели на указатели (уровень косвенности 2) используются довольно часто, например, для возвращения указателя из функции, которая возвращает ещё что-то, или для организации двумерных массивов. Пример такой функции из Windows API:

Функция принимает имя файла как указатель на си-строку lpFileName, а также размер буфера nBufferLength в символах и адрес буфера lpBuffer, куда записывается в виде си-строки полное имя файла. Функция возвращает длину строки, записанной в буфер, или 0, если произошла ошибка. Кроме того, последний параметр функции — указатель на указатель на си-строку lpFilePart, который используется, чтобы вернуть из функции указатель на последнюю часть имени файла, записанного в буфер.

В случае C++ с помощью ссылок и Стандартной библиотеки можно вообще избежать использования “классических” указателей. Так что “беззвёздочный” C++-программист возможен.

Неограниченный уровень косвенности

Несмотря на ограниченность применения уровня косвенности выше двух, довольно часто встречается то, что можно назвать неограниченным уровнем косвенности или рекурсивным типом данных. Типичный (и простейший) пример — структура данных, называемая “связанный список” linked list .

Следующий пример демонстрирует использование связанного списка для чтения последовательности строк и вывода этой последовательности в обратном порядке:

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

Язык C позволяет определять указатели на функции (в указателе хранится адрес точки входа в функцию) и вызывать функции по указателю. Таким образом, можно во время исполнения программы выбирать какая именно функция будет вызвана в конкретной точке, выбирая значение указателя. Язык C++ позволяет создавать также и ссылки на функции, но ввиду того, что ссылка после инициализации не может быть изменена, область применения ссылок на функции весьма узка.

Функцией высшего порядка higher order function называют функцию, принимающую в качестве параметров другие функции. Функции высшего порядка — одно из базовых понятий функционального программирования. Единственная форма функций высшего порядка в C — функции, принимающие указатели на функции. Язык C++ расширяет круг доступных форм функций высшего порядка, но в примерах ниже мы ограничимся возможностями C.

Простой пример использования указателя на функцию — функция, решающая уравнение вида f(x) = 0, где f(x) — произвольная функция. Конкретные функции f можно передавать по указателю. Приведение функций к указателю на функцию и наоборот производится неявно автоматически, поэтому при присваивании указателю адреса конкретной функции можно не использовать оператор взятия адреса & , а при вызове функции по указателю — не использовать оператор разыменования * (поведение, аналогичное поведению с массивами).

В качестве простого примера применения функции обратного вызова рассмотрим функцию, занимающуюся поиском набора корней уравнения f(x) = 0 на заданном отрезке. Сама функция будет работать по достаточно простому алгоритму (который, естественно, не гарантирует, что будут найдены все или даже какие-то из существующих на отрезке корней): предполагаем, что есть некая функция, способная найти один корень на отрезке, если он там есть (например, функция nsolve из примера выше). Теперь берём исходный отрезок поиска [a, b] и некоторое значение “шага” step и проходим по этому отрезку с этим шагом, проверяя участки [a + i step, min(b, a + (i + 1)step], i = 0, … пока не пересечём правую границу отрезка. На каждом участке проверяем, являются ли его границы корнями, и есть ли на нём корень (принимает ли функция f разнознаковые значения на границах). В последнем случае используем “решатель” вроде nsolve (переданный по указателю), чтобы найти корень. Каждый найденный корень — это событие, вызываем для него “обработчик” — функцию обратного вызова по указателю report.

Следующий пример демонстрирует “двухзвёздное программирование” и использование указателя на функцию для определения порядка сортировки массива строк с помощью стандартной функции qsort .

Функция qsort является частью Стандартной библиотеки C. Стандартная библиотека C++ предлагает более удобную и эффективную функцию sort (определённую в заголовочном файле ), однако её рассмотрение выходит за пределы темы данного раздела.

Следующий пример является развитием примера со списком из предыдущего подраздела и использует бестиповые указатели, указатели на указатели и указатели на функции для управления “обобщённым” связанным списком в стиле C. Звенья такого списка могут содержать произвольные данные. Основное требование к звеньям списка — наличие в начале звена указателя на следующее звено, фактически каждый предыдущий указатель указывает на следующий.

Теперь сама программа, выводящая строки в обратном порядке, упрощается:

Впрочем, необходимо отметить, что сочетая такие приёмы со средствами C++, выходящими за пределы “чистого” C, вы рискуете нарваться на неопределённое поведение. Низкоуровневые средства требуют особой внимательности, так как компилятор в таких случаях не страхует программиста. В частности, в общем случае нельзя интерпретировать произвольный указатель как void* и наоборот без выполнения приведения типа. А это может произойти неявно, например, в примере выше мы полагаем, что указатель prev, указывающий на объект структуры Line совпадает с указателем на поле prev этого объекта.

Правило чтения сложных описаний типов

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

  1. Начиная с имени (в случае typedef , в случае using имя находится вне — см. ниже), читать вправо, пока это возможно (до закрывающей круглой скобки или точки с запятой).
  2. Пока невозможно читать вправо, читать влево (убирая скобки).

Некоторые примеры “расшифровки” типов переменных:

Разница между typedef и using

Директива typedef объявляет синоним типа. Используется синтаксис определения переменной, к которой добавили ключевое слово typedef , только вместо собственно переменной вводится синоним типа этой как-бы переменной с её именем.

В С++11 появилась возможность объявлять синонимы типов с помощью using-директивы в стиле инициализации переменных:

Объявление typedef можно превратить в using-директиву, заменив typedef на using , вставив после using имя типа и знак равно и убрав это имя типа из объявления справа.

К элементам классов можно обращаться с помощью указателей. Для этого определены операции .* и ->*. Указатели на поля и методы класса определяются по-разному.

Формат указателя на метод класса:

возвр_тип (имя_класса::*имя_указателя)(параметры);

Например, описание указателя на метод класса monster

будет иметь вид:

Такой указатель можно задавать в качестве параметра функции . Это дает возможность передавать в функцию имя метода:

Можно настроить указатель на конкретный метод с помощью операции взятия адреса:

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

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

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

Формат указателя на поле класса:

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

Если бы поле health было объявлено как public , определение указателя на него имело бы вид:

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

Рекомендации по составу класса

Как правило, класс как тип, определенный пользователем, содержит скрытые поля и следующие функции:

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

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

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

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

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

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

Установка указателей и ссылок типа 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 "), такого рода решения начинают становиться неудобными и разваливаются.

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

Указатель на объект объявляется с использованием того же синтаксиса, что и указатели на данные других типов. В следующей программе создается простой класс с именем P_example и определяется объект этого класса ob, а также указатель р на объект P_example. Ниже проиллюс­трировано, как получить доступ к объекту ob непосредственно и опосредованно с использовани­ем указателя:

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

Инкремент или декремент указателя изменяет его таким образом, что он всегда указывает на следующий элемент базового типа. То же самое справедливо и для объектов. Следующий пример модифицирует предыдущую программу, в результате чего ob становится массивом из двух эле­ментов типа P_example. Обратим внимание на инкремент и декремент указателя р, с помощью которого осуществляется доступ к элементам массива:

Читайте также: