Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Ответы по основам программирования2.docx
Скачиваний:
3
Добавлен:
29.04.2019
Размер:
743.77 Кб
Скачать

В прямом порядке:

  1. Попасть в корень

  2. Пройти в прямом порядке левое поддерево

  3. Пройти в прямом порядке правое поддерево

В симметричном порядке:

  1. Пройти в симметричном порядке левое поддерево

  2. Попасть в корень

  3. Пройти в симметричном порядке правое поддерево

В обратном порядке:

  1. Пройти в обратном порядке левое поддерево

  2. Пройти в обратном порядке правое поддерево

  3. Попасть в корень

Давайте для закрепления полученных знаний напишем программу.

Создается бинарное упорядоченное дерево, заполняющееся псевдослучайными числами. Затем осуществляется его вывод при обходе дерева симметричным порядком.

Const n = 10; {Поле z (ниже) моделирует любые данные в записи,} Type pt=^uzl; uzl=Record lson, rson: pt; kl:byte; End; {lson (rson) - ссылки на левого(правого) сына узла} Var j:word; y:uzl; coren:pt; Procedure Vkl(Var cor:pt; y:uzl); {cor - ссылка на корень} Var nov,ss,tt: pt; b: Boolean; Begin New(nov); nov^:= y; {Порождение и заполнение переменной nov^;} If cor = Nil Then cor:= nov {если дерево пусто, делаем ее корнем} Else {Цикл движения по трассе поиска:} Begin tt:= cor; Repeat ss:= tt; b:= y.kl < ss^.kl; If b Then tt:= tt^.lson Else tt:= tt^.rson Until tt = Nil; If b Then ss^.lson:= nov Else ss^.rson:= nov End {Произошло связыванием нового узла с найденным отцом ss^} End; Procedure Obrab(ss:pt); Begin Write(ss^.kl:5) End; Procedure Obhod(ss:pt); {Блок обхода БДУ слева} {Ниже описывается тип элементов стека; в нем поле sl - ссылка на узел БДУ, ssl - цепная связь в стеке} Type pnt=^z; z=Record sl:pt; ssl:pnt End; Var tt: pt; q,t: pnt; {t - ссылка на верхушку стека} Begin t:= Nil; {стек - пуст} Repeat While ss <> Nil do Begin While ss^.lson <> Nil do {"Левая" трасса} Begin {Очередной узел включается в стек: } New(q); q^.sl:= ss; q^.ssl:= t; t:= q; ss:= ss^.lson{Продвижение по трассе} End; Obrab(ss); ss:= ss^.rson End; q:= t; If t <> Nil Then Begin ss:=t^.sl; t:= t^.ssl; {Взятие ссылки из стека} Dispose(q); {Освобождение памяти} Obrab(ss); {Обработка узла как корня поддерева} ss:= ss^.rson {Начало обхода правого поддерева} End Until q = Nil {Если стек опустел, обход БДУ закончен} End; BEGIN Randomize; coren:= Nil; {Порождено пустое дерево упорядочения} y.lson:= Nil; y.rson:= Nil; For j:= 1 to n do {Цикл включения n записей в дерево} Begin y.kl:= Random(50); {Переменная y подготовлена к включению} Vkl (coren, y) {Включение в ДДУ нового узла - псевдослучайной записи} End; Obhod(coren); {Получение упорядоченной последовательности ключей} Readln; { путем обхода дерева и их вывод} END.

\

Бинарное (двоичное) дерево (binary tree) - это упорядоченное дерево, каждая вершина которого имеет не более двух поддеревьев, причем для каждого узла выполняется правило: в левом поддереве содержатся только ключи, имеющие значения, меньшие, чем значение данного узла, а в правом поддереве содержатся только ключи, имеющие значения, большие, чем значение данного узла.

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

Узел дерева, не имеющий потомков, называется листом.

Схематичное изображение бинарного дерева:

Бинарное дерево

Бинарное дерево может представлять собой пустое множество.

Бинарное дерево может выродиться в список:

Вырожденное дерево

Построение бинарного дерева

Сначала мы должны определить структуру для создания корня и узлов дерева:

template<class T>

struct TNode {

T value;

TNode *pleft, *pright;

//constructor

TNode() {

pleft = pright = 0;

}

};

Здесь поля pleft и pright - это указатели на потомков данного узла, а поле value предназначено для хранения информации.

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

template<class T>

void makeTree(TNode<T>** pp, T x) {

if(!(*pp)) {

TNode<T>* p = new TNode<T>();

p->value = x;

*pp = p;

}

else {

if((*pp)->value > x)

makeTree(&((*pp)->pleft), x);

else

makeTree(&((*pp)->pright), x);

}

}

Эта функция добавляет элемент x к дереву, учитывая величину x. При этом создается новый узел дерева.

Обход дерева

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

В приведенной ниже реализации функция обхода дерева будет просто выводить на экран значение поля value каждого узла дерева (включая его корень):

template<class T>

void walkTree(TNode<T>* p) {

if(p) {

walkTree(p->pleft);

cout << p->value << ' ';

walkTree(p->pright);

}

}

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

Например, нерекурсивная функция для обхода дерева может выглядеть так:

template<class T>

void walkNotRec(TNode<T>* tree) {

if (tree) {

TNode<T> temp;

TNode<T>* ptemp = &temp;

TNode<T>* p = ptemp, *c = tree, *w;

while (c != ptemp) {

cout << c->value;

w = c->pleft;

c->pleft = c->pright;

if(c == p)

c->pright = 0;

else

c->pright = p;

p = c;

if (w) c = w;

}

}

}

Применение

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

На главную

Односвязные и двусвязные списки.

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

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

Линейные односвязные и двусвязные списки

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

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

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

