
- •Void main (){
- •Операции, определенные по умолчанию над структурированными объектами
- •Void binar(unsigned char ch)
- •Void main() { int k;
- •Расширение действия (перегрузка) стандартных операций
- •Доступ к компонентам структурированного объекта
- •Void ff(cl1 cl,cl2 c2) { тело_функции }
- •Void f1(...);
- •Void f2(...);
- •Классы и шаблоны
- •Int size; // Количество элементов в массиве
- •Vector(int); // Конструктор класса vector
- •Имя_параиетризованного_класса
- •Int length;
- •Void main () {
- •Наследование и другие возможности классов Наследование классов
- •2. Множественное наследование и виртуальные базовые классы
- •Void show ()
- •Void hide()
- •Void riesquare(void)
- •Void show()
- •Void hide()
- •Void show(void)
- •Void hide()
- •Void main()
- •Void show() // Изобразить на экране эллипс
- •Void hide() { int cc, bk;
- •Int min(int valuel, int value2)
- •Void show()
- •Void hide() // Убрать изображение с экрана дисплея
- •Void main()
- •3. Виртуальные функции и абстрактные классы
- •Void main (void)
- •Имя_проиаводного_класса: : show ()
- •Иня_объекта_производноро_класса. Show ()
- •Void main(void)
- •Void sos (int) ;
- •Void func(char);
- •Void sos (int) ;
- •Void chain::showAll(void) // Изображение элементов списка
- •Void main()
- •4. Локальные классы
- •Void showSeg() // Изобразить отрезок на экране
- •Void showSquare(void) // изобразить квадрат
- •5. Классы и шаблоны
- •Int size; // Количество элементов в массиве
- •Vector(int); // Конструктор класса vector
- •Имя_параиетризованного_класса
Расширение действия (перегрузка) стандартных операций
Одной из привлекательных особенностей языка Си++ является возможность распространения действия стандартных операций на операнды, для которых эти операции первоначально в языке не предполагались. Например, определив новый тип комплексное число нам наверняка захочется иметь возможность записать сумму двух объектов класса комплексное число (comp) в виде s+g. Для того чтобы это стало возможным программист должен определить специальную функцию, называемую "операция-функция" (operator function). Формат определения операции-функции:
тип_функции operator# (список формальных параметров) {операторы_тела_операции-функции}
При необходимости может добавляться и прототип операции-функции с таким форматом:
тип_функции operator#(список формальных параметров);
где # - знак операции, например, =, +, -, и т.д.; operator# - имя функции. Как мы видим определение и описание операции-функции практически ни чем не отличается от обычной функции, но отличия все-таки имеются, и состоят они в наличии двух абсолютно эквивалентных способах вызова оператора-функции. Так для операции сложения двух комплексных чисел после определения операции-функции
comp operator+( comp s, comp g){
comp d;
d.Re = s.Re+g.Re;
d.Im= s.Im+g.Im;
return d;
}
два способа её вызова выглядя следующим образом:
comp k(7,2), e(2,5);
operator+(k, e); //первый способ вызова
k+e; //второй способ вызова
как видно из примера первый способ очень похож на обычный вызов обычной глобальной функции имя_функции(список фактических параметров), а второй способ более краткий позволяет добиться от типов данных, определённых программистом, такой же функциональности как и у фундаментальных типов данных, т.е. перегрузить для новых типов данных необходимый им набор, определённых в языке Си++ операций.
В отличии от обычных функций при реализации операция-функция надо учитывать несколько ограничений, так количество параметров у операции-функции зависит от арности операции и от способа определения функции. Операция-функция определяет алгоритм выполнения перегруженной операции, когда эта операция применяется к объектам класса, для которого операция-функция введена. Чтобы явная связь с классом была обеспечена, операция функция должна быть либо компонентом класса, либо она должна быть определена в классе как дружественная, либо у нее должен быть хотя бы один параметр типа класс (или ссылка на класс). Начнем с последнего варианта.
Если для класса Т введена операция-функция с заголовком Т operator *(T х, T у) и определены два объекта А, В класса Т, то выражение А*В интерпретируется как вызов функции operator * (А,B).
Вторую возможность перегрузки бинарной операции представляют компонентные функции классов. Любая стандартная бинарная операция @ может быть перегружена с помощью нестатической операции-функции, входящей в число компонентов класса. В этом случае у нее должен быть только один параметр и заголовок может выглядеть так:
Т operator @ (Т х)
(здесь Т - определенный пользователем тип, т.е. класс). В этом случае выражение A@B c объектами А, В класса T в качестве операндов интерпретируется как вызов функции А.operator@(В), причем в теле операции-функции выполняется обработка компонентов объекта-параметра В и того объекта А, для которого осуществлен вызов. При необходимости принадлежность компонентов объекту А в теле операции-функции можно сделать явным с помощью указателя this.
Итак, механизм классов дает возможность программисту определять новые типы данных, отображающие понятия решаемой задачи. Перегрузка стандартных операций языка Си++ позволяет сделать операции над объектами новых классов удобными и общепонятными. Но возникают два вопроса. Можно ли вводить собственные обозначения для операций, не совпадающие со стандартными операциями языка Си++? И все ли операции языка Си++ могут быть перегружены? К сожалению (или как констатация факта), вводить операции с совершенно новыми обозначениями язык Си++ не позволяет. Ответ на второй вопрос также отрицателен - существует несколько операций, не допускающих перегрузки. Вот их список:
прямой выбор компонента структурированного объекта;
. * обращение к компоненту через указатель на него;
?: условная операция;
:: операция указания области видимости;
sizeof операция вычисления размера в байтах;
# препроцессорная операция;
## препроцессорная операция.
Рассмотрим еще несколько важных особенностей механизма перегрузки (расширения действия) стандартных операций языка Си++.
При расширении действия (при перегрузке) стандартных операций нельзя и нет возможности изменять их приоритеты.
Нельзя изменить для перегруженных операций синтаксис выражений, т.е. невозможно ввести унарную операцию = или бинарную операцию ++.
Нельзя вводить новые лексические обозначения операций, даже формируя их из допустимых символов. Например, возведение в степень ** из языка Фортран нельзя ввести в языке Си++.
Любая бинарная операция @ определяется для объектов некоторого класса двумя существенно разными способами: либо как компонентная функция с одним параметром, либо как глобальная (возможно дружественная) функция с двумя параметрами. В первом случае х@у означает вызов х.operator@(у), во втором случае х@у означает вызов operator@(x,y).
В соответствии с семантикой бинарных операций ' = ', ' [ ] ' , ' ->' операции-функции с названиями operator =, operator [ ], operator -> не могут быть глобальными функциями, а должны быть нестатическими компонентными функциями. "Это гарантирует, что первыми операндами будут lvalue".
Любая унарная операция '$' определяется для объектов некоторого класса также двумя способами: либо как компонентная функция без параметров, либо как глобальная (возможно дружественная) функция с одним параметром.
Для префиксной операции ' $' выражение $z означает вызов компонентной функции z..operator $ ( ) или вызов глобальной функции operator $(z).
Для постфиксной операции выражение z$ означает либо вызов компонентной функции z.operator$(), либо вызов глобальной функции operator$(z).
Синтаксис языка Си++ определяет некоторые встроенные операции над стандартными типами как комбинации других встроенных операций над теми же операндами. Например, для переменной long m = 0; выражение ++m означает m += 1, что в свою очередь означает выполнение выражения m = m + 1. Такие автоматические замены выражений не реализуются и не справедливы для перегруженных операций. Например, в общем случае определение operator *=() нельзя вывести из определений operator * () и operator = ().
Нельзя изменить смысл выражения, если в него не входит объект класса, введенного пользователем. В частности, нельзя определить операцию-функцию, действующую только на указатели. Невозможно для операнда m типа int изменить смысл выражения 2 + m и т.п.
"Операция-функция, первым параметром которой предполагается основной (стандартный) тип, не может быть компонентной функцией". Для объяснения этого ограничения предположим, что аа - объект некоторого класса и для него расширено действие операции ' + '.
При разборе выражения аа + 2 компилятором выполняется вызов операции-функции аа. operator + (2) или operator +(aa,2).
При разборе 2 + аа допустим вызов operator + (2 ,аа), но ошибочен 2.operator + (аа). Таким образом, расширение действия операции + на выражение стандартный _тип + объект_класса допустимо только с помощью глобальных операций-функций.
При расширении действия операций приходится предусматривать всевозможные сочетания типов операндов. Например, определяя операцию сложения ' + ' для комплексных чисел, приходится учитывать сложение комплексного числа с вещественным и вещественного с комплексным, комплексного с целым и целого с комплексным и т.д. Если учесть, что вещественные числа представлены несколькими ти-пами (float, double, long double) и целые числа имеют разные типы (int, long, unsigned, char), то оказывается необходимым ввести большое количество операций-функций. К счастью, при вызове операций-функций действуют все соглашения о преобразованиях стандартных типов параметров, и нет необходимости учитывать сочетания всех типов. В ряде случаев для бинарной операции достаточно определить только три варианта:
• стандартный_тип, класс
• класс, стандартный_тип
• класс, класс.
Например, для рассмотренного класса comp можно ввести как дружественные такие операции-функции:
comp operator +(comp x, comp у)
{return(comp(x.real + y.real, x.imag + y.imag)); }
comp operator + (double x, complex y)
{return(comp(x + y.real, y.imag)); )
comp operator + (complex x, double y)
{ return(comp(x.real + y, x.imag));}
После этого станут допустимыми выражения в следующих операторах:
comp СС(1.О,2.0); comp ЕЕ;
ЕЕ. = 4.0 + СС;
ЕЕ = ЕЕ + 2.0;
ЕЕ = СС + ЕЕ;
ЕЕ = СС + 20; // По умолчанию приведение int к double
СС = ЕЕ + 'е' ; //По умолчанию приведение char к double
Вместо использования нескольких (в нашем примере вместо трех) очень схожих операций-функций можно задачу преобразования стандартного типа в объект класса поручить конструктору. Для этого требуется только одно - необходим конструктор, формирующий объект класса по значению стандартного типа. Например, добавление в класс complex такого конструктора
comp (double x)
{ real = x; imag =0.0; }
позволяет удалить все дополнительные операции-функции, оставив только одну с прототипом:
friend comp operator +(comp, comp);
В этом случае целый операнд выражения 6+ее автоматически преобразуется к типу double, а затем конструктор формирует комплексное число с нулевой мнимой частью. Далее выполняется операция-функция
operator +(comp(double(6),double(0)), ЕЕ)
Вместо включения в класс дополнительного конструктора с одним аргументом можно в заголовке единственного конструктора ввести умалчиваемое значение второго параметра:
compl(double r, double i = 0.0) { Re=r; Im = i; }
Теперь каждое выражение с операцией ' + ', в которое входит, кроме объекта класса comp, операнд одного из стандартных типов, будет обрабатываться совершенно верно. Однако такое умалчивание является частным решением и не для всех классов пригодно.
В отличие от всех других унарных операций операции ++ и --имеют, кроме префиксной формы еще и постфиксную. Это привело к особенностям при их перегрузке. В начальных версиях языка Си++ при перегрузках операций ++ и -- не делалось различия между постфиксной и префиксной формами.
В современной версии языка Си++ принято соглашение, что перегрузка префиксных операций ++ и -- ничем не отличается от перегрузки других унарных операций, т.е. глобальные и, возможно, дружественные функции operator ++() и operator - -() с одним параметром некоторого класса определяют префиксные операции ++ и --. Компонентные операции-функции без параметров определяют те же префиксные операции. При расширении действия постфиксных операций ++ и -- операции-функции должны иметь еще один дополнительный параметр типа int. Если для перегрузки используется компонентная операция-функция, то она должна иметь один параметр типа int. Если операция-функция определена как глобальная (не компонентная), то ее первый параметр должен иметь тип класса, а второй - тип int.
Когда в программе используется соответствующее постфиксное выражение, то операция-функция вызывается с нулевым целым параметром.
Особенности реализации конструктора копии, деструктора и оператора-функции operator=() для классов имеющих компонентные данные в виде указателей
Если в структурированном типе определены компонентные данные в виде указателей, то для того чтобы объекты этого типа в программе работали правильно необходимо в 99% случаев обязательно явно написать код конструктора копии, деструктора и оператора-функции operator=().
Рассмотрим пример, приведённый ниже
Struct comp{
int Re, Im;
Comp(){Re= 0; Im = 0;} //конструктор по умолчанию
Comp(int r, int i){Re = r; Im = i;} // конструктор
};
В этом примере кроме двух конструкторов нет больше явно определённых компонентных функций, но есть неявные, т.е. те, которые компилятор генерирует сам по умолчанию. Для данного примера компилятор по умолчанию сгенерирует следующие функции со следующим содержанием
comp (comp& T) { Re = T.Re; Im=T.Im;}//Конструктор копии
const comp& operator=(const comp& T){ // оператор функция
Re = T.Re; Im = T.Im; return *this;}
~comp(){} //деструктор
В принципе, поскольку компонентные данные Re и Im не являются указателями, то сгенерированные компилятором функции будут работать правильно, и программисту нет необходимости писать их самостоятельно. Картина сильно изменится, если определить класс comp следующим образом:
Srtuct comp{
int* Re, *Im;
Comp(){Re=new int; Im=new int; ;*Re = 0; *Im = 0;}
Comp(int r, int i){Re=new int; Im=new int;*Re = r; *Im = i;}
};
Для этого примера компилятор по умолчанию сгенерирует те же самые функции с тем же самым содержанием.
comp (comp& T) { Re = T.Re; Im=T.Im;}//Конструктор копии
const comp& operator=(const comp& T){ // оператор функция
Re = T.Re; Im = T.Im; return *this;}
~comp(){} //деструктор
Только в данном случае эти функции будут работать неправильно, и поэтому программисту их необходимо будет написать самостоятельно. Поясним, в чём заключается их неправильность, и покажем, как правильно реализовать эти функции.
Во-первых, поскольку в конструкторах выделяется динамическая память, то в деструкторе она должна освобождаться, т.е. правильный деструктор должен выглядеть следующим образом
~comp(){delete Re; delete Im;}
Во-вторых, если у нас имеется код вида: Comp A(1,1), B(2,2); A=B;, то после выполнения операции присваивания с помощью сгенерированной по умолчанию операцией-функцией operator=() объекты A и B становятся взаимозависимыми, и это чревато трудно обнаруживаемыми ошибками. Сказанное поясним с помощью таблицы, приведённой ниже
До операции присваивания |
После операции присваивания |
A.Re 0x00000001 A.Im 0x00000001
B.Re 0x00000002 B.Im 0x00000002 |
0x00000001 0x00000001
B.Re 0x00000002 A.Re B.Im 0x00000002 A.Im |
До операции присваивания указатели Re и Im объектов A и В указывали на разные фрагменты динамической памяти, которые содержали числа 1 и 2. После выполнения операции присваивания, с одной стороны, на фрагменты динамической памяти, содержащие единицы не указывает ни один указатель, а это значит, что мы не сможем эту память освободить с помощью операции delete, т.е. произошла утечка памяти. С другой стороны, на фрагменты памяти с 2 указывают как указатели объекта A, так и В, а это значит, что эти объекты стали взаимозависимыми.
Для того чтобы избежать такой ситуации необходимо программисту самостоятельно определить операцию функцию operator=() следующим образом:
const comp& operator=(const comp& T){ // оператор функция
*Re = T.*Re; *Im = T.*Im; return *this;
}
Т.е. необходимо копировать не значения указателей, а значения, хранящиеся во фрагментах динамической памяти, на которые указывают эти указатели. При таком способе реализации operator=() объекты A и B останутся независимыми, что и показано в таблице, приведённой ниже
До операции присваивания |
После операции присваивания |
A.Re 0x00000001 A.Im 0x00000001
B.Re 0x00000002 B.Im 0x00000002 |
A.Re 0x00000002 A.Im 0x00000002
B.Re 0x00000002 B.Im 0x00000002 |
Со сгенерированным по умолчанию конструктором копии схожие проблемы. Поскольку конструктор копии вызывается неявно при передаче или возвращению из функции объекта типа comp по значению, то его неправильная реализация приводит к тому, что фактический параметр и локальный объект становятся взаимозависимыми. При выходе из функции локальные параметры уничтожаются, при этом происходит повреждение и внешних фактических параметров. При попытке обратится к фактическому параметру после того, как он был испорчен, приводит к ошибке времени исполнения программы. Поясним сказанное с помощью таблицы и фрагмента кода, приведённого ниже.
void fun(comp B){ }
Comp A(1,1); // неявно вызывается конструктор
fun(A); // неявно вызывается конструктор копии и после выхода из функции деструктор
A.*Re = 3; // Ошибка времени выполнения
До вызова функции fun |
Во время вызова функции fun |
После выхода из функции fun |
A.Re 0x00000001 A.Im 0x00000001 |
B.Re 0x00000001 A.Re B.Im 0x00000001 A.Im |
A.Re ошибочный указатель A.Im ошибочный указатель |
До вызова функции fun() существует объект A, указатели которого указывают на фрагменты динамической памяти с единицами. При вызове функции fun() конструктор копии создаёт локальный объект B и делает его зависимым от объекта A. После выхода из функции fun() вызывается деструктор, который уничтожает локальный объект B, освобождая с помощью операции delete фрагменты динамической памяти, на которые указывают указатели Re и Im объекта В, но на эти же фрагменты указывают и указатели объекта A. В результате объект A становится повреждённым. Его указатели становятся не действительными, и при попытке записать какое-либо значение в память по адресу в этих указателях возникает ошибка времени выполнения.
Чтобы избежать такой ситуации программист должен самостоятельно написать конструктор копии. Для данного примера он должен выглядеть следующим образом:
comp (comp& T) {
Re = new int; Im = new int; *Re = T.*Re; *Im = T.*Im;
}
При такой реализации зависимость между фактическим А и формальным В параметрами не возникает, что и показано в таблице.
До вызова функции fun |
Во время вызова функции fun |
После выхода из функции fun |
A.Re 0x00000001 A.Im 0x00000001 |
B.Re 0x00000001 B.Im 0x00000001 A.Re 0x00000001 A.Im 0x00000001 |
A.Re 0x00000001 A.Im 0x00000001 |
У локального параметра В свои фрагменты динамической памяти, которые никак не связаны с фрагментами фактического параметра A.