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