Рис.1. Односвязный список. Логическая структура.

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

Логическая структура линейного односвязного списка:

Имя списка (идентификатор), тип элементов списка, указатель начала списка, указатель текущего элемента списка.

Логическая структура элемента линейного односвязного списка:

Данные или указатель на данные, указатель на следующий элемент списка.

Основные операции над линейным односвязным списком:

  • перемещение по списку;

  • включение элемента в линейный односвязный список (рис.2);

  • исключение элемента из линейного односвязного списка (рис.3);

  • извлечение содержательного поля любого элемента;

  • изменение содержательного поля любого элемента;

  • подсчет количества элементов списка;

  • слияние линейных односвязных списков (частный случай операции включения линейного односвязного списка);

  • выделение линейного односвязного подсписка.

Продвижение в линейном односвязном списке возможно только в одном направлении. Результаты выполнения операций включения элемента в линейный односвязный список и исключения элемента из линейного односвязного списка – линейный односвязный список.

 

Рис.2. Выполнение операции включения элемента в линейный односвязный список.

Рис.3. Выполнение операции исключения элемента из линейного односвязного списка.

Физическая структура линейного односвязного списка:

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

Рис.4. Линейный односвязный список. Физическая структура.

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

Рис.5. Кольцевой односвязный список. Логическая структура.

В линейном двусвязном списке продвижение возможно в любом из двух направлений по цепочке элементов. Каждый элемент двусвязного списка содержит два указателя: указатель на следующий элемент, или прямой указатель, и указатель на предыдущий элемент, или обратный указатель (рис.6). У первого элемента двусвязного списка обратный указатель пустой, а у последнего элемента двусвязного списка – прямой указатель.

Рис.6. Линейный двусвязный список. Логическая структура.

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

Рис.7. Линейность двусвязного списка.

Логическая структура линейного двусвязного списка:

Имя списка (идентификатор), тип элементов списка, указатель начала списка, указатель конца списка, указатель текущего элемента списка.

Логическая структура элемента линейного двусвязного списка:

Данные или указатель на данные, указатель на следующий элемент списка, указатель на предыдущий элемент списка.

Основные операции над линейным двусвязным списком:

  • перемещение по списку;

  • включение элемента в линейный двусвязный список;

  • исключение элемента из линейного двусвязного списка;

  • извлечение содержательного поля любого элемента;

  • изменение содержательного поля любого элемента;

  • подсчет количества элементов списка;

  • слияние линейных двусвязных списков (частный случай операции включения линейного двусвязного списка);

  • выделение линейного двусвязного подсписка.

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

Физическая структура линейного двусвязного списка:

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

Рис.8. Линейный двусвязный список. Физическая структура.

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

Рис.9. Кольцевой двусвязный список. Логическая структура.

Примеры реализации на С++.

Пример (инициализация элементов структуры):

#include <iostream>

using namespace std;

struct Vector2D{

double x;

double y;

}; // !!!!!

void Print(Vector2D *pV){

cout << "(" << pV->x << ", " << pV->y << ")";

}

int main(){

Vector2D Vector = {1, 2};

Print(&Vector);

cout << endl;

return 0;

}

Существует два основных способа построения односвязного списка. Первый способ — помещать новые элементы в конец списка[1]. Второй — вставлять элементы в определенные позиции списка, например, в порядке возрастания. От способа построения списка зависит алгоритм функции добавления элемента. Давайте начнем с более простого способа создания списка путем помещения элементов в конец.

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

struct address {

char name[40];

char street[40];

char city[20];

char state[3];

char zip[11];

struct address *next; /* ссылка на следующий адрес */

} info;

Приведенная ниже функция slstore() создает односвязный список путем помещения каждого очередного элемента в конец списка. В качестве параметров ей передаются указатель на структуру типа address, содержащую новую запись, и указатель на последний элемент списка. Если список пуст, указатель на последний элемент должен быть равен нулю.

void slstore(struct address *i,

struct address **last)

{

if(!*last) *last = i; /* первый элемент в списке */

else (*last)->next = i;

i->next = NULL;

*last = i;

}

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

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

struct address {

char name[40];

char street[40] ;

char city[20];

char state[3];

char zip[11];

struct address *next;

struct address *prior;

} info;

Следующая функция, dlstore(), создает двусвязный список, используя структуру address в качестве базового типа данных:

void dlstore(struct address *i, struct address **last)

{

if(!*last) *last = i; /* вставка первого элемента */

else (*last)->next = i;

i->next = NULL;

i->prior = *last;

*last = i;

}

Функция dlstore() помещает новые записи в конец списка. В качестве параметров ей необходимо передавать указатель на сохраняемые данные; а также указатель на конец списка, который при первом вызове должен быть равен нулю (NULL).

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

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

struct address {

char name[40];

char street[40] ;

char city[20];

char state[3];

char zip[11];

struct address *next;

struct address *prior;

} info;

Следующая функция, dlstore(), создает двусвязный список, используя структуру address в качестве базового типа данных:

void dlstore(struct address *i, struct address **last)

{

if(!*last) *last = i; /* вставка первого элемента */

else (*last)->next = i;

i->next = NULL;

i->prior = *last;

*last = i;

}

Функция dlstore() помещает новые записи в конец списка. В качестве параметров ей необходимо передавать указатель на сохраняемые данные; а также указатель на конец списка, который при первом вызове должен быть равен нулю (NULL).

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

20.Классы и объекты.

Класс — это такая абстракция множества предметов реального мира, что

Предметы в этом множестве - объекты имеют одни и те же характеристики

Все объекты подчинены и согласованы с одним и тем же набором правил и линий поведений

