Информация о курсе в систематизированном виде излагаются основные понятия и описываются возможности языка C++. При этом основное внимание уделяется объяснению того, как теми или иными возможностями пользоваться



бет8/15
Дата16.07.2016
өлшемі1.19 Mb.
#204086
түріИнформация
1   ...   4   5   6   7   8   9   10   11   ...   15

class B : private A

{

. . .

};

В случае внутреннего наследования внешняя и защищенная части базового класса становятся внутренней частью производного класса. Внутренняя часть базового класса остается для производного класса недоступной.

Если перед именем базового класса поставить ключевое слово protected, то будет использоваться защищенное наследование. При нем внешняя и защищенная части базового класса становятся защищенной частью производного класса. Внутренняя часть базового класса остается недоступной для производного класса.

Фактически, при защищенном и внутреннем наследовании   производный класс исключает из своего интерфейса интерфейс базового класса, но сам может им пользоваться. Разницу между защищенным и внутренним наследованием почувствует только класс, выведенный из производного.

Если в классе A был определен какой-то метод:

class A

{

public:

int foo();

};

то запись



B b;

b.foo();

недопустима, так же, как и



class C

{

int m() {

foo();

}

};

если класс B внутренне наследует A. Если же класс B использовал защищенное наследование, то первая запись b.foo() также была бы неправильной, но зато вторая была бы верна.

10.6 Абстрактные классы

Вернемся к примеру наследования, который мы рассматривали раньше. Мы ввели базовый класс   Item, который представляет общие свойства всех единиц хранения в библиотеке. Но существуют ли объекты класса Item? То есть существует ли в действительности "единица хранения" сама по себе? Конечно, каждая книга (класс Book), журнал (класс Magazin) и т.д. принадлежат и к классу Item, поскольку они выведены из него, однако объект самого базового класса вряд ли имеет смысл. Базовый класс – это некое абстрактное понятие, описывающее общие свойства других, конкретных объектов.

Тот факт, что в данном случае объекты базового класса не могут существовать сами по себе, обусловлен еще одним обстоятельством. Некоторые методы базового класса не могут быть реализованы в нем, а должны быть реализованы в порожденных классах. Возьмем, например, тот же метод Name. Его реализация в базовом классе довольно условна, она не имеет особого смысла. Было бы логичнее вообще не реализовывать этот метод в базовом классе, а возложить ответственность за его реализацию на производные классы.

С другой стороны, нам важен факт наличия метода Name во всех производных классах и то, что этот метод виртуален. Именно поэтому мы можем работать с указателями (или ссылками) на объекты базового класса, не зная точно, на какой именно из производных классов этот указатель указывает. Виртуальный механизм во время выполнения программы сам разберется и вызовет нужную реализацию метода Name.

Такая ситуация складывается довольно часто в объектно-ориентированном программировании. (Вспомните пример с различными формами в графическом редакторе: рисование некой обобщенной формы невозможно.) В подобных случаях используется механизм абстрактных классов. Запишем базовый класс   Item немного по-другому:

class Item

{

public:

. . .

virtual String Name() const = 0;

};

Теперь мы определили метод Name как чисто виртуальный. Класс, у которого есть хотя бы один чисто виртуальный метод, называется абстрактным.

Если метод объявлен чисто виртуальным, значит, он должен быть определен во всех классах, производных от Item. Наличие чисто виртуального метода запрещает создание объекта типа Item. В программе можно использовать указатели или ссылки на тип Item. Записи

Item it;

Item* itptr = new Item;

не разрешены, и компилятор сообщит об ошибке. Однако можно записать:



Book b;

Item* itptr = &b;

Item& itref = b;

Отметим, что, определив чисто виртуальный метод в классе Book, в следующем уровне наследования его уже не обязательно переопределять (в классах, производных из Book).

