Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Объектно-ориентированное программирование.PDF
Скачиваний:
208
Добавлен:
01.05.2014
Размер:
3.64 Mб
Скачать

converted to PDF by BoJIoc

}

Ошибка возникает, поскольку функция f пытается модифицировать переменную i, наследуемую из класса A, но недоступную (так как она описана как private:). Если бы спецификаторы доступа управляли видимостью, а не доступом, то переменная i класса A была бы не видна и обновлению подверглась бы глобальная переменная i.

Родственные экземпляры

Модификаторы доступа относятся к классу, а не к его экземплярам. То есть поля данных, описанные как private, в языке C++ не соответствуют в точности концепции, разработанной нами ранее при общем обсуждении понятия видимости. Согласно этой концепции закрытые данные доступны только самому объекту, в то врем как в C++ они открыты любому объекту того же класса. Тем самым в языке C++ объекту разрешается манипулировать закрытыми полями другого экземпляра того же класса.

В качестве примера рассмотрим описание класса, приведенное ниже. Поля данных rp и ip, которые означают вещественную и мнимую части комплексного числа, помечены как private:

class Complex

{

private: double rp; double ip;

public:

Complex (double a, double b) { rp = a; ip = b;

}

Complex operator + (Complex & x)

{

return Complex(rp+x.rp, ip+x.ip);

}

};

Бинарная операция + перегружается с целью правильного сложения двух комплексных чисел. Несмотря на закрытую природу полей rp и ip, оператору-функции разрешен доступ к ним в аргументе x, поскольку аргумент и получатель относятся к одному классу.

Конструкторы и деструкторы, подобные функции Complex в приведенном примере, обычно описываются как public. Объявление конструктора как protected подразумевает, что только подклассы или дружественные классы (см. далее) могут создавать экземпляры этого класса, в то время как описание конструктора с ключевым словом private ограничивает создание новых экземпляров только «друзьями» и экземплярами самого класса.

Слабая форма законов Деметера частично выполняется при описании всех полей как защищенных (protected). Сильная форма реализуется при объявлении закрытых полей (private). Более подробный анализ приложения законов Деметера к языку C++ можно найти в работе [Sakkinen 1988b].

Хотя модификаторы доступа в C++ намного сильнее и гибче, чем в других рассматриваемых нами языках, эффективное использование этих свойств требует предусмотрительности и опыта. Как и в случае выбора между виртуальным и невиртуальным методами, уровень контроля, обеспечиваемый языками C++ или Delphi Pascal, приводит к тому, что легкость порождения подкласса зависит от того, что записал разработчик в исходном классе. Если класс является чрезмерно закрытым (защищенные поля данных объявлены закрытыми), то создание подкласса затруднено. Возникают серьезные проблемы, когда разработчик подкласса не может модифицировать исходную форму класса например, если исходный класс распространяется как часть библиотеки.

converted to PDF by BoJIoc

Закрытое наследование

Ключевые слова public и private также предшествуют именам надклассов при описании класса. В данном случае указанные ключевые слова определяют видимость информации, наследуемой из надкласса. Подкласс, который наследует от другого класса открыто (public), соответствует понятию наследования, которым мы руководствовались до сих пор: подкласс является подтипом. Если подкласс порождается закрыто (private), то общедоступные свойства надкласса урезаются до уровня модификатора. В результате данный модификатор указывает, что надкласс используется только для конструирования,

и результирующий класс не должен и не может рассматриваться как подтип исходного класса.

Когда класс порождается закрытым образом, экземпляры подкласса не должны присваиваться идентификаторам надкласса (такое возможно при открытом наследовании). Простой способ запомнить указанное ограничение воспользоваться условием «быть экземпляром». Наследование через модификатор public означает, что выполнено условие «быть экземпляром», и тем самым экземпляры подкласса могут использоваться везде, где встречаются экземпляры надкласса. Собака Dog «является экземпляром» класса млекопитающих Mammal, и, следовательно, Dog может использоваться во всех ситуациях, где встречается Mammal. Закрытое наследование не подразумевает выполнения условия «быть экземпляром», поскольку экземпляры

порожденного класса не могут во всех случаях использоваться вместо экземпляров родителя. Например, бессмысленно применять класс таблиц символов SymbolTable (наследующий от более общего класса словарей Dictionary через модификатор private) там, где требуется использование класса Dictionary. Если переменная описана с типом данных Dictionary, ей нельзя присваивать значение типа SymbolTable (это было бы разрешено, если бы наследование было открытым).

Дружественные функции

Другой аспект видимости в языке C++ — это дружественные функции. Они представляют собой обычные функции (не методы), которые описаны с модификатором friend в определении класса. Дружественным функциям разрешается читать и записывать в поля данных объекта, описанные и как private, и как protected.

