лекции (2003) (Глазкова) (1160821), страница 8
Текст из файла (страница 8)
P=new T;// это можно сделать более 1000000 раз
А выполнить команду fd=open(…); можно лишь порядка десятков раз, т.е. файл более дефицитный ресурс, чем память.
Мусор появляется тогда, когда динамическая память выделена, но не освобождена. Когда сказывается проблема мусора? Она сказывается в серверных программах. Мусор очень тяжело отловить. Проблема мусора сказывается, когда операция должна выполниться сотни тысяч или миллионы раз. В процессе эксплуатации программное изделие должно работать дни или даже месяцы. Проблема мусора реально скажется на стадии эксплуатации программного изделия. Поэтому необходимо использовать специальные средства, которые позволяют бороться с мусором.
2.Более острая проблема – висячие ссылки.
p=new T;
p1=p;
delete p;
*p1; // ошибка
Пример: «реклама»
While (p!= NULL)
{
free (p);
p=p->next;
}
Этот цикл уничтожает линейный список. Существуют системы, на которых этот фрагмент работает (однопоточная программа – один процесс). Если многопоточная программа, то будет работать неправильно, т.к. разные потоки используют один и тот же менеджер динамической памяти. Такие ошибки очень трудно обнаружить. Ошибка – нарушение защиты памяти. При переносе на другую среду эта программа может работать по-другому.
Висячие ссылки – очень опасная ошибка. Она проявляется значительно раньше, чем мусор. Кроме того эта проблема не статическая.
Борьба с проблемами
-
Отладочные менеджеры памяти. Любая система программирования (например, С, С++) содержит отладочные менеджеры памяти. Они ведут счетчик ссылок. Если образуется мусор, то будет выдано, что программа не освободила некоторый объем памяти. Т.е. они позволяют бороться и с мусором, и с висячими ссылками. Но окончательное решение проблемы состоит в том, откуда берется мусор и висячие ссылки. Мусор берется из-за того, что мы не выполняем delete, а висячие ссылки образуются, когда мы выполнили delete, но не почистили соответствующие ссылки на объект. Поэтому и первая, и вторая проблемы возникают из-за неправильного выполнения явной операции удаления мусора.
-
Бороться с этими проблемами можно с помощью автоматической сборки мусора. Это очень сильно повышает надежность, поскольку работа с динамической памятью – это неотъемлемое свойство всех современных программ.
В языке Ада вопросам надежности уделялось очень большое внимание. Стандарт языка Ада не навязывает конкретную реализацию. Разработчик компилятора имеет право ввести в язык автоматическую сборку мусора (например, с помощью механизма счетчик ссылок). С каждым объектом динамической памяти связывается счетчик ссылок. Когда указатель на объект создается, счетчик устанавливается на единицу. При каждом присваивании p:=p1; счетчик увеличивается на единицу. Но для автоматической сборки мусора необходимо отслеживать все операции присваивания и передачи параметров.
Почему разработчики языка Ада не пошли по пути обязательной автоматической сборки мусора?(Язык Oberon, например,– язык с автоматической сборкой мусора). Сборка мусора обязательно влечет накладные расходы. И еще, сборщики мусора обычно работают таким образом: они исчерпывают память, пока память не переполнится, и только тогда включается сборщик мусора. И это делается с точки зрения эффективности (если после каждого присваивания собирать мусор, то программа будет тормозить). Здесь возникают две проблемы:
-
общая неэффективность.
-
в какой-то момент времени неожиданно может включиться автоматический сборщик мусора и программа зависнет (т.е. ничего не будет делать, пока сборщик мусора не закончит свою работу.)
С точки зрения надежности, в языке Ада такая непредсказуемая сборка не годится.
С этой точки зрения, в некоторых реализациях нельзя было позволить автоматическую сборку мусора, а в некоторых можно.
В Аде существует специальный пакт, в котором есть процедура UNCHECKED_DEALLOCATION (p);
(это примерно то же самое, что free(p) в языке С).
Прежде чем выполнять такую процедуру, надо серьезно подумать, т.к. в этом случае может остаться висячая ссылка, что может быть еще хуже.
Во всех современных ЯП ( Java, C#), как и в языке Oberon, реализована автоматическая сборка мусора.
Но если язык нестрогий (С,С++), автоматической сборки мусора там не сделаешь. И кроме того, указатель служит просто некоторой абстракцией адреса. Указатель может указывать на все , что угодно (на статическую переменную, на область памяти массива, на функцию), и в нестрогих ЯП (основанных на С) любые преобразования между этими указателями разрешены.
Интересен подход, который Microsoft пытается применить к языку C#. Известно, что работа с указателями (в стиле языка С) эффективна, но не надежна, а работа со ссылками (в стиле языка Java) более надежна, но менее эффективна. В этом подходе Microsoft есть понятие управляемый код (manage code) и понятие безопасного (safe) и небезопасного (unsafe) кода. Управляемый код считается безопасным.
В управляемом коде нет понятия указателя, все операции проводятся только посредством стандартного распределителя памяти и т.д.
В то же время есть небезопасный код, но мы должны указать unsafe и далее некоторый блок ( в этом блоке можно использовать указатели с указательной арифметикой в стиле языка С, утилиты malloc, free).
ОС может запретить выполнение небезопасного кода при определенных условиях, а управляемый код она разрешает выполнять всегда.
Т.о,. сделан компромисс между надежностью и эффективностью.
Итак, современные языки проблему с динамической памятью решают динамической сборкой мусора, и в них понятие указателя замещено на понятие ссылки.
В языке Java все типы данных делятся на простые и референциальные (ссылочные).
Массивы и классы являются ссылочными типами данных.
Пусть
X x = new X, где X – некоторый класс.
Если в языке С++ это означало распределение памяти под объект класса Х, то в языке C#, Java и Delphi это означает ссылку на объект класса Х. Ссылка на объекты динамической памяти – это аналог понятия указателя в таких языках, как Oberon и Ada.
Лекция 8
Продолжим рассмотрение простых типов данных.
Мы уже достаточно подробно рассмотрели указатели, которые являются математической абстракцией адреса, и отметили, что с указателями связано много проблем, одна из которых – висячие ссылки и сборка мусора – решается с помощью автоматической сборки мусора.
Чистые объектно-ориентированные языки (C#, Java, Oberon) – это языки с автоматической сборкой мусора. Языки С++, Delphi – языки без автоматической сборки мусора. Кроме того, мы отмечали, что Ада – промежуточный язык, допускающий как реализацию с автоматической сборкой мусора, так и без нее. Ада 83 – строгий язык (указатели в строгих языках служат исключительно для ссылок на объекты динамической памяти).
Адреса разбиваются на 2 группы:
-
адреса объектов динамической памяти (и тогда ссылка только через указатели)
-
адреса квазистатических и статических объектов (и тогда через адреса на них ссылаться нельзя).
Интересно, что в Ада 95 появилась возможность указывать адреса произвольных переменных (т.н. базированные указательные типы – это такие типы, которые могут указывать на статические и квазистатические объекты). Т.е. вводится понятие адресная операция.
Остается ли при этом Ада 95 строгим языком?
Остается, потому что классические указательные типы не совместимы с такого рода указательными типами.
Спрашивается, зачем создатели Ада 95 пошли на послабление того, что адреса могут ссылаться только на объекты динамической памяти? Изменилась роль языка и, как следствие, изменились технологические потребности. Вспомним, на что ориентировалась Ада83? Она претендовала на роль тотального (единого) ЯП. Вообще говоря, когда мы программируем на разных ЯП, встает вопрос совместимости библиотек. В языке С++ это называется соглашением о вызовах.
Extern “C” {
// описания прототипов
}
Это соглашение о связях говорит о том, что функция, которая здесь будет описана, вызывается по соглашению языка С.
Аналогичная реализация для языка Фортран. Это необходимо для того, чтобы можно было связывать программы на языке С++ с программами на языке Фортран.
Эта же проблема встает перед всеми программистами. Почему создатели Ада 83 игнорировали эту проблему? Потому, что Ада претендовала на роль тотального языка и, следовательно, не нужно было вводить в языке такие конструкции, которые нужны исключительно для связи с другими языками. В 1995 ситуация изменилась в корне. Министерство обороны отменило документ, который заставлял всех программировать на Ада. Уже стало понятно, что кроме Ада люди будут программировать на многих других зыках.
Для чего в Ада95 нужны базированные указательные типы? Например, если Ада реализована под Windows, то для того, чтобы можно было вызывать системные вызовы Windows. Естественно, что из соображений надежности, базированные указательные типы ввели отдельно от обычных указательных типов. Интересно, что изменившаяся роль языка повлекла введение дополнительных типов, которые раньше не были нужны. В нестрогих же языках (основанных на С) работа с указателями является основным источником ненадежности, основным источником ошибок.
Поэтому из абстракции указателя надо оставить только понятие адреса, но исключить всевозможные проблемы, связанные с указателями (арифметические операции, преобразование указателей и т.д.). Тогда останется исключительно понятие ссылка.
В языках C#, Java, Delphi понятие ссылки совершенно отличается от понятия ссылки в языке С++.
Рассмотрим это на примере языка Java. В языке Java ТД подразделены на
-
простые ТД;
-
референциальные ТД (это типы данных, обращение к которым возможно только посредством ссылок);
К референциальным типам данных относятся:
-
массивы
-
классы
-
интерфейсы.
Т.е. к референциальным ТД относятся все составные ТД.
Вспомним язык Паскаль.
var x: integer { переменная }
PT ^T {указатель}
Y: PT {переменная типа РТ}
Обращаться к переменной y мы можем посредством разыменовывания указателей, либо только через имя (y^ - это то, на что ссылается y).
Интересно, что в языке Oberon операции разыменовывания указателей нет
TYPE T=…;
PT = POINTER TO T;
В языке Oberon синтаксически стирается разница между ссылкой и переменной.
Нужно просто записать X.NAME (NAME –некоторое поле), и компилятор сам разберется, если X:T, то все понятно, если X:PT, то компилятор сам ставит операцию разыменовывания.
В языке Java и некоторых других языках понятие самой переменной, с этой точки зрения, отсутствует. Иначе говоря, все переменные референциальных ТД хранятся в динамической памяти. Как следствие, мы можем обращаться к ним только через указатель, который, для избежания путаницы с языками С и С++, называется ссылкой. Иначе говоря, если есть Class C{…}, то объявление С х; - это не размещение переменной х в памяти, а это ссылка на объект типа с. Вначале все ссылки получают значение NULL.
Чтобы х куда-то указывал, надо либо инициализировать его с помощью new: C x = new C;
либо, если C y=x; то два объекта (y и x) ссылаются на один и тот же объект динамической памяти. Проблем с мусором и висячими ссылками не возникает, потому что есть автоматическая сборка мусора.
Та же самая ситуация и с массивами. Синтаксис массива: int a[ ];
Заметим, что в описании массива длина вообще не присутствует, а – это просто ссылка и сначала она никуда не указывает. Инициализировать массив можно так:
a = new int[10];
Здесь для людей, которые привыкли программировать на Паскале, возникает некоторое неудобство. Пусть есть два объявления:
int a[ ] = new int[20];
int b[ ] = a; //здесь речь идет о копировании ссылок
При присваивании ссылок не создается новой копии объекта. Надо помнить, что в Pascal, Ada присваивание одного массива другому – это создание копии объекта.
В связи с этим возникает следующая терминология:
-
поверхностное копирование (копирование самих ссылок)
-
глубинное копирование (создается новая копия объекта)
Стандартная семантика копирования в языках, где есть ссылки, (Delphi, Java, C#) – поверхностная.
Языки Java, C#, Delphi:
-
объектно-ориентированные;
-
вся система объектно-ориентированных типов представляет дерево (иначе говоря, все объектно-ориентированные типы (т.е. классы) выведены из единого корня). Этот корень в Java называется Object, т.е. любой класс в Java выводится из ТД Object (в Delphi – Tobject, в C# - object.). Т.о., Object - это тип данных, в котором нет данных, а есть только операции и эти операции наследуются каждым из объектов. В каждом из этих языков у типа Object есть операция clone() - клонирование. Именно эта операция, применимая к объекту a(т.е. a.clone() ), создает новую копию объекта. Интересно, что массивы тоже считаются типом данных, который тоже выведен из ТД Object, и поэтому у массивов тоже есть операция clone().
В языке С++ для копирования приходится применять более изощренные методы, о которых мы поговорим. Отметим, что языки Java, C#, Delphi – это ЯП прежде всего ссылочные. Правда, язык Delphi отягощен наследием Turbo Pascal. Язык Delphi можно разделить на старый Turbo Pascal (не объектно-ориентированный) и новый объектно-ориентированный язык Delphi, который является надстройкой над старым.
Ссылки в ЯП Java, C#, Delphi очень похожи на указатели в строгих языках, т.е. они ссылаются на объекты из динамической памяти. И , как следствие, в этих ЯП все объекты размещаются в динамической памяти. И поэтому работа с динамической памятью является одним из основных свойств реализации. С этой точки зрения, разница между ссылкой и объектом стирается.
Заметим, что в языке Delphi есть старое понятие ссылки, т.е. параметр процедуры мы можем передавать по ссылке - рrocedure P(var x:T);
Остановимся теперь на ссылках языка С++, поскольку это несколько иной объект.
Страуструп отмечал, что ссылка в С++ - это просто новое понятие имени.
Понятие ссылки введено в язык для удобной передачи параметров, которые требуют своей модификации, либо для передачи параметров по адресу.
Передавать параметры по ссылке нужно в двух случаях:
-
если мы хотим изменить значение параметра;
-
если мы не хотим копировать весь объект ( из соображений эффективности).
Понятие ссылки в С++ отличается от понятия ссылки в динамических ЯП тем, что единственная операция, которая применима к ссылке в С++, - это инициализация. Например, T j; T& i=j; /*теперь i – это просто другое обозначение переменной j*/