Заглавная страница Избранные статьи Случайная статья Познавательные статьи Новые добавления Обратная связь КАТЕГОРИИ: АрхеологияБиология Генетика География Информатика История Логика Маркетинг Математика Менеджмент Механика Педагогика Религия Социология Технологии Физика Философия Финансы Химия Экология ТОП 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; просмотров: 161; Нарушение авторского права страницы; Мы поможем в написании вашей работы! infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 3.137.185.180 (0.625 с.) |