Введение. Принципы объектно-ориентированного программирования 


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



ЗНАЕТЕ ЛИ ВЫ?

Введение. Принципы объектно-ориентированного программирования



Введение. Принципы объектно-ориентированного программирования

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

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

ООП базируется на 3-х основных принципах:

1) Инкапсуляция – сокрытие информации. Этот принцип предполагает создание пользовательских типов данных, включающих как данные, так и операции и функции, их обрабатывающие. Никакие другие данные не могут использовать эти операции и функции и наоборот. Контроль над санкционированным использованием данных и функций выполняет компилятор. Такие данные называются абстрактными в отличие от стандартных (встроенных) типов данных (int, char,...). Механизм создания абстрактных типов данных осуществляется через понятие класса.

 

 

2) Наследование – создание иерархии абстрактных типов данных. Определяется базовый класс, содержащий общие характеристики (прародительский класс), а из него по правилам наследования строятся порожденные классы, сохраняющие свойства и методы базового класса и дополненные своими характерными свойствами и методами.

3) Полиморфизм – множественность форм. Это принцип использования одинаковых имен функций и знаков операций для обозначения однотипных действий. В языке С++ полиморфизм используется в двух видах:

а) для обычных функций и операций над стандартными и абстрактными типами данных. Это так называемая “перегрузка функций и операций”;

б) для функций, определенных в иерархии наследования. Это так называемые “виртуальные функции”.

Язык С++ был создан в лаборатории Bell Labs в начале 80-х годов программистом Бьярном Страуструпом в течение нескольких месяцев путем добавления к С аппарата классов. Первый компиляторы появились в 1985 г.

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


 

Глава 1. Классы и объекты

1. Новые возможности языка С++

1.1. Операция разрешения области видимости::

 

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

Например,

 

int x = 1;

void F1()

{x++;}     // x(глоб) = 2

 

void  F2()

{ int  x = 3;

cout << x;   // x(лок) = 3

}

 

void  F3()

{int   x = 5, y;

 y =::x * x;    // 2(глоб) * 5(лок)

 cout << y;    // y = 10

}

Перечислимый тип

 

Перечислимый тип позволяет, и задавать символьные константы, и определять тип данных, значения которых можно перечислить.

Например, описание

 

enum {FALSE, TRUE};

 

опеределяет две константы FALSE = 0, TRUE = 1.

 

int   y;

if (x > 0)   cout << TRUE; else  cout << FALSE;

 

Можно задать тип данных, значения которых – FALSE и TRUE.

 

enum   boolean   {FALSE, TRUE};

 

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

 

boolean  FF(int   x)

{if (x > 0) return TRUE;

 return  FALSE; }

 

Можно задать булевский массив

 

boolean  a[20];

 

Значения переменных перечислимого типа необязательно меняются с 0.

Эти значения можно определить для каждого символьного имени.

Например,

 

enum {Up = 72, Left = 75, Right = 77, Down = 80}; // скэн-коды управляющих клавиш

 

С этими именами их удобно использовать для программирования игр или организации меню. Например,

 

char  s;

s = getch();

if (s = = 0)            // если символ – управляющий

switch(getch())

  {case  Up:...;break;

   case  Down:...;break;

  case  Left:...;break;

  case  Right:...;

}

Модификатор const

 

В языке С константу можно задать директивой define

 

#define  Pi  3.14159

 

В языке С++ принято константу задавать модификатором

 

const тип имя = значение

const double Pi = 3.14159;

 

Такое задание позволяет компилятору контролировать типы?констант. (данных.)

Константная переменная обязательно должна быть проинициализирована, и в дальнейшем ее менять не разрешается.

 

const  double  Pi;          // ошибка

Pi = 9.8;                        // ошибка

 

Константными могут быть и указатели.

Ниже приведены примеры описаний и операторов правильной и ошибочной работы с ними.

 

const  int  nine = 9;

const  int  one = 1;

int  number = 5, n = 7;

int * const  u1 = &number;  // указатель-константа, его значение нельзя менять

cout << *u1;   // вывод 5

*u1 = 10;      // новое значение переменной number

u1 = &n;        // ошибка: значение константы-указателя u1 менять нельзя

const  int * u2 = &nine; // указатель на константу, сам указатель не константа

*u2 = 7;         // ошибка: константу ‘nine’ менять нельзя