Классы предоставляют программисту возможность моделирования объектов, которые имеют атрибуты (данные-элементы) и варианты поведения (функции-элементы)

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

Например, абстрактный тип данных «строка текста» может быть оформлен в виде класса, и тогда все строки текста в программе будут являться объектами — экземплярами класса «строка текста».

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

Как и структуры, классы могут задавать поля — то есть переменные, принадлежащие либо непосредственно самому классу (статические), либо экземплярам класса (обычные). Статические поля существуют в одном экземпляре на всю программу (или, в более сложном варианте, — в одном экземпляре на процесс или на поток/нить). Обычные поля создаются по одной копии для каждого конкретного объекта — экземпляра класса. Например, общее количество строк текста, созданных в программе за время её работы, будет являться статическим полем класса «строка текста». А конкретный массив символов строки будет являться обычным полем экземпляра класса «строка текста», так же как переменная «фамилия», имеющая тип «строка текста», будет являться обычным полем каждого конкретного экземпляра класса «человек».

В ООП при использовании классов весь исполняемый код программы (алгоритмы) будет оформляться в виде так называемых «методов», «функций» или «процедур», что соответствует обычному структурному программированию, однако теперь они могут (а во многих языках обязаны) принадлежать тому или иному классу. Например, по возможности, класс «строка текста» будет содержать все основные методы/функции/процедуры, предназначенные для работы со строкой текста, такие как поиск в строке, вырезание части строки и т. д.

Как и поля, код в виде методов/функций/процедур, принадлежащих классу, может быть отнесен либо к самому классу, либо к экземплярам класса. Метод, принадлежащий классу и соотнесенный с классом (статический метод) может быть вызван сам по себе и имеет доступ к статическим переменным класса. Метод, соотнесенный с экземпляром класса (обычный метод), может быть вызван только у самого объекта, и имеет доступ как к статическим полям класса, так и к обычным полям конкретного объекта (при вызове этот объект передастся скрытым параметром метода). Например, общее количество созданных строк можно узнать из любого места программы, но длину конкретной строки можно узнать только указав, тем или иным образом, длину какой строки будем мерить.

Сам класс не является объектом, однако, в зависимости от языка программирования и платформы, программисту могут быть доступны те или иные объекты, позволяющие получить информацию о классе — название, список членов, объем памяти, занимаемой классом или отдельным экземпляром. Например, может существовать специальный класс «тип данных», экземпляры которого описывают тот или иной конкретный класс, существующий в программе.

Объект — некоторая сущность в виртуальном пространстве, обладающая определённым состоянием и поведением, имеет заданные значения свойств (атрибутов) и операций над ними (методов)[1]. Как правило, при рассмотрении объектов выделяется то, что объекты принадлежат одному или нескольким классам, которые в свою очередь определяют поведение (являются моделью) объекта. Время с момента создания объекта (конструкция) до его уничтожения (деструкция) называется временем жизни объекта. Объект наряду с понятием «класс», является важным понятием объектно-ориентированного подхода в программировании. Объекты обладают свойствами наследования, инкапсуляции и полиморфизма. [1]

Термин объект в программном обеспечении впервые был введен в языке Simula и применялся для моделирования реальности. Объект обладает состоянием, поведением и идентичностью; структура и поведение схожих объектов определяет общий для них класс; термины «экземпляр класса» и «объект» взаимозаменяемы. [2]

Инстанцирование (англ. instantiation) — создание экземпляра класса. В отличие от слова «создание», применяется не к объекту, а к классу. То есть, говорят: «(в виртуальной среде) создать экземпляр класса или инстанцировать класс». Порождающие шаблоны используют полиморфное инстанцирование.

Экземпляр класса (англ. instance) — это описание конкретного объекта в памяти. Класс описывает свойства и методы, которые будут доступны у объекта, построенного по описанию, заложенному в классе. Экземпляры используют для представления (моделирования) конкретных сущностей реального мира. Например, объектом может быть ваша стиральная машина, и иметь следующие атрибуты: компания-производитель «Вятка», наименование модели «Вятка-автомат», серийный номер изделия ВЯТ454647, емкость 20 л.

Имя объекта начинается обычно со строчной буквы.

Анонимный объект (англ. anonymous object) — это объект который принадлежит некоторому классу, но не имеет имени.

Инициализация (англ. initialization) — присвоение начальных значений полям объекта.

Обновлен: Ноябрь 2007

Объект — это совокупность кода и данных, которые воспринимаются как одно целое. Объект может являться частью приложения, как, например, элемент управления или форма. Приложение в целом также может быть объектом.

Повторное использование кода

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

Классы

Каждый объект в Visual Basic определяется с помощью класса. Класс описывает переменные, свойства, процедуры и события объекта. Объекты представляют собой экземпляры классов; после того как класс определен, можно создать любое количество объектов.

Хорошим примером связи между объектом и классом является форма для печенья и само печенье. Форма для печенья — это класс. Он определяет характеристики каждого печенья — например, размер и форму. Класс используется для создания объектов. В нашем примере объект — это печенье.

Два примера в Visual Basic могут помочь проиллюстрировать отношение между классами и объектами.

Элементы управления на Toolbox в Visual Basic представляют собой классы. При перетаскивании элемента управления из Toolbox на форму в действительности создается объект — экземпляр соответствующего класса.

Форма, с которой вы работаете в режиме проектирования — это класс. Во время выполнения Visual Basic создает экземпляр класса формы — то есть объект.

Несколько экземпляров

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

Открытые, закрытые и защищенные элементы классов.

Данные-члены

Данные-члены класса объявляются так же, как переменные. Например, у класса Screen могут быть следующие данные-члены:

#include

