ВВЕДЕНИЕ

2.2 Понятие перегрузки функций и операций, динамическое распределение памяти

2.2.1 Перегрузка и выбор функции

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

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

class Example {

int value;

public:
void Value(int v) { value = v; }

int Value(){return value;}

}
void main(){

Example ex;

e.Value(3);
int k = e.Value();

}

Примером перегруженных функций-членов класса являются конструкторы. Обычно конструкторов в каждом классе несколько.

Используя перегруженные конструкторы, возможно преобразование к типу класса любых других типов данных. Например, для класса Counter применяется конструктор, преобразующий тип Example в тип Counter:

Counter{
int count;

public:
Counter(Example & e) { count = e.Value(); }

}

Перегруженные функции - важная особенность языка С++. Перегружаемая функция выбирается в соответствии со списком параметров при обращении к функции и при объявлении функции. При вызове перегруженных функций компилятор должен иметь алгоритм для выбора соответствующей функции. Этот алгоритм зависит от того, какие преобразования типов доступны. Наилучшее соответствие должно быть уникально. Оно должно быть лучше всех, по крайне мере, для одного параметра, а для всех остальных параметров так же хорошо, как любое другое соответствие.

Алгоритм соответствия для каждого параметра следующий:

- Использовать точное соответствие, если оно найдено

- Проверить поддержку стандартных типов

- Проверить стандартные преобразования типов

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

- Использовать соответствие для аргументов, если оно найдено

Рассмотрим пример.

class complex {

double real,imag;

public:

complex(double r) { real=r; imag=0; }

void assign(double r,double i) { real=r; imag=i; }

void print() { cout << real << “+”<<imag<<”i”; }

// обеспечивает преобразование complex в double

operator double() { return sqrt(real*real+imag*imag); }

};

inline int greater(int i,int j) { return (i > j) ? i:j; }

inline double greater(double i,double j) { return (i > j) ? i:j; }

inline complex greater(complex i,complex j) { return (i > j) ? i:j; }

void main() {

int i=5,j=10;

float x=7;

double y=14;

complex w(0),z(0);

w.assign(x,y); // требует преобразования float x в double

z.assign(i,j); // оба параметра преобразуются в double

// выбирает первое определение greater по правилу соответствия

cout << greater(i,j) << endl;

// Выбирает второе определение greater,

//так как использовано стандартное расширяющее преобразование из float //в double

cout << greater(x,y) << endl;

// Второе определение greater выбирается из-за точного соответствия //правилу.

cout << greater(y,double(z)) << endl;

// точно соответствует третьему определению

cout << greater(w,z) << endl;

}

Функция-член преобразования operator double требуется для оценки условия w<z. Complex переменные w z преобразуются в double. Для возвращаемого типа не необходимости в преобразовании. Для избежания неоднозначности необходимо явное преобразование double(z). Если вместо этого будет использоваться вызов функции:

greater(y,z); // error

то он будет иметь два доступных преобразования, достигших соответствия. Преобразования параметра y из double в complex, определяемое пользователем, соответствует третьему определению. Преобразование z из complex в double, также определяемое пользователем, соответствует второму определению. Но второе определение функции имеет лучшее соответствие для первого параметра, между тем как третья функция имеет лучшее соответствие для второго параметра. Это нарушает требование о том, чтобы “Максимальное соответствие должно быть уникально. Оно должно быть лучше всего для, по крайне мере, одного параметра и одинаково хорошо для всех остальных”.

Одной из привлекательных возможностей языка С++ является переопределение стандартных операций на операнды типов, которых ранее в языке не было. Например, стандартные операции +, -, *, / и т. д., определенные для встроенных типов, таких как int, long, float, double и т. д., могут быть переопределены для новых типов, например, Counter и Example. Все операции делятся на двухоперандные и однооперандные. Эти операции перегружаются по-разному. При этом операторы могут быть перегружены как глобальные функции (обычно дружественные к классу) или как функции-члены класса.

class Example{

int value;

public:
Example( int v ) { value = v;}

Example operator + (Example & e) { return Example(e.value + this->value); }

friend Example operator - (Example &a, Example &b);

};
Example operator - (Example &a, Example &b) { return Example (a.value - b.value); }

void main() {

Example ex1(55);

Example ex2(22);

Example ex3(0);

ex3 = ex1 + ex2;

ex3 = ex1 - ex2;

}

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

Кроме того, надо обратить внимание на тот факт, что бинарные операции не являются коммутативными, то есть для объектов классов Example e и Counter c операции

e + c; и c + e; различны.

