Заглавная страница Избранные статьи Случайная статья Познавательные статьи Новые добавления Обратная связь FAQ Написать работу КАТЕГОРИИ: ТОП 10 на сайте Приготовление дезинфицирующих растворов различной концентрацииТехника нижней прямой подачи мяча. Франко-прусская война (причины и последствия) Организация работы процедурного кабинета Смысловое и механическое запоминание, их место и роль в усвоении знаний Коммуникативные барьеры и пути их преодоления Обработка изделий медицинского назначения многократного применения Образцы текста публицистического стиля Четыре типа изменения баланса Задачи с ответами для Всероссийской олимпиады по праву
Мы поможем в написании ваших работ! ЗНАЕТЕ ЛИ ВЫ?
Влияние общества на человека
Приготовление дезинфицирующих растворов различной концентрации Практические работы по географии для 6 класса Организация работы процедурного кабинета Изменения в неживой природе осенью Уборка процедурного кабинета Сольфеджио. Все правила по сольфеджио Балочные системы. Определение реакций опор и моментов защемления |
Определение класса. Сокрытие информации.Содержание книги
Поиск на нашем сайте Определение класса базируется на понятии структуры и имеет вид class имя_класса {тело_класса}; Тело класса содержит определение данных класса (член-данных) и объявление или определение функций, их обрабатывающих (член-функций). По иной терминологии член-данные – свойства, член-функции – методы. Например, определим класс String:
const int MS = 255; class String {char line[MS]; int len; void Fill(const char *); // объявление int Length() {return len;} // определение void Print() {cout << “\n Строка:” << line;} // определение char & Index(int i); // объявление };
Здесь член-данные – line, len; член-функции – Fill(), Print(), Length(), Index(). Член-функции отличаются от обычных функций следующим: а) они имеют привилегированный доступ к член-данным класса, то есть используют их непосредственно б) область их видимости (действия) – класс, то есть они могут использоваться только с переменными этого класса. Член-данные могут располагаться в любом месте описания класса, они “видны” всем член-функциям. Но таким образом определенный класс использовать не сможем. Единственное, что можно сделать – определить переменные этого типа или указатель.
Например,
String str1,*str;
Но запись str1.len = 10 вызовет сообщение об ошибке
‘String::len’ is not accesible - «Переменная len из класса String недоступна».
Для того, чтобы работать с классом, для его член-данных и член-функций надо определить тип доступа. Существуют 3 типа доступа: private –член-данные и член-функции доступны только член-функциям класса; protected – член-данные и член-функции доступны член-функциям базового и порожденного классов; public –член-данные и член-функции общедоступны. Обычно большую часть член-данных размещают в части private (сокрытие информации – инкапсуляция), а большую часть член-функций – в части public(интерфейс с программой). Для классов по умолчанию считается доступ – private (поэтому в нашем примере оказался тип доступа private, то есть всё как бы спрятано в “капсулу”), для структур, наоборот, – public. Итак, поставим перед первой функцией public: и тогда определение класса String примет следующий вид:
const int MS = 255; class String {char line[MS]; int len; public: void Fill(const char *); // объявление int Length() {return len;} // определение void Print() {cout << “\n Строка:” << line;} // определение char & Index(int i); // объявление };
В этом случе в программе можно определить оператор
int x = str1.Length();
который вернет длину строки str1, определенной выше. Описания private и public могут стоять в любом месте описания класса и повторяться. Теперь вернемся к член-функциям: две из них определены, две объявлены. Определить объявленные функции вне класса можно, используя операцию области видимости ‘::’. Формат определения: тип_возвращаемого_знач-я имя_класса:: имя_ф-ции(список_арг-в) { тело _ функции } Например,
void String:: Fill(const char * s) {for(len = 0; line[len]!= ‘\0’; line[len] = s[len], len++); line[len] = ‘\0’;} char & String:: Index(int i) {...}
Чем отличаются член-функции, определенные внутри тела класса и вне его? Отличие в том, что внутри класса они получают неявно статус inline(Поэтому, если функция определена внутри класса и содержит операторы цикла, компилятор выдаст предупреждение). Член-функциям, определенным вне класса, также можно присвоить статус inline явно первым словом
inline char & String:: Index(...) {...} Объект.
Класс – это тип данных, а не объект. Определение. Объект –это переменная, тип которой – класс, и определяется он обычным образом.
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). Приведем пример ее перегрузки для класса String: операция увеличивает коды символов на 1.
String String:: operator ++() {for(int i = 0; i < len; i++) line[i]++; return *this; }
Использование:
void main() {String d{“12345*678”); ++d; d.Print(); // d = ”234567+789” }
Аналогично перегружается операция --. Перегрузка операции (тип) Напомним, что операция (тип) используется для преобразования базовых типов данных. Например, если мы хотим вывести код символа char s = ‘*’, то сделать это можно оператором
cout << (int)s;
В С++ есть еще такая форма записи оператора (тип) тип (выражение). Например,
float a = 3.76, b = 0.5, c = 1.22, d = 7; int k = int(a * b – c * d / b);
Пусть задан такой фрагмент программы:
String s1, s2(“Солнце!”); char *str = ”Жарко!”;
Как отреагирует компилятор на следующие присвоения?
s1 = str; // допустимо: преобразование из char* в String выполняет конструктор String(char *) и // в s1 перепишется строка «Жарко!», поле len = 6.
Итак, преобразование
конструктор(базовый тип) базовый тип ----------> абстрактный
выполняет конструктор абстрактного класса с аргументом базового типа(если есть). Теперь рассмотрим присвоение наоборот
str = s2; // ошибка: компилятор не знает, какое поле из объекта s2 требуется переписать в str, то есть, // что понимается под преобразованием String –> char*
Таким образом, если требуется явно или неявно выполнять преобразование
str = (char *)s2;
то надо определить, что понимается под этим преобразованием. Перегрузка операции преобразования имеет общий вид operator тип () {…} В нашем случае, например, её можно определить следующим образом
String:: operator char *() {return line;}
Тогда присвоение
str = s2; // неявное преобразование Sring –> char *
или
str = (char *)s2; // явное преобразование
не вызовет ошибочного сообщения компилятора, и мы получили str = ”Солнце!”. Можно задать и такое преобразование из типа String в int:
String:: operator int() {return len;}
Тогда можно выполнить присвоениe
int k = s2; // k = 7, так как s2 = “Солнце!” и длина строки 7;
Таким образом, преобразование
operator тип абстрактный класс -------> базовый
задается специальным оператором (тип). Определим более полезное преобразование из String в int: преобразование числа-строки в форму целого числа.
String:: operator int() {int k = 0, i; for(i = 0; i < len; i++) k = k * 10 + line[i] - ‘0’; return k; }
Пример использования:
String digit(“12345”); int m; m = digit; // m = 12345, произошло преобразование числа-строки в форму целого числа
Рассмотрим преобразование для класса Complex.
Если в классе Complex определен конструктор вида
Complex(float d = 0) {re = im = d;},
то в функции main() будут справедливы такие действия:
Complex c1, c2(5, 3); float x = 3.3, y; c1 = x; // Работает конструктор Complex(float), который определит вещественную и мнимую части // комплексного числа равными x, то есть с1(3.3 + i * 3.3)
Присвоение
y = c2;
будет ошибочным, так как неизвестно, что надо взять, а что отбросить, когда преобразуем сложный объект в простой.
Поэтому обратное преобразование Complex –> float надо задать, например, таким образом:
Complex:: operator float() {return re;}
Тогда оператор
y = c2;
будет верным и y = 5 – вещественная часть c2. Можно определять преобразование
operator (абстрактный тип2) абстрактный тип1 ---------> абстрактный тип2
Например, необычное преобразование String –> Complex:
String:: operator Complex() {Complex z(len);
return z; // если в классе Complex есть конструктор с одним аргументом }
Тогда следующий фрагмент кода будет выглядеть совершенно нормально
String s(“Маша ела кашу”); Complex c; c = s; // c = 13 + i * 13
Задание. Определите столь же необычное, но полезное преобразование Complex –> String.
Особенности перегрузки операции (тип): 1) Нет аргументов и возвращаемого значения (даже void), так как тип – это и есть возвращаемое значение. 2) В теле операции обязательно должен быть оператор return со значением, тип которого является типом преобразования. Дружественность Пример. Пусть некоторая функция Show выводит строку в красивом виде – обрамленную «звездочками»:
void Show(String &s, int cf, int ct) {int i, m = s.len; for(i = 0; i <= m + 1; i++) cout << ‘*’; cout << endl; cout << ‘*’ << s.line << ‘*’ << endl; for(i = 0; i <= m + 1; i++) cout << ‘*’; cout << endl; }
Очевидно, что задать такую функцию компилятор не позволит, так как будет нарушена инкапсуляция член-данных len и line. Если все-таки необходимо разрешить некоторой не член-функции (внешней функции) использовать член-данные из части private какого-либо класса, ее можно объявить дружественной этому классу.
class String {friend void Show(String &, int, int); // в любом месте определения класса char *line; … };
Вообще другом класса может быть: 1) внешняя по отношению к классу функция, как в нашем примере; 2) член-функция известного на данный момент другого класса. Например, если член-функция f класса A использует член-данные класса B, то f надо объявить «другом» классу B.
class B {friend тип_возвр_знач A:: f(аргументы); // сама f определяется в A ... }; 3) другой определенный (или объявленный) на данный момент класс. class A {...}; class B {friend class A; .... };
Такое объявление означает, что всем член-функциям класса A разрешается доступ ко всем член-данным класса B, но не наоборот. Замечание 1. Дружественность нужно использовать оптимально, так как она нарушает принцип инкапсуляции. Замечание 2. Операции можно перегружать и как внешние дружественные классу функции. В этом случае одноместная операция имеет один аргумент – объект класса, а двуместная – два: объект класса и второй операнд. Пример. Покажем, как будет выглядеть перегрузка операции + как внешней дружественной функции.
class String {... friend String operator + (String & s, String & t) {String z(s.len + t.len + 1); // определим локальную переменную суммарной длины strcpy(z.line, s.line); strcat(z.line, t.line); z.len = strlen(z.line); return z; } … };
Используется она так же, как и перегруженная в классе. 9. Перегрузка операций потокового ввода >> и вывода <<. Для использования операций потокового ввода и вывода надо подключить заголовочный файл <iostream.h>. Библиотека iostream содержит стандартные классы ввода-вывода: класс istream – потоковый ввод со стандартного устройства stdin (клавиатура), класс ostream – потоковый вывод на стандартное устройство вывода stdout(монитор). Рассмотрим их. Ostream В классе ostream определена операция <<, перегруженная для форматизированного вывода базовых типов данных, т.е.
class ostream{... public: ostream & operator << (char *); ostream & operator << (char); ostream & operator << (int); ostream & operator << (long int); ostream & operator << (double); ... };
cout – это стандартное имя потока вывода, т.е. в системе есть описание
ostream cout;
Поэтому операцию cout << x рассматриваем как двуместную: слева первый операнд – имя потока вывода, справа второй операнд – имя переменной. Так как возвращаемое значение – ссылка на cout, то можно писать цепочки вывода. Например, пусть задана переменная x
int x;
Цепочка вывода
cout << ”x = “ << x << ’\n’;
представляет собой последовательное выполнение операции << с аргументами разного типа:
char * int char ((cout.operator <<(“x =”)).operator <<(x)).operator<<(‘\n’);
cout
cout
cout
Оператор, определенный как член-функция класса, первым операндом всегда имеет объект класса, т.е. *this. Первым операндом операции << является поток вывода, поэтому ее можно перегрузить для абстрактных типов только, как дружественную классу. Например, для класа String эта перегрузка может быть определена таким образом.
class String{ public: ... friend ostream & operator << (ostream &r, String &s) { r << s.line; return r; } };
Теперь и для объектов класса String можно применять операцию потокового вывода <<:
String s(«Иванов»); cout << s;
Istream В классе istream определена перегруженная операция >> для базовых типов данных
class istream{... public: istream & operator >> (char *); istream & operator >> (char &); istream & operator >> (int &); istream & operator >> (long int &); istream & operator >> (float &); istream & operator >> (double &); ..... };
Имеется определение стандартного имени cin:
istream cin;
Если определить переменную
Int x;
то операция
cin >> x;
означает, что введенное число со стандартного устройства ввода передается в переменну
|
||
|
Последнее изменение этой страницы: 2021-12-07; просмотров: 276; Нарушение авторского права страницы; Мы поможем в написании вашей работы! infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 216.73.216.198 (0.015 с.) |