Бьерн Страуструп. Язык программирования С++. Специальное издание (2011) (1004033), страница 61
Текст из файла (страница 61)
Для подобных логически связанных функций было бы желательно иметь возможность их последовательного использования в виде компактной цепочки вызовов. Например, хотелось бы иметь возможность писать что-то вроде го!йу (Рагеа й) ( // ... й, айй йау (1) . айй топЯ (1) . айй уеаг (1) 1 // ...
) чтобы в рамках единственного выражения изменить значения дня, месяца и года. Для этого каждая из функций должна возвращать ссылку но тип Рате: //прибавить п лет с1азз Разе гУ., Ра1еь айй уеаг (!пг и) 1 //а ток ошибка: отсутствует ключевое слово сопи Глава 1О. Классы 292 Рагеь аегй тоней (1пе и); У прибавить и месяцев Раееь аегй йау (1пе п) е й прибавить и дней )Е Каждая нестатическая функция класса знает объект, для которого она вызывается, и может ссылаться на него явным образом: Раееь Раее::ада' уеаг(1пе и) ( еу(е1==29 ь ь т==2 ьь ! Ееаруеаг (у+и) ) ( д = 1! т=Зе ) й не забудьте о Ребгиагу 29 уь=п' гвеигп *еЬЬе Рагев Риге:: аИ уваг (1пг и) ( У не забудыпе о гвбгиагу 29 ез(ел(в->й==29 ьь ел!в->т==2 ьь ! Ееаруеаг(ел(в->у+а) ) ( Елйв->ег = 1 Е ел(в->т = Зе ) бз|в->у ь= пе гееигн *б11ве ) В операциях со связными списками е7е1в применяется явным образом (924.3.7.4).
10.2.7.1. Физическое и логическое постоянство Иногда так бывает, что константной по логике функции-члену все-таки требуется изменить значение поля данных класса. С точки зрения пользователя функция ничего в состоянии объекта не меняет. А на самом деле она изменяет отдельные детали, пользователю недоступные. Такое явление называется логическим постоянппвом (!оя(са! сопвгпевв). Например, класс Роге мог бы возвращать строковое представление даты, которое пользователь использовал бы для вывода. Конструирование та- Выражение *Е!е1в как раз и означает такой объект.
В языке Б!п)ц!а это же обозначается как ТН!Б, а в языке Бена)!(аП( — как ве27: В нестатической функции-члене ключевое слово ЕЬ(в означает указатель но обьект, для которого функция вызвана. В неконстантной функции-члене класса Х тип Ей!в есть Х'. Однако ж, ЕЬЬ не является обычной переменной — невозможно взять ее адрес и нельзя ей ничего присвоить. В константной функции-члене класса Хтип е!е1в есть соим Х" для предотвращения изменения самого объекта (см. также 95.4.1). По большей части еЬЬ используется неявным образом. В частности, каждое обращение к нестатическому члену внутри класса опирается на неявное применение е!е1в для доступа к полям классового объекта.
Например, можно написать следующее эквивалентное (но утомительное) определение функции ада уеаг(): 10.2. Классы 293 кого представления может оказаться довольно дорогой (затратной) операцией. Поэтому имеет смысл хранить актуальную копию строки с тем, чтобы при последовательных запросах возвращать зту копию в случаях, когда дата не изменялась с момента последнего обращения. Кэширование (сасЬ)нК) данных чаще применяется для более сложных структур, но мы сейчас рассмотрим как все это работает на примере Разе: стазз Ваге Ьооз сасве газЫ; мпнд сасЬе; зоЫ сотрте сасае газне() ) //...
/У заполнить кэш риЫ)с: //... ззгзне зптпд гер () сопл() )' // строковое представление ззпнд Вазе::ззгзпя гер () сот( ( з/'(сасЬе гаРЫ ==/а)зе) ( Вазе* гд = сот/ сазе<Вазе*> (зЫз); зЬ->сотризе сасЬ га(ие(); зЬ->сасЬе газЫ = згие; гезигп сасве; ) /У снимаем сонм мы здесь применяем операцию сонм сам (915.4.2.1) для того, чтобы вместо тьзз получить указатель типа Разе*. Это не только не элегантно, но может и не сработать, когда объект класса изначально объявлен константой. Например: Вазе з(Ы сонм Вазе И2; згппд з1 = аз . з(гзпа гер (); мппд з2 = д2.ззгзпа гер (); // неопределенное поведение Для объекта Н все прекрасно работает. Но объект И2 объявлен константой и конкретная реализация С++ может включить дополнительные защитные механизмы, гарантирующие неизменность объекта. Из-за этого нельзя гарантировать, что зз2.игзпа гер() во всех реализациях будет иметь одинаковое и предсказуемое поведение.
С точки зрения пользователя, вызов ззгзпд гер () не меняет состояния Разе, так что естественно, что это константная функция-член класса Разе. С другой стороны, любой кэщ нужно хотя бы один раз проинициализировать до того, как он будет использоваться на чтение. Достигнуть этого можно, например, следующим образом (применяя грубую силу — Ьгиге/огсе); 294 Глава ) О. Классы 10.2.7.2. Ключевое слово л)ц1вЫе Можно избежать операции приведения сои«1 сазт и последующей зависимости от деталей реализации С++, если пометить кэшируемые данные класса Ваге ключевым словом тигаЬ1е: с1азз Ра!е ( ти!аЫе Ьоо1 сасйе га(Ы; ти!аЫе «!г(па сасйег гоЫ сотри!е сасйе ва1ие() сопзг; (у...
У заполнить (ти(аЫе) кзш риЫ(с: УУ... з«г(пе зи«пе гер () сот!; УУ строковое представление Ключевое слово тигаЫе требует обеспечить такое хранение помеченного им поля данных, чтобы это поле можно было гарантированным образом модифицировать, даже для обьектов, объявленных константаии. Иными словами, ти!аЫе означает «никогда не может быть сои«о. В результате упрощается определение функции зтг)ия гер (): з!г!пе Разе::з«г!пе гер () солт ( (!(!сасйе иа1Ы) ( соти!е сасЬе га!ие(); сасйе ва(Ы = иие; ) ге!игп сасйе; ) и становятся действительными все случаи ее разумного применения. Напри- мер: Ра«е 4З« сопзг Разе 44; згг(пе «З = ЫЗ.тппд гер () з«г(пе «4 = 64.
з~ппе гер (] УУ о/с! зи ис! сасйе ( Ьоо1 иа1Ы; згг(пе гер; Объявление части полей данных с модификатором тигаЫе приемлемо в случаях, когда это малая часть общего числа полей данных класса. Если же у логически константного объекта большая часть полей подвержена изменениям, будет лучше все изменяющиеся поля переместить в отдельный объект с косвенным доступом. В этой технике наш пример с кэшируемой строкой принимает следующий вид: 10.2. Классы 295 с1аяя Ва(е ( сасйв" с( ноЫ сотри(в сакле на(ие() сопя(( // ... //инициализируется конструктором 910.4.б) // заполнить кэи( риЫ1с: // ... я(г(па я(г1па гер () сопя(( ) // строковов представление я(г(пд Ва(в(:я(г(па гвр () сопя! ( (1 (! с->ной() ( сои(ри(в сасйв на1ив() ( с->най1 = йив; гв(игп с->гвр( ') Различные обобщения техник кэширования приводят к тем или иным формам так называемых ленивых вычислений (!агу ено!ио(!оп).
10.2.8. Структуры и классы По определению структура есть класс с открьпыми по умолчанию членами, так что яйис(я (... есть просто сокращенная форма записи для с!аяя я (риЫ(с:... с(аяя Ва(в! ( (п(й, (и, у„ риЫ(с ( (за(в! (т( йй, (п! т(п, (п(уу) ( гоЫ айй уваг ((п! и ) ( )( //прибавить и лет яй ис( (га(в2 ( рина(в ( 1п( й, и(, )'; риЫ(с: Ра(в2((п( йй, т( тт, (п(уу] ( ноЫайй уваг(т! и); //прибавить и лет Спецификатор доступа ргйа(е говорит, что все последующие за ним члены имеют закрытый режим доступа, точно так же, как риЬ!!с сообшает об открытом режиме доступа. За исключением различий и именах следующие объявления эквивалентны: Глава (О.
Классы Какой стиль объявления использовать, зависит от вкуса и обстоятельств. Я обычно предпочитаю использовать яггисг для классов данных с открытыми членами. О таких типах я думаю как о «не совсем классах, просто наборах данных», Конструкторы и функции доступа полезны и в этих случаях, просто для удобства, а не как фундаментальные элементы типов (524.3.7.1). Вовсе не обязательно объявлять данные первыми. Часто полезно первыми поместить функции, чтобы подчеркнуть особую важность открытого интерфейса пользователя. Например: с1ам РагеЗ ( риЫ1е: РагеЗ(Ы144, тг тт, (ивуу); иоЫ адд уеаг(Ы1 и); У1 прибавить п лет ргп аге: Гтг 4, ги, В реальном коде, где и данные, и открытый интерфейс классов весьма велики по сравнению с учебным кодом, я предпочитаю стиль, использованный только что для объявления РагеЗ. Спецификаторы доступа могут встречаться в объявлениях классов много раз.
Например: с1аев Раге4 риЫ)с: Ра1е4(Ы( 44, !ив тт, (и( уу) в ргшаге: ГЫ4, т,у; риЫ(с: гоЫ ад1 уеаг((иг и); )' ,(г прибавить п лет Множественные открытые и множественные закрытые секции класса (как в Эаге4) могут запутать программиста, но они нужны, например, для автоматической генерации кода. 10.2.9. Определение функций в теле определения класса Функция-член, определенная, а не только объявленная в теле определения класса, по умолчанию предполагается встраиваемой.
Ясно, что это полезно для небольших и часто применяемых функций. Как и определение класса, частью которого такие функции являются, они могут директивами Ипе!иде включаться во многие единицы трансляции. Как и для класса в целом, их определение должно быть всюду одинаковым (59.2.3). Стиль, в котором поля данных класса помещаются в конец тела определения класса, вызывает небольшие проблемы в связи со встраиваемыми функциями-членами, ссылающимися на поля данных.
Рассмотрим пример: 297 ) 0.3. Эффективные пользовательские типы с1азз Разе ( риЫ)с: 1пг Иау() сопм (геаип Ыг ) // ... рл'га1е: ЬмИ, и )г; )' // возвращает Ваге.:д Это абсолютно правильный код на С++, потому что функция-член класса может обращаться к любому члену класса так, как будто весь класс полностью определен до определения тела функции. Это, однако, может смутить человека, читающего программу. Поэтому я либо помешаю поля данных в начало определения класса, либо располагаю определения встраиваемых функций-членов после определения класса: с1азз Разе ( риЫс: 1птдау() сопев; // ... рг1гаге: 1птИ, пг, у; )' 1п11пе Ьзг Разе:: ггпу () сопзг (гегигп а'; ) 10.3. Эффективные пользовательские типы В предьиушем разделе отдельные части класса Васе рассматривались в связи с изучением основных средств С++, предназначенных для определения классов.
В настоящем разделе я переворачиваю акценты и рассматриваю в первую очередь простой и эффективный класс Разе и показываю, как средства С++ поддерживают дизайн таких типов данных. Небольшие, интенсивно используемые абстракции весьма типичны для большинства приложений: латинские буквы, китайские иероглифы, целые числа и числа с плавающей запятой, точки, указатели, координаты, преобразования, пары (указатель, смещение), даты, время, диапазоны, связи, ассоциации, узлы, пары (значение, единица измерения), местоположения на диске, расположение исходного кода, валюты, строки, прямоугольники, масштабируемые числа с фиксированной запятой, обыкновенные дроби, символьные строки, вектора и массивы.