bigpo.ru
добавить свой файл
  1 2 3 4 ... 7 8
const. Это значит, что функция не должна менять об’ект, который её вызвал. Только такие функции могут использоваться объектами класса, об’явленными константными.

void change(const double a) { re += a;}

Эту функцию мы не можем об’явить константной, так как она меняет поле класса.

Функции можно определять также и вне класса. Например, в классе мы напишем:

void print() const;

А вне его:

void Complex::print() const{ cout<

Четвероточие – оператор разрешения контекста. Об’явление функций должно в обоих случаях совпадать, то есть тип параметров, тип возвращаемого значения и спецификаторы функции должны быть идентичны.

Зачастую определение функции выносится из класса для удобства. Но есть различия и для компилятора. Если функция описана в классе, то её вызов будет компилироваться не как вызов функции, а просто на место вызова функции подставится её код, настроенный на фактические параметры. В этом случае мы проигрываем по памяти, но выигрываем по скорости. Разумно использовать эту возможность для маленьких функций. Результат будет аналогичным, если дописать к определению функции слово inline, а её тело вынести за класс. Чего-то вроде outline, к сожалению, не существует

Описанный класс содержит требуемые данные, но косяк в том, что мы не можем задать или изменить значения полей re и im. Выход – описать в public-части конструкторы или сеттеры/геттеры. Опишем конструктор:

Complex (double r, double i) {re = r; im = i;}

Теперь мы можем написать:

Complex a (3.5,2.7);

Можно описать ещё один конструктор для задания чисел вещественной оси:

Complex (double r) {re = r; im = 0;}

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

Создание хотя бы одного конструктора отменяет действие дефолтного. То есть, мы не можем сейчас написать Complex a ();

Но проще вместо всего этого гемора из трёх конструкторов написать

Complex (double r = 0, double i = 0) {re = r; im = i;}

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


Конструктор копирования. Оператор присваивания.

1) Complex d = a;

2) Complex e = Complex (1,2);

В описанных примерах работает конструктор копирования. По умолчанию он будет описан так:

Complex (const Complex &a) {re = a.re; im = a.im;}

В первом случае произойдёт копирование полей об’екта a в поля d. Во втором случае сначала создаётся безымянный временный об’ект, проинициализированный требуемыми значениями, а потом его поля копируются в поля e.

В C для структур допустим оператор присваивания: b = c;

Присваивание определено и для классов по умолчанию как простое копирование полей:

Complex& operator= (const Complex &a)

{re = a.re; im = a.im; return *this;}

Разумеется, оператор присваивания может быть перегружен.

Функция возвращает ссылку на об’ект, чтобы пользователь мог писать цепочки присваиваний типа, как принято в C/C++:

x = y = z;

Возникает логичный вопрос, почему мы возвращаем параметр по ссылке? Рассмотрим, что будет происходить, если об’явить оператор как

Complex operator= (const Complex &a);

Тип возвращаемого результата Complex. Значит, вернуть мы должны об’ект, которому мы что-то присвоили. this определённая в любой функции класса переменная, которая всегда имеет значение ссылки на об’ект, от которого вызвана функция. Поэтому мы возвращаем *this. Допустим, что пользователь написал цепочку x = y = z; Тогда первое присваивание (правое) вернёт копию y. Потом работает конструктор копирования, получаем временный об’ект, который потом присваивается иксу (левое присваивание). Если же написать амперсанд, то мы вернём ссылку на игрек, который присвоится иксу без всяких временных об’ектов. Зачем платить больше?

Однако на этом отличия не заканчиваются. Подумаем, что произойдёт, если написать:

(x = y).change(2);

Компилятор это одобрит. Но в последнем случае функция вызовется от временного об’екта, который потом умрёт, и эффекта никакого мы не увидим. В первом случае мы возвращаем ссылку на об’ект x, который и ‘изменится’.

Рассмотрим применение описанных функций.

