Для каждого класса может существовать только один конструктор по умолчанию

Лабораторная работа №1

СОЗДАНИЕ КЛАССОВ

Цель работы: Получить практические навыки проектирования и реализации классов

1.1. Теоретические сведения

Классы

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

o имя объекта;

o состояние (переменные состояния);

o методы (операции).

Объект ООП — это совокупность переменных состояния и связанных с ними методов (операций). Эти методы определяют как объект взаимодействует с окружающим миром.

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

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

Класс (class) — это группа данных и методов (функций) для работы с этими данными.

Объект (object) — это конкретная реализация, экземпляр класса. В программировании отношения объекта и класса можно сравнить с описанием переменной, где сама переменная (объект) является экземпляром какого — либо типа данных (класса).

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

Методы (methods) — это функции, принадлежащие классу.

Сообщение (message) — это практически тоже самое, что и вызов функций в обычном программировании. В ООП обычно употребляется выражение "послать сообщение" какому-либо объекту. Понятие "сообщение" в ООП можно объяснить с точки зрения основ ООП: мы не можем напрямую изменить состояние объекта и должны как бы послать сообщение объекту, что мы хотим так и так изменить его состояние. Объект сам меняет свое состояние, а мы только его просим об этом посылая сообщения

 

Пример

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

class CWorld
{
}


int main ()
{
CWorld world;
}

Рисунок 1 – Простейшее описание класса

 

О компиляции

Прежде всего, программист с помощью того или иного текстового редактора готовит файлы исходного кода на C/C++. После этого происходит построение программы, в котором можно выделить такие этапы:

· Компиляцию исходных файлов в файлы объектного кода (с расширением .obj).

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

· Компоновку ресурсов (ресурсы включают в себя битовые матрицы, курсоры, строковые таблицы, пиктограммы и т.п.). Это завершающий этап, на котором формируется конечный ехе-файл, запускаемый на выполнение.


Компилятор C/C++ генерирует стандартные объектные файлы с расширением .obj. (Их формат определен фирмой Intel и не зависит от конкретной операционной системы.) Файлы эти содержат машинный код, который снабжен дополнительной информацией, позволяющий компоновщику разрешать ссылки между объектными модулями. Так, в начале файла формируются две таблицы: таблица глобальных символов (это имена объектов, определяемых в данном файле, на которые могут ссылаться другие модули программы) и таблица внешних ссылок (имена объектов в других файлах, к которым обращается данный модуль). Пользуясь информацией этих таблиц, компоновщик модифицирует код, подставляя в него соответствующие адреса.

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

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

Заголовочные файлы (они имеют расширение .h или .hpp) подключаются к компилируемому файлу исходного кода (.с или .срр) с помощью директивы препроцессора #include, за которой следует имя заголовочного файла в кавычках или угловых скобках, например:

#include <stdlib.h>

include "myfile.h"

 

Препроцессор заменяет директиву #include содержимым указанного файла; после завершения препроцессорной обработки полученный текст передается компилятору, который транслирует его в объектный код.

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

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

В С++ раздельная компиляция реализуется путём создания проекта, в который включаются файлы, содержащие реализацию. Обычно, эти файлы имеют расширение «.cpp». При перкомпиляции проекта компилятор проверяет изменения, которые произошли с файлами, включёнными в проект и производит соответствующую компиляцию. Заголовочные файлы (с расширением «.hpp» или «.h») в проект не включаются, но они должны быть включены в каждый файл реализации, в котором используется то, что описано в заголовочных файлах. Типичным примером может служить разделение прототипа класса и реализация его методов. Обычно прототип файла описывается в одноимённом с классом файле .hpp, реализация — в одноимённом файле .cpp. Следует помнить, что препроцессор не проверяет, сколько раз на этапе препроцессинга файл был включен в программу, поэтому необходимо вручную с помощью условных комментариев контролировать единственность включения (например, этого можно достичь, используя #ifndef - #define, как показано в примере ниже).

Пример.Описание класса CWorld в двух файлах для раздельной компиляции.

//Файл CWorld.hpp

#ifndef CWORLD_HPP
#define CWORLD_HPP
#include "CIObject.hpp"
#include "CPlayer.hpp"
#define MAXOBJECTS 20
class CWorld
{
private:
unsigned long time, // общемировое время
btime; // время последнего бонуса
protected:
CIObject *objects[MAXOBJECTS];
int objectsn;

CPlayer player;

public:
CWorld();
~CWorld();
};
#endif

Рисунок 2 – заголовочный файл класса CWorld

 

//Файл CWorld.cpp

#ifndef CWORLD_CPP
#define CWORLD_CPP
#include "CIObject.hpp"
#include "CPlayer.hpp"
#include "CWorld.hpp"
CWorld::CWorld() : player(200,300, 0, 0) // инициализация игрока
{
objectsn=0;
time = 0;
btime = 0;
}

CWorld::~CWorld()
{
delete [] objects;
}
}

