Приветствую Вас Гость | RSS

ALLDev

Воскресенье, 08.09.2024, 03:57
Главная » Статьи » Программирование » C/C++/C#

Учебник C++. Дополнительные возможности классов

Переопределение операций

Язык Си++ позволяет определять в классах особого вида методы – операции. Они называются операциямипотому, что их запись имеет тот же вид, что и запись операции сложения, умножения и т.п. со встроенными типами языка Си++.

Определим две операции в классе String – сравнение на меньше и сложение:

class String
{
public:
 . . .
 String operator+(const String& s) const;
 bool operator<(const String& s) const;
};

Признаком того, что переопределяется операция, служит ключевое слово operator, после которого стоит знак операции . В остальном операция мало чем отличается от обычного метода класса. Теперь в программе можно записать:

String s1, s2;
. . .
s1 + s2

Объект s1 выполнит метод operator с объектом s2 в качестве аргумента.

Результатом операции сложения является объект типа String. Никакой из аргументов операции не изменяется. Описатель const при описании аргумента говорит о том, что s2 не может измениться при выполнении сложения, а описатель const в конце определения операции говорит то же самое об объекте, выполняющем сложение.

Реализация может выглядеть следующим образом:

String
String::operator+(const String& s) const
{
 String result;
 result.length = length + s.length;
 result.str = new char[result.length + 1];
 strcpy(result.str, str);
 strcat(result.str, s.str);
 return result; 
}

При сравнении на меньше мы будем сравнивать строки в лексикографической последовательности. Проще говоря, меньше та строка, которая должна стоять раньше по алфавиту:

bool
String::operator<(const String& s) const
{
 char* cp1 = str;
 char* cp2 = s.str;
 while (true) {
 if (*cp1 < *cp2)
 return true;
 else if (*cp1 > *cp2)
 return false;
 else {
 cp1++;
 cp2++; 
 if (*cp2 == 0) // конец строки
 return false;
 else if (*cp1 == 0) // конец строки 
 return true;
 }
 }
}

Как определять операции

Если для класса определяют операции, то обычно определяют достаточно полный их набор, так, чтобы объекты этого класса могли участвовать в полноценных выражениях.

Прежде всего, определим операцию присваивания. Операция присваивания в качестве аргумента использует объект того же класса и копирует значение этого объекта. Однако, в отличие от копирующего конструктора, у объекта уже имеется какое-то свое значение, и его нужно аккуратно уничтожить.

class String
{
public:
// объявление операции присваивания
String& operator=(const String& s);
};
// Реализация присваивания
String&
String::operator=(const String& s)
{
 if (this == &s)
 return *this;
 if (str != 0) {
 delete [] str;
 }
 length = s.length;
 str = new char[length + 1];
 if (str == 0) {
 // обработка ошибок
 }
 strcpy(str, s.str);
 return *this;
}

Обратим внимание на несколько важных особенностей операции присваивания. Во-первых, в качестве результата операции присваивания объект возвращает ссылку на самого себя. Это дает возможность использовать строки в выражениях типа:

s1 = s2 = s3;

Во-вторых, в начале операции проверяется, не равен ли аргумент самому объекту. Таким образом, присваивание s1 = s1 выполняется правильно и быстро.

В-третьих, перед тем как скопировать новое значение, операция присваивания освобождает память, занимаемую старым значением.

Аналогично операции присваивания можно определить операцию   +=.

Набор операций, позволяющий задействовать класс String в различных выражениях, представлен ниже:

class String
{
public:
 String();
 String(const String& s);
 String(const char*);
 String& operator=(const String& s);
 String& operator+=(const String& s);
 bool operator==(const String& s) const;
 bool operator!=(const String& s) const;
 bool operator<(const String& s) const;
 bool operator>(const String& s) const;
 bool operator<=(const String& s) const;
 bool operator>=(const String& s) const;
 String operator+(const String& s) const;
};

Преобразования типов

Определяя класс, программист задает методы и операции, которые применимы к объектам этого класса. Например, при определении класса комплексных чисел была определена операция сложения двух комплексных чисел. При определении класса строк мы определили операцию конкатенации двух строк. Что же происходит, если в выражении мы попытаемся использовать ту же операцию сложения с типами, для которых она явно не задана? Компилятор пытается преобразовать величины, участвующие в выражении, к типам, для которых операция задана. Это преобразование, называемоепреобразованием типов , выполняется в два этапа.

