Оценка качества декомпозиции проекта

Лабораторная работа 1

Разработка и использование класса Планета.

Цели работы:

- создание программных объектов пользовательских типов с использованием классов;

- создание многофайловых проектов (заголовочный файл для класса, файл с определением методов класса и файл с main-функцией);

- ввод-вывод объектов пользовательских типов в файл;

- перегрузка операций потокового ввода-вывода (<< , >>), операций отношения (<,==) для пользовательских типов;

- перегрузка конструктора копирования и операции присваивания;

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

Время выполнения работы – 12 час. (3 занятия).

Задание.

Лабораторная работа 1.1

1.1. Ознакомиться с приведенным ниже материалом (Введение и Пример 1). Перейти в примере 1 от структуры к классу.

1.2. Разработать класс «Планета» для планет солнечной системы (4 характеристики планет разного типа приведены в Приложении 1).Имя планеты должно иметь тип char*.

1.3. Создать многофайловый проект и отладить программу, которая создает один объект класса «Планета» и выводит значения его полей на экран. (Пример программы приведен в Приложении 3).

1.4. Организовать интерфейс пользователя с программой в виде меню, позволяющего выполнять следующие действия:

- чтение БД «Солнечная система» из файла;

- запись БД «Солнечная система» в файл;

- сортировка БД;

- редактирование БД;

- вывод БД на экран.

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

1.6. Добавить в программу ввод – вывод объекта класса «Планета» в текстовый файл.

Лабораторная работа 1.2 (продолжение лабораторной работы 1)

2.1. Ознакомиться с содержанием раздела «Перегрузка операций» и выполнить приведенный в нем пример 2.

2.2. Перегрузить операцию “ >> “ для класса «Планета» и ifstream и прочитать данные о планетах из файла в массив «Солнечная система» из объектов класса «Планета».

2.3. Перегрузить операцию “ << “ для классов «Планета» и ofstream и вывести на экран данные из массива.

2.4. Перегрузить конструктор копирования, деструктор и оператор присваивания.

2.5. Вставьте в конструкторы и деструктор печать типа «Создание (Удаление) ID n » , где n - номер объекта, для которого они вызываются. (Для реализации этого пункта задания разберите и выполните пример из Приложения 2.).

Лабораторная работа 1.3 (продолжение лабораторной работы 1)

3.1. Перегрузить операции сравнения “ < и ==” для класса «Планета», использовав для этого значение одного из полей.

3.2. Отсортировать массив планет солнечной системы, хранящийся в файле, с использованием перегруженных операций.

3.4. Выполните следующий эксперимент.

- Измените функцию сравнения так, чтобы параметры передавались по значению (а не по ссылке). Выполните сортировку и определите максимальное число объектов типа Planeta в памяти и как изменился вызов конструктора копирования. Объясните результат.

- Исключите (временно) из класса Planeta деструктор. Выполните несколько раз сортировку и определите максимальное число объектов типа Planeta в памяти. Объясните результат.

- Восстановите исходную программу (передача параметров по ссылке и наличие деструктора).

Требования к отчету

1. По работам 1.1, 1.2 и 1.3 составляется один отчет.

2. Отчет должен содержать следующие разделы:

· «Постановка задачи», в котором на основании задания уточняются задачи, для решения которых предполагается использовать разрабатываемый класс.

· «Разработка интерфейса класса», в котором описываются и обосновываются состав полей и методов класса, прототипы методов. Интерфейс класса должен обеспечить решение всех предполагаемых задач. При разработке интерфейса класса надо руководствоваться принципом: второстепенные детали или детали реализации должны быть упрятаны (инкапсулированы) внутрь класса.

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

· «Анализ результатов», в котором приводятся тестовые примеры, распечатки результатов выполнения программой тестовых примеров и анализ результатов.

Введение

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

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

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

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

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

Объектно-ориентированная программа строится в терминах объектов (типа «класс») и их взаимосвязей, а декомпозиция представляет собой иерархию классов, при которой потомки наследуют свойства своих предков, могут их изменять и добавлять новые. Свойства при наследовании повторно не описываются, что сокращает объем программы. Иерархия должна строиться таким образом, чтобы классы, стоящие выше в иерархии, содержали бы общие свойства классов – потомков.

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

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

