Компания ALT Linux
Опубликован: 07.03.2015 | Доступ: свободный | Студентов: 2135 / 486 | Длительность: 24:14:00
Лекция 10:

Объектно-ориентированное программирование

10.3 Создание и удаление объектов

10.3.1 Присваивание объектов, передача в функцию и возвращение объекта

Можно заметить, что в ряде случаев (например, при перегрузке операторов) мы использовали передачу объекта в функцию и возвращение не по значению, а с использованием ссылки. Для этого существует достаточно веская причина: передача объектов по значению, равно как и их присваивание, может приводить к нежелательным последствиям.

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

Рассмотрим в качестве примера класс matrix, хранящий в себе прямоугольную матрицу из элементов типа double. Размерность матрицы будет передаваться конструктору класса, после чего будет выполняться динамическое выделение памяти под нужное количество элементов. В классе будут также предусмотрены методы get_val() чтобы получить элемент матрицы с индексами (i,j) и set_val() чтобы установить в заданный элемент новое значение.

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

#include <iostream>
using namespace std;
class matrix
{
	double *m; //элементы матрицы
	size_t width, height; //число строк и столбцов в матрице
public :
	matrix ( size_t w, size_t h );
	double get_val ( size_t i, size_t j );
	void set_val ( size_t i, size_t j, double val );
	˜matrix ( );
};
matrix::matrix ( size_t w, size_t h )
{
	m = new double [w*h ];
	width = w;
	height = h;
}
matrix::˜ matrix ( )
{
	delete [ ] m;
}
double matrix::get_val ( size_t i, size_t j )
{
	return m[ i * width+j ]; //получить значение элемента матрицы в позиции [i,j]
}
void matrix : :set_val ( size_t i, size_t j, double val )
{
	//устанавливаем значение элемента матрицы в позиции [i,j]
	//если координаты не превышают размер матрицы
	if ( ( i<width )&&(j<height ) ) m[ i * width+j ]= val;
}
main ( )
{
	matrix a(2, 2 ); //объявляем матрицу размерности 2 х 2
	a.set_val ( 0, 0, 100 ); //устанавливаем a[0,0] = 100
	matrix b=a;//присваиваем матрицу
	b.set_val ( 0, 0, 200 ); //устанавливаем b[0,0] = 200
	cout << " a[0, 0 ] = " << a.get_val ( 0, 0 ) << "; " << " b[ 0, 0 ] = " << b.get_val
		( 0, 0 ) << endl;
}

При запуске программа выдаёт сообщение "a[0,0] = 200; b[0,0] = 200" вместо ожидаемого "a[0,0]=100", после чего и вовсе аварийно завершается с сообщением о попытке дважды освободить память. На самом деле это происходит по вполне очевидной причине. При побитовом копировании скопировался адрес указателя m, а не содержимое блока памяти, динамически выделенного по этому адресу. В результате оба объекта получают указатель на одну и ту же последовательность вещественных чисел.

Аналогично присваиванию, объекты можно передавать в функции в качестве аргументов, в точности так, как передаются данные других типов. Однако следует помнить, что в C++ по-умолчанию параметры передаются по значению. Это означает, что внутри функции (а точнее, в стеке) создаётся копия объекта-аргумента, и эта копия, а не сам объект, будет далее использоваться функцией. Благодаря этому функции могут произвольно изменять переданные значения, не влияя на оригинал.

Итак, при передаче объекта в функцию создаётся новый объект, а когда работа функции завершается, копия переданного объекта будет разрушена. Как всегда при разрушении объектов, при этом будет вызван деструктор копии. И здесь может наблюдаться очередной побочный эффект: если переданный в качестве параметра объект содержит в себе указатель на динамически выделенную область памяти, деструктор копии её освободит. Но так как копия создавалась побитовым копированием, деструктор копии высвободит область памяти, на которую указывал объект-оригинал. Исходный объект будет по-прежнему "видеть" свои данные по указанному адресу, однако для системы эта память будет считаться свободной. Рано или поздно она будет выделена какому-то другому объекту, и данные окажутся затёрты.

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

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