int main (){

Complex z (1.2,3.5); // y = 1.2 + i*3.5

Complex x (1.5); // x = 1.5

Complex y; // y = 0

const Complex t (5); // t – константа 5

Complex z1 = z; // конструктор копирования

Complex z2 = Complex (1,2); // создаётся временный об’ект

//Complex (1,2), потом вызывается конструктор копирования

// компиляторы часто оптимизируют и не создают вр. об’ект

// (стандартом это допускается; в то же время доступность

// конструктора копирования все равно проверяется)

z2 = z; // оператор присваивания

z = t; // для этого пишем const в параметре констр. копир-я

z2.operator= (t); // то же самое

Complex *p;

p = new Complex (1.5); // отводится память в куче

// и вызывается конструктор

/* работа с p */

delete p; // вызывается деструктор, память возвр. в кучу

Complex p[10]; // по адресу p выделяется память для 10

//об’ектов типа Complex. Работает конструктор умолчания

// если он не определён, ошибка!

//инициализировать массив конструкторами с параметрами,

// к сожалению, нельзя L

return 0;

}


Особенности работы с неплоскими классами.

Пример. Описание класса
MFC-строк (откуда дровишки про MFC? - Д.Ч.).

class String { // С-шная строка, с которой хранится её длина.

int size; // длина строки без ‘\0’

char *p; // указатель на строку

public:

String (const char *str){ // конструктор

p = new char [(size = strlen(str)) + 1];

strcpy (p, str);

} // теперь можно создавать строки: String a (“Hello!”);

~String () { delete[] p;}

Пусть теперь с классом работает некоторая функция:

void f(){

String s1 (“Hello!”);

String s2 = s1;

String s3 = String (“2007”);

s3 = s1;

}

Тут происходит поверхностное копирование конструкторами по умолчанию. Это значит, что копируются указатели, но не содержание строк. В этой ситуации s1.p и s2.p ссылаются на одну и ту же область памяти, а при присваивании в последней строчке мы оставляем в памяти мусор. Но самое интересное начнётся при выходе из функции, когда начнёт работать описанный нами деструктор. Он захочет три раза удалить память, занятую строчкой “Hello!”. Но двум смертям не бывать, получим runtime error.

Напишем специальные функции, поддерживающие глубинное копирование.

String (const String &a){

p = new char [(size = a.size) + 1];

strcpy (p, a.p);}

String& operator= (const String &a){

if (this == &a) return *this; // если s1 == s2

delete[] p; // освобождаем память для старой строки

p = new char [(size = a.size) + 1];

strcpy (p, a.p);

return *this;}


Абстрактные типы данных. Перегрузка операций.

Абстрактный тип данных (АТД) — это такой тип, который предоставляет для работы с элементами этого типа определённый набор функций, а также возможность создавать элементы этого типа при помощи специальных функций. Вся внутренняя структура такого типа спрятана от разработчика программного обеспечения — в этом и заключается суть абстракции. Абстрактный тип данных определяет набор независимых от конкретной реализации типа функций для оперирования его значениями. Например, можно определить абстрактные типы для стека, очереди, списка.

Перегрузка операций – описание своих операций (+, -, new и т.д.) вместо уже определённых.

Нельзя перегружать операции: ., ?:,::, .* , # , ## , sizeof, typeid.

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

К сожалению, мы не можем изменить арность операций, то есть бинарная операция всегда будет бинарной, а унарная – унарной. Также операции будут подчиняться законам старшинства, определённым раньше. Часто программисты сталкиваются с проблемой при перегрузке оператора ^ в качестве возведения в степень. От побитового сложения по модулю 2 нам достался приоритет, меньший, чем у сложения или умножения. Очевидно, это противоречит вкладываемому в него смыслу, и приходится использовать скобки.


Перегрузка бинарных операций.

Существует три возможности перегрузки операций:

1) написать функцию-член класса с одним параметром;

2) с помощью глобальной функции с двумя параметрами (в этом случае нам нужен доступ к скрытым полям класса с помощью сеттеров/геттеров, но так получается медленнее и вообще не используется);

3) функция-друг класса с двумя параметрами (если класс об’являет функцию своим другом, она может использовать все поля класса).

Пример. Продолжение описания класса комплексных чисел.

const Complex operator+ (const Complex &a) const{

Complex t (re + a.re, im + a.im);

return t;}

При использовании z = x + y, x – вызывающий об’ект, а y – параметр. Можно опустить const у параметра, но тогда нельзя будет складывать об’екты нашего класса с константами, то есть z = y + 5.