Оценка качества декомпозиции проекта

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

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

Слово «клиент» означает просто-напросто компонент, которо­му понадобились услуги другого компонента, исполняющего в этом случае роль сервера. Взаимоотношение клиент/сервер на самом деле очень старо и использо­валось уже в рамках структурного программирования, когда функция-клиент пользовалась услугами функции-сервера путем ее вызова.

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

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

□ Связанность) между компонентами — показатель, описывающий интерфейс между компонентом-клиентом и компонентом-сервером. Общее число входов и выходов сервера есть мера связанности. Чем меньше связанность между двумя компонентами, тем проще понять и отслеживать в будущем их взаимодействие. А так как в больших проектах эти компоненты часто разрабатываются разными людьми, то очень важно уменьшать связанность между компонентами.

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

Что принесло с собой ООП

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

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

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

Инкапсуляция — это ограничение доступа к данным и их объединение с метода­ми, обрабатывающими эти данные. Доступ к отдельным частям класса регулиру­ется с помощью специальных ключевых слов: public (открытая часть), private (закрытая часть) и protected (защищенная часть).

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

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

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

С ООП связаны еще два инструмента, грамотное использование которых повы­шает качество проектов: наследование классов и полиморфизм.

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

Полиморфизм дает возможность создавать множественные определения для опе­раций и функций. Какое именно определение будет использоваться, зависит от контекста программы. Вы уже знакомы с одной из разновидностей полиморфизма в языке C++ — перегрузкой функций.

В реальном проекте, разработанном на базе объект­но-ориентированной декомпозиции, находится место и для алгоритмически-ориентированной декомпозиции (например, при реализации сложных методов).

От структуры — к классу

Прообразом класса в C++ является структура в С. В то же время в C++ структу­ра обрела новые свойства и теперь является частным видом класса, все элементы которого по умолчанию являются открытыми. Со структурой struct в C++ мож­но делать все, что можно делать с классом. Тем не менее в C++ структуры обыч­но используют лишь для удобства работы с небольшими наборами данных без какого-либо собственного поведения.

Пример 1.

/*В текстовом файле хранится база отдела кадров предприятия. На предприятии не более 100 сотрудников.

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

фактическое число сотрудников. Формат записи:

фамилия (не более 20 позиций),

год рождения (4 позиции),

оклад (не более 8 позиций).

Написать программу, которая позволяла бы выводить на экран сведения о сотрудниках,

добавлять и удалять сотрудников из БД, корректировать данные о сотрудниках.

*/

#include <iostream>

#include <iomanip>

#include <fstream>

#include "d:\\C++\\MyLib\\ConsolCyr.h"

using namespace std;

const int l_name = 20;

struct Man {

char name[l_name];

int birth_year;

float pay;

};

int read_dbase(char* filename,Man* arr,int& n);

int menu();

int menu_f();

void print_dbase(Man* arr,int n);

int write_dbase(char* filename, Man* arr, int n);

/*

int add(Man arr, int n);

int edit(Man* arr, int n);

int remove(Man* arr, int n);

*/

int find(Man* arr,int n, char* name);

int find(Man* arr,int n, int birth_year);

int find(Man* arr,int n, float pay);

void find_man(Man* arr,int n);

//---------------------------------------- Главная функция

int main()

{

const int N=100;

Man arr[N];

char *filename="dbase.txt";

int n;

//чтение БД в ОП

if (read_dbase(filename,arr,n)){

cout<<"Ошибка чтения БД"<<endl;

return 1;

}

print_dbase(arr,n);

while (true) {

switch (menu())

{

//case 1: add(arr,n ); break;

//case 2: remove(arr,n); break;

case 3: find_man(arr,n); break;

//case 4: edit(arr,n); break;

case 5: print_dbase(arr,n); break;

case 6: write_dbase(filename,arr,n); break;

case 7: return 0;

default: cout<<" Недопустимый номер операции"<<endl; break;

}

}

return 0;

}