Частично проблема может быть решена перегрузкой оператора присваивания для данного класса. Кроме того, для объектов, которым противопоказано побитовое копирование, рекомендуется создавать особую разновидность конструктора — т. н. конструктор копирования (в некоторых источниках также можно встретить название "конструктор копии"). Конструктор копирования выполняет именно то действие, которое заложено в его названии: позволяет программисту лично проконтролировать процесс создания копии.

Любой конструктор копирования имеет следующую форму:

имя_класса ( const имя_класса & obj )
{
	... //тело конструктора
}

Читатель должен помнить, что в таком описании &obj — это ссылка на объект, известная ещё как скрытый указатель.

Оператор присваивания, перегруженный как член класса, связан со своим классом настолько же тесно, как конструктор и деструктор. Эту связь подчёркивает то, что оператор копирования разрешено перегружать только как функциючлен класса, и запрещено — как дружественную функцию. Приведём в качестве иллюстрации две почти одинаковые записи:

point p1, p2; //объявляем два объекта класса point
point p3 = p2; //используем конструктор копирования
p1 = p2; //используем оператор присваивания

Во второй строке примера переменная p3 и объявляется и определяется, а в третьей строке переменной p1 всего лишь присваивается значение. Иными словами, конструктор копирования вызывается для конкретной переменной за время её жизни только один раз, а присваивать значения ей можно многократно. В логике работы конструктора копирования и оператора присваивания настолько много общего, что часто рекомендуют описывать одну операцию в терминах другой. Фактически операция присваивания неявно используется в конструкторе копирования. Однако конструктор копирования может добавлять дополнительные действия по инициализации переменных в довесок к тем действиям, которые должен выполнять оператор присваивания.

Если оператор присваивания для класса не был определён, то в случае необходимости (если для объектов этого класса в тексте программы выполняется присваивание) компилятор автоматически генерирует оператор присваивания по умолчанию, выполняющий то самое побитовое копирование объекта.

10.3.2 Подробнее об указателях и ссылках на объекты

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

Недостаток передачи по указателю — худшая читаемость программы, когда приходится часто взаимодействовать с адресами объектов. Многочисленные операции адресации и разадресации (взятия адреса объекта и взятия содержимого по адресу) могут ухудшать визуальное восприятие текста программы, особенно в сочетании со скобками.

По этой причине в C++ был введён специальный тип данных — ссылка или скрытый указатель. На понятийном уровне ссылку можно воспринимать как другое имя (псевдоним) переменной. Фактически же это указатель на переменную, который выглядит так, как будто к переменной обращаются по значению: программист объявляет такую ссылку, присваивает ей какую-либо переменную и далее пользуется ссылкой как ещё одной переменной. Компилятор же сам автоматически подставляет ко всем обращениям к ссылке операции адресации и разадресации.

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

Напомним: ссылка объявляется так же как указатель, только с использованием знака "&quot; вместо "звёздочки". Сравним, как выглядит код при передаче аргумента по указателю и по ссылке, на примере функции zero(), устанавливающей в ноль координаты переданного ей объекта класса point:

//использование указателей
void zero ( point *p )
{
	p->set ( 0, 0 );
//мы использовали "->"
}
main ( )
{
	point a(3, 4 );
	zero (&a);
}
//использование ссылки
void zero ( point &p )
{
	p.set ( 0, 0 );
//мы использовали " ."
}
main ( )
{
	point a(3, 4 );
	zero ( a );
}

В приведённом примере при применении параметра-ссылки компилятор передаёт адрес переменной, но везде кроме объявления функции код выглядит так, как будто переменная передана по значению. Аналогично ссылки могут использоваться в качестве возвращаемого значения функции. Однако нельзя забывать, что функция, в которую передан параметр по ссылке, будет манипулировать не копией, а самим оригинальным объектом.