Рассмотрим описание класса, расширяющее приведенное выше определение комплексных чисел:

class Complex

{

private: double rp; double ip;

public:

Complex(double, double); friend double abs(Complex&);

};

Complex::Complex(double a, double b)

{

rp = a; ip = b;

}

double abs(Complex& x)

{

return sqrt(x.rp*x.rp + x.ip*x.ip);

}

Поля данных rp и ip в структуре данных, представляющей комплексные числа, описаны с модификатором private, и тем самым недоступны вне методов класса. Функция abs, которая перегружает функцию с тем же именем, определенную для вещественных

converted to PDF by BoJIoc

значений с двойной точностью, не является методом (это обычная функция). Однако поскольку она описана как дружественная с модификатором friend в классе комплексных чисел, ей разрешен доступ ко всем полям данных класса, в том числе и к закрытым.

Также разрешается описывать классы и даже отдельные методы классов как дружественные. Наиболее типичная причина использования дружественных функций состоит в том, что

функции требуется доступ ко внутренней структуре двух классов;

необходимо вызвать дружественную функцию именно как функцию, а не как сообщение, передаваемое объекту, то есть как abs(x), а не как x.abs().

Дружественные функции являются мощным средством, но они также легко могут стать источником проблем. В частности, они вводят в точности ту разновидность зацепления данных, которая идентифицировалась в начале этой главы как вредная для разработки многократно используемого программного обеспечения. Везде, где только возможно, более объектно-ориентированные методы инкапсуляции (например, методы) должны иметь предпочтение перед дружественными функциями. Тем не менее есть случаи, когда нет других средств например, функции требуется доступ ко внутренней структуре двух (или более) классов. В таких случаях дружественные функции являются полезной абстракцией [Koenig 1989c].

Пространства имен

Другое недавнее изменение в языке C++ — введение пространства имен (namespace). namespace помогает предотвратить размножение глобальных имен. Ключевое слово static ограничивает область видимости одним файлом. Поэтому когда прежде требовалось сделать некоторое имя совместно используемым в двух файлах, то единственный выход состоял в том, чтобы сделать его глобальным. Подобные имена могут теперь вкладываться внутрь описаний namespace:

namespace myLibrary

{

int x; class A

{

...

};

class B : public A

{

...

};

...

}

Переменные, описанные внутри namespace, не являются глобальными. Если программист хочет подключить какое-нибудь конкретное пространство имен, он его явно указывает. В результате, все имена верхнего уровня, определенные внутри указанного пространства имен, становятся видимыми:

using namespace myLibrary;

Индивидуальные элементы также могут экспортироваться из конкретного пространства имен с помощью явного указания либо пространства целиком, либо отдельного имени:

myLibrary::A anA; // явно

подключаем все пространство имен

using myLibrary::B; // импортируем только класс B

B aNewB;

// теперь B

— имя типа данных

converted to PDF by BoJIoc

Постоянные члены

В языке C++ ключевое слово const используется для указания на значение, которое остается неизменным во время существования объекта. Глобальные переменные, которые описаны таким образом, являются глобальными константами. Переменные, объявленные как const локально в процедуре, доступны только внутри нее и не могут модифицироваться после их инициализации.

Поля данных экземпляра часто ведут себя как константы, но их начальное значение не может быть определено до того, как создан соответствующий объект. Например, поля данных, представляющие собой вещественную и мнимую части комплексного числа в классе Complex (см. выше), никогда не должны изменяться раз, уж комплексное число создано. В главе 3 мы называли такие поля данных неизменяемыми. Они создаются с помощью ключевого слова const.

Так как присваивание постоянным полям данных экземпляра не разрешается, в C++ они инициализируются с помощью той же синтаксической конструкции, которая используется для вызова конструктора родителя (см. главу 7, где обсуждается вызов конструкторов родительских классов). Рассмотрим следующее описание класса:

class Complex

{

public:

const double rp; const double ip;

Complex(double, double);

};

Complex::Complex(double a, double b) : rp(a), ip(b)

{

/* пустая команда */

}

В этом случае поля данных rp и ip описаны через модификатор const, так что не представляет опасности сделать их полями public, поскольку они все равно не могут быть модифицированы. Чтобы присвоить им начальное значение, конструктор, по-видимому, вызывает rp и ip, как если бы они были надклассами. Это единственный способ присваивания значений постоянным полям. Когда начинает выполняться тело конструктора, значение постоянных полей данных уже не может быть изменено.

Поля данных, описанные как ссылки, инициализируются аналогичным образом. Ключевое слово const может присоединяться также к описанию функции-члена. Однако обсуждение этой темы выходит за рамки данной книги.