////////////////////////////////////////////////////////

int menu( )

{

cout<<" ============== ГЛАВНОЕ МЕНЮ ========================\n";

cout<<"l - добавление сотрудника\t 4 - корректировка сведений"<<endl;

cout<<"2 - удаление coтpyдникa\t\t 5 - вывод базы на экран"<<endl;

cout<<"3 - поиск сотрудника\t\t 6 - вывод базы в файл"<<endl;

cout<<"\t\t\t 7 - выход"<<endl;

cout<<"Для выбора операции введите цифру от 1 до 7"<< endl;

int resp;

cin>>resp;

cin.clear();

cin.ignore(10,'\n');

return resp;

}

// ------------------------------------ Чтение базы из файла

int read_dbase(char* filename, Man* arr, int& n)

{

ifstream fin(filename,ios::in);

if (!fin)

{

cout<<"Heт файла "<< filename <<endl;

return 1;

}

fin>>n;

if(n>100)

{

cout<<"Переполнение БД. n= "<< n <<endl;

return 1;

}

for(int i=0;i<n;i++)

fin>>arr[i].name>>arr[i].birth_year>>arr[i].pay;

fin.close();

return 0;

}

//------------------------------------ Вывод базы в файл

int write_dbase(char *filename, Man* arr,int n)

{

ofstream fout(filename,ios::out);

if (!fout)

{

cout<<"Ошибка открытия файла"<<endl;

return 1;

}

fout<<n;

for(int i=0; i<n; i++)

fout<< arr[i].name<<' '<< arr[i].birth_year<<' '<< arr[i].pay<<endl;

fout.close();

return 0;

}

//------------------------------ Вывод базы на экран

void print_dbase(Man* arr,int n)

{

cout<<" База Данных "<<endl;

for(int i=0;i<n;i++)

cout<<setw(3)<<i+1<<". "<<arr[i].name<<setw(20-strlen(arr[i].name)+6)

<<arr[i].birth_year<<setw(10)<<arr[i].pay<<endl;

}

//-----------------------------Поиск сотрудника в списке по фамилии

int find(Man* arr , int n, char* name) //возвращает индес элемента с данными о

//сотруднике в БД,реализованной в виде массива

{

int ind=-1;

for(int i=0;i<n;i++)

if (!strcmp(arr[i].name, name))

{

cout<<arr[i].name<<setw(20-strlen(arr[i].name)+6)

<<arr[i].birth_year<<setw(10)<<arr[i].pay<<endl;

ind=i;

}

return ind;

}

//------- Поиск и вывод более старших по возрасту сотрудников

int find(Man* arr , int n, int birth_year)

{

int ind=-1;

for(int i=0;i<n;i++)

if (arr[i].birth_year < birth_year)

{

ind=i;

cout<<arr[i].name<<setw(20-strlen(arr[i].name)+6)

<<arr[i].birth_year<<setw(10)<<arr[i].pay<<endl;

}

return ind;

}

//-------- Поиск и вывод сотрудников с окладом, большим чем "pay"

int find(Man* arr , int n, float pay)

{

int ind=-1;

for(int i=0;i<n;i++)

if (arr[i].pay > pay)

{

ind=i;

cout<<arr[i].name<<setw(20-strlen(arr[i].name)+6)

<<arr[i].birth_year<<setw(10)<<arr[i].pay<<endl;

}

return ind;

}

//--------------------------------------

int menu_f()

{

cout<<"\n----------------- ПОИСК -----------------\n";

cout<<"1 - поиск по фамилии 2 - по году рождения\n"

<<"3 - по окладу 4 - конец поиска\n ";

cout<<"Для выбора операции введите число от 1 до 4\n";

int resp;

cin>> resp;

cin.clear();

cin.ignore(10,'\n');

return resp;

}

//------------------------------------- Поиск

void find_man(Man* arr, int n)