class Screen {

string _screen; // string( _height * _width )

string::size_type _cursor; // текущее положение на экране

short _height; // число строк

short _width; // число колонок

};

Поскольку мы решили использовать строки для внутреннего представления объекта класса Screen, то член _screen имеет тип string. Член _cursor – это смещение в строке, он применяется для указания текущей позиции на экране. Для него использован переносимый тип string::size_type. (Тип size_type рассматривался в разделе 6.8.)

Необязательно объявлять два члена типа short по отдельности. Вот объявление класса Screen, эквивалентное приведенному выше:

class Screen {

/*

* _ screen адресует строку размером _height * _width

* _cursor указывает текущую позицию на экране

* _height и _width - соответственно число строк и колонок

*/

string _screen;

string::size_type _cursor;

short _height, _width;

};

Член класса может иметь любой тип:

class StackScreen {

int topStack;

void (*handler)(); // указатель на функцию

vector stack; // вектор классов

};

Описанные данные-члены называются нестатическими. Класс может иметь также и статические данные-члены. (У них есть особые свойства, которые мы рассмотрим в разделе 13.5.)

Объявления данных-членов очень похожи на объявления переменных в области видимости блока или пространства имен. Однако их, за исключением статических членов, нельзя явно инициализировать в теле класса:

class First {

int memi = 0; // ошибка

double memd = 0.0; // ошибка

};

Данные-члены класса инициализируются с помощью конструктора класса. (Мы рассказывали о конструкторах в разделе 2.3; более подробно они рассматриваются в главе 14.)

13.1.2. Функции-члены

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

Функции-члены класса объявляются в его теле. Это объявление выглядит точно так же, как объявление функции в области видимости пространства имен. (Напомним, что глобальная область видимости – это тоже область видимости пространства имен. Глобальные функции рассматривались в разделе 8.2, а пространства имен – в разделе 8.5.) Например:

class Screen {

public:

void home();

void move( int, int );

char get();

char get( int, int );

void checkRange( int, int );

// ...

};

Определение функции-члена также можно поместить внутрь тела класса:

class Screen {

public:

// определения функций home() и get()

void home() { _cursor = 0; }

char get() { return _screen[_cursor]; }

// ...

};

home() перемещает курсор в левый верхний угол экрана; get() возвращает символ, находящийся в текущей позиции курсора.

Функции-члены отличаются от обычных функций следующим:

функция-член объявлена в области видимости своего класса, следовательно, ее имя не видно за пределами этой области. К функции-члену можно обратиться с помощью одного из операторов доступа к членам – точки (.) или стрелки (->):

ptrScreen->home();

myScreen.home();

(в разделе 13.9 область видимости класса обсуждается более детально);

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

Функция-член может быть перегруженной (перегруженные функции рассматриваются в главе 9). Однако она способна перегружать лишь другую функцию-член своего класса. По отношению к функциям, объявленным в других классах или пространствах имен, функция-член находится в отдельной области видимости и, следовательно, не может перегружать их. Например, объявление get(int, int) перегружает лишь get() из того же класса Screen:

class Screen {

public:

// объявления перегруженных функций-членов get()

char get() { return _screen[_cursor]; }

char get( int, int );

// ...

};

(Подробнее мы остановимся на функциях-членах класса в разделе 13.3.)

13.1.3. Доступ к членам

Часто бывает так, что внутреннее представление типа класса изменяется в последующих версиях программы. Допустим, опрос пользователей нашего класса Screen показал, что для его объектов всегда задается размер экрана 80 ? 24. В таком случае было бы желательно заменить внутреннее представление экрана менее гибким, но более эффективным:

class Screen {

public:

// функции-члены

private:

// инициализация статических членов (см. 13.5)

static const int _height = 24;

static const int _width = 80;

string _screen;

string::size_type _cursor;

};

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

Если бы данные-члены класса Screen были открыты и доступны любой функции внутри программы, как отразилось бы на пользователях изменение внутреннего представления этого класса?

все функции, которые напрямую обращались к данным-членам старого представления, перестали бы работать. Следовательно, пришлось бы отыскивать и изменять соответствующие части кода;

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

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

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

закрытый член доступен только функциям-членам и друзьям класса. Класс, который хочет скрыть информацию, объявляет свои данные-члены закрытыми;

защищенный член ведет себя как открытый по отношению к производному классу и как закрытый по отношению к остальной части программы. (В главе 2 мы видели пример использования защищенных членов в классе IntArray. Детально они рассматриваются в главе 17, где вводится понятие наследования.)

В следующем определении класса Screen указаны секции public и private:

class Screen {

public:

void home() { _cursor = 0; }

char get() { return _screen[_cursor]; }

char get( int, int );

void move( int, int );

// ...

private:

string _screen;

string::size_type _cursor;

short _height, _width;

};

Согласно принятому соглашению, сначала объявляются открытые члены класса. (Обсуждение того, почему в старых программах C++ сначала шли закрытые члены и почему этот стиль еще кое-где сохранился, см. в книге [LIPPMAN96a].) В теле класса может быть несколько секций public, protected и private. Каждая секция продолжается либо до метки следующей секции, либо до закрывающей фигурной скобки. Если спецификатор доступа не указан, то секция, непосредственно следующая за открывающей скобкой, по умолчанию считается private.

