Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

OOP / Лекция 3

.pdf
Скачиваний:
27
Добавлен:
20.04.2015
Размер:
172.35 Кб
Скачать

Лекция 3

1. Объявление классов и объектов

Класс – это определяемый пользователем тип данных, описывающий с точки зрения объектно-ориентированного программирования некоторый объект из предметной области решаемой задачи. Объект – это экземпляр класса, переменная типа, описываемого классом. Таким образом, разница между понятиями класс и объект схожа с разницей между понятиями «тип данных» и «экземпляр типа». Объект существует в памяти, класс же является шаблоном, по которому создается объект. Определение класса в программе можно сделать одним из трех способов с использованием ключевых слов class, struct или union:

сlass имя класса { список членов}; struct имя класса { список членов}; union имя класса { список членов};

Здесь имя класса – имя определяемого пользовательского типа данных, список членов – список свойств и методов класса, сделанных согласно синтаксису языка С++. Различия между тремя представленными объявлениями класса заключаются, вопервых, в разных правах доступа, присваиваемых компонентам класса по умолчанию, а также в способе расположения компонент класса в памяти. Для классов, объявленных с использованием ключевых слов srtuct и union, компоненты класса по умолчанию являются доступными для окружения. Для классов, объявленных через class, компоненты по умолчанию недоступны вне класса. Рассмотрим пример объявления класса, описывающего работу с одномерным массивом целых чисел.

//Листинг 1. Определение класса – «массив целых чисел» struct array

{ int *mas; // указатель на начало массива int n; //количество элементов в массиве

void InitMas(int k) //функция инициализации — выделения динамической памяти под массив

{if (k>0) {n=k;

mas=new int[n];

}}

void DelMas() //функция уничтожения массива {n=0;

delete []mas;

}

void ReadMas() //функция ввода массива в клавиатуры

{cout<<"Вывод массива"; for (int i=0;i<n;i++) cin>>mas[i];

}

void WriteMas() //функция вывода элементов массива на экран

{cout<<"Вывод массива";

for (int i=0;i<n;i++) cout<<mas[i]; cout<<'\n';

}};

В определении класса описаны два компонентных члена данных: n и mas. Они хранят некоторые значения, описывающие параметры класса-массива. Также в классе определены компонентные функции или методы класса: InitMas, DelMas, ReadMas и WriteMas, назначение которых – обрабатывать данные, хранящиеся в членах данных класса. Согласно принципу инкапсуляции, методы класса являются его интерфейсной частью, посредством использования этих методов необходимо работать с массивом. Набор методов класса невелик, он позволяет лишь выделять и освобождать динамическую память под массив, а также вводить с клавиатуры и выводить на экран значения элементов массива. Теперь рассмотрим пример использования определенного выше класса.

//Листинг2. Пример использования объектов класса «массив целых чисел»из листинга 1

main()

{ array m,m1; m.InitMas(4); m1.InitMas(5); m.ReadMas(); m.WriteMas(); m1.ReadMas(); m1.WriteMas(); m1.DelMas(); m.DelMas();

}

Общий синтаксис определения объекта класса не отличается от определения обычной переменной:

имя_класса имя_объекта;

В приведенном примере сначала определяются два объекта класса array с именами m и m1. При этом в памяти выделяется по 6 байт на каждый объект: 2 под n и 4 под mas (при условии использования far указателей). При создании объекта память выделяется только под компонентные данные, методы класса существуют в памяти в единственном экземпляре и все объекты используют их совместно. Далее для каждого из объектов вызываются методы класса. Обращение к методам и компонентным данным возможно через имя уже определенного объекта по следующему синтаксису:

имя_объекта.имя_члена_данных имя_объекта.имя_метода(список_фактических_параметров)

Необходимо отметить, что метод класса нельзя вызвать независимо от объекта. Если рассмотреть, например, тело функции InitMas, то можно заметить, что эта функция изменяет значения компонентных данных n и mas. Однако нигде внутри тела метода не определяется, с компонентами какого именно объекта должна работать функция. Очевидно, что в теле класса это и невозможно определить, поскольку данный метод будет обрабатывать компонентные данные различных объектов. Привязка метода класса к конкретному объекту осуществляется в момент вызова метода. Компонентные функции при их вызове неявно получают дополнительный аргумент - указатель на переменную объектного типа, для которой вызвана функция, и в теле функции можно обращаться ко всем компонентам объекта. Так, например, для вызова m.InitMas(4) компонентная функция InitMas будет работать с компонентами объекта m, а для вызова m1.InitMas(5)- с компонентами объекта m1. Память под объект можно выделять динамически:

//Листинг3. Обращение к компонентам класса при динамическом выделении памяти под объект

main()

{ array *ptm; ptm=new array;

ptm->InitMas(4); //можно также (*ptm).InitMas(4) ptm->ReadMas();

ptm->WriteMas(); prm->DelMas();

}

2. Конструкторы и деструкторы

Приведенный в листинге 1 пример класса-массива обладает рядом недостатков. В частности, возможна такая работа с объектом:

//Листинг 4.Пример неверного обращения к методам класса «массив целых чисел» из листинга 1

main()