const  int *  const  u3 = &nine; // и указатель, и значение – константы, менять нельзя:

*u3 = 99;          // ошибка: константу ‘nine’ менять нельзя

u3 = &number; // ошибка: константный указатель менять нельзя

 

Аналогичны правила работы с указателями на строки:

 

const  char * s1 = “text”; // s1 – указатель на текст-константу, указатель не константа

s1[0] = ’T’;    // ошибка: текст изменять нельзя

const  char * t = ”ttt”;

s1 = t;    // это верно, значение неконстантного указателя можно изменить

char * const  s2 = ”текст”; // s2 – константный указатель

s2[0] = ’Т’;    // верно: в данном случае константа – адрес строки, а текст менять можно

s2 = t;          // ошибка: s2 – константный указатель – изменять нельзя

const  char * const  s3 = ”all - const!”; // Ничего изменить нельзя!

 

1.4. Новый тип данных – ссылка &

 

Ссылка задает альтернативное имя переменной (псевдоним ).

Формат

тип  & имя_1 = имя_2;

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

Например,

 

int  a, b = 4;

int  & x = a;

x = b;          // равносильно a = b

x++;            // равносильно a++

 

Если вывести адреса a и x

 

printf(“%u %u”, &x, &a);

 

то они будут показывать на одну ячейку.

Сравним ссылку с понятием указателя.

 

int  *y = &a;

 

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

 

*y = = x    // содержимое по адресу, который находится

// в указателе сравнимо с целочисленной переменной

y = = &x  // указатель можно сравнить с адресом

// целочисленной переменной

 

Таким образом, ссылку можно рассматривать как постоянный (константный) указатель, который всегда разадресован, т.е. к нему не нужно применять операцию разадресации *.

Этот тип данных в С++ введен по следующим причинам:

1) для того чтобы избежать копирования значения фактического аргумента в формальный аргумент функции; особенно это важно для сложных типов данных – структур, классов;

2) для того чтобы иметь возможность менять значения фактических аргументов (см. ниже пункт 6 б)).

1.5. Функции в С++

 

а) объявление и определение функций

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

Объявление имеет вид

тип_возв_знач  имя_функции   (тип_форм_арг1, тип_форм_арг2,...);

Например,

 

void  Prv(int, int*);

int  Fs(double);

double  Fc(double, double);

char  Ps(char*);

 

Определения этих функций (прототипов) можно поместить теперь где угодно.

 

б) передача аргументов в функцию

Передача аргументов в функцию может осуществляться 3 способами:

·  по значению. Значение фактического аргумента присваивается формальному.

При этом значение фактического аргумента никогда не может измениться.

·  по указателю. Формальному аргументу-указателю передается адрес фактического аргумента. В этом случае значение фактического аргумента может измениться, если применять операции разадресации * и присвоения =.

·  по ссылке. В этом случае формальный аргумент – это ссылка на фактический, то есть и фактический и формальный аргумент – имя одной и той же ячейки.

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

Рассмотрим 3 способа передачи аргументов на классическом примере функции обмена значений двух переменных.


void  Swap(int x, int y)

{int  z;

 z = x; x = y; y = z;

}

void  main()

{int  a = 7, b = 5;

 Swap(a, b);  

// обмена a и b нет

 cout  << a << ’,’  << b;

 // 7, 5

}

void Swap(int * x, int * y)

{int  z;

z = *x; *x = *y; *y = z;

}

void  main()

{int a = 7, b = 5;

Swap(&a, &b);

 cout << a << ’,’ << b;  

// 5, 7

}

 

void Swap(int &x, int &y)

{int z;

 z = x; x = y; y = z;

}

void  main()

{ int  a = 7, b = 5;

 Swap(a, b);

 cout << a << ’,’ << b;

 // 5, 7

}


 


в) inline-функции

 

Обычно определяемая функция компилируется отдельным модулем (подпрограммой). Поэтому обращение к функции и возврат из нее выполняются по определенным правилам (например, сохранение регистров общего назначения (РОН) в стеке при входе в функцию и восстановление их при выходе из нее и др.). Если код функции маленький, то каждое обращение ведет к лишним временным (“накладным” ) расходам. Избежать этого можно, если определить функцию со словом inline:

inline определение_функции