Если по каким-либо причинам в производном классе   чисто виртуальный метод не определен, то этот класс тоже будет абстрактным, и любые попытки создать объект данного класса будут вызывать ошибку. Таким образом, забыть определить чисто виртуальный метод просто невозможно. Абстрактный   базовый класс навязывает определенный интерфейс всем производным из него классам. Собственно, в этом и состоит главное назначение абстрактных классов – в определении интерфейса для всей иерархии классов. Разумеется, это не означает, что в абстрактном классе не может быть определенных методов или атрибутов.

Вообще говоря, класс можно сделать абстрактным, даже если все его методы определены. Иногда это необходимо сделать для того, чтобы быть уверенным в том, что объект данного класса никогда не будет создан. Можно задать один из методов как чисто виртуальный, но, тем не менее, определить его реализацию. Обычно для этих целей выбирается деструктор:



class A

{

public:

virtual ~A() = 0;

};

A::~A()

{

. . .

}

Класс A – абстрактный, и объект типа A создать невозможно. Однако деструктор его определен и будет вызван при уничтожении объектов производных классов (о порядке выполнения конструкторов и деструкторов см. ниже).

10.7 Множественное наследование

В языке Си++ имеется возможность в качестве базовых задать несколько классов. В таком случае производный класс наследует методы и атрибуты всех его родителей. Пример иерархии классов в случае множественного наследования приведен на следующем рисунке.


Рис. 10.2. Иерархия классов при множественном наследовании.

В данном случае класс C наследует двум классам, A и B.

Множественное наследование – мощное средство языка. Приведем некоторые примеры использования множественного наследования.

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

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

class Annotation

{

public:

String GetText(void);

private:

String annotation;

};

class Shape

{

public:

virtual void Draw(void);

};

class AnnotatedSquare : public Shape,

public Annotation

{

public:

virtual void Draw();

};

У объекта класса AnnotatedSquare имеется метод GetText, унаследованный от класса Annotation, он определяет виртуальный метод   Draw, унаследованный от класса Shape.

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

class A

{

public:

void fun();

int a;

};

class B

{

public:

int fun();

int a;

};

class C : public A, public B

{

};

При записи



C* cp = new C;

cp->fun();

невозможно определить, к какому из двух методов fun происходит обращение. Ситуация называется неоднозначной, и компилятор выдаст ошибку. Заметим, что ошибка выдается не при определении класса C, в котором заложена возможность возникновения неоднозначной ситуации, а лишь при попытке вызова метода fun.

Неоднозначность можно разрешить, явно указав, к которому из базовых классов происходит обращение:

cp->A::fun();

Вторая проблема заключается в возможности многократного включения базового класса. В упомянутом выше примере интеграции библиотечной системы и системы кадров вполне вероятна ситуация, при которой классы для работников библиотеки и для студентов были выведены из одного и того же базового класса   Person:



class Person

{

public:

String name();

};

class Student : public Person

{

. . .

};

class Librarian : public Person

{

. . .

};

Если теперь создать класс для представления студентов, подрабатывающих в библиотеке



class StudentLibrarian : public Student,

public Librarian

{

};

то объект данного класса будет содержать объект базового класса   Person дважды (см. рисунок 10.3).


Рис. 10.3. Структура объекта StudentLibrarian.

Кроме того, что подобная ситуация отражает нерациональное использование памяти, никаких неудобств в данном случае она не вызывает. Возможную неоднозначность можно разрешить, явного указав класс:



StudentLibrarian* sp;

// ошибка – неоднозначное обращение,

// непонятно, к какому именно экземпляру

// типа Person обращаться

sp->Person::name();

// правильное обращение

sp->Student::Person::name();

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

10.8 Виртуальное наследование

Базовый класс можно объявить виртуальным базовым классом, используя запись:



class Student : virtual Person

{

};

class Librarian : virtual Person

{

};

Гарантировано, что объект виртуального базового класса будет содержаться в объекте выведенного класса (см. рисунок 10.4) один раз. Платой за виртуальность базового класса являются дополнительные накладные расходы при обращениях к его атрибутам и методам наследование.


