Автор работы: Пользователь скрыл имя, 22 Декабря 2012 в 03:04, реферат
Объектно-ориентированный подход к проектированию программных систем является элементом так называемых наукоемких технологий проектирования программ. Использование этого подхода дает возможность на порядок по сравнению с обычным директивным программированием сократить трудоемкость отладки программ и внесения изменений в программу при ее последующем развитии. Платой за это является наукоемкость проектирования, т.е. уделение весьма большой части времени на детальную проработку предметной области программы, составление структуры данных и их взаимосвязи, а также проектирование программной архитектуры.
Введение………………………………………………………………………2
Механизм перегрузки………………………………………………………...3
Варианты и проблемы………………………………………………………..4
Перегрузка и полиморфные переменные…………………………………...8
Функциональная форма операторов…………………………………….....10
Перегрузка унарных операторов…………………………………………...17
Бинарные операторы. …………………………………………………….....20
Возвращаемые значения. …………………………………………………...21
Особые операторы…………………………………………………………...23
Рекомендации к форме определения операторов………………………….25
Заключение…………………………………………………………………...26
Литература……………………………………………………………………27
5.Функциональная форма операторов
Операторы (например, +) используются в двух вариантах: как особая синтаксическая форма или как функция. В C++ функциональная форма всегда представляет собой ключевое слово operator, за которым следует символ оператора.
class Foo {...}
Foo x, y, z;
z = x + y; // Инфиксная (нормальная) форма
z = operator+(x, y); // Функциональная форма (внешняя функция)
z = x.operator+(y); // Функциональная форма (функция класса)
С концептуальной точки зрения три последние строки эквивалентны, хотя на практике, вероятно, оператор будет определен либо в виде внешней функции, либо в виде функции класса, но не в обоих вариантах сразу. Для бинарных операторов знак оператора указывается между двух аргументов в инфиксной форме. В форме внешней функции оба аргумента передаются глобальной функции. В форме функции класса объект, которому принадлежит вызываемый оператор, указывается слева, а аргумент - справа от знака оператора. Унарные операторы (такие как ! и ~) тоже могут перегружаться. Форма внешней функции вызывается с одним аргументом, а форма функции класса вызывается без аргументов (операция выполняется с объектом, находящимся слева от оператора . или
->).
Не разрешается перегружать встроенные операторы (например, оператор целочисленного сложения). Чтобы обеспечить выполнение этого условия, компилятор требует, чтобы хотя бы один аргумент каждого перегруженного оператора относился к пользовательскому типу (обычно к классу). Выбор ограничен операторами, уже определенными в C++. Во время долгих ночных отладок мне часто хотелось создать оператор с именем #$%^&, но C++ на этот счет неумолим.
Перегруженные операторы наследуют приоритеты и атрибуты группировки от встроенных операторов, поэтому вы не можете, например, изменить стандартный порядок группировки «слева направо» для оператора +. Не существует ограничений на тип значения, возвращаемого перегруженным оператором, и один оператор можно перегружать произвольное число раз при условии, что сигнатуры остаются уникальными.
Перегрузка операторов в форме внешних функций
Чтобы перегрузить оператор в форме внешней функции, необходимо определить глобальную функцию.
class String {
friend String& operator+(const String&, const String&);
private:
char* s;
public:
// Конструкторы и т.д.
}
String& operator+(const String& s1, const String& s2)
{
char* s = new char[strlen(s1.s) + strlen(s2.s) + 1];
strcat(s, s1.s, s2.s);
String newStr(s);
delete s;
return newStr;
}
String s1 = "Hello";
String s2 = "Goodbye";
String s3 = s1 + s2;
Перегруженная функция выглядит так же, как и любая глобальная функция (если не считать странного имени). Именно для таких случаев и были придуманы друзья. Если бы мы не объявили функцию operator+ другом, то она не имела бы доступа к переменной s, и мы оказались бы перед выбором: то ли разрешить всем на свете доступ к char*, то ли перейти к менее эффективной реализации, при которой строка копируется при каждом обращении к ней. С концептуальной точки зрения operator+ является частью библиотеки String, поэтому нет ничего страшного в том, чтобы объявить эту функцию другом и вручить ей ключи к внутреннему устройству String.
Внешними функциями могут
Перегрузка операторов в форме функций класса
Синтаксис напоминает обычную перегрузку функций класса, разве что количество аргументов уменьшается на 1 по сравнению с формой внешней функции.
class String {
private:
char* s;
public:
// Конструкторы и т.д.
String& operator+(const String&) const;
};
String& String::operator+(const String& s1) const
{
char* s2 = new char[strlen(s1.s) + strlen(s) + 1];
strcat(s2, s1, s);
String newStr(s2);
delete s2;
return newStr;
}
48
String s1 = "Hello";
String s2 = "Goodbye";
String s3 = s1 + s2;
Любой оператор может быть перегружен
в форме функции класса. Если оператор
может перегружаться как
Первый аргумент относится к базовому типу (например, int или double).
Тип первого аргумента определен в коммерческой библиотеке, которую нежелательно
модифицировать.
Компилятор ищет перегрузку в форме функций класса, просматривая левую часть бинарных
операторов и единственный аргумент унарных. Если ваш тип указывается справа и вы хотите воспользоваться перегрузкой в форме функции класса, вам не повезло. Самый распространенный пример перегрузки в форме внешней функции - оператор << в библиотеке ostream.
ostream& operator<<(ostream& os, const String& s)
{
os << str.s; // Предполагается, что данная функция является другом
return os;
}
Перегрузка должна осуществляться в форме внешней функции, поскольку ваш тип, String, находится справа - если, конечно, вы не хотите залезть в готовые заголовки iostream.h и включить в класс ostream перегрузку в форме функции класса для своего класса String. Наверное, все-таки не хотите.
Примечание: предыдущий пример может не работать в вашем компиляторе, если функции strlen и strcat, как это часто бывает, по недосмотру разработчиков получают char* вместо const char*. Вы можете решить, что игра не стоит свеч, и объявить функцию неконстантной, но это выглядит слишком жестоко. Лучше избавиться от константности посредством преобразования типов, если вы абсолютно уверены, что библиотечная функция не модифицирует свои аргументы, и готовы смириться с предупреждениями компилятора.
String& String::operator+(const String& s1) const
{
char* s2 = new char[strlen((char*)s1.s) + strlen(s) + 1];
strcat(s2, (char*)s1.s, s);
String newStr(s2);
delete s2;
return newStr;
}
Видите, что происходит, если кто-то забывает о константности? Операторы преобразования Оператор преобразования - особый случай. Если конструктор представляет собой отображение аргументов на домен вашего класса, то оператор преобразования делает прямо противоположное: по экземпляру вашего класса он создает другой тип данных.
class String {
private:
char* s;
public:
operator long(); // Использует atol для преобразования к типу long
};
String::operator long()
{
// Вероятно, здесь следует проверить, что строка
// представляет собой число,
принадлежащее к диапазону
return atoll(s);
}
String s("1234");
long x = s; // Вызывается функция operator long()
Операторы преобразования должны быть функциями класса. Как видно из показанного фрагмента, операторы преобразования хороши тем, что компилятор обычно сам может определить, когда они должны вызываться. Если ему понадобится длинное целое, он ищет оператор long(). Если ему понадобится объект Foo, он ищет в классе Foo либо конструктор с аргументом String, либо operator Foo(). Возникает интересный вопрос: если оператор преобразования делает фактически то же, что и конструктор, почему бы не обойтись чем-нибудь одним? Преимущество конструкторов состоит в том, что они обеспечивают инкапсуляцию результирующего класса. Чтобы сконструировать объект другого класса, оператор преобразования должен очень много знать о нем. Вот почему для перехода от одного типа к другому обычно используются конструкторы. А если осуществляется
переход к базовому типу вроде int? Вряд ли вы будете требовать, чтобы компилятор создавал для int новые конструкторы, которые знают о существовании ваших пользовательских типов. А если и будете, то не рискнете признаться в этом вслух. Только оператор преобразования может автоматически перейти к базовому типу. Даже если результирующий тип не является базовым, он может быть частью готовой библиотеки, которую нежелательно модифицировать. И снова оператор преобразования справляется с задачей.
Операторы преобразования можно объявлять для любых типов данных. Они вызываются без аргументов, а тип возвращаемого значения определяется по имени оператора. Операторы преобразования, как и все остальные операторы, бывают константными или неконстантными. Часто определяется как константная, так и неконстантная версии одного оператора. Как правило, константная версия работает более эффективно, поскольку неконстантная версия обычно выполняет копирование данных.
class String {
private:
char* s;
public:
operator const char*() const { return s; }
operator char*():
};
String::operator char*()
{
char* newStr = new char[strlen(s) + 1];
strcpy(newStr, s);
return newStr;
}
Клиентский код, использующий неконстантную версию, должен взять на себя ответственность за удаление дубликат
6.Перегрузка унарных операторов.
Рассмотрим примеры перегрузки унарных операторов для определенного выше класса Integer. Заодно определим их в виде дружественных функций и рассмотрим операторы декремента и инкремента:
class Integer
{
private:
int value;
public:
Integer(int i): value(i)
{}
//унарный +
friend const Integer& operator+(const Integer& i);
//унарный -
friend const Integer operator-(const Integer& i);
//префиксный инкремент
friend const Integer& operator++(Integer& i);
//постфиксный инкремент
friend const Integer operator++(Integer& i, int);
//префиксный декремент
friend const Integer& operator--(Integer& i);
//постфиксный декремент
friend const Integer operator--(Integer& i, int);
};
//унарный плюс ничего не делает.
const Integer& operator+(const Integer& i) {
return i.value;
}
const Integer operator-(const Integer& i) {
return Integer(-i.value);
}
//префиксная версия
const Integer& operator++(Integer& i) {
i.value++;
return i;
}
//постфиксная версия
const Integer operator++(Integer& i, int) {
Integer oldValue(i.value);
i.value++;
return oldValue;
}
//префиксная версия
const Integer& operator--(Integer& i) {
i.value--;
return i;
}
//постфиксная версия
const Integer operator--(Integer& i, int) {
Integer oldValue(i.value);
i.value--;
return oldValue;
}
Теперь вы знаете, как компилятор различает префиксные и постфиксные версии декремента и инкремента. В случае, когда он видит выражение ++i, то вызывается функция operator++(a). Если же он видит i++, то вызывается operator++(a, int). То есть вызывается перегруженная функция operator++, и именно для этого используется фиктивный параметр int в постфиксной версии.
7.Бинарные операторы.
Рассмотрим синтаксис перегрузки бинарных операторов. Перегрузим один оператор, который возвращает l-значение, один условный оператор и один оператор, создающий новое значение (определим их глобально):
class Integer
{private:
int value;
public:
Integer(int i): value(i)
{}
friend const Integer operator+(const Integer& left, const Integer& right);
friend Integer& operator+=(Integer& left, const Integer& right);
friend bool operator==(const Integer& left, const Integer& right);
};
const Integer operator+(const Integer& left, const Integer& right) {
return Integer(left.value + right.value);
}
Integer& operator+=(Integer& left, const Integer& right) {
left.value += right.value;
return left;
}
bool operator==(const Integer& left, const Integer& right) {
return left.value == right.value;
}
Во всех этих примерах операторы перегружаются для одного типа, однако, это необязательно. Можно, к примеру, перегрузить сложение нашего типа Integer и определенного по его подобию Float.
8.Возвращаемые значения.
Как можно было заметить, в примерах используются различные способы передачи аргументов в функции и возвращения значений операторов.