В этом случае компилятор при каждом обращении просто подставляет код тела функции в место вызова. Такие функции называются встраиваемыми.

Ограничение на inline-функции:

·  объявление и определение таких функций должны совпадать;

·  с ключевым словом inline определяют только маленькие функции, в частности, не содержащие циклов и переключателя.

 

г) аргументы по умолчанию

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

Например,

 

void  Line(int k, char   s = ’*’)     // s – аргумент по умолчанию

{int  i;

  for(i = 0; i < k; i++) cout << s;

}

 

Возможные обращения

 

Line(10);      // выведутся 10 ‘*’

Line(50);      // выведутся 50 ‘*’

Line(20,’!’);   // выведутся 20 ‘!’

 

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

Например,

 

void  Prx(double x, int   k = 30, char   s = ’*’) {...}

 

Возможны обращения

 

Prx(15.5);                 // x = 15.5,  k = 30,  s = ‘*’

 Prx(x, 20);               // x = x,  k = 20,  s = ‘*’

 Prx(2 * x, 10, ’+’); // x = 2 * x,  k = 10,  s = ‘+’

 

Неверно обращение

 

Prx(5,, ’!’);

 

д) перегрузка функций

Это одно из проявлений полиморфизма в С++. Перегрузка функций – это использование одинаковых имен для однотипных функций.

Рассмотрим пример.

Пусть требуется написать 3 функции вывода:

· массива a из m целых чисел;

· длинного целого числа;

· строки символов.

Начинаем работу с «придумывания имен», например:

 

void   Printm(int * a, int   m) для массива,

void   Printl(long   n) для длинного целого,

void Prints(char * s) для строки.

 

В С++ все эти 3 функции могут быть заданы одним именем.

 

void  Print(int * a, int  m),

void   Print (long   n),

void  Print(char * s).

 

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

 

Print(“Hello!”);           // функция  Print(char *)

Print(a, 20);             //   функция  Print(int *, int)

Print(50l);              //  функция  Print(long)

 

е) передача функции константных параметров

Модификатор const может быть задан в списке формальных аргументов при определении функции. В этом случае мы хотим подчеркнуть, что значение фактического аргумента функция не имеет права изменять

Например,

 

void   Func(const  char * s)

{.....

s[0] = ’A’; // компилятор выдаст сообщение об ошибке

}

 

ж) константное возвращаемое значение

Рассмотрим пример

 

const  char * GetName();      // объявление

void   main()

{const  char * cp = GetName(); // cp – указатель на константу

 const  char * np = “Иван”;  // np – тоже указатель на константу

 cp[2] = ’н’                            // ошибка

 …

 cp = np;                                    // можно: cp – не константа

 ...

}

const  char * GetName()

{return   “Дима”;}

Объект.

 

Класс – это тип данных, а не объект.

Определение. Объект –это переменная, тип которой – класс, и определяется он обычным образом.

 

void  main()

{String  s1, s2, *s3;   // s1, s2 – объекты, s3 – указатель на объект.

}

 

Говорят также, что s1, s2 – экземпляры класса. Для каждого из них отведена будет память по 255 + 4 байтов.

 

Заметим, что указатель s3 пока не определен, т.е. там грязь.

Посмотрим, как мы теперь можем работать с объектами.

 

s1.Fill(“объект”);

 

s2.Fill(“ класса String ”);

 

s1[0] = ’O’;     // ошибка: s1 – это не массив, и операция [] в нем не определена!

s1.line[0] = ‘O’; // опять ошибка: line – приватное ч/данное, в main использовать нельзя!

s1.Index(0) = ‘О’; // Это верно – пока только так, через ч/функцию, можно «добраться» до символа строки

 

cout << s1.len;          // ошибка: len – приватное член-данное

cout << s1.Length();       // так можно получить длину строки

s3 = &s1;            // s3 – указатель на строку s1

s3 –> Index(0) = ‘O’; // используя функцию Index(int), заменим еще раз букву ‘о’ на ’О’

s3 –> Print();            // вывод слова «Объект»

s3 = &s2;                  // теперь s3 – указатель на объект s2

 

s3 –> Index(s3 –> Length() - 1) = ‘.’; // Используя член-функции класса Length() и Index()

// поставим в конце строки s3  символ '.'

 

s3 –> Print();            // вывод фразы «класса String.»

s3 = new String(«Динамическая память»); // определяется объект в динамической памяти

 

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