Рис. 10.4. Структура объекта StudentLibrarian при виртуальном множественном наследовании.

11 Контроль доступа к объекту





11.1 Интерфейс и состояние объекта

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

Определение класса можно поделить на три части – внешнюю, внутреннюю и защищенную. Внешняя часть предваряется ключевым словом public , после которого ставится двоеточие. Внешняя часть – это определение интерфейса. Методы и атрибуты, определенные во внешней части класса, доступны как объектам данного класса, так и любым функциям и объектам других классов. Определением внешней части мы контролируем способ обращения к объекту. Предположим, мы хотим определить класс для работы со строками текста. Прежде всего, нам надо соединять строки, заменять заглавные буквы на строчные и знать длину строк. Соответственно, эти операции мы поместим во внешнюю часть класса:



class String

{

public:

// добавить строку в конец текущей строки

void Concat(const String& str);

// заменить заглавные буквы на строчные

void ToLower(void);

int GetLength(void) const;

// сообщить длину строки

. . .

};

Внутренняя и защищенная части класса доступны только при реализации методов этого класса. Внутренняя часть предваряется ключевым словом private, защищенная – ключевым словом protected.



class String

{

public:

// добавить строку в конец текущей строки

void Concat(const String& str);

// заменить заглавные буквы на строчные

void ToLower(void);

int GetLength(void) const;

// сообщить длину строки

private:

char* str;

int length;

};

В большинстве случаев атрибуты во внешнюю часть класса не помещаются, поскольку они представляют состояние объекта, и возможности их использования и изменения должны быть ограничены. Представьте себе, что произойдет, если в классе String будет изменен указатель на строку без изменения длины строки, которая хранится в атрибуте length.

Объявляя атрибуты str и length как private, мы говорим, что непосредственно к ним обращаться можно только при реализации методов класса, как бы изнутри класса (private по-английски – частный, личный). Например:

int

String::GetLength(void) const

{

return length;

}

Внутри определения методов класса можно обращаться не только к внутренним атрибутам текущего объекта, но и к внутренним атрибутам любых других известных данному методу объектов того же класса. Реализация метода Concat будет выглядеть следующим образом:



void

String::Concat(const String& x)

{

length += x.length;

char* tmp = new char[length + 1];

::strcpy(tmp, str);

::strcat(tmp, x.str);

delete [] str;

str = tmp;

}

Однако если в программе будет предпринята попытка обратиться к внутреннему атрибуту или методу класса вне определения метода, компилятор выдаст ошибку, например:



main()

{

String s;

if (s.length > 0) // ошибка

. . .

}

Разница между защищенными (protected) и внутренними атрибутами была описана в предыдущей лекции, где рассматривалось создание иерархий классов.

При записи классов мы помещаем первой внешнюю часть, затем защищенную часть и последней – внутреннюю часть. Дело в том, что внешняя часть определяет интерфейс, использование объектов данного класса. Соответственно, при чтении программы эта часть нужна прежде всего. Защищенная часть необходима при разработке зависимых от данного класса новых классов. И внутреннюю часть требуется изучать реже всего – при разработке самого класса.

11.2 Объявление friend

Предположим, мы хотим в дополнение к интерфейсу класса String создать функцию, которая формирует новую строку, являющуюся результатом слияния двух строк, но не изменяет сами аргументы. (Особенно часто подобный интерфейс необходимо создавать при определении операций – см. ниже). Для того чтобы эта функция работала быстро, желательно, чтобы она имела доступ к внутренним атрибутам класса String. Доступ можно разрешить, объявив функцию "другом" класса String с помощью ключевого слова friend:

class String

{

. . .

friend String concat(const String& s1,

const String& s2);

};

Тогда функция concat может быть реализована следующим образом:



String

concat(const String& s1, const String& s2)

{

String result;

result.length = s1.length + s2.length;

result.str = new char[result.length + 1];

if (result.str == 0) {

// обработка ошибки

}

strcpy(result.str, s1.str);

strcat(result.str, s2.str);

return result;

}

