Объектно-ориентированное программирование 


Мы поможем в написании ваших работ!



ЗНАЕТЕ ЛИ ВЫ?

Объектно-ориентированное программирование



Введение в объектно-ориентированное программирование

 

Объектно-ориентированное программирование - это новый подход к программированию [4]. В ходе эволюции вычислительной техники и соответствующих операционных сред разрабатывались каждый раз более мощные методы программирования, чем предыдущие. Не останавливаясь на самых ранних методах программирования, сразу перейдем к анализу методов программирования, использующих языки высокого уровня. Язык программирования, легко понимаемый в коротких программах, становился плохо читаемым и неуправляемым, когда дело касалось больших программ. Избавление от неструктурированных программ пришло после изобретения в 1960 году языков структурного программирования. К ним относятся языки Алгол, Паскаль, и Си. Структурное программирование подразумевает точно обозначенные управляющие структуры, программные блоки, отсутствие или, по крайней мере, минимальное использование операторов GOTO, автономные подпрограммы, в которых поддерживается рекурсия и локальные переменные.

Сутью структурного программирования является возможность разбиения программы на составляющие ее элементы. Используя структурное программирование, средний программист может создавать и поддерживать программы свыше 50000 строк длиной. Но и это оказалось недостаточным на современном этапе. Чтобы писать более сложные программы, необходим был новый подход к программированию. В итоге были разработаны принципы объектно-ориентированного программирования (ООП). ООП аккумулирует лучшие идеи, воплощенные в структурном программировании и сочетает их с мощными новыми концепциями, которые позволяют оптимально организовывать программы. ООО позволяет разложить проблему на связанные между собой задачи. Каждая проблема становится самостоятельным объектом, содержащим свои собственные коды и данные. В этом случае вся процедура в целом упрощается, и программист получает возможность оперировать с гораздо большими по объему программами. Все языки ООП, включая С++, основаны на трех основополагающих концепциях, называемых инкапсуляцией, полиморфизмом и наследованием. Рассмотрим эти концепции.

Инкапсуляция

 

Инкапсуляция - это механизм, который объединяет данные и код, и защищает и то и другое от внешнего вмешательства или неправильного использования. В ООП код и данные могут быть объединены вместе; в этом случае говорят, что создается так называемый «черный ящик». Все необходимые данные и коды находятся внутри его, то есть создается объект. Другими словами, объект - это то, что поддерживает инкапсуляцию. Внутри объекта коды и данные могут быть закрытыми (private) или открытыми (public). По умолчанию действует принцип private. Закрытые коды или данные недоступны для тех частей программы, которые существуют вне объекта. В случае public коды и данные доступны для всех частей программы. Характерной чертой является ситуация, когда открытая часть объекта используется для того, чтобы обеспечить контролируемый интерфейс с его закрытой частью. Рассмотрим такое важное понятие как класс.

Класс - это механизм для создания объектов. Синтаксис описания класса на С++ похож на синтаксис описания структуры.

 

class имя класса {

   закрытые функции и переменные класса

public:

   открытые функции и переменные класса

} список объектов;

 

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

 

     Пример. В качестве простого примера рассмотрим программу, в которой используется myclass, описанный в тексте для задания значения a для объектов ob 1 и ob 2 и вывода на экран этих значений для каждого объекта:

# include <iostream.h>

class myclass {

int a; // закрыто вне myclass

public:

void set_a(int num); // член - функция

int get_a();             // член - функция

};

void myclass:: set_a(int num)

{

a = num;

}

int myclass:: get_a()

{

return a;

}

main()

{

myclass ob1,ob2;

ob1.set_a(10);

ob2.set_a(99);

cout << ob1.get_a() <<’’\n’’;

cout << ob2.get_a() <<’’\n’’;

return 0;

}

Как и следовало ожидать, программа выводит на экран величины 10 и 99. Переменная a в myclass является закрытой. Это означает, что она доступна только для функций членов myclass. Например, такое обращение вызовет ошибку

 

# include < iostream. h >

class myclass {

int a; // закрыто для myclass

public:

void set_a(int num);

int get_a();

};

main()

{

myclass ob1,ob2;

ob 1. a =10; // ОШИБКА! К закрытому члену нет доступа

ob 2. a =99; // ОШИБКА! К закрытому члену нет доступа

cout << ob1.get_a() <<’’\n’’;

cout << ob2.get_a() <<’’\n’’;

return 0;

}

Пример. Рассмотрим использование открытых переменных

 