Часто ссылки применяют в сочетании с указателем this. Рассмотрим в качестве примера переопределение оператора присваивания для класса point:

point& point::operator= ( point& p )
{
	x = p.x;
	y = p.y;
	return *this;
}

В объявлении функции мы указали ссылочный тип в качестве как аргумента, так и возвращаемого значения. Оператор присваивания должен возвращать результат операции, чтобы стало возможным каскадное присваивание наподобие a=b=c=0. В качестве возвращаемого значения мы указываем разадресованный указатель this, однако возвращён в качестве результата будет тот объект, который вызывал операцию "=", а не его копия.

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

#include <iostream>
using namespace std;
class matrix
{
	double *m; //элементы матрицы
	size_t width, height; //число строк и столбцов в матрице
public :
	matrix ( size_t w, size_t h );
	matrix ( const matrix& m1); //конструктор копирования
	matrix& operator=(matrix & m1); //оператор присваивания
	double get_val ( size_t i, size_t j );
	void set_val ( size_t i, size_t j, double val );
	˜matrix ( );
};
matrix::matrix ( size_t w, size_t h )
{
	m = new double [w*h ];
	width = w;
	height = h;
}
matrix::matrix ( const matrix& m1)
{
	//устанавливаем размер матрицы и выделяем под неё память:
	width = m1.width;
	height = m1.height;
	int size=width*height;
	m = new double [ size ];
	//копируем элементы матрицы:
	for ( int i =0; i < size; i++)
	m[ i ]=m1.m[ i ];
}
matrix& matrix::operator=(matrix& m1)
{
	int size=m1.width*m1.height;
	if ( size > width* height )
		//защищаемся от переполнения буфера
		size=width * height;
	m = new double [ size ];
	//копируем элементы матрицы:
	for ( int i =0; i < size; i++)
		m[ i ]=m1.m[ i ];
	return * this;
}
matrix::˜ matrix ( )
{
	delete [ ] m;
}
double matrix::get_val ( size_t i, size_t j )
{
	//получить значение элемента матрицы в позиции [i,j]
	return m[ i * width+j ];
}
void matrix : :set_val ( size_t i, size_t j, double val )
{
	//устанавливаем значение элемента матрицы в позиции [i,j]...
	//...если координаты не превышают размер матрицы
	if ( ( i<width )&&(j<height ) ) m[ i * width+j ]= val;
}
main ( )
{
	matrix a(2, 2 ); //объявляем матрицу размерности 2 х 2
	a.set_val ( 0, 0, 1 0 0 ); //устанавливаем a[0,0] = 100
	matrix b=a;//присваиваем матрицу
	b.set_val ( 0, 0, 2 0 0 ); //устанавливаем b[0,0] = 200
	cout << " a[0, 0 ] = " << a.get_val ( 0, 0 ) << "; " << " b[ 0, 0 ] = " << a.get_val
	( 0, 0 ) << endl;
}

Внимательный читатель может заметить в коде примера необычную особенность. И конструктор копирования, и оператор присваивания получают доступ к закрытой части переданного объекта m1. На самом деле это вполне естественно. Вспомним: переменные, объявленные в закрытой секции класса, доступны только для методов этого же класса (а не "этого же объекта"). Иными словами, объекты одного класса могут получать доступ к закрытым членам друг друга, хотя используется это не так часто.

Сергей Радыгин
Сергей Радыгин

Символы кириллицы выводит некорректно. Как сделать чтобы выводился читабельный текст на русском языке?

Тип приложения - не Qt,

Qt Creator 4.5.0 основан на Qt 5.10.0. Win7.

 

Юрий Герко
Юрий Герко

Кому удалось собрать пример из раздела 13.2 Компоновка (Layouts)? Если создавать проект по изложенному алгоритму, автоматически не создается  файл mainwindow.cpp. Если создавать этот файл вручную и добавлять в проект, сборка не получается - компилятор сообщает об отсутствии класса MainWindow. Как правильно выполнить пример?