А.В. Столяров - Введение в язык Си++ (1114949), страница 5
Текст из файла (страница 5)
В таких случаях говорят, что хранимая информацияи зб ы т о ч н а ; в этом нет совершенно ничего страшного, если только мы19можем обеспечить ц е л о с т н о с т ь информации, то есть гарантировать,что никогда в нашей программе ие возникнет такой ситуации, что значения рассматриваемых полей перестанут находиться в зафиксированномдля них соотношении. Заметим, что для обычной (открытой) структуры нам было бы тяжело это гарантировать, поскольку в любом местепрограммы мог бы появиться оператор, изменяющий только некоторыеиз взаимосвязанных значений и не изменяющий другие: если программа имеет существенный объём, и в особенности если над ней работают несколько программистов, уследить за правильностью использованиятакой структуры может оказаться проблематично. Когда же все полязакрыты, доступ к ним имеют только методы описываемой структуры,обычно находящиеся в рамках одного модуля.
Объём такого модуля, какправило, невелик в сравнении со всей программой, ну а другие программисты либо вовсе не станут редактировать «чужой» модуль, либо, еслитакая необходимость всё же возникнет, для начала разберутся, как этотмодуль должен функционировать.Очень важ н о понять, что защ и та в язы ке Си-|—|- п редназначена не д л я того, чтобы защ и щ ать нас от врагов, но исклю чительно д л я защ и ты нас от самих себя, от собственных ош ибок. Еслизадаться целью обойти механизм защиты, это можно сделать без особыхпроблем путём преобразования типов указателей или другими способами.
Защита работает только в случае, если программисты не предпринимают целенаправленных действий по её обходу. Но обычно программисты этого не делают, поскольку понимают полезность механизма защитыи выгоды от его использования, а также и то. что попытки его обойти,скорее всего, приведут к внесению в программу ошибок.Как уже говорилось, сокрытие деталей реализации некоторой частипрограммы от всей остальной программы называется инкапсуляцией.Защита, являясь непременным свойством объектно-ориентированногопрограммирования, позволяет проводить инкапсуляцию на более глубоком уровне.
Именно поэтому инкапсуляцию часто называют одним из«трёх китов» ООП.§2.1.5. К лассыПоскольку большинство внутренних полей объектов обычно закрыты.для описания объектов в Си--- используют специально введенный дляэтой цели тип составных переменных, называемый классом . Класс этотип переменной, напоминающий запись (структуру), но отличающийсятем. что к полям (членам) класса доступ по умолчанию есть только изметодов самого этого класса.Перепишем нашу реализацию комплексного числа в виде класса:c la s s Complex {20double r e , im;p u b lic :Complex(double a_re, double a_im){ re = a_re; im = a_im; }double g e t_ re () { return r e ; }double get_im () { return im; }double modulo() { return s q r t( r e *r e + im *im ); }double argument() { return atan2(im , r e ) ; }};Всё, что описано в классе до слова p u b lic :, компилятор рассматриваеткак детали реализации класса и запрещает доступ к ним отовсюду, крометел функций-членов данного класса. Слово p u b lic: меняет режим защиты с закрытого на открытый, т.
е. всё, что описано после этого слова (вданном случае это функции-члены Complex(. . . ) , modulo() и др.) будетдоступно во всём тексте программы.Важно помнить, что м одель защ и ты , вкл ю ч аем ая по ум олчанию — это единственное отличие классов от стру к ту р. Большеони ничем друг от друга не отличаются, по крайней мере, с точки зрения компилятора С и ++. Тем не менее, обычно программисты используют структуры для случаев, когда по смыслу поля должны быть открытыи доступны (например, при организации списков), а классы — для случаев, когда поля представляют собой детали реализации объекта, которыелучше всего скрыть.§2.2.
П ереоп ределени е символовстан д ар тн ы х операцийВ реальной задаче мы вряд ли захотим ограничиться единственнымдействием, осуществляемым с комплексными числами. Скорее всего, нампотребуются также сложение, вычитание, умножение и деление.Язык С и + + предоставляет возможности записи таких действий с помощью привычных символов арифметических операций. Кроме того, логично предусмотреть возможность узнать действительную и мнимую части комплексного числа, если вдруг нам это понадобится. Для этого дополним наш класс ещё несколькими функциями-членами, имена которыхбудут выглядеть несколько экзотически.
Эти функции как раз и будутописывать действия, которые должен по нашему замыслу означать символ той или иной арифметической операции.Отметим, что методы в этом примере будут обращаться к закрытымполям не только «своего» объекта, но и объекта, переданного как параметр. Это не нарушает защиту: как уже говорилось на стр. 17, единицей21защиты является не объект, а класс или структура как таковые.
Итак,пишем:c l a s s Complex {double r e , im;p u b l ic :Complex(double a _ r e , double a_im){ re = a _ re ; im = a_im; }double moduloO { re tu rn s q r t ( r e * r e + im *im ); }double argum entO { re tu rn atan 2 (im , r e ) ; }double g e t_ r e ( ) { re tu rn r e ; }double get_ im () { re tu rn im; }Complex operator*(C om plex op2){Complex r e s ( r e + o p 2 .r e , im + o p 2 .im );re tu rn r e s ;}Complex operator-(C om plex op2){Complex r e s ( r e - o p 2 .r e , im - o p 2 .im );re tu rn r e s ;}Complex operator*(C om plex op2){Complex r e s ( r e * o p 2 .r e - im *op2.im , re *o p 2 .im + im *o p 2 .r e );re tu rn r e s ;}Complex operator/(C om plex op2){double dvs = op2.
re *o p 2 .re + o p 2 . im *op2. im;Complex r e s ( ( r e *o p 2 . re + im *op2. im )/d v s ,(im *o p 2 .re - r e *o p 2 . im ) /d v s ) ;re tu rn r e s ;}Необходимо пояснить, что слово operator является зарезервированным(ключевым) словом языка Си-|—Ь Стоящие подряд две лексемы operatorи + образуют имя функции, которую можно вызвать и как обычнуюфункцию:а = b .o p e r a to r *( с ) ;однако, в отличие от обычной функции, появление функции-операциипозволяет записать то же самое в более привычном для математика виде:а = Ъ + с;22Теперь мы можем, к примеру, узнать, каков будет модуль суммы двухкомплексных чисел:Complex с 1 (2 .7 , 3 .8 );Complex с2 (1 .1 5 , - 7 .1 );double mod = (cl+c2) .moduloQ ;Как мы увидим позже, аналогичным образом в языке С и + + можнопереопределить символы любых операций, включая присваивания, индексирование, вызов функции и прочую «экзотику».
Существуют толькодве операции, которые нельзя переопределить: это тернарная условнаяоперация (а ? Ъ : с) и операция выборки поля из структуры или класса (точка). Необходимо сразу же заметить, что операция выборки поляпо указателю (стрелка) переопределяема, хотя и несколько экзотическимспособом. Ко всем этим вопросам мы вернёмся позже.§2.3. П ер егр у зк а имён ф у н к ц и йПрежде чем перейти к дальнейшему описанию возможностей, предоставляемых понятием конструктора в С и + + , необходимо обратить внимание на свойство языка, называемое перегрузкой имён функций.С и + + позволяет ввести в рамках одной области видимости несколькор азл и чн ы х функций, и м ею щ и х одно и м я и р азл и ч аю щ и х с я к о л и ч е с т в о ми /и л и т и п а м и п а р а м е т р о в .
Например, будет вполне корректен следующий код:void p r in t(in t n) { p rin tf("% d \n ", n ) ; }void p rin t(c o n st char * s ) { p r in tf("% s\n ", s ) ; }void p rin tQ { p r in tf("H e llo w o rld \n "); }При обработке вызова функции компилятор определяет, какую функциюиз имеющих одно имя необходимо вызвать в данном конкретном случае,используя количество и типы фактических параметров. Например:p r in t (5 0 );/ / вызывается p r in t(in t)print("H ave a nice d ay "); / / вызывается p rin t(c o n st char*)p r in t ( ) ;/ / версия без параметровТаким же образом можно перегружать и методы в классах и структурах. В особенности это свойство оказывается полезным при описанииконструкторов.Отметим, что введение перегруженных функций может привести ксовершенно неожиданным последствиям.
Так, следующий код сам по себекорректен:23void f(const char *str){printf("3TOстрока:%s\n", str);}void f ( f l o a t f){p r i n t f ("Это число с плавающей точкой: % f\n", f ) ;}Компилятор вполне справится с вызовами f ( " s tr in g " ) и f (2 .5 ), поскольку никаких разночтений тут не возникает. Более того, даже вызов f (1) будет успешно откомпилирован, поскольку целое можно неявнопреобразовать к типу f lo a t , но не к типу const char *. Однако вызов f (0) будет расценен компилятором как ошибочный, т. к. компиляторс совершенно одинаковым успехом может преобразовать 0 как к типуconst char*, так и к типу f lo a t.
Между тем, если бы в программе присутствовала только одна из двух вышеописанных функций f (любая!),вызов f (0) был бы заведомо корректен.§2.4. К о н стр у к то р у м олч ан и я. М ассивыобъектовВозвращаясь к нашему классу Complex, напомним (см. стр. 16), чтоописать переменную типа Complex привычным нам образом без всякихпараметров нельзя, т. к. для единственного конструктора класса Complexтребуются два параметра. Таким образом, следующий код будет некорректен:Complex sum; / / ошибка - нет параметров для конструктораsum = cl+c2;Кроме того, у нас возникнут сложности при создании массивов комплексных чисел; обычный массив заранее заданного размера мы создать сможем, но только при указании инициализатора, имеющего весьма нетривиальный синтаксис; забегая вперёд, отметим, что динамический массивкомплексных чисел мы создать не сможем вообще никак, поскольку синтаксис языка не позволяет задать параметры для конструкторов каждогоэлемента массива.Снять возникшие проблемы позволяет введение ещё одного конструктора.