Первый этап – попытка воспользоваться стандартными преобразованиями типов, определенными в языке Си++ для встроенных типов. Если это не помогает, тогда компилятор пытается применить преобразования, определенные пользователем. "Помочь" компилятору правильно преобразовать типы величин можно, явно задав преобразования типов.

Явные преобразования типов

Если перед выражением указать имя типа в круглых скобках, то значение выражения будет преобразовано к указанному типу:

double x = (double)1;
void* addr;
Complex* cptr = (Complex*) addr;

Такие преобразования типов использовались в языке Си. Их основным недостатком является полное отсутствие контроля. Явные преобразования типовтрадиционно использовались в программах на языке Си и, к сожалению, продолжают использоваться в Си++, что приводит и к ошибкам, и к путанице в программах. В большинстве своем ошибок в Си++ можно избежать. Тем не менее, иногда явные преобразования типов необходимы.

Для того чтобы преобразовывать типы, хотя бы с минимальным контролем, можно записать

static_cast < тип > (выражение)

Операция   static_cast позволяет преобразовывать типы, основываясь лишь на сведениях о типах выражений, известных во время компиляции. Иными словами, static_cast не проверяет типы выражений во время выполнения. С одной стороны, это возлагает на программиста большую ответственность, а с другой — ускоряет выполнение программ. С помощью static_cast можно выполнять как стандартные преобразования, так и нестандартные. Операция   static_cast позволяет преобразовывать типы, связанные отношением наследования, указатель к указателю, один числовой тип к другому, перечислимое значение к целому. В частности, с помощью операции   static_cast можно преобразовывать не только указатель на производный класс к базовому классу, но и указатель на базовый класс к производному, что в общем случае небезопасно.

Однако попытка преобразовать целое число к указателю приведет к ошибке компиляции. Если все же необходимо преобразовать совершенно не связанные между собой типы, можно вместоstatic_cast записать reinterpret_cast:

void* addr;
int* intPtr = static_cast < int* > (addr);
Complex* cPtr = reinterpret_cast < Complex* > (2000);

Если необходимо ограниченное преобразование типа, которое только преобразует неизменяемый тип к изменяемому (убирает описатель const ), можно воспользоваться операцией  const_cast:

const char* addr1;
char* addr2 = const_cast < char* > addr1;

Использование static_castconst_cast и reinterpret_cast вместо явного преобразования в форме (тип) имеет существенные преимущества. Во-первых, можно всегда применить "минимальное" преобразование, т. е. преобразование, которое меньше всего изменяет тип. Во-вторых, все преобразования можно легко обнаружить в программе. В-третьих, легче распознать намерения программиста, что важно при модификации программы. Сразу можно будет отличить неконтролируемое преобразование от преобразования неизменяемого указателя к изменяемому.

Стандартные преобразования типов

К стандартным преобразованиям относятся преобразования целых типов и преобразования указателей. Они выполняются компилятором автоматически. Часть правил преобразования мы уже рассмотрели ранее. Преобразования целых величин, при которых не теряется точность, сводятся к следующим:

  • Величины типа charunsigned charshort или unsigned short преобразуются к типу int, если точность типа int достаточна, в противном случае они преобразуются к типу unsigned int.
  • Величины типа wchar_t и константы перечисленных типов преобразуются к первому из типов intunsigned intlong и unsigned long, точность которого достаточна для представления данной величины.
  • Битовые поля преобразуются к типу int, если точность типа int достаточна, или к unsigned int, если точность unsigned int достаточна. В противном случае преобразование не производится.
  • Логические значения преобразуются к типу intfalse становится 0 и true становится 1.

Эти четыре типа преобразований мы будем называть безопасными преобразованиями.

Язык Си (от которого Си++ унаследовал большинство стандартных преобразований ) часто критиковали за излишне сложные правила преобразования типов и за их автоматическое применение без ведома пользователя. Основная рекомендация — избегать неявных преобразований типов, в особенности тех, при которых возможна потеря точности или знака.