Особые правила существуют для перегрузки операций [] ( индексирования ), = ( присвоения ) , () ( вызова функции ), new и delete, а так же префиксных и постфиксных операций ++ и - -.

Можно применять специальные операторы для преобразования из типа класса к встроенным типам. Например, преобразование из типа объекта Counter в тип (int *) может быть выполнено следующим образом:

Counter {

int count;

public:
Counter(int e) { count = e; }

operator int *() { return & count; }

}

void main() {

int * pi;

Counter c(55);

pi = (int * ) c;

}

Здесь определен оператор преобразования - специальный оператор, для которого не возвращается значение результата. Используя эти операторы, нельзя выполнять преобразование класса в другой класс. Операция имеет смысл только для преобразования классов во встроенные типы данных.

Кроме того, существует ряд операций, которые нельзя перегружать. Вот список этих операций:

. - выбор компонента объекта;

.* - обращение к компоненту через указатель на него;

?: - условная операция;

:: - указание области видимости;

sizeof - вычисление размера;

# и ## - препроцессорные операции.

Вопросы для самопроверки.

1. Можно ли перегружать деструкторы?

2. Можно ли перегружать статические функции?

3. Можно ли перегружать виртуальные функции?

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

5. В чем отличие между постфиксной и префиксной операциями?

6. Определите операцию конкатенации двух строк как +.

7. Постройте свой класс и перегрузите для него операции >> и << для ввода и вывода в потоки ввода-вывода.

8. Есть ли разница в следующих конструкциях?

class A {

public: A operator [] (int i );

// ...

};
и
class A {

public:
A& operator [] (int i );

// ...

};

2.2.2 Дружественные функции

Класс может предоставлять особые привилегии определенным внешним функциям или функциям-членам другого класса. Эти функции получили названия дружественных. Если функция или класс объявлены как дружественные данному классу, то такие функции или функции-члены такого класса могут осуществлять непосредственный доступ ко всем полям класса, для которого они дружественны. Дружественные функции и классы могут осуществлять прямой доступ к закрытым полям класса без использования функций-членов этого класса.

Ключевое слово friend - спецификатор функции, который дает функции- не члену класса доступ к скрытым членам класса. Он используется для того, чтобы выйти за строгие рамки типизации и сокрытия данных в С++.

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

Объявление friend функции должно появляться внутри объявления класса, которому она дружественна. Имени функции предшествует ключевое слово friend, и ее объявление может находиться как в public так и в private части класса, что не повлияет на значение. Функция-член одного класса может быть friend-функцией другого класса. Это происходит тогда, когда функция-член объявлена в friend классе с использованием оператора разрешения контекста для определения имени функции дружественного класса. Если все функции-члены одного класса являются friend-функциями другого класса, то это можно определить записью:

freind class имя класса

class t1 {

friend void a(); // friend-функция

int b(); // функция-член

};

class t2 {

friend int t1::b(); // функция-член класса t1 имеет доступ ко всем скрытым полям класса t2

};

class t3 {

friend class t1; // все функции-члены класса t1 имеют доступ ко всем полям класса t3

};

Рассмотрим класс matrix и класс vector. Функция умножения вектора на матрицу должна иметь доступ к private-членам обоих классов. Эта функция будет friend для обоих классов.

class matix;

class vect {

int *p;

int size;

friend vect mpy(const vect &,const matix &);

public:

};

class matrix {

int **base;

int row,column;

friend vect mpy(constr vect&,const matirx &);

};

vect mpy(const vect &v,const matrix &m) {

if(v.size!=m.row) { exit(1); }

vect ans(column);

//.....

return ans;

}

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

Friend-функции можно рассматривать как часть общего интерфейса класса. Существует ряд ситуаций, в которых они могут быть альтернативой функциям-членам. Использование freind-функций спорно, потому что они нарушают инкапсуляцию, окружающую private члены классов. Парадигма ООП утверждает, что объекты (в С++ они - переменные класса) доступны через их public члены. Только функции-члены должны иметь доступ к скрытой реализации АТД. Это ясный и строгий принцип проектирования. Friend-функция находится на самой его границе, поскольку имеет доступ к private членам, сама не являясь функцией-членом. С ее помощью можно организовать быстрый код для доступа к подробностям реализации класса.

Как уже сказано, компоненты класса, тип доступа к которым определяется ключевыми словами private и protected, недоступны извне. Но объявляя любую внешнюю по отношению к классу функцию дружественной, пользователь имеет возможность обращаться из нее к членам данного класса. Для получения статуса дружественной функция должна описана в теле класса с ключевым словом friend. Дружественная функция может быть глобальной функцией, не принадлежащей какому-либо классу, или может быть функцией-членом какого-либо класса. Можно определить дружественный по отношению к классу другой класс, позволяя во всех методах другого класса вызывать защищенные члены первого.

