Заглавная страница Избранные статьи Случайная статья Познавательные статьи Новые добавления Обратная связь FAQ Написать работу КАТЕГОРИИ: ТОП 10 на сайте Приготовление дезинфицирующих растворов различной концентрацииТехника нижней прямой подачи мяча. Франко-прусская война (причины и последствия) Организация работы процедурного кабинета Смысловое и механическое запоминание, их место и роль в усвоении знаний Коммуникативные барьеры и пути их преодоления Обработка изделий медицинского назначения многократного применения Образцы текста публицистического стиля Четыре типа изменения баланса Задачи с ответами для Всероссийской олимпиады по праву
Мы поможем в написании ваших работ! ЗНАЕТЕ ЛИ ВЫ?
Влияние общества на человека
Приготовление дезинфицирующих растворов различной концентрации Практические работы по географии для 6 класса Организация работы процедурного кабинета Изменения в неживой природе осенью Уборка процедурного кабинета Сольфеджио. Все правила по сольфеджио Балочные системы. Определение реакций опор и моментов защемления |
Введение. Принципы объектно-ориентированного программированияСодержание книги
Поиск на нашем сайте Введение. Принципы объектно-ориентированного программирования В настоящее время компьютеры используются во всех областях человеческой деятельности. В связи с этим в последние годы резко возросла сложность решаемых задач. Для увеличения эффективности их решения уже недостаточно использование традиционных подходов: лучшая организация труда, подбор кадров и т.д. Более важным в настоящее время является выбор инструментария, т.е. системы программирования, языка программирования, т.к. современные системы содержат приемы, позволяющие эффективно решать большие задачи, контролировать критические ситуации (исключения). Подобные приемы получили название – технология программирования. В последние 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; просмотров: 91; Нарушение авторского права страницы; Мы поможем в написании вашей работы! infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 216.73.217.21 (0.013 с.) |