# include < iostream.h>

class myclass {

public:

int a; // теперь а открыта и не нужны set _ a () и get _ a ()

};

main()

{

myclass ob1,ob2;

ob1.a = 10;

ob2.a = 99;

cout << ob1.a << ‘’\n’’;

cout << ob2.a <<’’\n’’;

return 0;

}

Рассмотрим пример, в котором создается класс - stack, реализующий стек, использующийся для хранения символов:

 

# include <iostream.h>

# define SIZE 10

class stack {

char stck[SIZE];              // содержит стек

int tos;                            // индекс вершины стека

public:

void init ();           // инициализация стека

void push (char ch); // помещает в стек символ

char pop ();        // выталкивает из стека символ

};

void stack:: init () // инициализация стека

{

tos =0;

}

void stack:: push (char ch)   // помещение символа в стек

{

if (tos==SIZE) {

           cout << ‘’ Стек полон ’’;

           return;

}

stck[tos] = ch;

tos++;

}

char stack:: pop () // выталкивание символа из стека

{

if(tos==0) {

              cout <<’’ Стек пуст ’’;

           return 0; // Возврат нуля при пустом стеке

}

           tos--;

           return stck[tos];

}

main()

{

stack s 1, s 2;  // создание двух стеков

int i;

s 1. init ();     // инициализация стека

s2.init();    

s1.push(‘a’); // заполнение стека

s2.push(‘x’);

s1.push(‘b’);

s2.push(‘y’);

for(i=0;i<2;i++) cout <<’’ символ из s1:’’<<s1.pop() <<’’\n’’; // выталкивание из стека

for(i=0;i<2;i++) cout <<’’ символ из s2:’’<<s2.pop() <<’’\n’’;

return 0;

}

   Эта программа выводит на экран следующее:

символ из s 1: b

символ из s 1: a

символ из s 2: y

символ из s 2: x.

Полиморфизм

 

Полиморфизм - это свойство, которое позволяет одно и то же имя использовать для решения двух или более задач, но технически разных. Целью полиморфизма, применительно к ООП, является использование одного имени для задания общих для класса действий. Выполнение каждого конкретного действия будет определяться типом и/или количеством данных. Например, в языке С++ можно использовать одно имя функции для множества различных действий. Это называется перегрузкой функции. В более общем смысле концепцией полиморфизма является идея «один интерфейс, множество методов». Это означает, что можно создать общий интерфейс для группы близких по смыслу действий. При этом выполнение конкретного действия зависит от данных. Преимуществом полиморфизма является то, что он помогает снижать сложность программ. Выбор же конкретного действия, в зависимости от ситуации, возлагается на компилятор. Полиморфизм может применяться и к функциям, и к операциям.

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

Рассмотрим пример определения абсолютного значения числа, представляемого как целое, как длинное целое и как число с плавающей точкой на основе введения перегрузки функции.

 

# include < iostream. h >

// Перегрузка abs () тремя способами

int abs(int n);

long abs(long n);

double abs(double n);

main()

{

cout << ‘’ Абсолютная величина -10:’’ << abs (-10) << ‘’\ n ’’;

cout << ‘’ Абсолютная величина -10 L:’’ << abs (-10 L) << ‘’\ n ’’;

cout << ‘’ Абсолютная величина -10.01:’’ << abs (-10.01) << ‘’\ n ’’;

return 0;

}

int abs(int n)

{ return n < 0? -n: n; }

long abs(long n)

{ return n < 0? -n: n;}

double abs(double n)

{ return n < 0? - n: n;}

Как можно заметить, в программе задано три функции abs (), своя функция для каждого типа данных. Внутри main () abs () вызывается с тремя аргументами разных типов. Компилятор автоматически вызывает правильную версию abs (), основываясь на используемом в аргументе типе данных. На этом простом примере видна ценность перегрузки функций. На компилятор возлагается задача выбора соответствующей конкретной версии вызываемой функции, а значит и метода обработки данных. Это имеет лавинообразный эффект для снижения сложности программ. В данном случае, благодаря использованию полиморфизма, из трех имен получилось одно. Перегружаемые функции могут отличаться не только типом своих переменных, но и числом параметров. Рассмотрим пример такого полиморфизма:

 

# include < iostream. h >

void f 1(int a);

void f1(int a,int b);

main()

{

f1(10);

f1(10,20);

return 0;

}

void f1(int a)

{

cout << ‘’ В f1:’’<<a<<’’ \n’’;

}