В С++ мы имеем возможность объявлять классы, которые инкапсулируют элементы-данные и функции-элементы. Эти функции изменяют и позволяют обращаться к значениям данных-элементов и выполняют другие задачи. Базовый класс Базовый класс определяется следующим образом (синтаксис): class className { private: <закрытые элементы-данные> <закрытые конструкторы> <закрытые функции-элементы> protected: <защищенные элементы-данные> <защищенные конструкторы> <защищенные функции-элементы> public: <открытые элементы-данные> <открытые конструкторы> <открытый деструктор> <открытые функции-элементы> }; Пример 1: class point { protected: double х; double у; public: point(double xVal, double yVal); double getX(); double getY(); void assign(double xVal, double yVal); point& assign(point &pt); }; Разделы класса Классы С++ имеют три различных уровня доступа к своим элементам - как к данным, так и к функциям:

  • Закрытые (частные) элементы

  • Защищенные элементы

  • Открытые элементы

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

  1. Разделы могут появляться в любом порядке.

  2. Один и тот же раздел можно определять несколько раз.

  3. Если не определен ни один раздел, компилятор (по умолчанию) объявляет все элементы закрытыми.

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

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

  6. Класс может иметь несколько конструкторов.

  7. Класс может иметь только один деструктор, который должен объявляться в открытом разделе класса.

  8. Функции-элементы (в том числе конструкторы и деструкторы), состоящие из нескольких операторов, должны определяться вне объявления класса. Определение функции может содержаться в том же файле, в котором определяется класс. Это напоминает порядок работы с обычными функциями: задание прототипа и определение функции.

