Бьерн Страуструп. Язык программирования С++. Специальное издание (2011) (1004033), страница 105
Текст из файла (страница 105)
Переменная типа «указатель на член класса Х» объявляется с применением декларатора вила Х:: *. Часто применяют оператор гурек(е1'для улучшения читаемости неудобоваримого синтаксиса деклараторов языка С. Обратите внимание на то, что синтаксис декларатора Х:: * в точности соответствует обычному декларатору * для указателей.
Указатель т на некоторый член класса можно применять как с указателем на объект, так и с самим объектом, для чего нужно использовать операции ->* и . ', соответственно. Например, выражение р->*гп связывает пг с объектом, на который указывает р, а выражение оЬ|. *т — связывает гп с объектом оЬ|. Результат этих операций используется в соответствии с типом»п. Невозможно сохранить результат операций ->* и . * для его дальнейшего использования. Конечно же, если бы мы заранее знали, какой именно член класса нам нужен, то мы могли бы работать с ним напрямую, а не затевать всю эту катавасию с указателями на члены классов. Как и в случае обычных указателей на функции, мы применяем указатели на функции-члены классов тогда, когда нам нужно сослаться на функцию-член, имя которой неизвестно. В отличие, однако, от обычных указателей на переменные или функции, которые представляют собой готовые целевые адреса в памяти, указатели на члены классов скорее являются смешениями в рамках некоторых структур (или индексами массивов).
Когда указатель на член класса комбинируется с указателем на объект класса соответствующего типа, тогда-то и вырабатывается точный адрес конкретного члена для этого конкретного объекта. Графически это можно изобразить следующим образом: Так как указатель на виртуальную функцию-член класса (з в нашем примере) является в некотором смысле смещением, то он не зависит от точного расположения объекта в памяти. Поэтому указатель на виртуальную функцию-член можно безопасно передавать из одного адресного пространства в другое адресное пространство, при условии одинаковой раскладки объекта в них обоих.
В то же время, указатели на невиртуальные функции-члены (как и обычные указатели на функции) нельзя передавать в другие адресные пространства. Очевидно, что функции, вызываемые через указатели на функции-члены, могут быть виртуальными. Например, когда мы вызываем зизреиИ() через указатель на функцию-член, вызывается правильная версия функции, соответствующая объекту, к которому применялся указатель на функцию-член. Это является важным аспектом поведения указателей на функции-члены классов.
Интерпретируюший код может использовать указатели на функции-члены для вызова функций-членов, представленных в строковом вице: 508 Глава 15. Иерархии классов тир<в!ггпя, Я!й !пгебгасе > оаг!аЫег тор<за(пя, РзЫ тет> орегайоп; юЫ саИ тетЬег (з!г(пх заг, з!г!пе орег) ( (ганаЫе! гаг) ->*орега!1оп (орег) ) (); ) // лог. орег() Чрезвычайно полезное применение указателей на функции-члены рассматривается в связи со стандартным шаблоном тегп!)зп () в 83.8.5 и 818.4. Так как статический член класса не ассоциируется с конкретным объектом класса, то указатель на статический член похож на обычный указатель.
Например: с1азз Таей //... таис когй зсьейи!е ( ); )' го!й ('р) () = ьТазл::зслейи1ег // о)г юЫ (Талл:: 'рт) () = ЬТазй::зслейи!е; //еггою обычный указатель присваивается //указателю на функцию-член класса Указатели на классовые поля данных рассматриваются в 8С.12.
15.5.1. Базовые и производные классы Производный класс в любом случае содержит члены, достающиеся ему от базовых классов. Кроме того, часто у него есть и собственные члены. Из этого следует, что мы можем безопасно присваивать значения указателей на члены базовых классов указателям на члены производных классов, но не наоборот. Например: с1ат !ехз: риЫ(с Ягй 1п!егТасе ( риЬВс: коЫ Фас! ( ) уоЫ зизрепй (); // ... Ыггиа1 ю1й рпт(); рейка!е: пес!ог ю гогй (51й (пзефасе:: *рт1) () = ь!ехг::рг(пз; ю1й (игл!:: *рт!) () = ьу!й 1пгег)асе::з1аго // еггог // о/с Это правило кажется противоположным известному правилу, что можно присваивать значения указателей на производные классы указателям на базовые классы.
Однако оба правила действуют абсолютно согласованно: они гарантируют, что указатели никогда не адресуют объекты, у которых отсутствуют свойства, подразумеваемые типом указателя. В нашем примере указатели типа Ягй 1п!еггасе::" могут использоваться с любыми объектами иерархии Я!й 1пгег)асе, часть из которых не относится к типу !ех!. Следовательно, у них может не быть функции-члена гех~::ргтг(), 509 ( 5.6. Свободная память которым мы пытались проинициализировать рпп'.
Отказывая в инициализации, ком- пилятор предотвращает ошибки времени выполнения. 15.6. Свободная память Можно управлять выделением памяти для класса, определив в нем функции орегагог пеп () и орегагог е!е1еге () (96.2.6.2). Однако замена глобальных функций орегагог пем() и орегагог Ие1еге() — занятие не для слабонервных. В конце концов, другие программисты могут рассчитывать на предопределенное поведение механизма работы с динамической памятью, или представить свои собственные версии. Более ограниченный и более надежный подход состоит в реализации этих операций в рамках конкретного класса. Причем этот класс может быть и базовым для многих других классов.
Например, для класса Етр1оуее из 9) 2.2.6 можно было бы определить специализированные операции выделения и освобождения памяти: с(азз Етр1оуее ( У.. риЫ1с: У ... ю!й* орегагог пет (пге П; юЫ орегагог йе1еге (коЫ", з(хе г); )) Функции-члены орега(ог пеп () и орегагог Ие(еге () неявно статические, так что у них отсутствует возможность работы с указателем гй1з и они не изменяют объекты. Они просто предоставляют память, которую конструктор может инициализировать, а деструктор — очищать (деинициализировать): коЫ* Етр(оуее:: орега<ог пет (згсе г з) ( У выделить з баит памяти и вернуть указатель на эту память юЫ Етр!оуее:: орегагог ае(еге ( юЫ* р, з(хе г з) ( (у'(р) ( жудаием только ески р/=О, зее зб.2.б, Зб.2.б.2 ~7 полагаем, что р указывает на з байт памяти, выделенной при помои(и Ю Етр(оуеекорега(ог пев() и освобождаем эту память для повторного использования ) Использование загадочного до сих пор аргумента типа з(хе г теперь становится понятным — это размер фактически уничтожаемого объекта.
Если, например, удаляется объект типа Етр!оуее, то этот аргумент равен зпеоу(Етр(оуее), а если удаляется объект типа Мапааег — то з1геоу(Мапаяег) . Это позволяет специфическому для класса аллокатору не хранить объем выделенной памяти для каждого объекта. Но, естественно, он может и хранить эту информацию (аллокаторы общего назначения Глава 15. Иерархии классов с1ат Мапааег: риа!1с Етр!оуее ( (п! 1еге1; уу ... )' гощу' ( ) ( Етр1оуее* р = пет Мапалег; ае1езе р; УУ беда: потерян истинный тип В этом случае компилятор не знает истинного размера.
Так же, как и при удалении массива, требуется помощь от программиста. Это делается добавлением виртуального деструктора к базовому классу Етр!оуее: с1азз Етргоуее ( риЫ1с: гоЫ* орегазог пеи (зйе !); гоЫ орегазог аегеге(гоЫ", з(зе з) Ыгзиа! -Етр(оуее ( ); уу ... ): Подойдет даже пустой деструктор: Етр1оуее:: -Етр1оуее ( ) ( ] В случае виртуальных деструкторов необходимый для освобождения размер памяти так или иначе связывается с вызовом правильного деструктора (который знает истинный размер объекта). Присутствие в классе Етр!оуее виртуального деструктора гарантирует, что каждый производный класс будет располагать деструктором (обеспечивающим информацию о правильном размере объекта), даже если таковой в этом производном классе явно и не определяется.
Например: гоЫу () ( Етр!оуее" р= пет Мапааег; !)е1есе р; УУ теперь все правильно (Етр!оуее - полиморфный) Выделение кода осуществляется при помощи генерируемого компилятором вы- зова Етр!оуее:: орегатг пеп (з(зеоЯМапаяег) ) обязаны это делать) и игнорировать аргумент типа з!зе ! у функции орега!ог Ие1е!е() . Последний подход затрудняет повышение производительности (в плане скорости и непроизводительных расходов памяти) механизмов работы с памятью по сравнению с таковыми же для общего назначения. Как компилятор узнает правильное значение второго аргумента (размер удаляемого объекта) при вызове функции орега!ог ~!е(е!е ( )? Пока операция г1е1е!е ассоциируется с истинным типом объекта, это просто. Однако так бывает далеко не всегда: ) 5.6.
Свободная память а освобождение памяти — при помощи другого генерируемого компилятором вызова: Етр1оуее:: орегатг ((е(е(е (р, в(еео/' (Ма»олег) ) Другими словами, если вы хотите предоставить собственную пару аллокатор/деаллокатор, которая корректно работает с производными классами, то вы должны либо определить виртуальный деструктор в базовом классе, либо воздержаться от использования аргумента типа в[ге ( в деаллокаторе. Естественно, можно было бы спроектировать этот аспект языка таким образом, чтобы избавить программиста от сопутствующих сложностей. Но тогда бы и не было возможности осуществлять оптимизацию работы с памятью, присущую лишь менее безопасным системам. 15.6.1.
Выделение памяти под массивы Функции прего(ог ае»() и орегаюг (!е!е(е() позволяют программисту брать на себя управление выделением/освобождением памяти для индивидуальных объектов; функции прага(огпе(г [] () и прего(ог(1е1е(е [] () играюттуже рольдля массивов. Например: с1аи Етр(оуее ( рибяс: юп(* прего(ог пе(г [ ] ( в((е () ( юЫ орега(ог ((е(е(е [ ] (во(а«, вие () (( ... юИ/'((п( в) Етр(оуее* р = пе(г Етр!оуее [в] ( р... Ие1е(е [] р( ) В этом случае память выделяется при помощи вызова Етр(оуее:: прего(ог пе(г [ ] (в((ео/" (Етр!оуее) "вч1е((а) [где (1е1(а — некоторая вспомогательная память, зависящая от реализации), а осво- бождается память при помощи вызова Етр(оуее:: орега(ог ((е(е(е [ ] (р) ( р освобождаем в«в(еео~(Етр!оуее)+((е((а байт Количество элементов а и избыточная память (1е1(а запоминаются системой.