void f1(int a,int b)

{

cout <<’’ В f1:’’<<a<<b’’ \n’’;

}

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

 

Фактически для каждого создаваемого объекта на практике необходима инициализация. Для разрешения этой проблемы С++ предоставляет функцию - конструктор, включаемую в описание класса. Конструктор класса вызывается автоматически каждый раз при создании объекта этого класса.

Конструктор имеет то же имя, что и класс, частью которого он является, и не имеет возвращаемого значения. Рассмотрим сказанное на примере:

 

# include < iostream. h >

class myclass {

int a;

public:

myclass();        // конструктор

void show();

};

myclass:: myclass()

{

cout <<’’ В конструкторе \n’’;

a=10;

}

void myclass::show()

{

cout << a;

}

main()

{

myclass ob;

ob.show();

return 0; }

 

В этом примере значение а инициализируется конструктором myclass (). Конструктор вызывается тогда, когда создается объект ob. Важно помнить, что в С++ оператор объявления является оператором действия.

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

# include <iostream.h>

class myclass {

int a;

public:

myclass();     // конструктор

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

void show();

};

myclass::myclass()

{

cout << ‘’C одержимое конструктора \n’’;

a = 10;

}

myclass::~myclass()

{

cout <<’’ Удаление...\n’’;

}

void myclass::show()

{

cout << a << ‘’\n’’;

}

main()

{

myclass ob;

ob.show();

return 0;

 }

В данном примере деструктор класса вызывается при окончании программы main (). Локальные объекты удаляются тогда, когда они выходят из поля видимости. Глобальные объекты удаляются при завершении программы. Применение конструкторов и деструкторов для действий, прямо не связанных с инициализацией, является очень плохим стилем программирования и его следует избегать. Рассмотрим применение конструктора и деструктора при создании простого класса для строк, который содержит саму строку и ее длину. Когда создается объект strtype, для хранения строки выделяется память, и начальная длина строки устанавливается равной начальному значению. Когда объект strtype удаляется, эта память освобождается. Будем использовать конструктор с параметром. Рассмотрим пример:

 

# include <iostream.h>

# include <malloc.h>

# include <string.h>

# include <stdlib.h>

class strtype

{

   char * p;        // p - указатель на переменную типа char

   int len;

public:

strtype(char *ptr);

~strtype();

void show();

};

strtype:: strtype(char *ptr)

{

len = strlen(ptr);

p = (char *) malloc(len + 1);

if (! p) {

             cout << ‘’ Ошибка выделения памяти \ n ’’;

             exit(1);

}

strcpy(p,ptr);

}

strtype:: ~strtype()

{

             cout << ‘’ Освобождение p \ n ’’;

             free(p);

}

void strtype:: show()

{

             cout << p<< ‘’- длина: ‘’ << len;

             cout << ‘’\ n ’’;

}

main ()

{

strtype s 1(‘’Это проверка’’), s 2(‘’ Мне нравится С++’’);

s1.show();

s2.show();

return 0;

}

Наследование

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

 

class base {                        // Задание базового класса

int i;

public:

void set_i(int n);

int get_i();

};

class derived: public base { // Задание производного класса

int j;

public:

void set_j(int n);

int mul();

};

void base::set_i(int n)

{   i = n; }       // Установка значения i в базовом классе

int base::get_i()

{    return i; }       // Возврат значения i в базовом классе

 

void derived::set_j(int n)

{   j = n; } // Установка значения j в производном классе

 

int derived::mul()

{  return j * get_i(); } // Вызов значения i из base и, одновременно, j из derived

main()

{

derived ob;

ob.set_i(10);          // загрузка i в base

ob.set_j(4);           // загрузка j в derived

cout << ob. mul (); // вывод числа 40

return 0;

}

Отметим, что функция get _ i (), которая является членом базового класса, а не производного, вызывается внутри производного класса без какой бы то ни было связи с каким-либо объектом. Это возможно потому, что открытые члены класса base становятся открытыми членами derived.

В функции mul (), вместо прямого доступа к i, необходимо вызывать get _ i () из-за того, что закрытые члены базового класса, в данном случае i, остаются закрытыми и недоступными для любого производного класса. Причина, по которой закрытые члены становятся недоступными для производных классов - поддержка инкапсуляции.

 



Поделиться:


Последнее изменение этой страницы: 2021-12-15; просмотров: 70; Нарушение авторского права страницы; Мы поможем в написании вашей работы!

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 3.142.40.43 (0.103 с.)