{ array m; m.ReadMas(); m.WriteMas();

}

Проблема здесь заключается в том, что для класса не предусмотрена защита

от некорректных вызовов методов, и метод чтения массива ReadMas может быть вызван еще до инициализации массива, то есть без выделения памяти под него. Это обязательно в дальнейшем приведет к потере данных. Таким образом, можно сказать, что для данного класса не продуман как следует интерфейс, который бы обеспечивал целостность объекта при любых операциях с ним. Решением проблемы могло бы стать введение дополнительного члена данных, который своим значением определял, проинициализирован ли массив или нет. Переопределим класс array:

//Листинг 5. Решение проблемы некорректности интерфейса класса введением дополнительного

//компонентного данного struct array

{int *mas, n; int present;

void InitMas(int k) {if (!present)

{if (k>0)

{n=k;

mas=new int[n]; present=1;

}

}

else cout<<”Память уже выделена”;

}

void DelMas() //функция уничтожения массива {if (present)

{n=0; delete []mas; present=0;

}

else cout<<”Память не была выделена”;

}

void ReadMas() //функция ввода массива в клавиатуры {if(present)

{cout<<"Ввод массива";

for (int i=0;i<n;i++) cin>>mas[i];

}

else cout<<”Ошибка! Память под массив не выделена”;

}

};

В программе из листинга 5 в класс введен дополнительный компонент present, который принимает единичное значение, когда память под массив выделена, и нулевое – в противном случае. При такой реализации методов класса их можно вызывать в программе в любой последовательности. Необходимо только позаботиться, чтобы при определении класса начальное значение свойтва present было равно нулю. Начальная инициализация члена данных может быть осуществлена аналогично инициализации полей структуры. array m={NULL, 0, 0}; Однако, такой способ инициализации компонентных данных не всегда удобен, поскольку при создании объекта зачастую необходимо не просто присвоить некоторые начальные значения компонентным данным, но и выполнять ряд действий: выделить динамическую память, открыть файл и т.п. В рассматриваемом примере с классом-массивом, например, при создании объекта было бы полезно сразу выделить под него динамическую память, что позволит избавиться от проблемы работы с неинициализированным объектом без введения дополнительной компоненты present. Для этих целей в класс вводится специальная компонентная функция, называемая конструктором. Конструктор – это метод класса, имя которого совпадает с именем класса. Конструктор вызывается автоматически после выделения памяти для переменной и обеспечивает инициализацию компонент-данных. Конструктор не имеет никакого типа (даже типа void) и не возвращает никакого значения в результате своей работы. Конструктор нельзя вызывать как обычную компонентную функцию в программе. Вызов конструктора в программе выглядит следующим образом:

имя_класса имя_объекта ( фактические_параметры_конструктора ); имя_класса * имя_указателя = new имя_класса(фактические_ параметры_ кон-

структора ); Для класса может быть объявлено несколько конструкторов, различающихся

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

//Листинг 6. Конструкторы по умолчанию struct MyClass

