Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Теория ответы.doc
Скачиваний:
39
Добавлен:
24.12.2018
Размер:
1.61 Mб
Скачать

22.Основные концепции ооп. Классы. Оператор разрешения области видимости. Объекты. Доступ к членам класса. Массивы объектов. Указатели на объекты.

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

Основным отличием языка Си++ от Си является наличие в нем средств объектно-ориентированного программирования (ООП). Часто в литературе язык Си++ определяют именно как язык объектно-ориентированного программирования. Базовые понятия ООП: это инкапсуляция, наследование и полиморфизм.

Класс это структурированный тип, включающий в себя в качестве элементов типизированные данные и функции, применяемые по отношению к этим данным. Таким образом, инкапсуляция (объединение параметров и методов) заложена в составе элементов класса: типизированные данные — это параметры, а методы peaлизованы через функции.

Тип «класс» устанавливается для объектов. Принято говорить: однотипные объекты принадлежат одному классу.

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

class имя i

{ тип1 переменная1 тип2 переменная2

public:

функция1; функция2;

};

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

  • private (частный);

  • public (общедоступный);

  • protected (защищенный).

Режим доступа private обозначает, что соответствующий момент может использоваться только функциями данного класса. Этот режим доступа устанавливается по умолчанию. Элементы с режимом доступа public доступны в других частях программы. О режиме protected будет сказано немного позже. Чаще всего режим доступа к данным (переменным) бывает private, а к функциям — public. Это отражено в приведенном выше формате объявления класса.

В качестве примера рассмотрим объявление класса обыкновенных дробей (см. пример 3 в разд. 3.23) с именем Drob, в котором значение дроби определено через структуру двух целых чисел (числитель и знаменатель), а к методам работы с дробью отнесены ввод дроби — функция Vvod; вычисление наибольшего общего делителя числителя и знаменателя — функция nod; сокращение дроби — функция Sokr; возведение дроби в целую степень — функция stepen — и вывод дроби на экран — функция Print. Объявление соответствующего класса выглядит так:

class Drob{

Frac A;

public:

void Vvod(void); int NOD(void); void Sokr(void);

void Stepen (int N); void Print(void);

};

Имеется в виду, что глобально по отношению к этому классу объявлена структура с именем Frac:

struct Frac{int P; int Q; } ;

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

void Drob::Vvod(void)

{cout<<"Числитель?"; cin>>A.P;

Соut<<"Знаменатель?"; cin>>A.Q;

}

В основной части программы (основной функции) класс Drob будет поставлен в соответствие определенным переменным в качестве типа. Например:

Drob Y;

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

Пример 1.

//В программе объявлен класс простых дробей,

//описана переменная этого класса, выполнена

//обработка переменной

#include <iostream.h>

#include <math.h>

#include <conio.h>

struct Frac {fint P; int Q;};

Frac F;

//Объявление класса

class Drob{

Frac A;

public:

void Vvod(void); int NOD(void); void Sokr(void);

void Stepen(int N) ; void Print(void); };

//Описания функций — членов класса