{

char buf[l_name];

int birth_year;

float pay;

while(true)

{

switch (menu_f())

{

case 1: cout<<"Введите фамилию сотрудника\n";

cin>> buf;

if(find(arr, n, buf)<0)

cout<<"Сотрудника с фамилией "<< buf<<" в списке нет\n";

break;

case 2: cout<<"Введите год рождения"<< endl;

cin>> birth_year;

if(find(arr, n, birth_year)<0)

cout<< "В списке нет сотрудников, родившихся до "

<<birth_year<<" года\n";

break;

case 3: cout<<"Введите оклад"<< endl;

cin>> pay;

if(find(arr, n, pay)<0)

cout<< "В списке нет сотрудников с окладом, большим "

<< pay<<" руб.\n";

break;

case 4: return;

default:

cout<<"Неверный ввод\n";

}

}

}

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

const int l_name=30;

struct Man {

char name[l_name];

int birth_year;

float pay;

};

Начнем с того, что преобразуем эту структуру в класс, так как мы предполагаем, что наш новый тип будет обладать более сложным поведением, чем просто чте­ние и запись его полей:

class Man {

char name[l_name];

int birth_year;

float pay; };

Замечательно. Это у нас здорово получилось! Все поля класса по умолчанию — закрытые (private). Так что если клиентская функция main() объявит объект Man man, а потом попытается обратиться к какому-либо его полю, например: man.pay = value, то компилятор быстро пресечет это безобразие, отказавшись компилиро­вать программу. Поэтому в состав класса надо добавить методы доступа к его по­лям. Эти методы должны быть общедоступными, или открытыми (public).

Однако предварительно вглядимся внимательнее в определения полей. В решении задачи на языке Си поле name объявлено как статический массив длиной l_name. Это не очень гибкое решение. Мы хотели бы, чтобы наш класс Man можно было использовать в будущем в разных приложениях. Например, если предприятие находится в России, то значение 1_name = 20, по-видимому, всех устроит, если же приложение создается для некой восточной страны, может потребоваться, ска­жем, значение l_name = 200. Решение состоит в использовании динамического мас­сива символов с требуемой длиной. Поэтому заменим поле char name[l_name] на поле char* pName. Сразу возникает вопрос: кто и где будет выделять память под этот массив? Вспомним один из принципов ООП: все объекты должны быть самодостаточными, то есть полностью себя обслуживать.

Таким образом, в состав класса необходимо включить метод, который обеспечил бы выделение памяти под указанный динамический массив при создании объек­та (переменной типа Man). Метод, который автоматически вызывается при созда­нии экземпляра класса, называется конструктором. Компилятор безошибочно находит этот метод среди прочих методов класса, поскольку его имя всегда сов­падает с именем класса.

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

Ясно, что если в конструкторе была выделена динамическая память, то в де­структоре нужно побеспокоиться об ее освобождении. Напомним, что объект, созданный как локальная переменная в некотором блоке { }, уничтожается, ко­гда при выполнении достигнут конец блока. Если же объект создан с помощью операции new, например:

Man* pMan = new Man;

то для его уничтожения применяется операция delete, например: delete pMan. Итак, наш класс принимает следующий вид:

class Man { public:

Man(int I_name = 20) { pName = new char[l_name]: } // конструктор

~Man() { delete [] pName: } // деструктор

private:

char* pName;

int birth_year:

float pay: };

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

Рассмотрим теперь одну важную семантическую деталь: в конструкторе класса параметр l_name имеет значение по умолчанию (20). Если все параметры конст­руктора имеют значения по умолчанию или если конструктор вовсе не имеет па­раметров, он называется конструктором по умолчанию. Зачем понадобилось спе­циальное название для такой разновидности конструктора? Разве это не просто удобство для клиента — передать некоторые значения по умолчанию одному из методов класса? Нет! Конструктор — это особый метод, а конструктор по умол­чанию имеет несколько специальных областей применения.

Во-первых, такой конструктор используется, если компилятор встречает опреде­ление массива объектов, например: Man man[25]. Здесь объявлен массив из 25 объ­ектов типа Man, и каждый объект этого массива при создании вызывает конструкто­р по умолчанию! Поэтому если вы забудете снабдить класс конструктором по умолчанию, то вы не сможете объявлять массивы объектов этого класса. Исключение представляют классы, в которых нет ни одного конструктора, так как в та­ких ситуациях конструктор по умолчанию создается компилятором.

