Б. Страуструп - Язык программирования С++. Специальное издание, 3-изд. Бином. 2004 (1160791), страница 104
Текст из файла (страница 104)
Иерархии классов $ !ир' г Г> !с!р* ре, !!е1<Бл!р" >й с) с.риИ !гол1 (ре), 0-. ге1игл срор угол1(); Совместно с виртуальными функциями такой метод годится в большинстве случаев. 15.5. Указатели на члены Многие классы предоставляют простые и очень общие интерфейсы, которые предполагается вызывать несколькими различными способами. Например, во многих «объектно-ориентированных> пользовательских интерфейсах определяется набор запросов, на которые каждый представленный на экране объект должен быть готов отреагировать.
Кроме того, такие вызовы могут осуществляться, непосредственно или косвенно. пз других программ. Рассмотрим простой вариант этой идеи: с1аее БЫ !л1еггасе ( риЫк. Иггиа1 ооЫе1аг! () = О, Ыгсиа! иоЫ еиерелс1() = О, и!ггиа! ооЫ гееите () = О; Иг1иа! ооЫ дш1 () = О; Ыгсиа! ио!ауи!! Нее () = О, и!г1иа! ооЫ ета!! () = О, о!ггиа1-БЫ !л1еггасе () () ); Точное значение каждой операции определяется объектом, для которого она вызывается. Часто между пользователем илп программой, выдающей запросы, и объектом, их получающим, находится программная прослойка. В идеале, этот промежуточный код не должен ничего знать об индивидуальных операциях, таких как гезите () и уй!! з!яе (). В противном случае, промежуточный код пришлось бы модыфицироватг при каждом изменении набора операций.
Следовательно, подобный промежуточный код должен просто передавать получателю запроса некоторые данные, представляющие операции, которые должны быть вызваны, от его источника. Одним нз простых способов реализации этого подхода является пересылка строкового представления операции, которхся должна быть вызвана. Например. чтобы вызвать зазрела! (), мы могли бы послать строку "еизрепс(".
Однако, кто-то должен создать эту строку и кто-то — декодировать, чтобы определить, какой операции она соответствует (или никакой не соответствует). Порой это кажется неудобным и сложным способом. Вместо этого мы могли бы посылать просто целые числа, соответствующие операциям. Например, 2 может означать еизрегЫ(). Однако, хотя числа и удобны для компьютера, для людей их значение неочевидно. И мы по-прежнему вынуждены писать код для определения того, что 2 означает еизрепй (), и для вызова зизрепЫ ().
С++ предлагает средство косвенной ссылки на член класса. Указатель па член является значением, идентифицирующим член класса. Вы можете его рассматривать как позицию члена в объекте класса, но конечно же, компилятор принимает в расчет различия между данными, виртуальными функциями, невпртуальными фупкциямп н т. д. 475 15.5. Указатели на члены Рассмотрим интерфейс 5Ы 1п1ег7асе. Если я хочу вызвать функцию еиересЫ () для некоторого объекта, при этом не указывая явно визрепй (), мне потребуется указатель на член $Ы 1п!ег~асезвизрепй ().
Кроме того, мнс потребуется указатель пли ссылка на обьект, к которому нужно применить операцию зизрепй(). Рассмотрим тривиальный пример: Гурейе~оорс7 (БЫ рпгег~асе "РвЫ теса)(, 1/ указарпель нср функапю-член ооЩ(5Ы рпгет~асе* р) ( РхЫ гпегп в = &БЫ ьпсегГасеьвпврепй; О поялоа вызов р — >виьуепс1 (), Указатель на член можно получить при помоьци применения оператора получения адреса & к полностью квалифицированному имени члена класса, например, &ВЫ сл1еггасе: виерелИ. Переменная типа «указатель на член класса Х» объявляется с использованием формы Хс . С целью компенсации нечитабель ности синтаксиса объявлений С обычно поп ьзуются 1дредеу.
Однако обратите внимание, что синтаксис объявления Хс* точно соответствует трад1щионному использованию * в объявлении. Указатель на член т можно использовать в комбинации с объектом. Операторы — >* н.* позволяют программисту выразить такую комбинацию. Например, р — >*сп связывает т с объектом, на который указывает р, а оЬ|'.*гп связывает т с объектом оЬ|'. Результат можно использовать в соответствии с типом т. Невозможно сохранить результат операций ->* илп .* для его дальнейшего использования. Естественно, если бы мы знали, какой член нам нужно вызвать, мы могли бы вызвать его непосредственно, а не затевать эту путаницу с указателями. Также как и указатели на обычные функции, указате.ли на функции-члены используются тогда, когда возникает необходимость сослаться на функцию, имя которой неизвестно.
Однако в отличие от указателя на переменную или обыкновенную функцию, указатель на член не является просто указателем на область памяти. Он болыпе соответствует смещетпо в структуре илп индексу в массиве. Сочетание указателя на член с указателем на соотвечттвующпй объект даст то, что идентифицирует конкретный член конкретного объекта. ь ьто можно представить в графическом виде следующим образом: ИЫ: ~х::~ ви ) хс: .р р~ Так как указатель на виртуальную функцию-член Гв нашем примере, е) является в некотором смысле смещением, он не зависит от расположения объекта в памяти.
Поэтому указатель на виртуальный член можно безопасно передавать из одного адресного пространства в другое, при условии что в них обоих обьект размещен Глава 15. Иерархии классов одинаковым образом. Также как и указатели на обыкновенные функции, указатели на невиртуальные функции-члены нельзя перславать в дру~ое адресное пространство, Обрати~с внимание на то, что функции, вызываемые через указатели, люгут быть виртуальными. Например, когда мы вызываем виврепч( () через указатель на функппю, вызывается версия виврепч( (), соответствующая объекту, к которому применялся указатель на функцию.
Это является суь!цественвым аспектом механизма указателей на функции. Интерпретатор может использовать указатели на ч.лены для вызова функций, представленных в строковом виде: тар<вйтпр, $Ы !п1ег/асв*> иаг!аЫв; тарчвгггпу, Рвы тат> ореганоп; иоЫ са!! тетЬег (в!ггпу оаг, в1ппд орег) ( // оаг.орег() (па г(аЫв(оаг)->*орегаг!оп(орег)) () Пример небезопасного использования указателей на функции-члены можно найти в тет /ип () Я 3.8.5, $18А!. Статический член не ассоциируется с конкретным объектом, так что указатель на статический член похож на обыкновенный указатель. Например: с!авв ТавЬ ( //- в!а 1!с иоЫ всЬеь!и!е (); ); ооЫ (*р) () = ЬТавЬсвсЬеии!е; иоЫ (Тазу .' рт) () = !>ТавЬчвсЬвг!и!е,' // правильно // ошибка: обьшныи указатель // нрисваиваетсл указателю на член Указатели на члены данных описаны в ~ В.12.
15.5.1. Базовые и производные классы ооЫ ($Ы т1вгуасвс" рт!) () = Ь1ехгсрпп1; ооЫ (1ех1::*ртг) () = $$1б игвг~асесв1акй // ошибка // привил ьно Производный класс по крайней мере содержит члены, которые он унаследовал от базовых классов. Часто он содержит много дополнительных членов. Из этого следует, что мы можем безопасно присвоить указатель на член базового класса указателю на член производного класса, но не наоборот. Это свойство часто называют контрвариацией.
Например: с!авв 1вх1: риЫ1с $Ы т1ег~асе ( риЫЫ ооЫ в1аг1 (); сои!виврепб(); //.. тг1на! оо(б рп'п1 (). рг(оа1в. оес1ог в; ); 477 15.6. Свободная память Создается впечатление, что правило контрвариации имеет смысл, противоположный правилу, которое гласит, что мы можем присвоить указатель на производный класс указателю на его базовый класс. В действительности, оба правила нужны для фундаментальной гарантии того, что указатель ни при каких условиях не укажет на объект, который не реализует (по крайней мере) свойств, подразумеваемых типом этого указателя.
В нашем случае Б!с1 !п1ег~асе::м можно использовать с любым $Ы 1п1ег3асе, но в общем случае не все этн объекты имеют тип 1ех1. Следовательно, у них нет члена 1ех1срНп1, с помощью которого мы пытались инициализировать рт!. Отказывая в инициализации, компилятор предохраняет нас от ошибки на этапе выполнения. 15.6. Свободная память Можно самостоятельно управлять памятью для класса, определив собственные орегп1ог пеги () и орега!ос с1е!е!е () Я 6.2.6.2).
Однако замена глобальных орега1ог пев () и орега1ог де!е1е () — занятие не для слабонервных. В конце концов, какой- нибудь другой пользователь может рассчитывать на некоторые аспекты поведения по умолчанию нли даже реализовать собственные версии этих функций. Более ограниченный и часто более надежный подход состоит в реализации этих операций для конкретного класса. Этот класс может являться базовым для множества производных классов.
Например, мы могли бы реализовать специализированные операции выделения и освобождения памяти для класса Етр1оуее из 6 12.2.6 п всех его производных классов: с1аев Етр!оуее ( О.. риб!!с. О.. ио!д орега!ог пею (в!ее !); ио!д орега!ог с!е!еге (иои1", в!ее 1) ); Операторы-члены пеш () и с!е!е1е () неявно являются статическими членами. Следовательно, у них нет указателя !й!в и они не изменяют объект. Они предоставляют память, которую конструктор может инициализировать, а деструктор — о щстить. ио!д* Етр!оуее, ирека!ог пеил (в!хе 1 я) ( гг,г выделить в байт памяти и вернуть указатель на выделенную облатпь ) ио!дЕтр!оуее: орега1ог Йе!е!е (ио!д' р, е!хе ! я) ( 1~'(о) ( !1 удаляелг только если рыд; с ли 2 б.2.б, 2 б.2.б.2 л'гг полагаем, ипо р указывает на ь байтов памяти, выделенной О Етр!оуее::прего!огнен~ !) и освобождаем зту память л'лг для дальнешиего использования Использование загадочного до сих пор аргумента типа з!хе 1становнтся очевидным.
Он является размером фактически уничтожаемого объекта. При удалении «просто> Етр1оуее аргумента имеет значение и!хеоЯЕтр1оуее) при удалении Мападегзначе- Глава 15. Иерархии классов 478 шге аргумента равно з!яеоЯМападег), Это позволяет конкретной реализации операции выделения памяти не хранить информацию о размере при каждом выделении памяти. Естественно, реализация этой операции для конкретного класса может хранить эту информацию (стандартная функция выделения памяти должна это делать) п игнорировать аргумент типа зие !в орега1огс(е!е1е (). Однако такой подход затрудняет более эффективную, в смысле скорости и требуемой памяти, реализацию распределителя памяти общего назначения. Как компилятор узнает, как передать истинный размер в орега1огс1е1е1е ()? Если в операции с!е1е1е указан тип, соответствующий реальному типу объекта, — задача проста.