С помощью механизма friend можно разрешить обращение к внутренним элементам класса как отдельной функции, отдельному методу другого класса или всем методам другого класса:



class String

{

// все методы класса StringParser обладают

// правом доступа ко всем атрибутам класса

// String

friend class StringParser;

// из класса Lexer только метод CharCounter

// может обращаться к внутренним атрибутам

// String

friend int Lexer::CharCounter(const

String& s, char c);

};

Конечно, злоупотреблять механизмом friend не следует. Каждое решение по использованию friend должно быть продумано. Если только одному методу какого-либо класса действительно необходим доступ, не следует объявлять весь класс как friend.

11.3 Использование описателя const

Во многих примерах мы уже использовали ключевое слово const для обозначения того, что та или иная величина не изменяется. В данном параграфе приводятся подробные правила употребления описателя const.

Если в начале описания переменной стоит описатель const, то описываемый объект во время выполнения программы не изменяется:

const double pi = 3.1415;

const Complex one(1,1);

Если const стоит перед определением указателя или ссылки, то это означает, что не изменяется объект, на который данный указатель или ссылка указывает:



// указатель на неизменяемую строку

const char* ptr = &string;

char x = *ptr;

ptr++;

*ptr = '0';

// обращение по указателю — допустимо

// изменение указателя — допустимо

// попытка изменения объекта, на

// который указатель указывает –

// ошибка

Если нужно объявить указатель, значение которого не изменяется, то такое объявление выглядит следующим образом:



char* const ptr = &string;

// неизменяемый указатель

char x = *ptr;

ptr++;

*ptr = '0';

// обращение по указателю – допустимо

// изменение указателя – ошибка

// изменение объекта, на который

// указатель указывает – допустимо

11.4 Доступ к объекту по чтению и записи

Кроме контроля доступа к атрибутам класса с помощью разделения класса на внутреннюю, защищенную и внешнюю части, нужно следить за тем, с помощью каких методов можно изменить текущее значение объекта, а с помощью каких – нельзя.

При описании метода класса как const выполнение метода не может изменять значение объекта, который этот метод выполняет.



class A

{

public:

int GetValue (void) const;

int AddValue (int x) const;

private:

int value;

}

int

A::GetValue(void) const

{

return value; }

// объект не изменяется

int

A::AddValue(int x) const

{

value += x;

// попытка изменить атрибут объекта

// приводит к ошибке компиляции

return value;

}

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

В описании класса String один из методов – GetLength – представлен как неизменяемый (в конце описания метода стоит слово const). Это означает, что вызов данного метода не изменяет текущее значение объекта. Остальные методы изменяют его значение. Контроль использования тех или иных методов ведется на стадии компиляции. Например, если аргументом какой-либо функции объявлена ссылка на неизменяемый объект, то, соответственно, эта функция может вызывать только методы, объявленные как const:

int

Lexer::CharCounter(const String& s, char c)

{ int n = s.GetLength(); // допустимо

s.Concat("ab");

// ошибка – Concat изменяет значение s

}

Общим правилом является объявление всех методов как неизменяемых, за исключением тех, которые действительно изменяют значение объекта. Иными словами, объявляйте как можно больше методов как const. Такое правило соответствует правилу объявления аргументов как const. Объявление константных аргументов запрещает изменение объектов во время выполнения функции и тем самым предотвращает случайные ошибки.




12 Классы – конструкторы и деструкторы

При определении класса имеется возможность задать для объекта начальное значение. Специальный метод класса, называемый конструктором, выполняется каждый раз, когда создается новый объект этого класса. Конструктор – это метод, имя которого совпадает с именем класса. Конструктор не возвращает никакого значения.

Для класса String имеет смысл в качестве начального значения использовать пустую строку:



Достарыңызбен бөлісу:
1   ...   4   5   6   7   8   9   10   11   ...   15




©dereksiz.org 2024
әкімшілігінің қараңыз

    Басты бет