Выбери формат для чтения
Загружаем конспект в формате doc
Это займет всего пару минут! А пока ты можешь прочитать работу в формате Word 👇
Лекция №3. Элементы объектно-ориентированного программирования на основе алгоритмического языка Си++
Содержание: объектно-ориентированное программирование как продолжение структурного; понятие класса и объекта; поля и методы класса; личные и общие элементы класса; конструкторы и деструкторы; пример консольного приложения, использующего классы; принципы объектно-ориентированного программирования: инкапсуляция; наследование; полиморфизм; виртуальные методы: понятие о раннем и позднем связывании.
1. Основные понятия объектно-ориентированного программирования
1.1. О структурном и объектно-ориентированном
программировании
В курсе программирования (и в процессе проработки лекции 2) вы убедились (во всяком случае, должны были убедиться), что грамотное программирование является структурным программированием.
Структурное программирование подразумевает:
• точно обозначенные управляющие (основные) структуры алгоритмов;
• соответствующее логике программы разбиение ее на программные блоки;
• автономные подпрограммы, в которых преимущественно используются локальные переменные;
• отсутствие (или, по крайней мере, ограниченное использование) операторов безусловного перехода – goto, break и др.
Последовательное применение концепций структурного программирования предполагает на каждом этапе проектирования программы разделение задачи на связанные между собой подзадачи, при этом программа разбивается на логически завершенные и стандартно кодируемые части. В результате код программы легко читается не только ее автором, но и другими программистами, и число ошибок при программировании существенно уменьшается. Естественно, преимущества структурного программирования наиболее ярко проявляются при написании больших программ, когда проектирование программы просто невозможно без разделения части. Но и при создании средних и малых программ структурный подход дает хорошие результаты.
Развитие программирования привело к новому подходу – объектно-ориентированному программированию (ООП). Этот подход включает в себя лучшие идеи структурного программирования и дополняет их новыми мощными концепциями. ООП, так же как и структурное программирование, позволяет разложить задачу на подзадачи, но при этом каждая подзадача становится самостоятельным объектом, содержащим свои коды и данные.
Объектно-ориентированный подход к программированию эффективен в тех случаях, когда некоторый реально существующий предмет характеризуется очень большим числом параметров. Тогда подпрограммы, описывающие алгоритмы обработки или функционирования такого предмета, имеют настолько большое число формальных параметров, что программирование их вызовов слишком трудоемко и сопряжено с большим количеством ошибок. Эффективным методом программирования таких задач является создания в программе объекта, соответствующего предмету. Этот объект будет включать в себя данные, соответствующие параметрам предмета, и подпрограммы (в Си функции), описывающие алгоритмы обработки или функционирования предмета. Данные объекта должными являться по умолчанию доступными для алгоритмов объекта (как бы быть глобальными для этих алгоритмов) и не включаться в списки параметров соответствующих подпрограмм. Таким образом, количество параметров подпрограмм существенно уменьшится.
Примером такого «предмета» с большим количеством параметров может служить окно приложения какой-либо оконной операционной системы, например, Windows. Подсчитайте, сколько параметров у окна приложения или окна папки!
1.2. Определение класса и объекта в C++
В С++ (и во многих других объектно-ориентированных алгоритмических языках, например, в Объектном Паскале) вводится новый тип данных – класс. Переменная типа класс называется объектом. Иногда объект называют также экземпляром класса. Формально описание класса напоминает структуру, но класс, кроме полей (переменных) содержит методы, которые задают допустимые действия над полями класса. Общее название для полей и методов – элементы класса.
Описание класса на языке С++:
class имя_класса {
private:
описание личных элементов класса
public:
описание общих элементов класса
};
Под личными элементами класса (private) понимаются такие элементы, которые могут использоваться только методами своего класса. К общим элементам класса (public) доступ разрешен в любом месте программы. Существуют и другие способы доступа к элементам класса (protected,
published – см. п. 1.3).
Поля класса описываются внутри класса как обычные переменные. Методы – это функции, которые можно применять к полям. Внутри класса методы задаются своими прототипами. Вне класса приводится полное описание метода.
Как правило, поля являются личными элементами класса, т. е. доступ к полям разрешен только внутри методов класса. Это положение можно рассматривать как простейшую формулировку принципа инкапсуляции (см. п. 1.3).
Если внутри класса метод описан прототипом:
тип_результата имя_метода (список типов параметров),
то при полном описании метод имеет заголовок:
тип_результата имя_класса::имя_метода (список параметров),
т. е. в заголовке перед именем метода указывается имя класса (через знак ::).
Заметим, что в C++ Builder в качестве типа результата часто используется fast_call. В таком случае возвращаемое методом значение записывается в регистр.
Среди методов класса существуют две специальные группы: конструкторы и деструкторы. Назначение конструктора заключается в инициализации полей экземпляра класса. Имя конструктора совпадает с именем класса. Конструктор никогда не вызывается явно, его вызов осуществляется компилятором в момент создания экземпляра класса.
Деструктор вызывается для уничтожения экземпляра класса. Имя деструктора образуется как ~имя_класса. Деструктор может вызываться в программе явно или (что происходит обычно) его вызов обеспечивается компилятором в момент уничтожения экземпляра класса.
Наличие конструктора и деструктора для любого класса обязательно; при их отсутствии компилятор автоматически создает стандартные варианты конструктора и деструктора.
Экземпляры класса могут создаваться автоматически или динамически. Уничтожение автоматически созданных экземпляров классов происходит также автоматически при завершении выполнения блока функции, в котором они были определены. Определение (описание) автоматического экземпляра класса может встречаться в любом месте функции и имеет вид:
имя_класса имя_экземпляра(параметры конструктора);
Перед созданием динамического экземпляра класса (по аналогии с любыми динамическими переменными) необходимо объявить указатель на экземпляр:
имя_класса* указатель_на_экземпляр;
Динамический экземпляр класса создается с помощью оператора new, а уничтожается с помощью оператора delete:
указатель_на_экземпляр= new имя_класса (параметры конструктора);
delete указатель_на_экземпляр;
Метод класса (по аналогии с полем структуры) вызывается одним из следующих способов:
имя_экземпляра.имя_метода или имя_экземпляра->имя_метода
1.3. Пример консольной программы, использующей классы
Пример 1. Ниже приведена программа, в которой описан и используется класс cl_mas для обработки одномерных массивов.
#include
#include
class matr {private:
float **a;
int n,m;
char c;
float *min_str;
public:
matr(int n1, int m1,char c1);
void calc_min();
void vvod();
void vivod();
};
matr::matr(int n1, int m1, char c1)
{n=n1; m=m1; c=c1;
a=new float* [n];
int i,j;
for (i=0;ia[i][j])
min_str[i]=a[i][j];
}
}
void main()
{ matr *a, *b;
a=new matr(2,3,'a');
b=new matr(4,2,'b');
a->vvod();
b->vvod();
a->calc_min();
b->calc_min();
a->vivod();
b->vivod();
_getch();
delete a;
delete b;
}
1.4. Три принципа объектно-ориентированного
программирования
1.4.1. Принцип инкапсуляции
Инкапсуляция – такое объединение внутри класса полей и методов, при котором доступ к полю возможен только путем вызова соответствующего метода.
При идеальном выполнении принципа инкапсуляции поля класса могут быть только личными (private).
Ниже перечислены уровни инкапсуляции, т. е. уровни доступа к элементам класса, два из которых (private и public) мы уже рассмотрели:
• private (личный). Этот уровень накладывает самые жесткие ограничения на доступ к элементам класса. Именно, эти элементы могут быть использованы только методами данного класса. Как правило, поля класса объявляются private.
• public (общий). Элементы класса данного уровня доступны из любой точки программы (самый «широкий» доступ).
• protected (защищенный). Элементы класса данного класса доступны методам данного класса и его наследников (определение класса-наследника см. в п. 1.4.2).
1.4.2. Принцип наследования
Наследование – это возможность определения для базового класса (предка) иерархии производных классов (наследников), в каждом из которых доступны элементы базового класса (их описание становится частью описания производного класса).
Иначе говоря, наследование – механизм, посредством которого класс может наследовать элементы другого класса и добавлять к ним свои элементы.
Как правило, базовый класс является более общим, производные – более специальными, конкретными. Естественно, у класса-наследника обычно больше полей и методов, чем у класса-предка, так как при наследовании обычно добавляются новые элементы.
Класс предок и совокупность его наследников всех уровней образуют иерархическое дерево классов. Примеры иерархии классов приведены на рис. 1, 2.
Если имеется иерархия классов, то можно рассматривать защищенные (protected) элементы класса, которые доступны для методов своего класса и его наследников (см. п. 1.4.1).
Наследование может быть единичным (наследник имеет одного предка) и множественным (количество предков больше 1).
Наследование может быть общим, личным и защищенным.
Видимость полей и функций базового класса из производного определяется секцией, в которой находится объявление компонента, и видом наследования (см. табл. 1):
Таблица 1. Видимость компонентов базового класса в производном
Вид наследования
Объявление компонентов в базовом классе
Видимость компонентов в производном классе
private
не доступны
private
protected
private
public
private
private
не доступны
protected
protected
protected
public
protected
private
не доступны
public
protected
public
public
public
Как видно из таблицы 1, при общем наследовании общие и защищенные элементы класса-предка сохраняют свой уровень инкапсуляции и в классе-наследнике. При личном наследовании общие и защищенные элементы класса-предка становятся личными для класса-наследника. При защищенном наследовании защищенные элементы сохраняют свой уровень в классе-наследнике, а общие становятся защищенными. Обратите внимание, что, независимо от вида наследования, личные элементы базового класса не доступны в производных классах. Таким образом, чтобы «достать» их из производного класса надо использовать унаследованные общие и защищенные методы класса-предка.
Описание производного класса в общем виде:
class Имя_производного_класса:
Вид_наследования_1 Имя_базового_класса_1,
Вид_наследования_2 Имя_базового_класса_2,
...
Вид_наследования_n Имя_базового_класса_n,
{
Описание_добавляемых_элементов_производного_класса
};
Если Вид_наследования не указан, то по правилу умолчания принимается public.
Самая простая ситуация — это единичное общее наследование. Заголовок производного класса в этом случае имеет вид:
class имя_наследника: public имя предка;
Ограничение доступа к элементам класса при наследовании имеет смысл. Так как личные элементы класса-предка не доступны в классе-наследнике, то не проблем в том случае, когда два класса-предка имеют личные элементы (чаще всего поля) с одинаковыми именами. Проблемы с одинаковыми именами защищенных и общих элементов классов-предков также разрешаются ограничением доступа при наследовании. Заметим, что в Объектном Паскале эти проблемы отсутствуют, так как допустимо только единичное наследование.
Также следует отметить, что повторное объявление в классе-наследнике полей класса-предка недопустимо! Повторное использование имен методов (т. е. переопределение методов) разрешено.
1.4.3. Полиморфизм
Полиморфизм – возможность определения единого по имени метода для всей иерархии производящих классов, причем в каждом классе этот метод может реализовываться со своими особенностями.
Для примера рассмотрим простую иерархическую структуру классов геометрических фигур (см. рис. 2)
Каждый из классов может иметь метод, показывающий на экране геометрическую фигуру. Эти методы различны, но удобно, чтобы они имели одинаковое имя (например, show ), так как выполняют похожие действия. Принцип полиморфизма дает возможность существования в каждом классе метода show(), реализующего действия, нужные для данного класса.
Полиморфизм гарантирует, что для любого экземпляра класса будут вызываться методы именно этого класса, а не какого-либо другого класса иерархии (несмотря на одинаковые имена).
Полиморфизм реализуется путем введения виртуальных методов, которые подключаются к программе на этапе ее выполнения; такое подключение называется «поздним связыванием». Обычные, не виртуальные методы, как известно, подключаются к программе на этапе компиляции («раннее связывание»).
Технически «позднее связывание» реализуется следующим образом. В любой экземпляр класса с виртуальными методами добавляется скрытое поле – указатель на таблицу виртуальных методов (VMT – Virtual Method Table), в которой для каждого виртуального метода указывается адрес его реализации в данном классе. Инициализация скрытого поля осуществляется с помощью конструктора.
При описании виртуального метода внутри базового класса перед его шаблоном добавляется слева ключевое слово virtual. Тогда в классе-наследнике этот метод может быть переопределен, причем при переопределении метода virtual можно не писать.
Если в переопределенном методе нужно вызвать одноименный метод класса-предка, то к нему обращаются так:
имя_предка ::имя_метода(фактические параметры)
Ниже приведен пример – фрагмент программы, содержащий описание класса Pnt (точка) и его производного класса Crcl (круг). Рассмотрим методы класса Pnt:
• Конструктор Crcl содержит вызов Pnt — конструктора класса-предка. Этот метод не является виртуальным.
• Метод Move описывает движение фигуры. Он состоит в уничтожении изображения (спрятывании) фигуры с помощью метода Hide, изменении положения центра фигуры (x,y) на величину (x, y) и в показе фигуры c помощью метода Show. Так как рассматривается только движение центра фигуры (поступательное движение), то метод Move для всех фигур-наследников одинаков, он не будет переопределяться в классах-наследниках.
• Методы Hide и Show различны для разных классов-наследников и вызываются внутри метода класса-предка Move (доступного во всех наследниках), так что при их вызове не указывается имя экземпляра класса. Следовательно, методы Hide и Show должны быть объявлены как виртуальные. Код этих методов не приводится.
Если методы Hide и Show не объявить виртуальными, то метод Move будет вызывать методы Hide и Show класса-предка Pnt (см. рис.2).
Пример 2. Это не работающая программа, а лишь заготовка программы: нет реализации методов show и hide и определения цветов.
class Pnt /*точка*/
{ private:
int x,y,color;
public:
Pnt(int a, int b, int c);
virtual void Show(); /*будут переопределяться,*/
virtual void Hide(); /*вызываются Move*/
void Move (int dx, int dy);/*не будет переопределяться, вызывает Show и Hide*/
}
Pnt :: Pnt (int a, int b, int c);
{ x=a; y=b; color=c;
}
void Pnt :: Show();
{ …
}
void Pnt :: Hide();
{ …
};
void Pnt :: Move(int dx, int dy);
{ Hide();
x=x+dx; y=y+dy;
Show();
}
class Crcl: public Pnt
{ private:
int r;
public:
Crcl (int a, int b, int c, int rad);
void Show();
void Hide();
/*Move не переопределяется, берется из Pnt*/
}
Crcl :: Crcl (int a, int b, int c, int rad);
{
Pnt (a, b, c,); r=rad;
};
void Crcl :: Show();
{…
}
void Crcl :: Hide();
{ …
}
void main()
{Pnt* P; Crcl* C;
P=new Pnt (10,10,cl1);
C=new Crcl (50,50,cl2,10);
P->Move(10,10);//двигается точка
C->Move(50,50); //двигается круг
delete P; delete C;
}