Вернемся к приведенному выше описанию класса. В нем методы класса опреде­лены как встроенные (inline) функции. При другом способе методы только объявляются внутри класса, а их реализация записывается вне определения класса, как показано ниже:

// Man.h (интерфейс класса)

class Man {

public:

Man(int I_name = 30); // конструктор

~Man(); // деструктор

private:

char* pName;

int birth_year;

float pay;

};

// Man.cpp (реализация класса)

#include "Man.h"

Man::Man(int l_name) { pName = new char[l_name]; }

Man::~Man() { delete [] pName; }

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

Продолжим процесс проектирования интерфейса нашего класса. Какие методы нужно добавить в класс? С какими сигнатурами? На этом этапе очень полезно задаться следующим вопросом: какие обязанности должны быть возложены на класс Man?

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

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

Для записи значений полей добавим методы SetName(), SetBirthYear(), SetPay(). Чтобы определиться с сигнатурой этих методов, надо представить себе, как они будут вызываться клиентом.

Константные методы.Обратите внимание, что заголовки тех методов класса, которые не должны изменять поля класса, снабжены модификатором const после списка параметров. Если вы по ошибке попытаетесь в теле метода что-либо при­своить полю класса, компилятор не позволит вам это сделать. Другое достоинст­во ключевого слова const — оно четко показывает сопровождающему программисту намерения разработчика программы. Например, если обнаружено некорректное поведение приложения и выяснено, что «кто-то» портит одно из полей объекта класса, то сопровождающий программист сразу может исключить из списка подозреваемых методы класса, объявленные как const. Поэтому использование const в объявлениях методов, не изменяющих объект, считается хорошим стилем программирования.

Отладочная печать в конструкторе и деструкторе.Вывод сообщений типа «Constructor is working», «Destructor is working» очень помогает на началь­ном этапе освоения классов. Да и не только на начальном — мы сможем убе­диться в этом, когда столкнемся с проблемой локализации неочевидных ошибок в программе.

-----------------------------------------------------------------------------------------------------

Вставляйте отладочную печать типа «Здесь был я!» в тела конструкторов и де­структоров, чтобы увидеть, как работают эти невидимки. Использование этого приема особенно полезно при поиске трудно диагностируемых ошибок.

Перегрузка операций

Любая операция, за исключением “::”, “?:”, “.”, “.*”, определенная в C++, может быть перегружена для создан­ного вами класса. Это делается с помощью функций специального вида, назы­ваемых функциями-операциями (операторными функциями). Общий вид такой функции:

возвращаемый_тип operator # (список параметров) { тело функции }

где вместо знака # ставится знак перегружаемой операции.

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

Например, покажем два варианта перегрузки операции сложения для класса Point.

Первый вариант— в форме метода класса:

class Point {

double x, у;

public:

//. . .

Point operator +(Point&);

};

Point Point::operator +(Point& p)

{ return Point(x + p.x, у + р.у); }

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

class Point {

double x, у: public: //. . .

friend Point operator +(Point&, Point&);

};

Point operator +(Point& p1, Point& p2) {

return Point(p1.x + p2.x. p1.у + p2.y);

}

Независимо от формы реализации операции “+” мы можем теперь написать:

Point p1(0, 2), р2(-1, 5);

Point рЗ = p1 + р2;

Следует понимать, что, встретив выражение p1 + р2, компилятор в случае первой формы перегрузки вызовет метод p1.operator +(p2), а в случае второй формы перегрузки — глобальную функцию operator +(pl, р2).

Результатом выполнения данных операторов будет точка р3 с координатами х = -1, у = 7. Заметим, что для инициализации объекта р3 будет вызван конструк­тор копирования по умолчанию, но он нас устраивает, поскольку в классе нет полей-указателей. (Подумайте, почему указатели нельзя просто скопировать, чтобы получить копию объекта?)