Взаимодействие между перегрузкой и переопределением

Другой приводящий в смущение аспект правил видимости в C++ — это связь понятий перегрузки и переопределения. Имена функций, включая методы классов, могут перегружаться двумя или более определениями до тех пор, пока списки их аргументов достаточно различны с точки зрения компилятора. Это показывает следующее описание класса, которое перегружает функцию test за счет использования целочисленного аргумента в одном случае и вещественного в другом:

class A

{

public:

void test(int a)

{

cout << "This is the integer version\n";

}

void test(double b)

converted to PDF by BoJIoc

{

cout << "This is the floating point version\n";

}

};

Стараясь подогнать сообщение под подходящий метод, C++ сперва просматривает область имен, в которой определен селектор сообщения, а затем ищет наиболее подходящую функцию, определенную в пределах этой области имен. Даже если есть более подходящая функция, наследуемая из другого пространства имен, она не будет рассматриваться. Это иллюстрируется следующим описанием классов:

class A

{

public:

void test(double b)

{

cout << "This is the floating point version\n";

}

};

class B : public A

{

public:

void test(int a)

{

cout << "This is the integer version\n";

}

};

Попытка послать сообщение test с вещественным аргументом экземпляру класса B приведет к предупреждению компилятора, поскольку в области имен, в которой компилятор нашел метод с именем test (а именно, внутри класса B), нет определения функции с вещественным аргументом. Это происходит, несмотря на то, что нужная функция может быть унаследована от класса A. Результат будет тем же самым независимо от того, описана функция test как virtual или нет. Чтобы справиться со всем этим, программисту нужно определить обе версии в классе B, одна из которых будет просто вызывать соответствующую функцию родителя:

class B : public A

{

public:

void test(double b)

{

A::test(b);

}

void test(int a)

{

cout << "This is the integer version\n";

}

};

17.3.4. Видимость в Java

Как мы видели на примерах программ в языке Java, модификаторы private и public размещаются отдельно для каждого поля данных и функции-члена.

Java вводит новый интересный модификатор с именем final. «Финальный» класс не может порождать подклассы. «Окончательный» метод не может переопределяться другим методом. Переменной экземпляра, описанной как final, нельзя присваивать значения. Использование ключевого слова final позволяет компилятору оптимизировать код.

converted to PDF by BoJIoc

Как и в C++, модификатор private в языке Java относится к классам, а не к экземплярам. Для экземпляров одного класса разрешен доступ к закрытым полям данных друг друга.

Другое средство управления областью видимости, предоставляемое языком Java, — это пакеты. Пакет содержит классы и интерфейсы. Пакеты служат для безконфликтного управления большими областями имен. Пакет описывается с помощью ключевого слова package, которое должно быть первым оператором в файле:

package packageName;

Код одного пакета может ссылаться на классы и интерфейсы из другого пакета, либо явно указывая пакет, в котором находится объект, либо импортируя весь пакет. Следующие команды иллюстрируют первый механизм:

// получить тип данных foo из пакета bar bar.foo newObj = new bar.foo();

Импортирование пакета делает имена всех его открытых классов и интерфейсов доступными так же, как если бы они были определены в текущем файле:

//импортировать все объекты и интерфейсы

//из пакета с именем bar

import bar.*;

При желании можно задать имена отдельных объектов и интерфейсов вместо универсального символа *.

//импортировать идентификатор foo

//из пакета bar

import bar.foo;

17.3.5. Видимость в Objective-C

В языке Objective-C объявление зкземплярных переменных должно помещаться в интерфейсное описание класса. Нельзя объявлять новые поля в разделе реализации (даже несмотря на то, что эти поля не станут частью интерфейса), поскольку они доступны только изнутри методов (в терминах языка C++ они являются защищенными).

Видимость экземплярных переменных модифицируется с помощью ключевого слова @public, которое делает все поля, следующие за ключевым словом, доступными для пользователя. Например, следующий пример показывает описание интерфейса класса Ball, который представляет собой графический объект-шар. Положение шара задается координатами, хранящимися в полях данных x и y. Оно общедоступно, в то время как направление движения и энергия шара являются защищенными.

@interface Ball : Object

{

double direction; double energy;

@public double x; double y;

}

В отличие от экземплярных переменных в разделе implementation разрешается описывать методы, не упомянутые в интерфейсной части. Такие методы оказываются видимыми и могут вызываться только в той части программного кода, которая следует за определением нового метода.

Нельзя создать метод, который бы вызывался клиентами-подклассами, но при этом был бы не доступен клиентам-пользователям. Также невозможно ввести истинно закрытые значения экземплярных переменных.