Назначение конструктора

 

В С++ при определении переменных часто их сразу инициализируют, например,

 

int  x = 5;

 

Предположим, что при определении объекта

 

String   s;

 

мы хотели бы проинициализировать его, например, пустой строкой:

len = 0; line[0] = ’\0’.

 

Для структур эта инициализация выполняется так:

 

String  s = {“”, 0};

 

Для объектов класса такая инициализация запрещена в силу принципа инкапсуляции.

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

 

s.len = 0; s.line[0] = ’\0’;

 

тоже нельзя, т.к. член-данные из части private недоступны. (Заметим, что если определить их в части public, то их можно инициализировать таким образом

 

String  s = {«», 0};

 

то есть как структуру)

Следовательно, инициализацию должна выполнять специальная член-функция класса.

Определение. Член-функция класса, предназначенная для инициализации член-данных класса при определении объектов класса, называется конструктором. Конструктор всегда имеет имя класса.

Для решения нашей задачи можно записать такой конструктор

 

Strring:: String()  { len = 0; line[0] = ’\0’;}                    (1)

 

объявив его обязательно в теле класса следующим образом:

 

String();

 

Тогда при определении объектов, например,

 

String   s1, s2;

 

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

В классе можно задать не один конструктор, а несколько. Для класса String можно задать конструктор с аргументом, аналогичный функции Fill().

 

String:: String(const   char *  s)

{for(len = 0; line[len]!= ‘\0’; line[len] = s[len], len++);

line[len] = ‘\0’;}

 

Тогда объекты можно определить таким образом:

 

String s1, s2(«Иванов»), *s3 = new String(«Петров»);

 

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

Особенности конструктора, как функции:

1) Главная – конструктор не имеет возвращаемого значения (даже void), так как его назначение – инициализировать собственные член-данные объекта;

2) Конструктор имеет имя класса;

3) Конструктор работает неявно при определении объектов класса.

Недостаток определенного класса String – это то, что он берет для каждого объекта 257 байтов памяти, хотя фактически использует меньше.

Изменим определение класса String таким образом:

 

class  String { char *line; int len;

                  public:

                  ....

                }

;

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

Зададим такие конструкторы.

В классе объявим 2 конструктора:

 

String(int  I = 80);         // с аргументом по умолчанию

String(const char *);     // с аргументом строкой

 

и определим их вне класса

 

String:: String(int  l)          // l = 80 – не повторять!              (2)

{line = new char[l]; len = 0;

  line[0] = ’\0’;

}

String:: String(const  char * s)

{line = new char [strlen(s) + 1];

 for(len = 0; line[len]!= ‘\0’; line[len] = s[len], len++);

line[len] = ‘\0’;

}

 

Эти конструкторы можно использовать таким образом:

 

String   s1(10), s2, s3(«без слов»);

 

Заметим, что в классе должен быть или конструктор по умолчанию без аргументов (вида (1)), или конструктор по умолчанию с аргументом по умолчанию (вида (2)). В противном случае, следующее определение:

 

String ss;

 

вызовет сообщение о двусмысленности.

Конструктор копирования

 

В С++ кроме инициализации значением

 

int  x = 5;

x++;

 

используется инициализация одного данного значением другого

 

int   y = x;

 

 

В классе String подобная инициализация может привести к ошибкам. Рассмотрим почему.

Пусть заданы определения

 

String   s(«паровоз»);

String   r = s;

r.Index(4) = ‘х’; r.Index(6) = ‘д’;

 

Если вывести теперь объекты s и r

 

s.Print();

r.Print();

 

то увидим, что выведется пароход в обоих случаях.

Разберемся, почему это происходит.

 

 

При определении объекта s выделилась память для член-данных len и line, затем конструктор взял динамическую память для слова “паровоз”, в поле line записал адрес, а затем в динамическую область – слово «паровоз». При объявлении объекта r выделяется память только для поля len и указателя line, память для значения line не берется. При инициализации String r = s; выполняется присвоение r.len = s.len и r.line = s.line (говорят, что операция ‘=’ предопределена в компиляторе, как копирование). А последнее означает, что s.line и r.line будут показывать на одну и ту же динамическую область. Поэтому изменение в объекте r приводит к изменению объекта s.

Что неграмотно и недопустимо!

Поэтому для инициализации одного объекта другим надо задать специальный конструктор копирования, заголовок которого имеет вид

X:: X (X &);      // где X – имя класса

В классе String его можно задать следующим образом

 

String:: String(String & s)

{ line = new char[s.len + 1];

for (len = 0; line[len]!= ‘\0’; line[len] = s[len], len++);

line[len] = ‘\0’;}

Тогда инициализация

 

String   r = s; // или String r(s);

 

выполнится грамотно.

 

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

Деструктор

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

Рассмотрим пример. Определена функция

 

void  F()

{ int  k;

String  s1(20), s2(«ФПМК»), *s3;

  s3 = new String («ха-ха»);

}

При выходе из функции освобождается память для локальных объектов, то есть k, s1, s2, s3. Но рассмотрим внимательнее, как это будет реализовано.

 

 

Таким образом, память в динамической области, связанная с объектами s1 и s2, будет считаться занятой («брошенной»). Чтобы этого не происходило, надо задать специальную функцию деструктор.

Определение. Деструктор – это член-функция класса, предназначенная для освобождения динамической памяти, занимаемой член-данными класса, при выходе из функций. Деструктор имеет формат

~ имя_класса() { … }

Для класса String его можно определить таким образом

 

~ String() {delete [ ] line;}

 

В этом случае при выходе из области видимости функции F() память для объектов s1, s2, которую брал конструктор, будет освобождена. Заданный деструктор это будет делать по умолчанию.

 

int k;

String s1(20), s2(“ФПМК”);

 

 

Особенности деструктора как функции:

1) он не имеет аргументов;

2) он не возвращает значения;

3) работает неявно для всех объектов при выходе из функций.

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

 

s3 = new String (“ха-ха”);

delete s3;

 

При выполнении этого оператора память для объекта *s3 будет освобождаться в 3 этапа:

1) деструктором от слова «ха-ха»;

2) операцией delete от полей line и len;

3) стандартным освобождением от локальных переменных.

 

 

В заключение запишем класс String с конструкторами и деструктором:

 

Class  String{ char * line; int   len;

                 public:

                 String(int  l = 80);             // конструктор по умолчанию

                 String(const  char *);   // конструктор с аргументом

                 String(String &);            // конструктор копирования

                 ~String() {delete  line;}    // деструктор

                 void  Print() {cout << ”\nСтрока: “ << line;}

                  int Length() {return  len;};

                  char & Index(int);

                  void  Fill(const   char*);

            };

 

Определим функцию Index() за классом.

 

char & String:: Index(int  i)

{if (i < 0 || i >= n) {cout << «\n Индекс за пределами строки»;

return  line[0];

}

return  line[i];}

 

Тип возвращаемого значения char & – ссылка, то есть возвращается не просто значение символа, а ссылка на ячейку, где он находится. Это и позволяет выполнить присвоение вида

 

r.Index(4) = ’х’;

 

Если бы тип был просто char, то такое присвоение было бы ошибочным, так как компилятор трактует его как присвоение одного кода символа другому коду, как в данном примере

 

‘в’=’х’;

 

что невозможно.

 

5. Неявный указатель this

 

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

Например,

 

s2.Print();

 

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

Например,

void  Print() {cout << this –> line;}

 

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

Например, в классе String определим функцию, которая будет к первой строке приписывать вторую и результатом возвращать первую (конкатенация строк), объявив ее в классе

 

String  Plus(String &);

 

и определив ее за классом:

 

String  String::  Plus(String  &s2)