Конструкторы являются специфическим типом функций-элементов, тип возвращаемого значения для которых не указывается, а имя должно совпадать с именем класса-хозяина. Вызываются они при создании нового представителя класса. Деструктор вызывается для разрушения представителя класса. При определении функции-элемента вы должны указать ее имя и имя ее класса. Сначала вы должны Сначала необходимо указать имя класса (т.н. квалификатор), а затем, через два двоеточия (::), имя функции. В качестве примера рассмотрим такой класс: class point { protected: double x; double y; public: point(double xVal, double yVal); double getX(); // другие функции-элементы }; Определения конструктора и функций-элементов должны выглядеть так point::point (double xVal, double yVal) { // операторы } double point::getX() { // операторы } После того, как вы объявили класс, вы можете использовать имя класса в качестве спецификатора типа данных при объявлении представителей класса. Синтаксис объявления тот же, что и при объявлении переменной. В листинге 8.1 приведен исходный текст программы RECT.CPP. Программа предлагает вам ввести длину и ширину прямоугольника (в данном примере прямоугольник является объектом). Затем программа выводит значения длины, ширины и площади определенного вами прямоугольника.

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

public

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

private

Доступ к типу или члену можно получить только из кода в том же классе или структуре.

protected

Доступ к типу или элементу можно получить только из кода в том же классе или структуре, либо в производном классе.

internal

Доступ к типу или члену возможен из любого кода в той же сборке, но не из другой сборки.

protected internal

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

В следующих примерах демонстрируется указание модификаторов доступа для типа или члена.

C#

public class Bicycle

{

public void Pedal() { }

}

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

Доступность класса и структуры

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

Члены структуры, включая вложенные классы и структуры, могут быть объявлены как открытые, внутренние или закрытые. Члены классов, включая вложенные классы и структуры, могут быть открытыми, защищенными внутренними, защищенными, внутренними или закрытыми. Уровень доступа для членов класса и членов структуры, включая вложенные классы и структуры, является закрытым по умолчанию. Закрытые вложенные типы недоступны за пределами содержащего типа.

Производные классы могут обладать лучшей доступностью, чем их базовые типы. Другими словами, открытый класс B, являющийся производным от внутреннего класса A, не может использоваться. Его использование может быть приравнено к переводу класса A в открытый тип, поскольку все защищенные или внутренние члены A доступны из производного класса.

При помощи InternalsVisibleToAttribute можно сделать возможным доступ других определенных сборок к внутренним типам. Дополнительные сведения см. в разделе Дружественные сборки (C# и Visual Basic).

Доступность члена класса и структуры

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

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

Тип любого элемента, являющегося полем, свойством или событием, должен, по меньшей мере, быть таким же доступным, как и этот элемент. Аналогичным образом тип возвращаемого значения и типы параметров любого члена, который явл. методом, индексатором или делегатом, должны иметь по меньшей мере такой же уровень доступности, как сам элемент. Например, метод M, возвращающий класс C не может быть открытым, если C также не является открытым. Подобным образом, свойство типа A не может быть защищенным, если A объявлен закрытым.

Определенные пользователям операторы также должны быть объявлены как открытые. Дополнительные сведения см. в разделе operator (Справочник по C#).

Деструкторы не могут иметь модификаторов доступности.

Чтобы задать уровень доступа для класса или элемента структуры, воспользуйтесь соответствующим ключевым словом в объявлении элемента, как показано в следующем примере.

Методы класса и свойства класса.

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

При применении свойств

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

можно указать, что это свойство только для чтения.

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

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

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

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

записи ассоциируются с обычными полями или методами.

Конструкторы и деструкторы (синтаксис, назначение, порядок вызова).

Конструктор – специальная функция-элемент класса, которая используется для инициализации данных объекта.

-Конструктор вызывается автоматически при создании объекта.

-Имя конструктора совпадает с именем класса.

-Класс может иметь несколько перегруженных конструкторов.

Деструктор – специальная функция-элемент класса, осуществляет подготовку к удалению объекта.

-Деструктор вызывается автоматически перед удалением объекта.

-Имя деструктора состоит из символа тильда (~) и именем класса.

-Класс НЕ может иметь несколько перегруженных конструкторов.

Пример (класс «Массив»):

// ________________IntArray.h________________

#pragma once

class CIntArray{

public:

CIntArray(); // Конструктор

CIntArray(int nSize, int nVal); // Конструктор (прегруженный)

~CIntArray();// Деструктор

// Данные класса

int *m_pData;

int m_nSize;

};

// ________________IntArray.cpp________________

#include "IntArray.h"

#include <iostream>

using namespace std;

CIntArray::CIntArray(){

m_pData = NULL;

m_nSize = 0;

}

CIntArray::CIntArray(int nSize, int nVal){

m_nSize = nSize;

m_pData = new int[m_nSize];

for(int i=0; i<m_nSize; i++) m_pData[i] = nVal;

}

CIntArray::~CIntArray(){

if(m_pData) delete[] m_pData;

}

// ________________main.cpp________________

#include "IntArray.h"

#include <iostream>

using namespace std;

 

int main(){

CIntArray A, B(10,3), C(4, 7);

 

cout << "A\n Size:\t" << A.m_nSize << "\n Data:\t";

for(int i=0; i<A.m_nSize; i++) cout << A.m_pData[i] << " ";

cout << endl << endl;

 

cout << "B\n Size:\t" << B.m_nSize << "\n Data:\t";

for(int i=0; i<B.m_nSize; i++) cout << B.m_pData[i] << " ";

cout << endl << endl;

 

cout << "C\n Size:\t" << C.m_nSize << "\n Data:\t";

for(int i=0; i<C.m_nSize; i++) cout << C.m_pData[i] << " ";

cout << endl << endl;

 

return 0;

}

Перегрузка конструкторов.

Стандартные C++ могут быть применены к пользовательским типам. Поведение объектом при применении к ним этих операций определяется посредством их перегрузки.

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

// ________________IntArray.h________________

#pragma once

class CIntArray{

friend void PrintArray(const CIntArray &Arr);

friend bool operator!=(const CIntArray &Arr1, const CIntArray &Arr2){

return !(Arr1 == Arr2);

}

public:

CIntArray(); // Конструктор

CIntArray(int nSize, int nVal); // Конструктор (прегруженный)

~CIntArray();// Деструктор

int GetSize() const {return m_nSize;} //inline функция

int GetAt(int nIndex) const;

void SetAt(int nIndex, int nVal) const;

// Проверка на равенство

bool operator==(const CIntArray &)const;

// Перегрузка оператора присваивания

const CIntArray &operator=(const CIntArray &);

// Данные класса

protected:

int *m_pData;

int m_nSize;

};

Перегрузка методов класса.

// ________________IntArray.h________________

#pragma once

class CIntArray{

friend void PrintArray(const CIntArray &Arr);

public:

CIntArray(); // Конструктор

CIntArray(int nSize, int nVal); // Конструктор (прегруженный)

~CIntArray();// Деструктор

int GetSize() const {return m_nSize;} //inline функция

int GetAt(int nIndex) const;

void SetAt(int nIndex, int nVal) const;

// Проверка на равенство

bool operator==(const CIntArray &)const;

// Перегрузка оператора присваивания

const CIntArray &operator=(const CIntArray &)const;

// Данные класса

protected:

int *m_pData;

int m_nSize;

};

Пример (перегрузка оператора присваивания в классе «Массив» - реализация):

// ________________IntArray.cpp________________

. . .

const CIntArray &CIntArray::operator=(const CIntArray &Src){

if(m_pData) delete[] m_pData;

m_nSize = Src.m_nSize;

m_pData = new int[m_nSize];

for(int i=0; i<m_nSize; i++)m_pData[i] = Src.m_pData[i];

return *this;

}

Другой вариант реализации:

// ________________IntArray.cpp________________

. . .

const CIntArray &CIntArray::operator=(const CIntArray &Src){

if(m_pData) delete[] m_pData;

memcpy(this, &Src, sizeof(CIntArray));

m_pData = new int[m_nSize];

memcpy(m_pData, Src.m_pData, sizeof(int)*m_nSize);

return *this;

}

21.Инициализация элементов класса.

Инициализация класса

Рассмотрим следующее определение класса:

class Data {

public:

int ival;

char *ptr;

};

Чтобы безопасно пользоваться объектом класса, необходимо правильно инициализировать его члены. Однако смысл этого действия для разных классов различен. Например, может ли ival содержать отрицательное значение или нуль? Каковы правильные начальные значения обоих членов класса? Мы не ответим на эти вопросы, не понимая абстракции, представляемой классом. Если с его помощью описываются служащие компании, то ptr, вероятно, указывает на фамилию служащего, а ival - его уникальный номер. Тогда отрицательное или нулевое значения ошибочны. Если же класс представляет текущую температуру в городе, то допустимы любые значения ival. Возможно также, что класс Data представляет строку со счетчиком ссылок: в таком случае ival содержит текущее число ссылок на строку по адресу ptr. При такой абстракции ival инициализируется значением 1; как только значение становится равным 0, объект класса уничтожается.

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

Data dat01( "Venus and the Graces", 107925 );

Data dat02( "about" );

Data dat03( 107925 );

Data dat04;

Бывают ситуации (как в случае с dat04), когда нам нужен объект класса, но его начальные значения мы еще не знаем. Возможно, они станут известны позже. Однако начальное значение задать необходимо, хотя бы такое, которое показывает, что разумное начальное значение еще не присвоено. Другими словами, инициализация объекта иногда сводится к тому, чтобы показать, что он еще не инициализирован. Большинство классов предоставляют специальный конструктор по умолчанию, для которого не требуется задавать начальных значений. Как правило, он инициализирует объект таким образом, чтобы позже можно было понять, что реальной инициализации еще не проводилось.

Обязан ли наш класс Data иметь конструктор? Нет, поскольку все его члены открыты. Унаследованный из языка C механизм поддерживает явную инициализацию, аналогичную используемой при инициализации массивов:

int main()

{

// local1.ival = 0; local1.ptr = 0

Data local1 = { 0, 0 };

// local2.ival = 1024;

// local3.ptr = "Anna Livia Plurabelle"

Data.local2 - { 1024, "Anna Livia Plurabelle" };

// ...

}

Значения присваиваются позиционно, на основе порядка, в котором объявляются данные-члены. Следующий пример приводит к ошибке компиляции, так как ival объявлен перед ptr:

// ошибка: ival = "Anna Livia Plurabelle";

// ptr = 1024

Data.local2 - { "Anna Livia Plurabelle", 1024 };

Явная инициализация имеет два основных недостатка. Во-первых, она может быть применена лишь для объектов классов, все члены которых открыты (т.е. эта инициализация не поддерживает инкапсуляции данных и абстрактных типов - их не было в языке C, откуда она заимствована). А во-вторых, такая форма требует вмешательства программиста, что увеличивает вероятность появления ошибок (забыл включить список инициализации или перепутал порядок следования инициализаторов в нем).

Так нужно ли применять явную инициализацию вместо конструкторов? Да. Для некоторых приложений более эффективно использовать список для инициализации больших структур постоянными значениями. К примеру, мы можем таким образом построить палитру цветов или включить в текст программы фиксированные координаты вершин и значения в узлах сложной геометрической модели. В подобных случаях инициализация выполняется во время загрузки, что сокращает затраты времени на запуск конструктора, даже если он определен как встроенный. Это особенно удобно при работе с глобальными объектами1.

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

Статические элементы класса.

21 Статические элементы классов с++.

Спецификатор класса памяти static м.б. использован в объявлении элементов данных и методов.

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

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

class A

{

static int A;

static func();

}

b=A::A;

A::func();

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

Пример:

class X

{

int member;

static Func(int i, X*ptr);

}

X:: Func(int i, X*ptr);

{

member = i; неправильно

ptr -> member=i; правильно

}

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

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

Дружественные функции и дружественные классы.

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

Чтобы объявить функцию как друга класса, её прототип должен находиться в описании класса и перед ним должно находится ключевое слово friend.

Чтобы объявить класс ClassTwo другом класса ClaccOne, в определении класса ClassOne должно быть записано:

friend ClassTwo;

Пример (объявление дружественной функции PrintArray в классе «Массив»):

// ________________IntArray.h________________

#pragma once

class CIntArray{

friend void PrintArray(const CIntArray &Arr);

public:

CIntArray(); // Конструктор

CIntArray(int nSize, int nVal); // Конструктор (прегруженный)

~CIntArray();// Деструктор

int GetSize() const {return m_nSize;} //inline функция

int GetAt(int nIndex) const;

void SetAt(int nIndex, int nVal) const;

// Данные класса

protected:

int *m_pData;

int m_nSize;

};

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

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

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

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

Функция, дружественная некоторому классу, имеет полный доступ ко всем его полям и методам.

Любой метод класса, дружественного некоторому классу, также имеет полный доступ к его полям и методам.

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

Для указания дружественных функций и классов используется ключевое слово friend.

Для объявления дружественной функции необходимо после ключевого слова friend записать прототип функции:

class ИмяКласса {

...

friend Тип ИмяДружественнойФункции(Параметры);

...

};

Пример. Дружественная функция для вычисления расстояния между двумя токами

class Point {

private:

double x,y;

...

friend double Distance(Point A,Point B);

};

double Distance(Point A,Point B) {

return hypot(A.x-B.x,A.y-B.y);

}

Для объявления дружественного класса достаточно после ключевого слова friend записать имя класса:

class ИмяКласса {

...

friend class ИмяДружественногоКласса;

...

};

Пример. Дружественный класс

class Point {

private:

double x,y;

...

friend class Triangle;

};

Мы уже познакомились с основным правилом ООП - данные(внутренние переменные) объекта защищены от воздействий из вне и доступ к ним можно получить только с помощью методов(функций) объекта. Но бывают такие случаи, когда нам необходимо получить доступ к данным объекта не используя его интерфейс. Зачем это может понадобиться ? Как я уже как-то упоминал, при доступе к внутренним переменным объекта через его методы уменьшается эффективность работы за счет затрат на вызов метода. В большинстве случаев нам это не критично, но не всегда. В некоторых случаях это может играть существенную роль. Конечно, можно добавить новый метод к классу для получения прямого доступа к внутренним переменным. Однако, в большинстве случаев, интерфейс объекта (методы) спланирован для выполнения определенного круга операций, и наша функция может оказаться как бы ни к месту. А если хуже того, нам необходимо получить прямой доступ к внутренним данным двух разных объектов ? Возникает проблема. Именно для решения подобных задач и существует возможность описание функции, метода другого класса или даже класса как дружественного(friend).

Итак, как же работает этот механизм.

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

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

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

А теперь приведу несколько примеров использования дружественных функций(и не только) в языке С++.

// Описание класса A

class A

{

//...

void z(); // Описание функции z класса A

};

// Описание класса B

class B

{

//...

friend void A::z(); // Описание функции z класса A как дружественной

// классу B, т.е. из функции z класса A можно

// получить доступ к внутренним переменным класса B

};

// Описание класса C

class C

{

//...

friend class A; // Описание класса A как дружественного классу C,

// все функции класса A будут дружественны классу C и

// из любой функции класса A можно получить доступ к

// внутренним переменным класса C

}

Дружественные члены класса (методы) позволяют получить доступ к защищенным модификатором private членам класса из методов других классов. Методы и классы, объявляемые дружественными, иногда также называются друзьями класса. Если метод класса A внутри тела класса B объявляется с модификатором friend, что указывает на то, что он является другом класса, то из него разрешен доступ ко всем членам класса B. Например: class A { public: int Fx();} class B { public: friend int A::Fx(); private: } Дружественные классы Объявление дружественного класса позволяет всем его методам получить доступ ко всем переменным и методам другого класса. Например: class A {public: int Fx();} class B {public: friend class A; private: } Дружественный класс или член класса будет доступен только в том случае, если он был объявлен в области видимости самого класса или ранее во внешней области видимости, внутри которой располагается область видимости, содержащая объявление класса с объявлениями друзей класса. Например: class A {public: // Класс расположен во внешней // области видимости int Fx1(); } namespace classB { class B {public: friend class A; friend class C; private: } class C { public: // Класс расположен в том же // пространстве имен int Fx2();  } } Дружественные классы не наследуются, и их дружественность не является транзитивной. Например: class A {int Fx();} class B {friend class A;} class C {friend class B;} // Класс А не является // дружественным классу C class D : public B {}  // Класс А не является // дружественным классу D

К друзьям и дружественности применимы следующие правила:

• на описания friend не влияют спецификаторы public,

protected или private;

• описания friend не взаимны: если А объявляет В другом, то это не означает, что А является другом для В;

• дружественность не наследуется: если А объявляет В другом, классы, производные от В, не будут автоматически получать доступ к элементам А;

• дружественность не является переходным свойством: если А объявляет В другом, классы, производные от А, не будут автоматически признавать дружественность В.

Обычное объявление функции-члена гарантирует три логически разные вещи:

• во-первых, функция имеет право доступа к закрытой части объявления класса;

• во-вторых, функция находится в области видимости класса;

• в-третьих, функция должна вызываться для объекта класса, то есть имеется указатель this,

Объявив функцию-член как static, мы придаем ей только первые два свойства. Объявив функцию как friend, мы наделяем ее только первым свойством.

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

Так же как и объявление члена, объявление friend не добавляет новое имя в охватывающую область видимости. Например:

class Matrix

{

friend class Xform;

friend Matrix invert (const Matrix&);

};

Xform x; /* ошибка: в текущей области видимости нет имени Xform */

Matrix (*p)(const Matrix&) = &invert; /* ошибка: в текущей области видимости нет имени invert */

Класс-друг должен быть предварительно объявлен в охватывающей области видимости или определен в области видимости, непосредственно охватывающей класс, объявивший его другом. При этом не принимаются во внимание области видимости вне области видимости самого внутреннего охватывающего пространства имен. Например:

class AE {…}; // не друг класса Y

namespace N

{

class X {…}; // друг класса Y

class Y

{

friend class X;

friend class Z;

friend class AE;

};

class Z {…}; // друг класса Y

};

Функцию-друга можно явно объявить точно так же, как и класс-друг, или ее поиск осуществляется по типам ее аргументов так, как будто она была объ-явлена вне класса, но в области видимости, непосредственно охватывающей класс. Например:

void f(Matrix &m)

{

invert(m); // функция invert – друг класса Matrix

}

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

class X

{

friend void f(); /* бесполезно, т.к. в этой области видимости нет имени f()*/

friend void h(const X&); /* можно найти по типу ар-гумента */

};

void g(const X &x)

{

f(); // нет имени f() в области видимости

h(x); // функция h(x) – друг класса X

}

Члены класса-члена не имеют свободного доступа к членам внешнего класса. Аналогично члены внешнего класса не имеют свободного доступа к членам вложенного класса; нужно соблюдать обычные правила доступа. Например:

class Outer //внешний

{

typedef int T;

int i;

public:

int i2;

static int s;

class Inner // внутренний

{

int x;

T y; // ошибка: Outer::T – закрытый

public:

void f(Outer *p, int v);

};

int g(Inner *p);

};

void Outer::Inner::f(Outer *p, int v)

{

p->i = v; // ошибка: Outer::i – закрытый

p->i2 = v; // правильно: Outer::i2 – открытый

}

int Outer::g(Outer *p)

{

p->f(this, 2); // правильно: Inner::f – открытый

return p->x; // ошибка: Inner::x – закрытый

}

Однако часто полезно позволить классу-члену обращаться к его внешнему классу. Этого можно добиться, сделав член другом. Например:

class Outer

{

typedef int T;

int i;

public:

class Inner; /* предварительное объявление класса члена */

friend class Inner; /* разрешаем доступ к Outr::Inner */

class Inner

{

int x;

T y; // правильно: Inner – друг

public:

void f(Outer *p, int v);

};

};

void Outer::Inner::f(Outer *p, int v)

{

p->i = v; // правильно: Inner – друг

}

Ключевое слово friend – спецификатор функции, который дает функции – не члену класса доступ к скрытым членам класса. Он используется для того, чтобы выйти за строгие рамки типизации и сокрытия данных в C++. Однако, должны быть весьма серьезные причины для выхода за пределы этих ограничений, поскольку от них зависит надежность программирования. Поэтому использование friend функций в языке C++ спорно. Одна из причин их использования состоит в том, что некоторые функции нуждаются в привилегированном доступе более, чем к одному классу. Вторая причина – friend-функция передает все параметры через список параметров, и значение каждого из них подчинено преобразованию, совместимому с назначением. Такие преобразования применяются к явно переданным аргументам-классам и поэтому особенно полезны в случаях перегрузки оператора. Это будет показано в следующем разделе. Объявление friend (дружественной) функции должно появляться внутри объявления класса, которому она дружественна. Имени функции предшествует ключевое слово friend, и ее объявление может находится как в public, так и в private части класса, что не повлияет на значение. Функция-член одного класса может быть friend-функцией другого класса. Это происходит тогда, когда функция-член объявлена в friend классе с использованием оператора разрешения контекста для определения имени функции дружественного класса. Если все функции-члены одного класса являются friend-функциями второго класса, то это можно определить записью