Почему мы возвращаем значение не по ссылке? Если бы мы действительно сделали так, мы вернули бы адрес локальной переменной, которая лежит в стеке, и её память освобождается после выхода из функции. Конечно, можно рассчитывать на то, что компилятор не заругается (зависит от реализации), и значение t не успеет затереться, просто переместится указатель стека. Но тогда, если мы напишем цепочку сложений, то при следующем вызове функции об’ект t скорее всего будет создан на том же месте, и тогда точно программа полетит. Не возвращайте локальные переменные по ссылке!

l-value – переменная, которой возможно присваивание (она обладает адресом) (Вообще-то, термины lvalue и rvalue пишутся без дефисов... - Д.Ч.).

r-value – кусок памяти, которому нельзя присваивать, например, константа или (не всегда!) временный результат вычисления.

Самый первый const превращает полученный результат из l-value в r-value. Иначе можно будет вызвать, например (a+b).print(), а это не по-C++ному.

int main (){

double t = 7.5;

Complex x(1,2), y(5);

const Complex z(1,3);

w = x + y; // равносильно w = x.operator+(y);

// или w.operator= (x.operator+(y));

w = x + z; // можно из-за const у параметра

w = z + x; // можно из-за const в конце

w = x + t; // преобразование типа double к const Complex

// w = x.operator+ (Complex(t)); т.к. есть Complex(double)

w = t + x; // ашыпка

return 0;}

Теперь рассмотрим перегрузку с помощью дружественной функции. Для этого в классе нужно написать:

friend const Complex operator+ (const Complex &a, const Complex &b);

Зона видимости функции совпадает с областью видимости класса. private: на неё не действует! Описание в классе или вне класса (тот же профиль без friend) приводит к одинаковому результату.

const Complex operator+ (const Complex &a, const Complex &b){

Complex t (a.re + b.re, a.im + b.im);

return t;}


int main(){

double t = 7.5;

Complex x(1,2), y(3), z;

z = x + y; // z = operator+ (x, y);

z = x + t; // z = operator+ (x, Complex(t));

z = t + x; // z = operator+ (t, Complex(x));

return 0;}

Так что, если мы хотим смешивать данные, лучше заводить друзей. ;)

Формально можно перегрузить оператор и в классе, и с помощью друга.

z = x + y; // неоднозначность - ошибка

z = x + t; // неоднозначность - ошибка

z = t + x; // друг работает


Перегрузка унарных операций.

Есть те же три возможности перегрузить унарный оператор (так мы используем термин «операция» или «оператор» для перевода 'operator'? Выше то же самое - Д.Ч.) (функция-член класса без параметров, обычная глобальная функция или функция-друг с 1 параметром).

const Complex operator- () const{

Complex t (-re,-im); return t;}


int main (){

double t = 7.5;

Complex x(1,2), y;

const Complex z (1);

y = -x; // y = x.operator-();

y = -z; // y = z.operator-();

-x = y; // низя, т.к. –x не l-value

x = -t; // x = Complex (-t); - здесь нет нашего минуса

return 0;}

Друга пишут аналогично прошлому разу.


Особенности перегрузки операций ++ и --.

Как обычно, есть 3 варианта.

Чтобы отличать описание префиксной формы от описания постфиксной, договорились, что функция для префиксной формы описывается без формальных параметров, а для постфиксной – с одним параметром типа int.

const Complex& operator++ () // ++x;

{ ++re; ++im; return *this;}


const Complex operator++ (int notused) // x++;

{Complex t = *this;

++re; ++im;

return t;}


int main () {

Complex x(1,2); y;

y = ++x; // y = x.operator++();

y = x++; //y = x.operator++(0); -обычно там реально ноль

++ ++x; // нельзя, т.к. ++x не l-value

y = (x + y)++; // нельзя, так мы определили сложение

return 0;}

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


Виды отношений между классами.

Выделяют следующие темы межклассовых отношений:

  • ассоциация

  • наследование

  • агрегация

  • использование

  • инстанцирование (выделяют не всегда)

Ассоциация – тот факт, что пара об’ектов связана между собой. Связь не конкретизируется, обычно такая связь появляется в начале планирования, а потом уточняется и переходит в другие формы.

Далее будем приводить UML-подобные примеры, точнее, мы будем рисовать ER-диаграммы (Entity Relations), которые пришли из области баз данных. (к картинке: справа еще один вариант 'N'; да и бесконечность надо поаккуратней нарисовать – Д.Ч.)


Степень связи (мощность) показывает, сколько об’ектов с каждой стороны могут участвовать в связи. Обозначается спецификацией около карточки.

