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

Вступление.

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

Копирование и создание объектов.

Как мы уже выяснили, для создания объектов нужен конструктор. Но когда возникает потребность в его вызове? Таких случаев больше чем Вы думаете. Как вы, наверное, уже знаете, параметры в функции можно передавать либо, по значению либо по переменной. В первом случае создается побайтовая копия переданного объекта, которая размещается в стеке функции, в которую произошла передача параметра. Во втором случае функция работает с внешней переменной (по отношению к функции), не создавая копии. Сейчас подробнее рассмотрим первый случай и следующий класс:
class cArray{
public:
char *ptr;
cArray( void ){ptr=new char[1000000];}
~cArray(delete[]ptr;)
};

Это “массив”. Сейчас Вы поймете, почему я настроен так скептически:
void	f( cArray a ){}
//где-то в программе
cArray a1;
f( a1 );
printf( “%s” , a1.ptr );

Теперь максимум внимания: создали объект, конструктор выделил память, передали его в функцию по значению, в процессе передачи создалась побайтовая копия объекта (в том числе создались побайтовый копии указателей, а не памяти на которые они указывают). Это значит, что теперь указатели обоих объектов указывают на одну и ту же область памяти. Теперь функция отрабатывает своё, в ней вызывается деструктор для стековой копии объекта, и память УДАЛЯЕТСЯ, в то время как исходный объект ничего не знает об этом. Естественно при попытке поработать с ним возникнет ошибка. Как же этого избежать? Для того чтобы стать хозяевами положения нам нужно разобраться с новым понятием:

Ссылки.

Они появились только в CPP, в оригинальном С их не было. Что же это такое? По сути, ссылки являются синонимами какой-либо переменной. Они имеют другое имя, нежели переменная, но при работе с ней все изменения происходят в той же области памяти, как если бы мы работали с самой переменной. Например:
int	i;
int &j=i;
j=5;
//теперь и i==5

Так же нужно помнить, что ссылка не может существовать отдельно от переменной (в программе вы можете увидеть, что я сначала определяю переменную или область памяти, а потом ссылку). Чем же они отличаются от указателей? Прежде всего, удобством в использовании:
//ссылочная организация
int i,j;
int &k=i, &m=j;
//swap
int &n = i;
k = m;
m = n;

//swap в стиле С
int i,j;
int *k, *m;
*k=i;
*m=j;
//swap
int n = i;
*k = *m;
*m = n;

Согласитесь, первый вариант выглядит элегантнее… В сторону лирику, нам нужно объекты по значению передавать. Итак:

Конструкторы копий.

Перепишем наш класс следующим образом:
class cArray{
public:
char *ptr;
cArray( void ){ptr=new char[1000000];}
~cArray(){delete[]ptr;} cArray( const cArray &a ){ ptr = new char [1000000]; memcpy( ptr , a.ptr , 1000000 ); printf( “%s” , ”конструктор копий” ); } };
Теперь при передаче по значению будет вызываться этот конструктор, не допуская копирования указателей. Я позволю себе лишь несколько замечаний: в данный конструктор передается ссылка на объект, которая не позволяет создавать копию объекта, иначе была бы бесконечная рекурсия, а вот const ничего не дает, это своего рода предохранитель, что бы в исходном объекте в процессе написание кода конструктора ничего не испортить (ссылка – это передача по переменной, помните?). Осталось только попросить Вас: когда будете думать передавать объект по значению или по переменной, подойдите к этому серьёзно, бинарное дерево по значению передавать как-то тоскливо :). Результатом выполнения следующего кода (класс массива взят из предыдущего примера):
cArray f( void ){
cArray a;
return( a );
}

void main( void ){
f();
}

Будет:
конструктор копий
Следовательно, возвращение результата (в конкретном случае) происходит также по значению, и без конструктора создалась бы побайтовая копия, но к счастью у нас все под контролем.

Перегрузка оператора присваивания.

Но все-таки наш класс еще не обладает должной гибкостью. Обыкновенные переменные еще можно присваивать. А что будет с объектом класса? Все очень просто и одновременно очень сложно – компилятор просто создаст оператор присваивания по умолчанию, который будет… внимание: создавать побайтовую копию объекта, стоящего справа от оператора (на самом деле есть некоторая оговорка, но об этом позже)! Что ж придется все брать в свои руки:
class cArray{
public:
char *ptr;
cArray( void ){ptr=new char[1000000];}
~cArray(){delete[]ptr;} cArray( const cArray &a ){ ptr = new char [1000000]; memcpy( ptr , a.ptr , 1000000 ); printf(“%s”,”конструктор копий”); } cArray operator=( cArray & a ){ ptr = new char [1000000]; memcpy( ptr , a.ptr , 1000000 ); printf(“%s”,”оператор присваивания”); return( *this ); } };
Теперь в любом месте программы, где вы будете присваивать один объект другому, вызовется этот метод. Заметьте, что оператор возвращает измененный объект. Это Вам потребуется, если захотите выстроить несколько операторов присваивания в цепочку:
banana b1,b2,b3;
b1 = b2 = b3;

Поскольку оператор присваивания выполняется справа на лево, то последовательность действий такая: b3 присваивается b2, возвращается копия b2 после присваивания, b2 присваивается b1, возвращается копия b1 после присваивания. Причем такой порядок действий будет не зависимо от того, перегружен ли оператор присваивания для класса banana или нет. Но копию возвращать как-то нерационально – будем возвращать сам объект. Код такой:
cArray &operator=( cArray & a ){
	ptr = new char [1000000];
memcpy( ptr , a.ptr , 1000000 );
printf(“%s”,”оператор присваивания”);
return( *this );
}

Возвращается просто ссылка (суть синоним переменной).

Объекты в качестве полей класса.

Таким образом, что бы создать рабочий и, самое главное, безопасный класс надо 1) конструктор копий 2) перегрузить оператор присваивания Многовато, не находите? А что если наш класс является полем класса B, который является полем класса С, который … и так далее, вплоть до класса Z. Неужели для каждого из этих классов надо прописывать все эти методы? На самом деле все проще и хитрее. Смотрите:
class	apple{
public: void operator=( apple& ){printf("operator = apple\n");}
apple( void ){} apple( apple& ){printf("copy constructor apple \n");}
}; class banana{
public: int i; apple a;
banana( void ){ printf("default constructor\n"); } banana( int i ){ printf("int param constructor %i\n",i); } ~banana(){ printf("dectructor %i\n",i); } }; int main( void ){ banana b1,b2; b1 = b2; getch(); return 0; }
В классе banana НЕТ перегруженного оператора присваивания. При запуске этой программы Вы увидите следующее:
default constructor
default constructor
operator = apple

Теперь становится понятно, что компилятор сначала ищет операторы присваивания полей класса, если есть, то выполняет присваивание, если нет, то создает побайтовую копию поля. Аналогичная ситуация с конструкторами копий:
//классы возьмите из предыдущего примера
void f( banana ){
}

int main( void ){
banana b1,b2;
f( b1 );
getch();
return 0;
}

Результат будет:
default constructor
default constructor
copy constructor apple
destructor

Заключение.

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

ЗЫ: связь по-прежнему dodonov_a_a (___AT) inbox.ru

ЗЫЫ: как всегда в конце статьи интересный и неочевидный пример:

class	cBanana{
public:
	int	&ref;

	//ссылка инициализируется, но переменной НЕТ!
	//только область памяти, и никакого мошенничества!!!
	cBanana( void ):ref( * new int( NULL ) ){};
};

Смежные вопросы:
Урок 1. Основы классов.
© 2004-2005 Savardge.ru
Hosted by uCoz