#endif

Рисунок 3 – файл реализации класса CWorld

 

Конструкторы

Часто во время создания объекта его элементам необходимо присвоить начальные значения. Сделать это непосредственно в определении класса нельзя, например, следующее описание приведет к ошибке компиляции:

class СWorld

{
unsigned long objectsn=0, // ошибка
time = 0; // ошибка
};

Рисунок 4

Оставлять переменные класса неинициализированными крайне нежелательно, так как в этом случае может возникнуть ситуация некорректной работы программы.

Избежать подобной ситуации можно, например, написав функцию, которая будет присваивать нулевые значения элементам objectsn и time, и вызывать эту функцию каждый раз сразу после объявления объекта. Но в языке С++ есть такой механизм как конструкторы. Конструктор (construct - создавать) - это специальная функция-член класса с тем же именем, что и класс.

class СWorld

{
public:

CWorld(); // конструктор класса CWorld
};

CWorld::CWorld()
{
time = 0;
objectsn = 0;
}

Рисунок 5- Пример конструктора класса CWorld

Конструктор хотя и является методом класса, для него не определяется тип возвращаемого значения. Отсюда вытекает недопустимость использования в конструкторе оператора return. Конструктор автоматически вызывается при создании объекта, т.е не нужно специально вызывать его - это сделает компилятор.

Конструктор без параметров называют конструктором по умолчанию. Такой конструктор обычно присваивает переменным-членам класса наиболее часто используемые значения. В описанном выше примере класса CWorldиспользуется конструктор по умолчанию. В нем переменным time и objectsn присваиваются нулевые значения. Таким образом, после создания экземпляра объекта типа CWorld, он будет содержать допустимые значения, а не "мусор".

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

Если класс имеет конструктор по умолчанию (определенный программистом или созданный компилятором), можно определить объект класса без передачи параметров, например :

 

CWorld world;

Рисунок 6- Пример определения объекта класса CWorld без передачи параметров

Если параметры не передаются конструктору, в определении объекта не нужно включать пустые круглые скобки.

Следующая команда, например, не создает объект типа my_Time, с вызовом конструктора по умолчанию а является определением функции world, которая не принимает параметров и возвращает объект типа CWorld,

 

CWorld world();

Рисунок 7- Пример определения объекта класса CWorld с вызовом конструктора по умолчанию

Для каждого класса может существовать только один конструктор по умолчанию.

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

class CObject

{
float x;
float y;
CObject(float a, float b);

};

СObject::CObject(a,b)
{
x = a;
y = b;
}

Рисунок 8- Пример определения для класса CWorld конструктора с параметрами

Деструкторы

Деструктор решает задачу, обратную задаче конструктора. Деструктор (destruct - разрушать) - это специальная функция-член класса. Имя деструктора состоит из символа тильда (~) и имени класса.

~СWord()

{ // тело деструктора
}

Рисунок 9- Пример определения деструктора для класса CWorld

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

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

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

 

Типы доступа

Член класса может иметь следующие модификаторы доступа:

· public - общедоступный член класса;

· protected - доступный только для членов и объектов данного класса и наследуемых классов (наследуемых с модификаторами доступа public или protected);

· private- защищенный метод, доступный только внутри класса.


class CPlayer: public CIObject
{

private: // приватные поля
float ax; // ускорение
float dx; // направление ускорения
long atime; // время акселерации

protected: // защищённый метод
void accelerate(float d);

public: // открытые методы
CPlayer(){};
~CPlayer(){};
void draw(); // перегружена
void interact(unsigned long t);

};

Рисунок 10- Пример задания прав доступа

 

Рекомендации по использованию прав доступа:

· private используется для полей класса, которые ни в коем случае не должны быть изменены напрямую, даже наследниками этого класса. Типичная ситуация — поле, запрещённое для записи (чтение его происходит через публичный метод-«геттер»). Также этот модификатор доступа может быть использован для методов класса, которые используются только для внутренних нужд.

· protected используется для членов класса, которые должны быть доступны в классах-потомках, но которые не должны быть видны вне класса. Обычно под это определение попадает почти вся логика класса, кроме той, что отмечена, как private.

· public используется для всего, что, будучи использовано, не может повредить работе класса. Обычно, всё, что помечено этим модификатором, является «интерфейсом» класса, с помощью которого происходит взаимодействие между объектом класса и остальной программой. Класс должен быть готов к тому, что в открытые члены могут попасть совершенно невероятные и противоречивые данные, и должен уметь адекватно реагировать на это. Поэтому часто публичными делают только методы класса, и переданные в них данные проверяют.

Кроме указания модификаторов доступа для членов класса, также может быть указаны модификаторы доступа при наследовании класса. Их также три и они влияют на то, как члены базового класса станут видны из класса-потомка.
Изменение это происходи по простым правилам:

· public наследование не изменяет доступ;

· protected наследование превращает public-члены в protected;

· private превращает все члены в private.

 

class CPlayer: public CIObject
{
}

Рисунок 11- Пример public-наследования.

Поля и методы

В терминологии объектно-ориентированного программирования переменные, включённые в класс, называют «поля» или «свойства».
Описание полей мало отличается от описаний переменных, происходит внутри описания класса и имеет вид:

тип_поля имя_поля;

Также в ООП функции называются методами или методами-членами класса, для того чтобы подчеркнуть, что конкретная функция включается в данный класс.
Функция, используемая в качестве члена класса, может иметь прототип, включающий в себя тип функции, имя функции и список параметров с их типами. Прототип — это некоторое предварительное описание функции, заканчивающееся символом «;».

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

Прототип метода может иметь следующее формальное описание:

тип_метода имя_метода (список_параметров);

Объявление метода может иметь следующее формальное описание:

тип_метода имя_метода (список_параметров)
{ тело_метода }

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

 

class CWorld
{

private: // установка прав доступа
unsigned long time, // поле
btime; // поле

protected:
CIObject *objects[MAXOBJECTS]; // поле-массив
int objectsn; // поле

CPlayer player; // поле-объект класса CPlayer

public:

CWorld(); // конструктор
~CWorld(); // деструктор

void render(); // метод
void addObject(CIObject *o); // метод с параметром
void deleteObject(int i); // метод с параметром
void interact(unsigned long t); // метод с параметром

};

Рисунок 12- Пример описания полей и методов

 

Встроенные функции

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

 

inline void Fx(void) {

std::cout<<"Функция Fx"<<std::endl;}

...

Fx(void);

// Этот код будет заменен компилятором на код встроенной функции

Рисунок 13

1.2 Задание

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

Варианты заданий

1. Построить описание класса, содержащего информацию о почтовом адресе организации.

2. Составить описание класса «магазин»

3. Составить описание класса для определения одномерных массивов целых чисел (векторов).

4. Составить описание класса многочленов одной переменной, задаваемых степенью многочлена и массивом коэффициентов.

5. Составить описание класса «моб.телефон»

6. Составить описание класса «студенческая подгруппа». (поиск студентов по какому-либо признаку).

7. Составить описание класса «библитотека»

8. Построить описание класса для объектов-векторов, задаваемых координатами векторов.

9. Построить описание класса для объектов-векторов, задаваемых координатами векторов.

10. Составить описание класса «лес»

11. Составить описание класса «овощи»

12. Составить описание класса «фрукты»

13.Составить описание класса, обеспечивающего представление матрицы произвольного размера с возможностью изменением кол-ва строк и столбцов, вывода на экран подматрицы любого размера

1.3. Порядок выполнения работы

1) Создать классы.

2) Определить конструктор без параметров, конструктор с параметром, конструктор с несколькими параметрами.

3) Определить деструктор.

4)Определить данные классов.

5) Проверить работу созданных классов.

 

1.4. Содержание отчета

1) Название лабораторной работы и задание.

2) UML – диаграммы разработанных классов.

3) Описание разработанных классов.

4) Текст программы.

5) Тестирование программы

 

1.5. Контрольные вопросы и упражнения

Лабораторная работа №2