{//конструкторы по-умолчанию (создаются компилятором) MyClass() // без параметров

{…}

MyClass(MyClass &copy) //конструктор копирования {…} };

main()

{

MyClass m; //вызов конструктора без параметров MyClass m1(m);//вызов конструктора копирования

}

Для класса array вместо метода InitMas необходимо определить конструктор, который выделял бы динамически память под массив.

//Листинг 7. Переопределение класса «массив целых чисел» с использованием конструктора

struct array

{

array(int k) {if(k>0)

{n=k;

mas=new int[n];

}

}

};

main()

{ array m(5); //вызов конструктора. Память выделяется под 5 элементов массива

m.ReadMas(); //ввод элементов массива с клавиатуры

}

Описание конструктора можно упростить, если компонентные данные принадлежат к базовым типам или являются объектными переменными, имеющими конструктор. При описании конструктора после заголовка функции можно поставить двоеточие и за ним список инициализаторов вида имя_компонента (выражение). Например, для класса array можно было определить конструктор так:

class array ( .....

public:

array ( int k): n(k) {mas=new int[n] ;

}

}; Еще одним специальным методом класса является деструктор. Деструктор

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

~имя_класса() {тело деструктора} Деструктор всегда имеет то же имя, что и имя класса, но перед именем

записывается знак ~ (тильда). Деструктор не имеет параметров и подобно конструктору не возвращает никакого значения. Таким образом, деструктор не может быть перегружен и должен существовать в классе в единственном экземпляре.

Деструктор вызывается автоматически при уничтожении объекта.

Таким образом, для статически определенных объектов деструктор вызывается, когда заканчивается блок программы, в котором определен объект (блок вданном случае – составной оператор или тело функции). Для объектов, память для которых выделена динамически, деструктор вызывается при уничтожении объекта операцией delete.

//Листинг 8. Вызов деструктора объекта main()

{MyClass m; //создание объекта статически

MyClass *ptm=new MyClass; //создание объекта динамически

delete ptm; //вызов деструктора для динамического объекта

//вызов деструктора для статического объекта

}

Определим деструктор для класса array. struct array

{ … ~array()

{delete []mas;

}

}; Деструктор в отличие от конструктора допускает явный вызов вида:

имя_обекта.~имя_класса() адрес_объекта->~имя_класса()

3.Область видимости компонент класса

Урассмотренного выше класса array есть еще один существенный недостаток – все компоненты этого класса являются общедоступными. Это означает, что к любому компонентному данному объекта можно обратиться, например, из функции main(), минуя вызов методов класса.

//Листинг 9. Нарушение принципа инкапсуляции main()

{ array m(10); delete m.mas;

for(int i=0;i<m.n;i++) cin>>m.mas[I];

}

Таким образом, проигнорировав интерфейс объекта, программист может работать с компонентными данными, и целостность объекта не может быть гарантирована. Выход из создавшегося положения уже известен – необходимо выделить в объекте интерфейс и внутреннюю реализацию и запретить доступ из окружения объекта к компонентам, составляющим его внутреннюю реализацию. Для каждого компонента класса устанавливается область видимости либо явно - указанием уровня доступа одним из ключевых слов public, protected или private с двоеточием, либо неявно - по умолчанию. Указание области видимости относится ко всем последующим компонентам класса, пока не встретится указание другой области видимости. Область видимости public разрешает доступ к компонентам класса из любого места программы, в котором известна переменная этого класса. Такие компоненты называются общедоступными. Область видимости private разрешает доступ к компонентам класса только из методов этого класса. Компоненты, определенные с такой областью видимости, называются частными. Область видимости protected определяется для защищенных компонент, она имеет смысл только в иерархической системе классов и разрешает доступ к компонентам этой области из методов производного класса. В теле класса каждое из ключевых слов public, protected, private может указываться неоднократно, то есть в классе может быть два и более блока общедоступных компонент, частных компонент, защищенных компонент. По умолчанию для всех компонент класса типа struct принимается область видимости public, но можно явно задавать и другие уровни доступа, область видимости к

компонентам класса типа class по умолчанию private, явно можно определять и другие уровни, для класса типа union область видимости public и не может быть изменен.

// Определение областей видимости компонент для класса, объявленного через

struct

struct MyClass

{ … //здесь определяются общедоступные компоненты класса private:

//здесь определяются частные компоненты класса protected:

//здесь определяются защищенные компоненты класса public:

//здесь определяются общедоступные компоненты класса

};

//Определение областей видимости компонент для класса, объявленного через

class

class MyClass

{ … //здесь определяются частные компоненты класса protected:

//здесь определяются защищенные компоненты класса public:

//здесь определяются общедоступные компоненты класса private:

//здесь определяются частные компоненты класса

};

Изменим тело класса array так, чтобы он полностью удовлетворял требованиям принципа инкапсуляции.

//Листинг 10.Переопределение класса «массив целых чисел» с ограничением прав доступа к

//внутренней реализации объекта class array

{ int *mas, n; public:

array(int k) {…} //конструктор и деструктор должны быть общедоступными. ~array() {…} // Иначе нельзя будет создать и уничтожить объект класса ReadMas() {…}

WriteMas(){…}

};

main() {array m(5);

m.n=10; //Ошибка!!! Компонент n частный и доступ к нему из функции main запрещен

m.ReadMas(); // Метод RadMas – общедоступный, что допускает обращение к нему из

// внешней функции main()

}

После определения класса приведенным в последнем примере способом, обращение к компонентам n и mas разрешено только из методов класса. Функция main() не является методом класса и обращаться в ней к частным компонентам класса запрещено, что и отмечено в примере. Попытка откомпилировать данный пример приведет к ошибке на этапе компиляции программы «array::n is not accessible»

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

4. Определение компонентных функций класса

Компонентные функции класса могут быть определены как в теле класса (такой способ использован во всех приведенных примерах), так и за телом класса на внешнем уровне программы. Для методов класса, определенных внутри тела класса, компилятор применяет модификатор inline, то есть объявляет такие методы

встраиваемыми (подставляемыми) функциями. Таким образом, на эти функции накладываются ограничения, аналогичные ограничениям для inline-функций (не могут содержать циклов, оператор-переключатель и оператор безусловного перехода, не могут быть рекурсивными и т.д.). Если метод класса определен в теле класса и не удовлетворяет какому-либо из перечисленных условий (например, содержит оператор цикла for), то компилятор транслирует данный метод как обычную, не подставляемую функцию, при этом выводя предупреждение времени компиляции. Для компонентных функций, определенных вне тела класса, никаких ограничений не существует, подставляемыми по умолчанию они не объявляются и предупреждений при их компиляции не возникает. При объявлении метода вне тела класса внутри класса помещается лишь прототип метода, а на внешнем уровне программы вслед за объявлением класса помещается определение функции в виде:

тип_функции имя_класса :: имя_метода (список_формальных_параметров)

{тело метода}

Для класса array на внешнем уровне можно определить функции ReadMas и WriteMas, поскольку они содержат циклы.

// Листинг 11. Определение метода класса на внешнем уровне class array

{

public:

….

void ReadMas(); void WriteMas(); };

void array::ReadMas()

{cout<<"Ввод массива";

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

cin>>mas[i];

}

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

Соседние файлы в папке OOP