Доступ к компонентам структурированного объекта
Язык Си++ поддерживает одну из концепций объектно-ориентированного программирования, так называемую инкапсуляцию (сокрытие) данных. Она заключается в том, что внутренняя реализация (компонентные данные) не должны быть доступны за пределами определений тел компонентных функций класса. Если же возникает необходимость всё же получить доступ или значение компонентных данным, то это делается за счёт общедоступных (интерфейсных) компонентных функций. Другими словами, в правильно сконструированном классе должна присутствовать скрытая часть, которая, безусловно, содержит все компонентные данные и может содержать некоторое количество компонентных функций, и открытая (общедоступная, интерфейсная) часть, представленная набором компонентных функций, которые могут быть вызваны в любом месте программы. Предполагается, что скрытая часть может меняться, модифицироваться и т.д., а интерфейсная часть остаётся неизменной (т.е. остаются неизменными сигнатуры интерфейсных компонентных функций, следовательно, остаются неизменными и способ вызова этих функций во всей остальной программе, но определение, безусловно, может меняться).
Таким образом, при использовании принципа инкапсуляции при программировании программу можно мысленно разбить на две достаточно независимые части. Одна часть это объект класса, который имеет внутреннюю (скрытую) часть и некоторый набор интерфейсных функций, а другая часть это вся остальная программа, которая взаимодействует с объектом по средствам вызова интерфейсных функций, и если в результате любых модификаций скрытой части объекта сигнатура этих интерфейсных функций не меняется, то во всей остальной программе нет необходимости, что-либо менять, а значит нет необходимости заново её отлаживать, анализировать, находить ошибки и т.д.. Поэтому инкапсуляция в больших программах экономит очень много сил и времени разработчикам.
В языке Си++ инкапсуляция поддерживается с помощью трёх спецификаторов доступа (ключевых слов) private (частный ), protected (защищённый ), public (общедоступный ). В определении класса компонентные данные и компонентные функции, стоящие после любого из ключевых слов и до конца определения класса или до другого спецификатора доступа считаются частными, защищёнными или общедоступными соответственно. Если при определении класса используется ключевое слово struct, то по умолчанию считается, что компонентные данные являются общедоступными, если class, то частными. Это принятое по умолчанию положение можно изменить явно указав спецификатор доступа. Например:
class F { //поскольку использовано ключевое слово class, то по умолчанию все компонентные //данные и функции размещённые в определении c начала определения и до нового //спецификатора доступа, а если он отсутствует, то до конца определения класса будут //частными (private)
компонентные данные; //private данные
компонентные функции; //private компонентные функции
public: //C этого точки определения и до следующего спецификатора доступа или, если он //отсутствует, до конца определения класса все определённые компонентные данные и //компонентные функции будут считаться общедоступными
компонентные данные; //public данные
компонентные функции; //public компонентные функции
};
Перепишем класс комплексное число с учётом принципа инкапсуляции. Определим все его компонентные данные как частные ( private ), а компонентные функции как общедоступные (public).
class comp { //Скрытая (частная) часть класса comp
double Real, Im;
static int count;
public: //Интерфейсная (общедоступная) часть класса comp
comp (){Real= 0; Im= 0; count++;} //Компонентная функция (конструктора по умолчанию)
comp(double r, double i) {Re = r; Im = i; count++;} //Конструктор
comp(comp &T) { Re= T.Re; Im = T.Im; count++;} // Конструктор копии
comp(double r) {Re = r; Im=0; count++;} // Конструктор преобразования типа
~comp(){count--;} // Деструктор
void display(){cout<<“\n Re =”<<Re<<”\t Im =“<<Im;}//Компонентная функции display()
static int GetCountComp(){return count;}
void SetRe(double r){Real = r;}
void SetIm(double i){ Im = i;}
double GetRe(){return Re;}
double GetIm(){return Im;}
friend comp raz(comp& A, comp& B);
};
int comp::count = 0; //Инициализация статического компонентного данного
Поскольку теперь компонентные данные Re, Im и count класса сomp являются частными, то к ним с помощью операции точка или стрелочка теперь можно обратится только в теле компонентных функций класса comp. Например, в конструкторе копии мы видим такое обращение Re= T.Re; Во всех других функциях, в том числе и main(), эта строка выдавала бы на этапе компиляции сообщение об ошибке. Поэтому, если все таки возникает необходимость изменять или получать значения реальной и мнимой части объекта комплексное число вне тел компонентных функций класса comp, то необходимо написать несколько интерфейсных функций SetRe(), SetIm(), GetRe(), GetIm(), которые и решают эту задачу. Причём поскольку тела этих функций очень маленькие, то мы делаем их online (подставляемыми), поэтому вызов такой функции не сильно отличается от прямого обращения к полю данных структурированного объекта, следовательно, почти не замедляет скорость работы программы.
Перепишем определение функции sum () с учётом того, что доступ к полям Re и Im теперь можно получить только через вызов компонентных функций SetRe(), SetIm(), GetRe(), GetIm().
Определение обычной (не компонентной) функции sum()
comp sum(comp A, comp& С){
comp D;
D.SetRe( A.GetRe() +С.GetRe());
D.SetIm ( A.GetIm() +С.GetIm());
return D;
}
Как видно из определения функции sum (), оно стало несколько громоздким. Этого можно избежать воспользовавшись ещё одной возможностью, предоставляемой языком Си++. Она состоит в том, что можно объявить функцию, дружественной к классу. В этом случае дружественная функция будет иметь право доступа к любым данным класса в не зависимости от их спецификатора доступа. Для того чтобы сделать функцию дружественной необходимо, чтобы она была описана в определении класса со спецификатором friend, как показано выше в определении класса на примере функции raz().
Определение обычной (не компонентной) функции raz()
comp raz(comp& A, comp& С){
comp D;
D.Re = A.Re -С.Re;
D.Im = A.Im -С.Im;
return D;
}
Дружественная функция не является компонентной функцией – это обычная глобальная функция, но она в своём теле имеет право обращаться с помощью операции точка или стрелочка ко всем компонентным данным и функциям класса comp в не зависимости от их спецификатора доступа.
Конечно аппарат дружественных функций несколько нарушает принцип инкапсуляции, но с другой стороны даёт разработчику возможность самому найти ту золотую середину, между принципом инкапсуляции и слишком большим разрастанием набора интерфейсных функций, т.е. дружественные функции позволяют упростить интерфейс.
Итак, дружественная функция:
• не может быть компонентной функцией того класса, по отношению к которому определяется как дружественная;
• может быть глобальной функцией (как в предыдущей программе):
class CL { friend int f1 (...); ... };
int f1(...) { тело_функции }
• может быть компонентной функцией другого ранее определенного класса:
class CL1 { ... char f2 (...); ... };
class CL2 { ... friend char CL1::f2(...); ... } ;
В примере класс CL1 c помощью своей компонентной функции f2 () получает доступ к компонентам класса CL2. Компонентная функция некоторого класса (CL1) может быть объявлена дружественной функцией другому классу (CL2), если только определение этого первого класса размещено раньше, чем определение второго (CL).
• может быть дружественной по отношению к нескольким классам:
// Предварительное неполное определение класса class CL2;
class CL1 { friend void ff(CLl,CL2); ... };
class CL2 { friend void ff(CLl,Cl2); ... };
void ff(CL1 cl,CL2 c2) { тело_функции }
Кроме того, класс может быть дружественным другому классу. Это означает, что все компонентные функции класса являются дружественными для другого класса. Дружественный класс должен быть определен вне тела класса, "предоставляющего дружбу". Например, так:
class X2 {
friend class X1;
};
class X1 { ... // Определение дружественного класса
void f1(...);
void f2(...);
};
В данном примере функции f1 и f2 из класса X1 являются друзьями класса Х2, хотя они описываются без спецификатора friend.
Все компоненты класса X2 доступны в дружественном классе X1. Дружественный класс может быть определен позже (ниже), нежели описан как дружественный.
Классы и шаблоны
Шаблоны, которые иногда называют родовыми или параметризованными типами, позволяют создавать (конструировать) семейства родственных функций и классов.
Как уже говорилось, шаблон семейства функций {function template) определяет потенциально неограниченное множество родственных функций. Он имеет следующий вид:
template <список_параметров_шаблона> определекие_функции
Здесь угловые скобки являются неотъемлемым элементом определения. Список параметров шаблона должен быть заключен именно в угловые скобки.
Аналогично определяется шаблон семейства классов:
template <список_параметров_шаблона> определение класса
Шаблон семейства классов определяет способ построения отдельных классов подобно тому, как класс определяет правила построения и формат отдельных объектов. В определении класса, входящего в шаблон, особую роль играет имя класса. Оно является не именем отдельного класса, а параметризованным именем семейства классов.
Как уже отмечалось в связи с шаблонами функций, определение шаблона может быть только глобальным.
Следуя авторам языка и компилятора Си++, рассмотрим векторный класс (в число данных входит одномерный массив). Какой бы тип ни имели элементы массива (целый, вещественный, с двойной точностью и т.д.), в этом классе должны быть определены одни и те же базовые операции, например доступ к элементу по индексу и т.д. Если тип элементов вектора задавать как параметр шаблона класса, то система будет формировать вектор нужного типа (и соответствующий класс) при каждом определении конкретного объекта.
Следующий шаблон позволяет автоматически формировать классы векторов с указанными свойствами:
template <class T> // Т - параметр шаблона
class Vector {
Т *data; // Начало одномерного массива
int size; // Количество элементов в массиве
public:
Vector(int); // Конструктор класса vector
~Vector() { delete[] data; } // Деструктор
// Расширение действия (перегрузка) операции "[]":
Т& operator[] (int i) { return data[i]; }
};
// Внешнее определение конструктора класса:
template <class T>
Vector<T>::Vector(int n) { data = new T[n]; size = n; };
Когда шаблон введен, у программиста появляется возможность определять конкретные объекты конкретных классов, каждый из которых параметрически порожден из шаблона. Формат определения объекта одного из классов, порождаемых шаблоном классов:
имя_параиетризованного_класса<фактические_параметры_шаблона> имя_объекта (паракетры_конструктора) ;
В нашем случае определить вектор, имеющий восемь вещественных координат типа double, можно следующим образом:
Vector <double> Z(8); Проиллюстрируем сказанное следующей программой:
#include "templatevec.h" // Шаблон классов "вектор"
#include <iostream.h>
main(){ // Создаем объект класса "целочисленный вектор":
Vector <int> Х(5);
// Создаем объект класса "символьный вектор":
Vector <char> C(5);
// Определяем компоненты векторов:
for (int i = 0; i < 5; i++) { X[i] = i; C[i] = 'A' + i;}
for (int i = 0; i < 5 ; i++) cout << " " << X[i] << ' ' << C[i]; }
Результат выполнения программы:
0 A 1 B 2 С 3 D 4 E
В программе шаблон семейства классов с общим именем vector используется для формирования двух классов с массивами целого и символьного типов. В соответствии с требованием синтаксиса имя параметризованного класса, определенное в шаблоне (в примере Vector), используется в программе только с последующим конкретным фактическим параметром (аргументом), заключенным в угловые скобки. Параметром может быть имя стандартного или определенного пользователем типа. В данном примере использованы стандартные типы int и char. Использовать имя Vector без указания фактического параметра шаблона нельзя - никакое умалчиваемое значение при этом не предусматривается.
В списке параметров шаблона могут присутствовать формальные параметры, не определяющие тип, точнее - это параметры, для которых тип фиксирован:
#include <iostream.h>
template <class T, int size = 64>
class row {
T *data;
int length;
public: row() { length = size; data = new T[size]; }
~row() { deleted data; }
T& operator [] (int i) { return data[i]; }
};
void main () {
row <float,8> rf;
row <int> ri;
for (int i = 0; i < 8; i++) { rf[i] = i; ri[i] = i * i; }
for (i = 0; i < 64; i++) cout<<« " " << rf[i] << ' ' << ri[i];
}
Результат выполнения программы:
0 0 1 1 2 4 3 9 4 16 5 25 6 36 7 49
В качестве аргумента, заменяющего при обращении к шаблону параметр size, взята константа. В общем случае может быть использовано константное выражение, однако выражения, содержащие переменные, использовать в качестве фактических параметров шаблонов нельзя.
В 11-ом стандарте языка Си++ появилось понятие объектов типа rvalue и соответстветствующие им типы type&&. Поскольку type&& это тип, то его можно использовать как формалный парметр функции, например, так:
template<class T> void helper(T&& D){}
Возникает вопрос, какой тип будет у параметра шиблона Т и каким будет тип формального параметра D функции helper(T&& D) если вызовы функции будут выглядеть следующим образом:
int i = 1;
const int j = 1;
helper(i); //1 фактический параметр lvalue int
helper(j); //2 фактический параметр const lvalue int
helper(1); //3 фактический параметр rvalue int
В таблице ниже приведены типы параметров.
Тип фактического параметра | Тип параметра шаблога (T) | Тип формального параметра (D) |
lvalue int | int & | int& && приводит к int& |
const lvalue int | const int& | const int& && приводит к const int& |
rvalue int | int | int&& |
Проектные задания
1. Определите новый тип данных, описывающий товар в магазине.
2. Перечислите какие операции определены над структурированными объектами по умолчанию.
3. Назовите какие операции определены над линейным списком.
4. Перечислите чем структура отличается от объединения.
5. Определите с помощью оператора define синоним для имени структурированного типа.
6. Напишите в произвольном порядке фамилии учеников вашей группы и составте на основании этого списка бинарное дерево.
7. Набирите и отладте пример реализации типа «комплексное число».
8. Перепишите класс комплексное число, перегрузив операции +, ‑
9. Перепишите класс комплексное число, сделав его параметризированным
10.
Дата добавления: 2020-12-11; просмотров: 393;