ЛР1_Классы (1079096), страница 2
Текст из файла (страница 2)
Пример 1.
В текстовом файле хранится база отдела кадров предприятия. На предприятии 100 сотрудников. Каждая строка файла содержит запись об одном сотруднике. Формат записи: фамилия и инициалы (30 позиций, фамилия должна начинаться с первой позиции), год рождения (5 позиций), оклад (10 позиций). Написать программу, которая по заданной фамилии выводит на экран сведения о сотруднике, подсчитывая средний оклад всех запрошенных сотрудников.
В программе, предложенной для решения задачи, при структурном программировании для хранения сведений об одном сотруднике использовалась бы структура 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 = 30, по-видимому, всех устроит, если же приложение создается для некой восточной страны, может потребоваться, скажем, значение 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 = 30) { pName = new char[l_name]: } // конструктор
~Man() { delete [] pName: } // деструктор
private:
char* pName;
int birth_year:
float pay: };
Обратим ваше внимание на одну синтаксическую деталь — объявление класса должно обязательно завершаться точкой с запятой (;). Если вы забудете это сделать, то получите от компилятора длинный список маловразумительных сообщений о чем угодно, но только не об истинной ошибке.
Рассмотрим теперь одну важную семантическую деталь: в конструкторе класса параметр l_name имеет значение по умолчанию (30). Если все параметры конструктора имеют значения по умолчанию или если конструктор вовсе не имеет параметров, он называется конструктором по умолчанию. Зачем понадобилось специальное название для такой разновидности конструктора? Разве это не просто удобство для клиента — передать некоторые значения по умолчанию одному из методов класса? Нет! Конструктор — это особый метод, а конструктор по умолчанию имеет несколько специальных областей применения.
Во-первых, такой конструктор используется, если компилятор встречает определение массива объектов, например: 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(). Чтобы определиться с сигнатурой этих методов, надо представить себе, как они будут вызываться клиентом.
Обратите внимание на следующие моменты.
Ввод/вывод кириллицы.. Если вы работаете в интегрированной среде, поддерживающей кодировку символов в стандарте ASCII (например, любой среде на платформе MS-DOS), то проблем с вводом/выводом кириллицы у вас не будет. Проблемы возникают при работе в интегрированной среде на платформе Windows (например, Visual C++ 6.0), поскольку кодировка по стандарту ANSI, принятая в Windows, отличается во второй (национальной) части кодов от кодировки ASCII.
Заголовочные файлы. Стандартная библиотека C++ имеет несколько реализаций. В первоначальной версии библиотеки использовались заголовочные файлы с расширением .h, например <iostream.h>. Если вы работаете с компилятором, который поддерживает версию библиотеки, вошедшую в стандарт языка ISO/IEC 14882 (1998), то заголовочные файлы можно указывать без расширения, например <iostream>. Кроме того, обычно используется директива using namespace std;, так как все имена в стандартной версии библиотеки принадлежат пространству std. В старых версиях библиотеки отсутствуют некоторые очень удобные средства, например не реализован класс string.
Константные методы. Обратите внимание, что заголовки тех методов класса, которые не должны изменять поля класса, снабжены модификатором 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).
Результатом выполнения данных операторов будет точка рЗ с координатами х = -1, у = 7. Заметим, что для инициализации объекта рЗ будет вызван конструктор копирования по умолчанию, но он нас устраивает, поскольку в классе нет полей-указателей. (Подумайте, почему указатели нельзя просто скопировать, чтобы получить копию объекта?)
Если операция может перегружаться как внешней функцией, так и функцией класса, какую из двух форм следует выбирать? Ответ: используйте перегрузку в форме метода класса, если нет каких-либо причин, препятствующих этому. Например, если первый аргумент (левый операнд) относится к одному из базовых типов (к примеру, int), то перегрузка операции возможна только в форме внешней функции. (Вспомните, что по умолчанию с помощью указателя this в методы класса передается передается адрес объекта, вызвавшего метод).
Пример 2.
Пример перегрузки операции << для вывода в файл некоторой структуры element.
//overload.cpp - запись структур в файл перегруженной
// операцией <<
#include <iostream>
#include <fstream>
using namespace std;