Возможные виды связей:

  • один к одному

  • один ко многим

  • многие ко многим



Наследование – взаимодействие, выражающее связь ‘частное - общее’ (‘is a‘), например, любое млекопитающее есть животное.


Общее считается базовым классом, а частное – производным. В другой терминологии, принятой, например, в Java, это суперкласс и подкласс. На диаграммах стрелочки рисуются от производного класса к базовому. Производный класс может расширить базовый класс, а может переопределить некоторые его члены.

Агрегация – соотношение ‘часть - целое’ (‘has a’) – ситуация, при которой об’ект одного класса внутри себя содержит об’екты другого класса.


Различают строгую и нестрогую агрегацию. Строгая агрегация (композиция) – агрегация, при которой вложенный об’ект не может исчезнуть, пока существует его хозяин. Тогда ромбик на диаграмме закрашивают.

class Triangle { ... Point v1, v2, v3; ... }

Нестрогая агрегация (часто просто агрегация) – часть может исчезнуть, пока существует целое, например, у треугольника можно отрезать вершину. J Тогда ромбик не закрашивают.


class Shareholder { ... Share *asserts; ... }

Возможно, share == NULL. Вот такой у нас безNULL. J

Использование (зависимость) возникает в трёх случаях:

  • ситуация, при которой имя класса используется в профиле функции-члена другого класса (тип параметров или возвращаемого значения);

  • функция-член одного класса для реализации использует локальную переменную типа – другого класса;

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

При этом используемый класс называется сервером, использующий – клиентом.

тип
Инстанцирование – получение из некоторого абстрактного типа конкретного, из которого потом генерируются об’екты, то есть это связь шаблона класса и того, что генерируется по шаблону.


Единичное наследование в C++

Теперь можно рассказать о модификаторе доступа protected. Если имена попали в эту секцию, это значит, что вне класса они не видимы, так же, как private, однако они доступны в производных классах.

Пример. Сотрудники.

class Person { // человек

protected:

char *name;

int age;

char *address;

public:

Person (char *n, int a, char *addr);

~Person ();

char* getName () const {return name}

void print () const { /* печать данных */ };

};

Person::Person (char *n, int a, char *addr){

name = new char [strlen(n) + 1];

strcpy (name, n); age = a;

addr = new char [strlen(addr) + 1];

strcpy (address, addr);

}

Person::~Person () {delete[] name; delete[] address;}

Опишем теперь класс-сотрудника. Наследование осуществляется с указанием одного из трёх модификаторов доступа, но, как привило, используется public. Конструкторы и деструкторы (кстати говоря, операции присваивания тоже) не наследуются, поэтому в следующем коде мы унаследуем 3 поля и 2 метода.

class Employee: public Person { // работник

protected:

char *appointment;

int level;

double earnings;

public:

Employee (char *n, int a, char *addr,

char *app, int l, int ear); // ear – креатив ТВ J

~Employee();

void print () const;

};

Возникает вопрос, как повторно использовать код конструктора? А вот так:

Employee::Employee (char *n, int a, char *addr,

char *app, int l, int ear): Person (n, a, addr){

appointment = new char [strlen(app) + 1];

strcpy (appointment, app);

level = l; earnings = ear;

}

Сначала работает конструктор Person, а потом наш конструктор. Если не написать двоеточие с «вызовом» конструктора класса Person, будет вызываться конструктор умолчания для Person (а в данном случае мы получим ошибку компиляции). Опишем теперь деструктор, иначе будет работать дефолтный, а он нам не подходит.

Employee::~Employee {delete[] appointment;}

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

Оставим getName() такой, какая она была, а print() перегрузим. Можно считать базовый класс об’емлющей областью описания, а производный – внутренней. Таким образом, если описать функцию, которая была об’явлена в базовом классом, то функция из базового класса скрывается.

void Employee::print() const{

Person::print(); // вызываем print() базового

/* печать appointment, level и earnings */

}

int main (){

Person h (“Andrey Ivanov”, 20, “fds7”);

Employee e (“Boris I. Berezin”, 58, “635”,

top-manager”, 12, 65536.00);

h.print(); // информация об Андрюхе – 3 поля

e.print(); // информация о БИБ – 6 полей

cout<

<< предыдущая страница   следующая страница >>