НАСЛЕДОВАНИЕ

Цель работы: Получить практические навыки …

Теоретические сведения

Виртуальные функции

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

Функция-член класса объявляется как виртуальная указанием ключевого слова virtual. Методы в производных классах, заменяющие виртуальную функцию базового класса должны объявляться с тем же именем, тем же списком параметров и типом возвращаемого значения, что и соответствующая функция базового класса. Если из производного класса не образуется новых производных классов, ключевое слово virtual в описании функции можно опустить.

Если в производном классе нет объявления функции с тем же именем, что и виртуальная функция базового класса, будет вызываться функция базового класса.

 

class CObject

{

protected:

float x,y;

public:

CObject() {x=0; y=0;};

CObject(float x,float y){this->x=x; this->y=y;}

~CObject() {};

virtual void draw() = 0;

 

};

Рисунок 14 –Пример виртуальной функции

Такая функция называется “чистой” (pure) виртуальной функцией, а объектный тип, содержащий ее объявление, называется абстрактным объектным типом. В программе не могут создаваться экземпляры абстрактных типов, такой тип может использоваться только для образования производных типов, причем в производном типе следует либо снова определить эту виртуальную функцию как чистую, либо объявить ее как обычную виртуальную функцию, выполняющую конкретные действия. Виртуальные функции особенно полезны, когда к методом класса требуется обращаться через указатель на экземпляр класса, а сам этот указатель имеет тип указателя на базовый класс.

В примере ниже как раз проиллюстрирован подобный случай. Класс CWorld содержит массив указателей на объекты класса CIObject. При работе с ними вызываются виртуальный метод класса CIObject draw(), который должен отрисовывать текущий объект на экране. Метод этот вызывается через указатель на класс CIObject. Однако, эти указатели на самом деле могут указывать на классы-потомки CIObject, такие, как CCircle, в которых метод draw() переопределён для рисования уникальной формы.

 

class CCircle: public CIObject
{
public:
CCircle() {x=0;y=0;};
~CCircle() {};
virtual void draw(); // переопределена
}

void CCircle::draw()
{
setcolor(14);
circle(x,y,10);
}

Рисунок 15 - Переопределение виртуальной функции draw()

 

void CWorld::render()
{
for (int i=0;i<objectsn; i++)
objects[i]->draw();

player.draw();

}

Рисунок 16 - Вызов draw() через указатель базового класса

 

 

Дружественные функции

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

Тем не менее, как показывает практика, иногда необходимо получить прямой доступ к полям класса, не используя его интерфейс. Зачем это может понадобиться? Одна из причин может быть следующей: при доступе к внутренним переменным класса через его функции уменьшается эффективность работы за счет затрат на вызов функции. В большинстве случаев это не критично, но не всегда. Иногда это может играть существенную роль. Или, еще один пример, нам необходимо получить прямой доступ к внутренним данным двух разных классов.

Для решения подобного круга задач в С++ и существует возможность описания функции, функции-члена другого класса как дружественной (friend). Класс может предоставлять особые привилегии определенным внешним функциям или функциям-членам другого класса. Рассмотрим как работает этот механизм.

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

 

class СPlayer

{

friend int F(const CPlayer &player)
}

Рисунок 17 - Объявление дружественной функции

Если обычные функции-члены имеют автоматический доступ ко всем данным своего класса за счет передачи скрытого параметра - указателя this на экземпляр класса, то дружественные функции требуют явной спецификации этого параметра. Действительно, объявленная в классе СPlayer дружественная функция F не принадлежит этому классу, а, значит, не может быть вызвана операторами player.F(), где player - экземпляр класса CPlayer. Синтаксически корректными будут обращения F (& player).

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

 

int intersect(CPlayer &p, CCircle &o)

{
return fabs(o.x-p.x) < 20 && fabs(o.y-p.y) < 20;
}

Рисунок 18 – Реализация функции, дружественной для классов CPlayer и CCircle

Для того, чтобы данная функция могла получить доступ к закрытым полям классов CPlayer и CCircle, необходимо в описании этих классов добавить прототип данной функции с ключевым словом friend

class СPlayer

{

friend int intersect(CPlayer &, CCircle &);
}

class СCircle
{

friend int intersect(CPlayer &, CCircle &);
}

Рисунок 19 – Описание функции, дружественной для классов CPlayer и CCircle