Лекция 1. Объектно ориентированные языки программирования. Сравнение с традиционными языками

Содержание лекции

Традиционные и объектно ориентированные языки программирования, промежуточное место C++ в этой классификации. Язык Java как пример объектно ориентированного языка.

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

Язык C++ обычно считают объектно ориентированным. На самом деле C++ занимает промежуточное положение и, пожалуй, ближе к традиционным языкам. Наиболее точно C++ характеризуется термином "Объектно ориентированный Ассемблер". Внешне C++ похож на объектно ориентированные языки, но его внутреннее устройство традиционное, это как бы объектно ориентированный способ записи программ на традиционном языке C.

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

Класс — это только описание, но не сами данные (как бы чертеж автомобиля, но не сам автомобиль). В соответствии с описанием класса создаются объекты класса. Одно из главных преимуществ объектно ориентированных языков — возможность создания множества однотипных объектов, в языке для этого достаточно использовать оператор "new" или в C++ просто описать переменную типа класс. Новые объекты инициализируются с помощью специального метода, называемого конструктором.

Все это в равной мере относится как к "настоящим" объектно ориентированным языкам, так и к C++. Основное различие между обычными и объектно ориентированными языками состоит в организации памяти, способах размещения данных программы в памяти и методах доступа к ним. Здесь C++ выступает в качестве традиционного языка.

В традиционных языках имеется 3 вида памяти и, соответственно, 3 различных вида переменных: статическая память, стековая память, динамическая память.

Статическая память выделяется под объекты еще до начала работы программы (на стадии построителя задачи — link, ld и т.п.) или во время начальной инициализации. Во время работы программы эту память не нужно ни захватывать, ни освобождать. Обычно в статической памяти размещают глобальные переменные. В классическом Fortran'e это был единственный используемый тип памяти.

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

Наконец, имеется динамическая память, или "куча" (heap). На размер динамической памяти в современных операционных системах практически нет ограничений. Часть ее размещается в физической памяти компьютера, часть вытесняется ("своппируется") на диск. Для размещения объекта в динамической памяти следует сначала захватить достаточный кусок памяти, в C это делается с помощью функции "malloc", в C++ — с помощью оператора "new". После того как объект становится ненужным, следует освободить занимаемую им память, в C для этого используется функция "free", в C++ — оператор "delete". Обьекты, созданные в динамической памяти, в течение всего времени их существования имеют постоянный адрес, так же как и статические объекты. Главный недостаток динамической памяти состоит в том, что захват и освобождение памяти — крайне медленные операции, причем предсказать время их выполнения заранее невозможно. Связано это с тем, что свободное место в динамической памяти фрагментировано, и на поиск подходящего куска и поддержания структуры динамической памяти может уходить много времени. К тому же от программиста требуется аккуратность, чтобы не забыть в нужном месте освободить захваченную ранее память.

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

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

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

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

Наиболее подходящая область для применения объектно ориентированных языков — это программирование в оконных средах. Собственно, первый объектно ориентированный язык SmallTalk был разработан фирмой Xerox для программирования в оконной системе, идея которой также принадлежит фирме Xerox.

Вернемся к сравнению объектно ориентированных и традиционных языков на примере Java и C++. Сравним два фрагмента программ.

Традиционный язык — C++

class A ... ... A x; A y; ... x = y;

Оператор описания

A x; приводит к захвату памяти под объект типа "A", этот объект получает имя "x". Для него вызывается конструктор класса A. При выполнении строки x = y; происходит копирование содержимого объекта y в объект x (если для класса определен "operator=", то происходит его вызов, если нет, то содержимое памяти, занимаемой объектом y, копируется в память, занимаемую объектом x).

Объектно ориентированный язык — Java

При трансляции приведенного выше фрагмента программы будет выдано сообщение об ошибке в строке

x = y; с диагностикой "переменная y не инициализирована". Дело в том, что при выполнении строки A y; создается переменная y, которая предназначена для хранения ссылки на объект, но никакой объект еще не создан! Корректный фрагмент выглядит так: class A... ... A x; A y; x = new A(); y = new A(); ... x = y; При выполнении строки x = new A(); создается объект класса A, и его handle записывается в переменную x. Совершенно непривычно работает оператор x = y; Никакого копирования объектов не происходит! Просто в x помещается handle того же самого объекта, что и в y. Ссылка на объект, handle которого был раньше в переменной x, пропадает, и если на него нет других ссылок, то система уничтожит его. Таким образом, после выполнения оператора "x = y" останется только один объект, на который будут указывать обе переменные x и y.