Вопросы для самопроверки.

1. Для чего используются друзья класса?

2. Имеет ли значение, в какую секцию объявления класса помещается объявление дружественной функции?

· Дружественные функции для каждого типа T могут быть друзьями всех классов типа Т. Это обычные дружественные функции.

· Дружественные функции для типа Т могут быть друзьями класса типа Т.

· Дружественные функции могут предваряться спецификацией template. Для типов T и U, функции-шаблоны типа U являются дружественными функциями каждому классу типа Т.

template<class T> class Person {

freind void Pet();

friend void Spouse(Person &);

template<class U> freind void Coworker(U&);

};

void Pet() { /*...*/ } // Обычная функция

template<class T>void Spouse(Person &p) {}

template<class U>void Coworker(U &u) {]

Здесь Pet() - функция, дружественная Person<T> для каждого типа T.

Person<int>

Pet() Person<char>

....

Person<float>

Для любого типа T, скажем int, Spouse(Person<int>&) - функция, дружественная Person<int>, но не Person<char>, или любому другому типу.

Spouse(Person<int>&) Person<int>

Spouse(Person<char>&) Person<char>

... ...

Spouse(Person<float>&) Person<float>

Coworker(U &) - функция, дружественная Person<T>, для любого типа T и любого типа U.

Coworker(int&) Person<int>

Coworker(char&) Peson<char>

... ...

Coworker(float&) Person<float>

· Дружественные функции могут быть функциями-членами другого класса.

template<class T> class Perosn {

freind void Family::Sibilding();

freind void Acquaintance::Casual(Person &);

template<class U>friend void Neighbor<U>::Nextdoor(U&);

};

Здесь Family::Sibilding() - фукнция, дружественная Person<T>, для каждого типа T. Для любого типа Т, скажем для int, Acquaintance<int>::Causal(Person<int>&) - функция, дружественная Person<int>, но не Person<char>, или любому другому типу. Neighbor<U>::NextDoor(U&) -функция, дружественная Person<T>, для любого типа U.

· Дружественные функции могут быть объявлены для всего класса.

template<class T>class Person {

friend class Family;

freind class Acquaintance;

template<class U> freind class Neighbor;

};

Здесь для каждого типа Т все функции-члены класса Family - это функции, дружественные Person<T>. Для любого типа, скажем для int, все функции-члены класса Acquaintance<int> являются друзьями Person<int>, но не Person<char>, или любого другого типа. Для каждого типа Т и каждого типа U все фунции-члены Nieghbor<U> являются друзьями Person<T>.

· Дружественные функции могут также быть друзьями нешаблонных классов.

2.2.3 Перегружаемые операции

Для перегрузки встроенных операторов С++ можно использовать ключевое слово operator. Ему может быть присвоен ряд значений, зависящих от параметров. Таким образом оператору типа + можно присвоить дополнительные значения. Это облегчает написание программ и делает их более читабельными.

vect oprator *(const vect &,const matix &)

где * является двухместным оператором умножения.

matix t; vect s,r; s=t*r; // s=mpy(r,t);

Перегруженный оператор можно вызвать и использованием функциональной формы записи: s=operator*(t,r)

2.2.4 Операторы управления динамической памятью

В библиотеке Си имеются две функции управления динамической памятью - malloc() и free(), которые выделяют и освобождают область памяти заданного размера (в байтах). В этой области программа может разместить переменную (или массив), которая называется динамической. При выделении памяти под динамическую переменную необходимо при помощи операции sizeof определять количество байтов, необходимое для размещения переменной указанного типа. В Си++ введены два оператора, аналогичные функциям malloc и free new и delete. Они отличаются от соответствующих функций тем, что допускают использования в качестве аргументов непосредственно спецификацию типа создаваемой динамической переменной и ссылки на динамическую переменную:

Си++ "Классический" Си

------------------------- ---------------------------------

char *s,x[80]; char *s,x[80];

dat *p,*q; struct dat *p,*q;

void main() { void main() {

p = new dat; p = malloc(sizeof (struct dat));

q = new dat[15]; q = malloc(15*sizeof (struct dat));

gets(x); gets(x);

s = new char[strlen(x)+1]; s = malloc(strlen(x)+1);

... ...

delete p; free(p);

delete q; free(q);

delete s; free(s);

}

Операторы имеют вид:

<результат: ссылка на динамическую переменную> new <абстрактный описатель типа> delete <ссылка на динамическую переменную>