лекции (2004) (1160823), страница 7
Текст из файла (страница 7)
Unicode – средство для хранения универсального набора символов. Однако кроме манипуляций может происходить и передача данных, для которой фиксированная разрядность не лучшее решение. Приняли UTF (может помочь снизить объем транспортируемой информации) – UCS Transformation Format. UTF-8 - способ многобайтового представления символов UCS-2, это набор байтов представляющий многобайтовые последовательности:
0 |
11 | 0 | 5 бит | 1 | 0 | 6 бит |
старшие биты 111 – всего 3 байта
111 | 0 | 4 бита | 1 | 0 | 6 бит | 1 | 0 | 6 бит |
Б) от 128 до 2047 - 2 байта
В) старше 2047 – 3 байта
В Си# и Java то, что называется тд char – это двухбайтовое целое число в формате Unicode, при этом нет никакой совместимости с целочисленными типами данных, можно явно переводить только в ushort и обратно. В Си++ появился wchar_t, аналог unsigned short. В языках, которые служат для передачи данных, основной кодировкой стала UTF-8 (XML).
В современных языках просматривается тенденция к «юникодизации», при этом не допускается совместимость с целыми типами (только явные преобразования). А для поддержки национальных языков появляются специальные средства записи символов - \u хххх – запись в юникоде.
Указатели и ссылки
Указатель – аналог адреса. При рассмотрении указателей возникает ряд проблем:
-
Для чего они нужны?
В Паскале указатели использовались только для работы с динамическими тд, нельзя было, например, написать указатель на integer. Динамические типы данных обычно состоят из ссылки на другой динамический объект и некоторой хранимой информации (список, дерево) – основой служила запись. Допустимые операции в Паскаль:
- присваивание между указателями P:=P1
- инициализация new(P) – отведение памяти (аналог malloc, new в Си и Си++)
- удаление dispose (P) - удаление объекта, на который ссылался указатель (аналог free, delete в Си и Си++)
- разыменование P^ - сам объект
При этом не было никакой возможности смешивать объекты из динамической памяти со статическими или квазистатическими объектами, как следствие увеличивалась надежность языка.
В Модула2 (наследник Паскаль) есть специальный тип ADDRESS (аналог void* в Си и Си++), абстрактный не типизированный адрес, к которому можно привести любой указатель. Операция ADR (x) – аналогична &х.
Программисты на Си при разработке серьезных проектов используют свои отладочные библиотеки вместо стандартных, переписывая основные функции, работающие с указателями и динамическими объектами – программа работает медленней, зато надежней. Многих ошибок можно было бы избежать, если бы придерживались принципов из Ада и Паскаль:
-
фиксированный набор операций над указателями (см.выше)
-
объекты из динамической памяти не пересекаются с объектами не из динамической памяти
Основные проблемы, возникающие при работе с динамической памятью в любых языках:
-
неконтролируемых преобразований адресов и возможности взятия адреса от любой переменной)
-
проблема мусора (из-за оператора освобождения)
-
проблема висячих ссылок (из-за оператора освобождения)
При введении указателей появляется еще одна проблема – базового набора типов. Наличие абстрактного адреса, получения адреса указателя и преобразование указателя одного к другому – все это в совокупности позволяет обойти любое ограничение в системе типов. Иногда система типов может быть слишком жесткой, так в реальной жизни один объект может выступать в разных ролях - а во многих языках существует принцип – объект принадлежит одному и только одному тд. И именно для преодоления этого ограничения программисты и пытаются преобразовать указатели - T* = T1*.
Рассмотрим указатели из Паскаля (см. Выше) и Ада 83. В Аде:
type PT is access T; // Т либо запись, либо массив
x: PT;
x1: PT;
1) х:=х1; //можно присваивать
2)x:= new T; // можно инициализировать – отведение динамической памяти под объект, указатель на объект присваивается х
3) Пусть T record
a: integer
end record
x.a // допустимо, а например в Обероне указатели могли быть только на запись и х.а аналог х->а в Си++
х.all //аналог *х в Си и х^ в Паскале, в Обероне синтаксис указателей на записи такой же как в Ада
4) формально в стандарте языка нет операции освобождения динамической памяти из соображений надежности. Процесс автоматической сборки мусора ресурсоемкий и, кроме того, garbage collector начинает работать в непредсказуемые моменты времени, а Ада разрабатывалась как язык для встроенных систем реального времени. В Ада 83 введен стандартный модуль и в нем есть процедура UNCHECKED_DEALLOCATION(x) – неконтролируемое удаление памяти, используется там, где необходимо повысить эффективность. В система, где больше заботятся о надежности используется динамическая сборка мусора.
В Си#, Java – исключительно динамическая сборка мусора, так же и в управляемом Си++.
Языки программирования. Лекция 8.
Указатели и ссылки (продолжение)
Изначально указатели используются в двух видах:
1. Ссылки на объекты из динамической памяти (Ада, Модула2 и Паскаль используют указатели только в таком виде)
2. Используется понятие адреса, понятно, что с помощью адреса можно ссылаться на любые объекты
В Си и Си++ указатели участвуют и в работе с динамической памятью, а также используются как низкоуровневая абстракция понятия адреса.
Возникают различные проблемы, связанные с понятием указателя:
1. Смещение адресов
T *p;
p = &a; // если переменная а является локальной, т.е. объявленной где-то в стеке, а указатель является глобальным по отношению к стеку, то в этой точке значение указателя определено, но он указывает "в никуда". Попытка разыменования указателя приводит к доступу к содержимому стека, которое не является актуальным.
// если а статическая переменная, то free(p); - ошибка, которую тяжело определить, могут помочь только отладочные версии библиотеки управления памятью.
2. Проблема висячих ссылок
проблема висячих ссылок возникает даже в языках, в которых нет смешения адресов, в связи с тем, что явно присутствуют механизмы освобождения памяти. Например:
T *p1, *p2;
p1 = new T;
p2 = p1; // в результате p1 и p2 указывают на один объект.
delete p1; // в результате разрушается ссылка р1 на объект, сам объект уничтожается, а ссылка р2 остается висячей. После этого можно написать *p1;
а) до удаления
p2
б) после удаления
р1
р2
Один из типичных примеров:
while (p != NULL) {
free(p);
p = p->next; //пример работы с висячей ссылкой – р уже освобождена, а мы работаем с ней} (*)
Чем сложна проблема висячих ссылок? Если прогнать кусок (*) в разных средах, разных вычислительных системах и в разных конфигурациях – результат везде будет разный. В VC++ Debug – немедленно сообщение об ошибке, Release - возможно никакой ошибке зафиксировано не будет. Почему так происходит? Если представить одно поточное приложение, то у каждого такого приложения свой менеджер динамической памяти. Менеджеру памяти по оператору free сообщаем, что блок памяти на больше не нужен, но обычно менеджер ничего с этой памятью не делает. Только в том случае, если другой поток запросит операцию calloc (malloc и т.д.), и в процессе выполнения операции память утилизируется и в нее будет что-то записано, тогда только и произойдет порча памяти. А в примере с циклом – память освобождается, но ее содержимое не стирается. Если у нас многопоточное приложение, то (*) - не единственный поток команд. Менеджер памяти обычно тоже используется несколькими потоками и в этом случае никто не гарантирует, что после free управление вернется тому же потоку. Другой поток может запросить и испортить эту память. И в примере с циклом на следующей операции мы можем влезть и освободить уже чужую память. При этом если разные потоки одного процесса – никто не отловит не точность. Такие ошибки возникают исключительно из-за некорректного освобождении памяти.
3. Мусор
если совсем не выполнять освобождения памяти, то возникает проблема мусора, например:
T *p1;
p1 = new T;
p1 = new T; // старое значение теряется. И на этот участок памяти больше ничего не ссылается. Сообщение об ошибке не выдаётся.
а)
б)
В результате на первый объект никто не ссылается, объект из памяти не стирается (не выполнен оператор delete), менеджер динамической памяти не знает, что объект освобожден – и объект становиться мусором. Мусор - не менее страшная угроза, чем висячие ссылки. Программа, у которой присутствует мусор, отъедает оперативную память. Ошибка может проявляться:
А)замедление работы приложения (при каждом прогоне приложения – виртуальная память загрязняется все больше)
Б)в некоторой момент оператор new откажет – при нехватке памяти
Мусор очень трудно обнаружить – в основном проявляется на стадии эксплуатации.
Пункты 2 и 3 относятся только к указателям, которые работают с элементами динамической памяти и обусловлены только наличием явного оператора освобождения динамической памяти (delete).
Рассмотрим способы борьбы с рассмотренными проблемами.
1. не надо смешивать адреса – так яп Ада, Модула2 и Паскаль отличаются тем, что указатели используются исключительно для работы с объектами динамической памяти. Никакого смешения не происходит ни в одном из рассмотренных случаев.
В Паскале есть процедуры NEW(P) – отведение объекта динамической памяти и DISPOSE(P) извлечения.
В Ада:
type PT is access T;
x:PT;
X := new T; //размещаем объект в динамической памяти
Нет понятия адресной операции, нарушая систему контроля типов можно совершить следующие вещи:
Y:T;
Y' ADDRESS //такие действия используются для низкоуровнего программирования и делают программу нестандартизованной
В Модуле2 операция ADR возвращает адрес переменной, но опять же использование таких конструкций не приветствуется и делает программу ненадежной.
В языке Ада 95 (второй стандарт) появилось наличие указателей на нединамические объекты (в связи с необходимостью интеграции с библиотеками, написанными на других яп, в частность на Си). Возникает необходимость передавать объекты по адресу, а использование низкоуровневых конструкций неприменимо. При некотором усложнении языка удалось решить следующие появившиеся проблемы:
а) приходится рассматривать указатели на нединамические объекты
б) новые конструкции должны быть надежные
type PT is access T; //старый указательный тип
X:PT;
a:T;
X:= a' access; // - ошибка
Есть специальный атрибут access операции access – это получение адреса от нединамической переменной а.
type PTT is access all T;// объекты PTT могут адресовать произвольные объекты типа T
Y: PTT;
X:=new T; //единственное что можно сделать с объектом типа PT
Y :=new T; // допустимо
Y:=X; //допустимо
Y:= a' access; // допустимо
Именно объекты типа PTT можно передавать в произвольные программы, в тоже время компилятор относится осторожно к таким объектам.
X:=Y; // Запрещено
Таким образом, проблема смешения адресов решена за счет того, что типы указателей разные и лишь немного совместимы.
2. рассмотрим методы борьбы с висячими ссылками.
А) Метод с использованием надгробий.
p1 = new T; // в данном методе считается, что указатель р1 указывает не на объект динамической памяти, а на надгробие. Надгробие - это специальная ячейка памяти, которая всегда указывает на объект. Надгробие появляется как только появляется сам объект динамической памяти. И все указатели ссылаются не на сам объект, а на надгробие.
p2 = p1; // переприсваиваются адреса надгробий
delete p1; // а надгробие заносится нулевой адрес, заметим что у указателя не может быть нулевого адреса, каждый указатель всегда должен ссылаться на какое-нибудь надгробие.
*p2 (разыменование р2 после delete p1) // ошибка, и она будет обнаружена именно в тот момент когда она возникла, т.е. при любой попытке разыменования р2
В современных языках такой алгоритм не используется, потому что
1) у нас тратиться место на добавления ячейки-надгробия
2) получается двойная операция разыменования – вначале разыменовываем указатель, получая доступ к надгробию, а потом вынуждены разыменовывать надгробие
В результате получаем большие накладные расходы при доступе к соответствующим указателям (нужно учитывать использование кэширования в современных машинах, и возможность одновременного попадания в кэш и надгробия и объекта динамической памяти весьма невелика).