{char  *t = new  char[len + 1];

strcpy(t, s.line); delete  [ ]line;

len += s2.len; line = new char[len + 1];

strcpy(line, t); strcat(line, s2.line);

delete  [ ] t;

return * this;    // возвращаем “этот” объект  

 

Пример использования этой функуции:

 

String  s1(“Объект “), s2(“класса String.”);

String * s3 = new String(s1.Plus(s2));// работает функция Plus(), а затем конструктор копирования

s3 –> Print();     // вывод  *s3 = ”Объект класса String.”;

Перегрузка операций

 

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

Формат перегрузки двуместной операции имеет вид

тип_возвращаемого_значения operator @  (операнд_2)

{тело_операции},

где @ – знак операции.

Первым операндом является объект, с которым эта операция вызывается, то есть * this, второй операнд – произвольный.

Используется перегруженный знак так же, как для стандартных типов данных

операнд1 @ операнд2

В классе String вместо функции Plus() можно определить операцию ‘+=’.

 

String&  String:: operator +=(String & s2)

{char  *t = new char[len + 1];

strcpy(t, line); delete [ ]line;

len += s2.len; line = new char[len + 1];

strcpy(line, t); strcat(line, s2.line);

delete [ ] t;

return    * this;

}

 

Тогда в примере из п.5 вместо оператора

 

String  *s3 = new  String(s1.Plus(s2));

 

можно записать

 

String  *s3 = new  String(s1 += s2);

 

И еще пример использования.

 

String s(«Студент»), r(«Петров»);

s += r;         // s = «Студент Петров»

 

В классе String определим функцию сравнения двух строк.

 

int  String:: EqStr(String &s)

{if (strcmp(line, s.line))   return 0;        // строки не равны

 return 1;                                            // строки равны

}

 

Использовать ее можно так.

 

String s1(“Иванов”), s2(“Петров”);

if (s1.EqStr(s2)) cout << ”Строки равны”;

else cout << ”Строки не равны”;

 

Но было бы нагляднее для сравнения строк использовать операцию = =. Перегрузим ее для класса String.

 

int  String:: operator = =(String & s)

{ if (strcmp(line, s.line))   return 0; // также как и в функции EqStr()

return 1;            

}

 

Cравнение теперь выглядит привычнее:

 

if (s1== s2) cout << ”\n Строки равны”;

else {s1.Print(); cout << ” – это не “;s2.Print();}  //”Иванов – это не Петров”

 

Формат перегрузки одноместной операции имеет вид

тип_возвращаемого_значения operator @(пусто)

{тело_операции},

где @ – знак операции.

Напишем в качестве примера операцию реверса строки, т.е. перестановки символов в обратном порядке.

 

String  String:: operator ~()

{int  i; char  t;

 for(i = 0; i < len / 2; i++)

    t = line[i], line[i] = line[len – i –1], line[len – i – 1] = t;

return *this;

}

С помощью двух этих операций решим задачу: является ли слово «перевертышем».

 

void  main()

{String  s1(“шалаш”);

String  s2 = s1;      // Работает конструктор копирования

s1.Print();

if (s1 == ~s2) cout << ” – перевертыш”;

else  cout  << ” – не перевертыш”;

}

 

Правила перегрузки:

1)  При перегрузке операции, как член-функции класса, двуместная операция имеет один аргумент, одноместная – ни одного;

2) Знак одноместной операции может быть перегружен только как одноместный, а двуместной – только как двуместный;

3) Наряду с обычным использованием перегруженного знака

obj 1 @ obj 2 для двуместной

и

@ obj           для одноместной

он может использоваться как член-функция класса

 

obj1.operator @(obj2)

и

obj.operator @()

4) Нельзя перегружать операции для стандартных типов данных. Например, + для массивов, определенных, как int * a или int a[20].

5) Нельзя перегружать операции

::    .    ?:    sizeof

Примеры перегрузки некоторых операций

7.1. Перегрузка операции [ ]

 

Пусть определен объект

 

String s(“Еденица”);

 

Заметив ошибку, попытаемся ее исправить

 

s[2] = ’и’;    // ошибка: операция [ ] в классе String не определена

 

Действительно, объект может иметь несколько полей данных типа «массив» и компилятору неизвестно, к какому массиву мы хотим применить операцию [ ]. Следовательно, ее надо определить. Для этого переопределим функцию Index() (см. п.4), как операцию [ ].

 

char &  String:: operator [ ](int  i)

{if (i < 0 || i >= len)

 {cout << ”\n Индекс за пределами строки”;  return  line[0];}

 return  line[i];

}

 

В этом случае можно записать оператор

 

s[2] = ’и’;

 

Заметим (как и в пояснении к функции Index() из п.4), что если возвращаемое значение задать просто как char, то присвоение s[2] = ’и’ выполнить было бы нельзя, так как никакому конкретному значению что-либо другое присвоить невозможно. char & означает, что возвращается имя элемента – ссылка на его место в памяти. Это позволяет и использовать значение символа в операторах и операциях (выводить, сравнивать,…), и менять его значение.

Перегрузка операции ()

