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

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

10.2.2 Создание и удаление объекта: конструкторы и деструкторы

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

class spatial_vector
{
	double x, y, z;
public :
	double get_x ( );
	void set_x ( double x );
... .
double spatial_vector::get_x ( ) { return x; }
... .
}

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

Однако есть особая ситуация, когда требуется за один раз присвоить значения переменным-членам класса (всем или большинству): это момент создания объекта, т. е. переменной-экземпляра класса.

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

Конструктор может вызываться явно или неявно. Компилятор сам вызывает конструктор в том месте программы, где создаётся объект класса.

У описания конструкторов в C++ есть следующие особенности:

  • имя конструктора в C++ совпадает с именем класса;
  • конструктор не возвращает никакое значение, но при описании конструктора не используется и ключевое слово void.

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

В C++ деструкторы имеют имена, состоящие из имени класса с префиксом- тильдой: "˜имя_класса". Как и конструктор, деструктор не возвращает никакое значение, но в отличие от конструктора он не может быть вызван явно. Причины такого ограничения очевидны: код, предположительно освобождающий динамическую память, будет обязательно вызван при выходе из области видимости. Его явный вызов привёл бы к тому, что память уже освободилась заранее, а при уничтожении объекта программа попыталась бы сделать это снова и сгенерировала бы ошибку.

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

#include <iostream>
#include <math.h>
using namespace std;
class spatial_vector
{
	double x, y, z;
public :
	spatial_vector ( );
	˜ spatial_vector ( ) { cout << "Работа деструктора\n "; }
	double abs ( ) { return sqrt ( x*x + y*y + z*z ); }
};
spatial_vector::spatial_vector ( )
{
	//конструктор класса vector
	x=y=z =0;
	cout << "Работа конструктора\n ";
}
main ( )
{
	spatial_vector a;//создаётся объект a c нулевыми значениями
	cout << a.abs ( ) << endl;
}

Будучи выполнена, программа выводит следующие сообщения:

Работа конструктора
0
Работа деструктора

Обратите внимание на то, что тела функции abs() и деструктора были описаны непосредственно при объявлении, внутри описания класса. Такой подход обычно применяют для очень простых и коротких методов с тривиальным содержимым. В соответствии с традицией, унаследованной от языка С, описания классов в больших программах на C++ обычно выносятся в заголовочные файлы, в отличие от описания методов. Однако помещение описания простых методов внутрь описания класса имеет дополнительный практический смысл. Компилятор пытается сделать код таких методов встроенным (англ. inline). Это означает, что при обращении к методу вызов соответствующей функции будет заменён непосредственно на её код. Благодаря такому трюку массовое обращение к свойствам класса через его методы (геттеры или сеттеры) не обязательно снижает производительность в сравнении с тем, если бы свойства находились в открытой секции.

10.2.3 Передача параметров в конструкторы

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

#include <iostream>
#include <math.h>
using namespace std;
class spatial_vector
{
	double x, y, z;
public :
	spatial_vector ( double x, double y, double z );
	˜ spatial_vector ( ) { cout << "Работа деструктора\n "; }
	double abs ( ) { return sqrt ( x*x + y*y + z*z ); }
};
spatial_vector::spatial_vector ( double x1, double y1, double z1 )
{
	x = x1;
	y = y1;
	z = z1;
	cout << "Работа конструктора\n ";
}
main ( )
{
	spatial_vector a(3, 4, 0 );
}

В отличие от конструктора, деструктор не может иметь параметров. Это не удивительно: поскольку деструктор не вызывается явно, передавать ему параметры некому.

10.2.4 Указатель this

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

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

При желании программист может явно использовать этот указатель — например, если имена аргументов метода совпадают с именами переменных-членов класса. Посмотрим как это выглядит на примере конструктора для некоторого класса point, содержащего координаты двумерной точки:

... .
class point
{
	int x, y;
public :
	point ( int x, int y )
	{
		this->x=x; this->y=y;
	}
... .
}

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

Иногда использование методов для доступа к защищённым элементам класса из внешней среды может оказаться неудобным. На такой случай предусмотрен специальный обходной путь.

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

class point
{
private :
	int x, y;
	..... .
	friend void find_point ( point* first, point * last, point arg );
};
void find_point ( point * first, point * last, point arg )
{
	for ( point *p= first; p<=last; p++)
		if ( ( p->x == arg.x ) && ( p->y == arg.y ) )
			cout << "Точка с координатами " << p->x << ", " << p->y << " найдена\n ";
}

Важно понимать, что функция find_point() не является членом класса point, хотя и имеет доступ к его закрытой части.

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

Функция-элемент одного класса может быть дружественной другому классу. Например:

class x
{
	..... .
public :
	void f ( ) { };
};
class y
{
	..... .
	friend void x::f ( );
};

Если все функции одного класса дружественны другому классу, можно использовать сокращённую запись:

class y
{
	//.. .
	friend class x;
};

10.2.6 Статические свойства и методы класса

В C++ предусмотрен дополнительный способ совместного использования элемента данных несколькими объектами — статические члены класса.

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

Чтобы объявить статический элемент класса, перед ним необходимо указать ключевое слово static. Для примера добавим в класс point статическое свойство count — счётчик, указывающий, сколько экземпляров данного класса существует в памяти в настоящий момент. Очевидно, что управлять содержимым счётчика будут конструктор и деструктор класса.

#include<iostream>
using namespace std;
class point
{
	int x, y;
	//.. .
	static int count;
public :
	point ( ) { cout << "Создаётся точка с номером " << ++count << endl; }
	˜ point ( ) { cout << "Разрушается точка с номером " << count-- << endl; }
};
int point::count;
main ( ) {
	point a,b,c;
}

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

Создаётся точка с номером 1
Создаётся точка с номером 2
Создаётся точка с номером 3
Разрушается точка с номером 3
Разрушается точка с номером 2
Разрушается точка с номером 1

Метод класса также можно объявить статическим. Такой метод будет вести себя одинаково для всех объектов, т. е. не будет различать, для какого именно объекта он вызван. По этой причине статическим методам не передаётся скрытый указатель this. Однако взамен статические методы получают преимущество: их можно вызвать, не создавая объект класса. Например, статическими могут быть содержащиеся в классе сервисные функции, если они не используют никаких данных объекта, т. е. сам контекст вызова им по сути не нужен.

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

#include<iostream>
#include<time .h>
using namespace std;
class alarm
{
	time_t alarm_t;
public :
	static void current_time ( );
	//....
};
void alarm::current_time ( )
{
	time_t t = time (NULL); //получаем текущее время в нотации Unix, в виде числа секунд,
		//прошедших с 1 января 1970 г.
	struct tm tm = * localtime (& t ); //переводим в местное текущее время
	cout << tm.tm_hour << " : " << tm.tm_min << " : " << tm.tm_sec << endl;
}
main ( )
{
	alarm::current_time ( );
}

Как видно из примера, для доступа к статическому методу класса без указания объекта достаточно всего лишь написать перед именем метода имя класса и поставить оператор разрешения области видимости.

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

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

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

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

 

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

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