void Drob::Vvod(void) { cout<<"Числитель?"; cin>>A.P;

cout<<'' Знаменатель?"; cin>>A.Q; }

int Drob::NOD(void)

{ int M,N;

M=abs(A.P); N=A.Q;

while(M!=N) {

if(M>N)

if(M%N!=0) M=M%N; else M=N;

else if(N%M!=0) N=N%M; else N=M;

} return M;

}

void Drob::Sokr(void)

{ int X;

X=NOD();

if(A.P!=0)

{ A.P=A.P/X; A.Q=A.Q/X;}

else A.Q=1;

}

void Drob::Stepen(int N)

{ int i;

F.P=F.Q=1;

for(i=l;i<=N;i++)

{ F.P*=A.P; F.Q*=A.Q;}

}

void Drob::Print(void) { cout<<"\n"<<A.P<<"/"<<A.Q<<"\n"; }

//Основная функция

void main() { clrscr();

Drob Y; //Объявление объекта

Соut<<"Введите дробь ! "<<"\n" ;

Y.Vvod();

Y.Sokr();

Cout<<” Дробь после сокращения:"<<"\n";

Y.Print ();

Y.Stepen(2);

cout<<"Дробь, возведенная в квадрат:"<<"\n";

cout<<F.P<<"/"<<F.Q<<"\n";

getch(); }

В результате выполнения этой программы на экране получим:

Вводите дробь!

Числитель? 6

Знаменатель? 15

Дробь после сокращения: 3/5

Дробь, возведенная в квадрат: 9/25

Доступ к членам класса

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

//270

// Демонстрация доступа к членам класса.

#include <conio.h>

#include <iostream.h>

class myclass {

int a; // закрытые данные

public:

int b; // открытые данные

void setab(int i); // открытые функции

int geta();

void reset () ; };

void myclass::setab(int i) {

a = i; // прямое обращение к переменной а

b = i*i; // прямое обращение к переменной b

}

int myclass::geta()

{ return a; } // прямое обращение к переменной а

void myclass::reset (){

// Прямой вызов функции setab()

setab(0); // для уже известного объекта.

}

int main () {clrscr();

myclass ob;

ob.setab(5); // Устанавливаем члены данных ob.a и ob.b.

cout << "Объект ob после вызова функции setab (5): ";

cout << ob.geta()<< ' ';

cout << ob.b; // К члену b можно получить прямой доступ,

// поскольку он является public-членом,

cout << '\n';

ob.b = 20; // Член b можно- установить напрямую,

// поскольку он является public-членом,

cout << "Объект ob после установки члена ob.b=20: ";

cout <<ob.geta()<< ' ';

cout << ob.b; cout <<'\n';

ob.reset ();

cout << "Объект ob после вызова функции ob.reset () : ";

cout << ob.geta() << ' ';

cout << ob.b;

cout << '\n';

getch(); return 0; }

При выполнении этой программы получаем следующие результаты.

Объект ob после вызова функции setab (5) : 5 25

Объект ob после установки члена ob.b=20: 5 20

Объект ob после вызова функции ob.reset (): 0 0

Теперь рассмотрим, как осуществляется доступ к членам класса myclass. Прежде всего обратите внимание на то, что для присвоения значений переменным а и b в функции setab() используются следующие строки кода.

а = i; // прямое обращение к переменной а

b = i*i; // прямое обращение к переменной b

Поскольку функция setab() является членом класса, она может обращаться к членам данных а и b того же класса непосредственно, без явного указания имени объекта (и не используя оператор "точка"). Как упоминалось выше, функция-член всегда вызывается для определенного объекта (а коль вызов состоялся, объект, стало быть, известен). Таким образом, в теле функции-члена нет необходимости указывать объект вторично. Следовательно, ссылки на переменные а и b будут применяться к копиям этих переменных, относящимся к вызывающему объекту.

Теперь обратите внимание на то, что переменная b — открытый (public) член класса myclass. Это означает, что к b можно получить доступ из кода, определенного вне тела класса myclass. Следующая строка кода из функции main (), при выполнении которой переменной b присваивается число 20, демонстрирует реализацию такого прямого доступа.

ob.b = 20; // К члену b можно получить прямой доступ,

// поскольку он является public-членом.

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

Теперь обратите внимание на то, как вызывается функция-член reset () из функции main (). ob.reset ();

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

Наконец, рассмотрим код функции reset(). Тот факт, что она является функцией-членом, позволяет ей непосредственно обращаться к другим членам того же класса, не используя оператор "точка" или конкретный объект. В данном случае она вызывает функцию-член setab (). И снова-таки, поскольку объект уже известен (он используется для вызова функции reset ()), нет никакой необходимости указывать его еще раз.

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

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

Массивы объектов

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

//287

// Пример использования массива объектов

#include <conio.h>

#include <iostream.h>

enum resolution {low, medium, high};

class display {

int width;

int height;

resolution res;

public:

void set_dim(int w, int h) {width = w; height = h; }

void get_dim(int &w, int &h) {w = width; h = height;}

void set_res(resolution r) {res = r;}

resolution get_res() {return res;} };

char names[3][8] = { "низкий", "средний", "высокий"};

int main() {clrscr();

display display_mode[3]; int i, w, h;

display_mode[0].set_res(low) ;

display_mode[0].set_dim(640, 480);

display_mode [ 1 ].set_res (medium);

display_mode[1].set_dim(800, 600);

display_mode[2].set_res(high);

display_mode[2].set_dim(1600, 1200);

cout <<"Возможные режимы отображения данных:\n\n";

for(i=0; i<3; i++) {

cout << names[display_mode[i].get_res()] <<": ";

display_mode[i].get_dim(w, h);

cout<<w<<" x "<< h << "\n"; }

getch(); return 0; }

При выполнении эта программа генерирует такие результаты.

Возможные режимы отображения данных:

низкий: 640 х 480

средний: 800 х 600

высокий: 1600 х 1200

Обратите внимание на использование двумерного символьного массива names для преобразования перечислимого значения в эквивалентную символьную строку. Во всех перечислениях, которые не содержат явно заданной инициализации, первая константа имеет значение 0, вторая — значение 1 и т.д. Следовательно, значение, возвращаемое функцией get_res (), можно использовать для индексации массива names, что позволяет вывести на экран соответствующее название режима отображения.

Многомерные массивы объектов индексируются точно так же, как многомерные массивы значений других типов.

Инициализация массивов объектов

Если класс включает параметризованный конструктор, то массив объектов такого класса можно инициализировать. Например, в следующей программе используется параметризованный класс samp и инициализируемый массив sampArray объектов этого класса.

//288

// Инициализация массива объектов.

#include <conio.h>

#include <iostream.h>

class samp {

int a;

public:

samp(int n) { a = n; }

int get_a() { return a; } };

int main() { clrscr(); samp sampArray[4] = { -1, -2, -3, -4 };

int i; for(i=0; i<4; i++) cout << sampArray[i].get_a() << ' '; cout << "\n";

getch(); return 0; }

Результаты выполнения этой программы

-1 -2 -3 -4

подтверждают, что конструктору samp действительно были переданы значения от-1 до -4. В действительности синтаксис инициализации массива, выраженный строкой

samp sampArray[4] = { -1, -2, -3, -4 };,

представляет собой сокращенный вариант следующего (более длинного) формата:

samp sampArray[4] = { samp(-l), samp(-2), samp(-3), samp(-4) };

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

//289

//длинный формат инициализации

#include <conio.h>

#include <iostream.h>

class samp {

int a, b;

public:

samp (int n, int m) { a = n; b = m; }

int get_a() { return a; }

int get_b() { return b; } };

int main () { clrscr();

samp sampArray[4][2] = { samp(1, 2), samp(3, 4), samp(5, 6),

samp(7, 8), samp(9, 10), samp(11, 12),

samp(13, 14), samp(15, 16) };

int i; for(i=0; i<4; i++){

cout << sampArray[i][0].get_a()<< ' ' ;

cout << sampArray[i][0].get_b()<< "\n";

cout << sampArray[i][1].get_a()<< ' ';

cout << sampArray[i][1].get_b()<< "\n";}

cout << "\n";

getch(); return 0; }

В этом примере конструктор класса samp принимает два аргумента. В функции main () объявляется и инициализируется массив sampArray путем непосредственных вызовов конструктора samp(). Инициализируя массивы, можно всегда использовать длинный формат инициализации, даже если объект принимает только один аргумент (короткая форма просто более удобна для применения). Нетрудно проверить, что при выполнении эта программа отображает такие результаты.

1 2

3 4

5 6

7 8

9 10

11 12

13 14

15 16

Указатели на объекты

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

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

//290

// Простой пример использования указателя на объект.

#include <conio.h>

#include <iostream.h>

class P_example {

int num;

public:

void set_num(int val) {num = val;}

void show_num(); };

void P_example::show_num() { cout<<num<<"\n"; }

int main() { clrscr();

P_example ob, *p; // Объявляем объект и указатель на него.

ob.set_num(1); // Получаем прямой доступ к объекту ob.

ob.show_num();

p = &ob; // Присваиваем указателю р адрес объекта ob.

p->show_num(); // Получаем доступ к объекту ob с помощью указателя.

getch(); return 0; }

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

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

//291

// Инкрементация и декрементация указателя на объект,

#include <conio.h>

#include <iostream.h>

class P_example {

int num;

public:

void set_num(int val) {num = val;}

void show_num();

};

void P_example::show_num() {

cout << num << "\n"; }

int main() { clrscr();

P_example ob[2], *p;

ob[0].set_num(10); // прямой доступ к объектам

ob[1].set_num(20);

p = &ob[0]; // Получаем указатель на первый элемент,

p->show_num(); // Отображаем значение элемента ob[0] с помощью указателя.

p++; // Переходим к следующему объекту.

p->show_num(); // Отображаем значение элемента ob[l] с помощью указателя.

p--; // Возвращаемся к предыдущему объекту.

p->show_num(); // Снова отображаем значение элемента ob[0].

getch(); return 0; }

Вот как выглядят результаты выполнения этой программы.

10

20

10

Как будет показано ниже в этой книге, указатели на объекты играют главную роль в реализации одного важнейших принципов C++: полиморфизма.