Если операция может перегружаться как внешней функцией, так и функцией класса, какую из двух форм следует выбирать? Ответ: используйте перегрузку в форме метода класса, если нет каких-либо причин, препятствующих этому. Например, если первый аргумент (левый операнд) относится к одному из базо­вых типов (к примеру, int), то перегрузка операции возможна только в форме внешней функции. (Вспомните, что по умолчанию с помощью указателя this в методы класса неявно, скрытым первым параметром, передается адрес объекта, вызвавшего метод).

Пример 2.

Пример перегрузки операции << для вывода в файл некоторой структуры element.

//overload.cpp - запись структур в файл перегруженной

// операцией <<

#include <iostream>

#include <fstream>

using namespace std;

struct element { // Определение некоторой структуры

int nk,nl;

float zn;

};

// Операция-функция, расширяющая действие операции <<

ofstream& operator<<(ofstream& out,element el){

out << ' ' << el.nk << ' ' << el.nl <<' ' << el.zn << '\n' ;

return out; }

int main()

{ const int numbeEl = 5; // Количество структур в массиве

element arel[numbeEl] = { 1, 2, 3.45, 2, 3, 4.56,

22, 11, 45.6, 3, 24, 4.33, 3, 6, -5.3 };

// Определяем поток и связываем его с новым файлом abc:

ofstream filel("abc.txt",ios::app);

if (!filel)

{ cout << "Неудача при открытии файла abc.\n";

return 1; }

// Запись в файл abc массива структур:

for (int i = 0; i < numbeEl; i++)

filel << arel[i];

return 0;

}

Результат выполнения программы - создание файла с именем авс в текущем каталоге и запись в этот файл элементов массива из пяти структур element.Содержимое файла abc:

1 2 3.45

2 3 4.56

22 11 45.6

3 24 4.33
3 6 5.3

Приложение 1

Исходные данные

Название Диаметр Жизнь Спутники

Mercury 4878 0 0Venus 12104 0 0Earth 12774 1 1Mars 6786 1 2Jupiter 142796 0 16Saturn 120000 0 17Uranus 51108 0 5Neptune 49600 0 2Pluto 2280 0 1

Приложение 2.

class gamma

{

private:

static int total; //всего объектов

//(только объявление)

int id; //ID текущего объекта

public:

gamma() //конструктор без аргументов

{

total++; //увеличить счетчик объектов

id = total; //id равен текущему значению total

cout << "Создание ID " << id << endl;

}

~gamma() //деструктор

{

total--;

cout << "Удаление ID " << id << endl;

}

static void showtotal() // статическая функция

{

cout << "\nBcero: " << total << endl;

}

void showID() // нестатическая функция

{

cout<< "\nID: " << id << endl;

}

};

//---------------------------------

int gamma:: total=0;

void main()

{ gamma :: showtotal();

gamma g1;

gamma g1.showtotal();

gamma g2,g3;

g3.showtotal();

g1.showID();

g2.showID();

g3.showID();

cout<<”Конец программы”<<endl;

}

//--------------------------------------------------------

Приложение 3

Пример программы, использующей класс «planet»

Файлы:

planet.h – интерфейс класса,

planet.cpp – определение методов класса.

// sunsys.cpp

#include <fstream>

#include "planet.h"

#include "planet.cpp"

//#include "ConsolCyr.h"

using namespace std;

int read_db(char*, Planet*, const int);

int menu();

void print_db(Planet*, int);

int write_db(char*,Planet*, int);

int find(Planet*,int);

void sort_db(Planet*, int);

const int Size = 12;

const int l_record = 80;

int main()

{

char *file_name = "sunsys.txt";

Planet planets[Size];

int n_planet;

int ind;

while (true) {

switch (menu())

{

case 1: n_planet=read_db(file_name,planets,Size);

break;

case 2: write_db(file_name,planets,n_planet); break;

case 3: if((ind=find(planets,n_planet))>=0)

planets[ind].edit();

else

cout<<"Такой планеты нет"<<endl;

break;

case 4: print_db(planets,n_planet); break;

case 5: sort_db(planets,n_planet); break;

case 6: return 0;

default: cout<<" Неправильный ввод"<<endl; break;

}

}

return 0;

}

Наши рекомендации