Лекция № 4.
Понятия класса и объекта настолько тесно связаны, что невозможно говорить об объекте безотносительно к его классу. Однако существует важное различие этих двух понятий. В то время как объект обозначает конкретную сущность, определенную во времени и в пространстве, класс определяет лишь абстракцию существенного в объекте. Таким образом, например, можно говорить о классе "Млекопитающие", который включает характеристики, общие для всех млекопитающих. Для указания же на конкретного представителя млекопитающих необходимо сказать "это – млекопитающее" или "то - млекопитающее". Можно поэтому дать еще одно определение класса. Класс - это некое множество объектов, имеющих общую структуру и общее поведение.
Помимо раздельных определения функции-члена в классе и последующей её реализации (как было приведено выше) можно реализовать функцию непосредственно в рамках определения класса:
class Complex {public: int real; // вещественная часть int imaginary; // мнимая часть // прибавить комплексное число void Add(Complex x) { real = real + x.real; imaginary =imaginary + x.imaginary; }};
Конструкторы классов
Объекты нельзя инициализировать так, как это возможно делать для обыкновенных типов данных.
int i=1; // верно
Struct tColor
{
int r;int g;int b;
};
tColor color={255,0,0}; // верно
Complex number={10,6}; // не верно!!!
Причина, по которой нельзя инициализировать объект таким привычным способом, заключается в том, что данные имеют статус собственных. То есть, программа не может получить прямой доступ к элементам данных. Получить доступ к элементам можно в основном только при помощи функций-элементов. Поэтому для инициализации объекта необходимо разработать специальную функцию-элемент.
Существуют специальные функции, получившие названия конструкторов и деструкторов, которые обычно резервируются за каждым классом. Конструкторы как раз решают задачу инициализации объекта.
Функция-конструктор выполняется каждый раз, когда создается новый объект этого класса. Конструктор – это метод, имя которого совпадает с именем класса. Конструктор не возвращает никакого значения.
Конструктор без аргументов называется стандартным конструктором или конструктором по умолчанию. Такой конструктор используется при объявлении такого вида:
Complex number;
Конструктор по умолчанию используется всегда, если для класса не определен никакой другой конструктор. Как только программист определяет свой конструктор, то при создании экземпляра класса этот конструктор и вызывается.
Определим для нашего класса следующий конструктор.
Зададим прототип конструктора:
Complex(int p1,int p2); Реализация конструктора: Complex::Complex(int p1,int p2){ real=p1; imaginary=p2;}Теперь создаем и инициализируем с помощью конструктора объект Complex.
Complex number(10,5);
Однако что делать, если мы не всегда хотим инициализировать объект при его создании? Можно создать дополнительно к нашему созданному конструктору еще один конструктор – пустой.
Прототип конструктора:
Complex(); Реализация конструктора: Complex::Complex(){}Вообще, можно определить несколько конструкторов, каждый из которых будет инициализировать объект класса одним из предопределенных способов. Тогда при создании объекта компилятор будет просматривать все конструкторы, имеющиеся в классе, и вызывать подходящий по прототипу конструктор. Если такового не найдется, то компилятор выдаст сообщение об ошибке.
Итак, наш класс Complex теперь будет выглядеть следующим образом
class Complex {public: int real; // вещественная часть int imaginary; // мнимая часть Complex(); // конструктор 1 Complex(int p1,int p2);// конструктор 2 // прибавить комплексное число void Add(Complex x); };
Деструкторы классов
В отличие от конструктора, который вызывается для инициализации объекта при его создании, деструктор является полной противоположностью конструктора.
В момент завершения существования объекта, например, когда объект удаляется, программа автоматически вызывает специальную функцию-элемент, которая называется деструктором. Деструктор должен уничтожить весь оставшийся от объекта "мусор".
Например, если ваш конструктор использует спецификатор new для выделения памяти, то деструктор с помощью оператора delete освобождает эту занятую память.
Как и конструктор, деструктор имеет специальное имя: имя класса, которому предшествует тильда (~). Добавим для нашего класса Complex деструктор.
Прототип деструктора:
~Complex(); Поскольку у деструктора класса Complex нет никаких важных обязанностей, мы можем закодировать его как функцию, которая не выполняет никаких действий. Итак, реализация деструктора будет следующая: Complex::~Complex(){}Однако только для того, чтобы можно было увидеть, когда производится обращение к деструктору, запишем его в таком виде:
Complex::~Complex(){cout «"Bye!\n";
}Деструктор всегда вызывается автоматически, когда объект класса прекращает функционировать. Если программист не позаботился о своем деструкторе, то компилятор предоставит деструктор, заданный по умолчанию, который не выполняет никаких действий.
Интерфейс и состояние объекта
Основной характеристикой класса с точки зрения его использования является интерфейс, т.е. перечень методов (функций), с помощью которых можно обратиться к объекту данного класса. Кроме интерфейса, объект обладает текущим значением или состоянием, которое он хранит в атрибутах класса. В С++ имеются богатые возможности, позволяющие следить за тем, к каким частям класса можно обращаться извне, т.е. при использовании объектов, а какие части являются "внутренними", закрытыми, необходимыми лишь для реализации интерфейса. Данный механизм контроля и управления доступом к атрибутам и методам класса есть не что иное, как инкапсуляция, о которой мы говорили выше. Она позволяет какие-то элементы интерфейса скрыть от доступа из вне, а к каким-то элементам разрешить доступ. Рассмотрим данный механизм в действии.
Определение класса можно поделить на три части –
ü внешнюю,
ü внутреннюю,
ü защищенную.
Внешняя часть предваряется ключевым словом public, после которого ставится двоеточие. Внешняя часть – это определение интерфейса. Методы и атрибуты, определенные во внешней части класса, доступны как объектам данного класса, так и любым функциям и объектам других классов. Другими словами, все переменные-члены и методы, описанные с атрибутом Public – доступны и открыты всем, кто видит определение данного класса.
Определением внешней части мы контролируем способ обращения к объекту.
Предположим, мы хотим определить класс для работы со строками текста. Прежде всего, нам надо соединять строки, заменять заглавные буквы на строчные и знать длину строк. Соответственно, эти операции мы поместим во внешнюю часть класса:
class String{ public: //добавить строку в конец текущей строки void Concat(const String str); // заменить заглавные буквы на строчные void ToLower(void); // сообщить длину строки int GetLength(void) const;...};
Внутренняя и защищенная части класса доступны только при реализации методов этого класса, а также так называемымми функциями-“друзьями” класса.
Внутренняя часть предваряется ключевым словом private. Для всех переменных-членов и методов, описанных с атрибутом Private, – доступ открыт только самому классу (т.е. функциям-членам данного класса) и так называемым друзьям (friend) данного класса, как функциям, так и классам.
Защищенная часть описывается ключевым словом protected. В отличие от внутренней части (private), элементы защищенной части (protected) также могут быть доступны и для методов и функций классва, для которых данный класс является базовым, то есть, для производных классов при наследовании. То есть, если мы хотим, чтобы какая-то функция класса была доступна его потомку при реализации наследования, то она может быть описана с атрибутом protected.
Ключевые слова public, private, protected называются атрибутами доступа при описании классов.
Рассмотрим пример использования атрибутов доступа.
class String{ public: //добавить строку в конец текущей строки void Concat(const String str); //заменить заглавные буквы на строчные void ToLower(void); // сообщить длину строки int GetLength(void) const; private: char* str; int length;};
В большинстве случаев атрибуты во внешнюю часть класса не помещаются, поскольку они представляют состояние объекта, и возможности их использования и изменения должны быть ограничены. Представьте себе, что произойдет, если в классе String будет изменен указатель на строку без изменения длины строки, которая хранится в атрибуте length.
Объявляя атрибуты str и length как private, мы говорим, что непосредственно к ним обращаться можно только при реализации методов класса, как бы изнутри класса.
Например:
int String::GetLength(void) const{ return length;}Внутри определения методов класса можно обращаться не только к внутренним атрибутам текущего объекта, но и к внутренним атрибутам любых других известных данному методу объектов того же класса.
Реализация метода Concat будет выглядеть следующим образом:
void String::Concat(const String x){ length += x.length; char* tmp = new char[length + 1]; strcpy(tmp, str); strcat(tmp, x.str); delete [] str; str = tmp;}Однако если в программе будет предпринята попытка обратиться к внутреннему атрибуту или методу класса вне определения метода, компилятор выдаст ошибку, например:
main(){ String s; if (s.length > 0) // ошибка... }При описании (определении) классов мы помещаем первой внешнюю часть, затем защищенную часть и последней – внутреннюю часть. Дело в том, что внешняя часть определяет интерфейс, использование объектов данного класса. Соответственно, при чтении программы эта часть нужна прежде всего. Защищенная часть необходима при разработке зависимых от данного класса новых классов. И внутреннюю часть требуется изучать реже всего – при разработке самого класса.