Интерфейс и состояние объекта

Лекция № 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) // ошибка . . . }

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

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