Определение класса. Сокрытие информации. 


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



ЗНАЕТЕ ЛИ ВЫ?

Определение класса. Сокрытие информации.



Определение класса базируется на понятии структуры и имеет вид

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 с.)