А.В. Столяров - Введение в язык Си++ (1114949), страница 4
Текст из файла (страница 4)
Действительно, поля ге и im доступны из любого места в программе, где доступна сама структура str_complex. Длятого, чтобы скрыть детали реализации объекта, в языке С и + + введенмеханизм з а щ и т ы , который позволяет запретить доступ к некоторымчастям структуры (как полям, так и функциям-методам) из любых местпрограммы, кроме тел функций-методов.Для поддержания этого механизма в язык введены ключевые словаpublic и p riv ate , которыми в описании структуры могут быть помечены поля и методы, доступные извне структуры (p u b lic :) и, наоборот,доступные только из тел функций-методов (p r iv a te :)1.
Попытаемся переписать нашу структуру с использованием этих ключевых слов.stru c t str_complex {p r iv a te :double r e , im;p u b lic :double modulo() { return s q r t ( r e * r e + im *im ); }>;1Несколько позже мы введем ещё и ключевое слово protected.14Теперь поля re и im доступны только из тела метода modulo().Легко заметить, однако, что пользоваться такой структурой мы несможем, т. к. в ней не предусмотрено никаких средств для задания значений полей ге и im. Так, фрагмент кода, приведенный на стр. 13, попросту будет отвергнут компилятором, ведь поля теперь недоступны, в томчисле и для присваивания. Решить проблему можно, введя соответствующий метод для задания значений полей.
Описание нашей структурымогло бы выглядеть в этом случае, например, так:stru c t str_complex {p r iv a te :double r e , im;p u b lic :void set(d ouble a_re, double a_im){ re = a_re; im = a_im; }double modulo(){ return s q r t ( r e * r e + im *im ); }};а код, вычисляющий модуль заданного числа, мы теперь перепишем так:str_complex z ;double mod;z. s e t (2 .7 , 3 .8 );mod = z.m oduloQ;Данное решение имеет очень серьёзный недостаток2. Дело в том, что смомента объявления переменной z до вызова функции s e t ( ) наш объект(переменная z) оказывается в неопределённом состоянии, т. е. попыткиего использовать будут заведомо ошибочны.
Очевидно, было бы лучшепроизвести инициализацию объекта непосредственно в момент его создания. Более того, было бы логично зап рети ть создание объекта в обходинициализации.Язык С и + + позволяет задавать компилятору чёткие инструкции относительно действий, необходимых при инициализации объекта. Для этого вводится ещё одно важное понятие — к о н с т р у к т о р о б ъ е к т а .
Конструктор — это функция-метод специального вида, которая может, как иобычная функция, иметь параметры или не иметь их; тело этой функциикак раз и представляет собой порядок действий, которые необходимо выполнить всякий раз, когда создаётся объект описываемого типа. Написавконструктор, мы тем самым «объясняем» компилятору, как (в соответствии с какой инструкцией) создавать новый объект данного типа, какие2Более того, большинство компиляторов выдаст предупреждение при попытке компиляции такого объявления структуры.15параметры должны быть для этого заданы и как воспользоваться значениями этих параметров.Компилятор отличает конструкторы от обычных методов по имени,которое совпадает с именем описываемого типа (в данном случае структуры).
Поскольку конструктор играет специальную роль и в явном видене вызывается, тип возвращаемого значения для конструктора указывать нельзя (он не возвращает никаких значений; в определённом смысле результатом работы конструктора является сам объект, для которогоего вызвали).Проиллюстрируем сказанное, переписав структуру str_complex:stru c t str_complex {p r iv a te :double r e , im;p u b lic :str_complex(double a_re, double a_im){ re = a_re; im = a_im; }double modulo(){ return s q r t( r e *r e + im *im ); }};Введя конструктор (функцию-метод с именем str_complex), мы сообщили компилятору, что для создания объекта типа str_ complex необходимознать два числа типа double, и задали набор действий, подлежащих выполнению при создании такого объекта.
В этом наборе действий говорится в том числе и о том, как воспользоваться заданными числами типаdouble: первое из них использовать в качестве действительной части,второе — в качестве мнимой части создаваемого комплексного числа.Вообще, в семантике Си++ любая переменная создается с помощью конструктора. Во многих случаях компилятор считает конструктор существующим («неявно»), несмотря на то, что в программе конструктор не описан.Код вычисления модуля теперь можно переписать вот так:str_complex z (2 .7 , 3 .8 );double mod;mod = z .modulo();Более того, для такой операции нам не обязательно описывать переменную, имеющую имя.
Мы могли бы написать и так:double mod = str_com plex(2.7, 3 . 8 ) .modulo( ) ;В последнем случае мы создали временную анонимную переменнуютипа str_complex и для этой переменной вызвали метод modulo().Здесь необходимо сделать очень важное замечание. После введенияконструктора, имеющего параметры, компилятор отк аж ется создавать16объект типа str_complex без указания требуемых конструктором двухзначений, то есть предыдущая версия кода, содержавшая описаниеstr_complex z ;компилироваться больше не будет. Если это создаёт неудобства, то принеобходимости можно заставить компилятор снова считать такие объявления корректными; о том, как это делается, мы узнаем из § 2.4.Следует отметить, что синтаксис создания переменной по заданномупараметру допустим в Си-|—Ь и для переменных встроенных типов. Так,например, операторin t v (7 );означает абсолютно то же самое, что и привычное по языку Си описаниеin t ч = 7 ;Отметим ещё один немаловажный момент.
Единицей защ и ты вСиН—|- является не объект, а тип (в данном случае стру к ту ра)целиком. Это означает, что из тел методов мы можем обращаться к закрытым полям не только «своего» объекта (того, для которого вызванметод), но и вообще любого объекта того же типа.§ 2.1.4. Зачем нуж на защ итаСмысл механизма защиты часто оказывается непонятен программистам, начинающим осваивать объектно-ориентированное программирование. Попробуем пояснить его, основываясь на нашем примере.Представим себе, что наша структура str_complex используется вбольшой программе, активно работающей с комплексными числами.
Может случиться так, что в программе будет очень часто требоваться вычисление модулей комплексных чисел. Более того, может оказаться итак, что именно модуль комплексного числа требуется нам даже чаще,чем его действительная и мнимая части. В такой ситуации, скорее всего,наша программа будет проводить значительную часть времени своего выполнения в вычислениях модулей. Обнаружив это, мы можем в какой-томомент понять, что в данной конкретной задаче удобнее хранить комплексные числа в полярных координатах, а не в декартовых, то есть ввиде модуля и аргумента, а не в виде действительной и мнимой частей.Если мы не применяли защиту, то, скорее всего, все модули нашейпрограммы, в которых используются комплексные числа, содержат обращения к полям ге и im. Если в такой программе изменить способ хранения комплексного числа, убрав поля ге и im и введя вместо них, скажем,17поля mod и arg для хранения модуля и аргумента (т.
е. полярных координат), то все части программы, использовавшие нашу структуру, перестанут компилироваться и нам придётся их исправлять. Если программадостаточно большая (а современные программные проекты состоят измногих сотен и даже тысяч модулей), такое редактирование может потребовать существенных трудозатрат, приведёт к внесению в программуновых ошибок ит.п., так что, вполне возможно, нам придётся отказатьсяот изменений, несмотря на всю их полезность.Допустим теперь, что мы использовали защиту и сделали все полянедоступными откуда бы то ни было, кроме методов нашей структуры.Конечно, при использовании комплексных чисел часто бывает нужноузнать отдельно действительную и мнимую части числа, но для этогоможно ввести специальные методы, например3:stru c t str_complex {p r iv a te :double r e , im;p u b lic :str_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 ) ; }};Поскольку к полям re и im теперь не могут обращаться никакие части программы, кроме наших методов, мы можем быть уверены, чтопереписать нам придётся только наши методы, а весь остальной текстпрограммы, из скольки бы модулей он ни состоял, сохранится без изменений и будет работать, как и раньше.
Конечно, методы g e t_ re () иget_im () теперь станут гораздо сложнее, чем были, зато упростятся методы modulo() и argument():stru c t str_complex {p r iv a te :double mod, arg;p u b lic :str_complex(double r e , double im) {mod = s q r t( r e *r e + im *im );arg = atan2(im , r e ) ;}З3десь мы введём заодно функцию вычисления аргумента, которая будет использовать стандартную функцию atan2() для определения соответствующего арктангенса.18doubledoubledoubledoubleg e t_ re () {get_im () {modulo() {argument()return mod * c o s (a r g ); }return mod * s in ( a r g ) ; }return mod; }{ return a rg ; }В такой ситуации мы даже можем себе позволить иметь две реализацииструктуры str_complex, между которыми выбор осуществляется директивами условной компиляции.
Таким образом, не меняя текста программы, мы сможем откомпилировать её с использованием одной реализации, измерить быстродействие, откомпилировать программу с использованием другой реализации, снова измерить быстродействие, сравнитьрезультаты измерений и принять решение, какую реализацию использовать. В случае, если используемые в программе алгоритмы изменятся инам снова станет выгодно хранить комплексные числа в декартовых координатах, то для возврата к старой модели вообще не придётся ничегоредактировать.Более того, мы можем в некий момент решить, что и те, и другиевычисления производить довольно накладно, а памяти нам не жалко, иначать хранить в объекте как декартово, так и полярное представлениечисла:stru c t str_complex {p r iv a te :double r e , im, mod, arg;p u b lic :str_complex(double a_re, double a_im) {re = a_re; im = a_im;mod = s q r t ( r e * r e + im *im );arg = atan2(im , r e ) ;}double g e t_ re () { return r e ; }double get_im () { return im; }double modulo() { return mod; }double argument() { return a rg ; }};Теперь в нашем объекте четыре поля, причём любые два из них можно вычислить, пользуясь значениями двух других, то есть, иначе говоря,значения наших полей не могут быть произвольными, а должны всегданаходиться в некотором соотношении (в данном случае — пары (re,im) и(mod,arg) должны задавать одно и то же комплексное число, хотя и разными способами).