Правила стандартных преобразований при выполнении арифметических операций следующие:

  • вначале, если в выражении один из операндов имеет тип long double, то другой преобразуется также к long double ;
    • в противном случае, если один из операндов имеет тип double, то другой преобразуется также к double ;
    • в противном случае, если один из операндов имеет тип float, то другой преобразуется также к float ;
    • в противном случае производится безопасное преобразование.
  • затем, если в выражении один из операндов имеет тип unsigned long, то другой также преобразуется к unsigned long ;
    • в противном случае, если один из операндов имеет тип long, а другой – unsigned int, и тип long может представить все значения unsigned int, то unsigned int преобразуется к long, иначе оба операнда преобразуются к unsigned long ;
    • в противном случае, если один из операндов имеет тип long, то другой преобразуется также к long ;
    • в противном случае, если один из операндов имеет тип unsigned, то другой преобразуется также к unsigned ;
    • в противном случае оба операнда будут типа int.
(1L + 2.3) результат типа double
(8u + 4) результат типа unsigned long

Все приведенные преобразования типов производятся компилятором автоматически, и обычно при компиляции даже не выдается никакого предупреждения, поскольку не теряются значащие цифры или точность результата.

Как мы уже отмечали ранее, при выполнении операции присваивания со стандартными типами может происходить потеря точности. Большинство компиляторов при попытке такого присваивания выдают предупреждение или даже ошибку. Например, при попытке присваивания

long x;
char c;
c = x;

если значение x равно 20, то и c будет равно 20. Но если x равно 500, значение c будет равно -12 (при условии выполнения на персональном компьютере), поскольку старшие биты, не помещающиеся в char, будут обрезаны. Именно поэтому большинство компиляторов выдаст ошибку и не будет транслировать подобные конструкции.

Преобразования указателей и ссылок

При работе с указателями и ссылками компилятор автоматически выполняет только два вида преобразований.

Если имеется указатель или ссылка на производный тип, а требуется, соответственно, указатель или ссылка на базовый тип.

Если имеется указатель или ссылка на изменяемый объект, а требуется указатель или ссылка на неизменяемый объект того же типа.

size_t strlen(const char* s); 
 // прототип функции
class A { };
class B : public A { };
char* cp;
strlen(cp); 
 // автоматическое преобразование из
 // char* в const char*
 
B* bObj = new B; 
 // преобразование из указателя на производный класс
A* aObj = bObj; 
 // к указателю на базовый класс

Если требуются какие-то другие преобразования, их необходимо указывать явно, но в этом случае вся ответственность за правильность преобразования лежит на программисте.

Преобразования типов, определенных в программе

В языке Си++ можно определить гораздо больше типов, чем в Си. Казалось бы, и правила преобразования новых типов должны стать намного сложнее. К счастью, этого не произошло. Все дело в том, что при определении классов программист может контролировать, какие преобразования допустимы и как они выполняются при преобразовании в данный тип или из данного типа в другой.

Прежде всего, выполнение тех или иных операций с аргументами разных типов можно регулировать с помощью методов и функций с разными аргументами. Для того чтобы определитьоперацию сложения комплексного числа с целым, нужно определить две функции в классе Complex:

class Complex {
 . . .
 friend Complex operator+(const Complex& x, 
 int y);
 friend Complex operator+(int y, 
 const Complex& x);
};

При наличии таких функций никаких преобразований типа не производится в следующем фрагменте программы:

int x;
Complex y;
. . .
Complex z = x + y;

Тем не менее, в других ситуациях преобразования типа производятся. Прежде всего, компилятор старается обойтись стандартными преобразованиями типа. Если их не хватает, то выполняютсяпреобразования либо с помощью конструкторов, либо с помощью определенных программистом операций   преобразования.

Задав конструктор класса, имеющий в качестве аргумента величину другого типа, программист тем самым определяет правило преобразования:

class Complex
{
public:
 // неявное правило преобразования
 // из целого типа в тип Complex
 Complex(int x);
};

Операции   преобразования имеют вид:

operator имя_типа ();

Например, преобразование из комплексного числа в целое можно записать так:

class Complex
{
public:
 // операция преобразования из типа 
 // Complex в целый тип
 operator int();
};

При записи:

Complex cmpl;
int x = cmpl;

будет вызвана функция operator int().

Категория: C/C++/C# | Добавил: artkil (23.09.2012)
Просмотров: 2984 | Теги: c++ | Рейтинг: 5.0/1
Всего комментариев: 0
Имя *:
Email *:
Код *: