Перейти к основному содержимому

Пример 1. Пример перехода от структуры к классу

/*
* Каждая строка файла содержит запись об одном сотруднике. Первая запись в файле –
* фактическое число сотрудников. Формат записи:
* - фамилия (не более 20 позиций),
* - год рождения (4 позиции),
* - оклад (не более 8 позиций).
* Написать программу, которая позволяла бы выводить на экран сведения о сотрудниках,
* добавлять и удалять сотрудников из БД, корректировать данные о сотрудниках.
*/

#include <iostream>
#include <iomanip>
#include <fstream>

using namespace std;

#define 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\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:

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 l_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 в методы класса неявно, скрытым первым параметром, передается адрес объекта, вызвавшего метод.