Если объект – матрица, то для обращения к ее элементам нельзя перегрузить [ ][ ]. В этом случае можно использовать перегрузку операции ().

 

class  Matrix{int  **a, m, n;

         public:

         Matrix(int, int, int t = 0);

         ~Matrix();

         void Show();

         int& operator() (int, int);

              };

 

 

Matrix:: Matrix(int  mm, int  nn, int  t)   // mm – строк, nn – столбцов, t!= 0 – генерация случайных чисел

 {m = mm; n = nn; int i, j;

a = new int *[m];

for(i = 0; i < m; i++)

       a[i] = new  int [n];

if(t)

for(i = 0; i < m; i++)

for(j = 0; j < n; j++)   a[i][j] = random(50);

}

void  Matrix:: Show()

{int i, j;

 

 for(i = 0; i < m; i++)

{ cout << "\n";

for(j = 0; j < n; j++)

{cout.width(5);             // число позиций для вывода

    cout << a[i][j];}      // или printf("%5d", a[i][j]);

};

 

int& Matrix:: operator() (int i, int j)

{if  (i < 0 || i >= m || j < 0 || j >= n)

{cout << "\n Значения индексов недопустимы. Выход.";exit(1);}

return a[i][j];

}

 

Пример использования.

 

void main()

{randomize();

 Matrix  B(4, 4, 1);

 B.Show();

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

   B(i, i) = 0;             // записать нули на главную диагональ

cout << "\nB:" << endl;

B.Show();

...

}

 

Замечание. Операция () – единственная, которая может иметь произвольное количество аргументов.

 

7.3. Перегрузка операции =

Если объект использует динамическую область, то для него надо перегрузить операцию ‘= ‘– присвоение. Рассмотрим почему.

 

Пусть заданы 2 объекта

 

String s1, s2(“ФПМК”);

...

s1 = s2;

 

Картина присвоения напоминает ситуацию с инициализацией:

 

 

до присвоения

 

 

s1 = s2;

после присвоения:

 

При выполнении операции s1 = s2 для полей line и len выполнится предопределенная операция копирования s2.line = s1.line, s2.len = s1.len.

Это недопустимо по следующим причинам:

1) память в 80 байтов у объекта s1 будет «брошена» (считаться занятой);

2) объекты s1 и s2 будут использовать одну и ту же динамическую память по указателю поля line, что приведет к тому, что любое изменение в поле line объекта s1 приведет к изменению line объекта s2 и наоборот;

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

В классах, где используется динамическая память, операция ‘=’ обязательно перегружается.

Запишем пример перегрузки операции = для класса String.

String  String:: operator =(String  s)

{ if (this!= &s)                          // на случай присвоения s = s

{ delete [ ] line;

   line = new  char [(len = s.len) + 1];

   strcpy(line, s.line);

}

return  *this;

}

 

Теперь присвоение s1 = s2 будет выполняться грамотно.

 

 

7.4. Перегрузки операций + и +=

 

При рассмотрении вопроса о перегрузке операций в абстрактных классах в п.6 был рассмотрен пример перегрузки операции ‘+=’, меняющей первый операнд, то есть *this. В классе String  определим операцию +, которая не меняет ни первого операнда, ни второго, как это принято при сложении базовых типов данных. Например, когда мы выполняем операцию a + b, то результат не записывается ни в a, ни в b, если мы не выполним соответствующего присвоения (например,  a = a + b, b = a + b, c = a + b).

Определение операции + может быть задано таким образом:

 

String  String:: operator + (String  &s)

 {String  z(len + s.len + 1); // определим локальную строку суммарной  длины

strcpy(z.line, line);    // перепишем в нее строку первого операнда

  strcat(z.line, s.line);   // прибавим строку второго операнда

  z.len = strlen(z.line);  // сформируем длину результата

  return z;// работает конструктор копирования результата, затем деструктор разрушает локальный объект z

 }

 

Пример использования операции для сложения 3-х строк.

 

void main()

 {String  s1(“Объект ”), s2(“класса “), s3(“ String”);

  String  s4 = s1 +  s2 + s3; // работают 2 операции ‘+’ и конструктор копирования

  s4.Print();                          // вывод «Объект класса String»

}

7.5. Перегрузка операции ++

Одноместная операция ‘++’ перегружается только в префиксной форме (++i).

Приведем пр



Поделиться:


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

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