И.Г. Головин, И.А. Волкова - Языки и методы программирования, страница 12
Описание файла
PDF-файл из архива "И.Г. Головин, И.А. Волкова - Языки и методы программирования", который расположен в категории "". Всё это находится в предмете "языки программирования" из 7 семестр, которые можно найти в файловом архиве МГУ им. Ломоносова. Не смотря на прямую связь этого архива с МГУ им. Ломоносова, его также можно найти и в других разделах. .
Просмотр PDF-файла онлайн
Текст 12 страницы из PDF
Иногда это удобно, так как позволяет использоватьпроизвольные границы индексов (например, с 1 или с отрицательногочисла). В современных языках программирования индексы в массиве53начинаются только с 0 и могут быть только целыми, поэтому областьприменения типов диапазонов существенно сужается.В языках C++, Java и C# диапазонных типов нет.Указательные типы данныхУказатели являются абстракцией понятия машинного адреса.Средства работы напрямую с адресами, с одной стороны, удобны,так как обеспечивают возможности, сравнимые с возможностямипрограммирования на машинном языке (т.
е. использование всехвозможностей машинной архитектуры без ограничений), а с другойстороны, — опасны (если нет ограничений, то можно сделать все,что угодно, в том числе и ошибки).Опасности применения указателей перевешивают их удобства,поэтому языки C# и Java не используют понятие указателя (точнее,в C# понятие указателя есть, но оно используется крайне ограниченно и только для работы с библиотеками, написанными на С илидругих императивных языках; в нашем курсе эти средства не рассматриваются). Вместо указателей в этих языках используется болееабстрактный и безопасный тип данных — ссылки.Указатели есть в языке C++ (где они без изменений заимствованыиз языка С). Синтаксис объявления указательной переменной в Си C++ следующий:имя_типа_данных * имя_указателяНапример:int * pi; X * рХ; char * message = "Hello!";Зачем вообще нужны указатели (другими словами, работа с адресами) в языках программирования? Можно назвать следующиеосновные причины использования указателей:• передача адресов объектов данных в подпрограммы;• работа с объектами из динамической памяти;• использование адресной арифметики;• использование адресов подпрограмм как объектов данных (передача параметров—подпрограмм в другие подпрограммы и т.п.).Рассмотрим подробнее каждую причину.Передача адресов данных в подпрограммы.
Если подпрограммаменяет содержимое своего параметра — объекта данных, то единственный способ это сделать — передать адрес объекта. Кроме того,если параметр — большой объект, то копировать его в подпрограммунакладно, поэтому и передают только адрес объекта.Для получения адреса объекта служит адресная операция & (не путайте ее с побитовой операцией И, которая имеет два аргумента):&переменная54Если переменная имеет тип т, то адресная операция возвращаеттип указателя на Т (т.
е. т*).Например, функция форматного ввода scanf из стандартной библиотеки языка С вводит (т. е. изменяет) значение целой переменнойк, поэтому необходимо передать туда адрес к:scanf ("%d", &к);Первый параметр (формат) "%d" сообщает функции, что онадолжна ввести целое число, представленное в десятичной системесчисления. Если бы параметром был "%х", то число должно былобы быть представлено в шестнадцатиричной системе.Как получить доступ к объекту, адрес которого находится в указателе? Для этого служит операция * (операция разыменования):int j = -1;int * pi = &j ; // в pi — адрес j*pi = 0;//*pi — это j, поэтому j получает значение ООчевидно, что для любой переменной var верно: var эквивалентно * ( &var).В языке C++ можно не передавать указатели в подпрограммы, а использовать ссылки (как и рекомендуется делать), однако в языке Стакой возможности нет.Работа с объектами из динамической памяти. Это самая важная(и нередко единственная) причина использования указателей.В языке C++ объект типа т размещается в динамической памятис помощью операции new имя типа:Т * р = new Т;Отведенная динамическая память обязана быть освобождена с помощью операции delete указатель:delete р;Указатель должен содержать адрес, полученный от операцииnew.
Иначе говоря, это адрес объекта, размещенного в динамическойпамяти. Объект после выполнения delete перестает существовать,и все ссылки на него становятся ошибочными.Если в памяти необходимо разместить массив (непрерывную последовательность) объектов типа Т, то следует использовать другиеформы операций new и delete:Т * pArr = new Т [256]; ... delete [] pArr;Операция new [ ] возвращает адрес первого элемента размещенного массива. Обратите внимание, что вид объявления указателяне зависит от того, указывает он на единственный объект или напоследовательность объектов (массив).
Такая неразличимость ука55зателя на объект и указателя на массив (точнее, на первый элементмассива) — характерная особенность языков С и C++,Еще одной особенностью языков С и C++ является неразличимость адресов, полученных от адресной операции и от операцииnew.Размещение объектов данных в динамической памяти — ценнаявозможность современных языков. Некоторые языки, как уже отмечалось, размещают объекты классов и массивы только в динамическойпамяти, правда, используя для этого аппарат ссылок, более надежныйи абстрактный, чем указатель. Поэтому операция new присутствуетво всех рассматриваемых здесь языках.Однако наличие (и требование!) операции явного освобожденияпамяти (delete) становится причиной частых и труднообнаруживаемых ошибок в программах.
Две ошибки работы с динамическимиобъектами, которые могут возникнуть при выполнении (или невыполнении) операции delete — это висячие ссылки и мусор.Висячие ссылки — это адреса уже уничтоженных объектов. Попытка обратиться к объектам по этим адресам ошибочна (по этомуадресу, например, могут располагаться совершенно другие объекты,размещенные уже после «смерти» уничтоженного объекта), однакопроконтролировать и обнаружить эту ошибку очень трудно.Пример возникновения висячей ссылки:int * рХ = new int; // объект размещен в памятиint * pY = рХ;// теперь рХ и pY ссылаются на один и тот же объектdelete рХ; рХ = 0;// объект уничтожен*pY = -1; // ошибка!!! Объект * pY не существуетЗдесь указатель pY становится висячей ссылкой после выполнениярХ.
Предсказать, как, когда и где проявится эта ошибка,практически невозможно. Если в примере обнулить не только рХ, нои pY, то висячая ссылка исчезнет, и ошибка немедленно обнаружится при попытке обратиться к нулевому адресу памяти (виртуальныекомпьютеры современных ОС генерируют в этом случае ошибкузащиты памяти).Конечно, можно легко избежать висячих ссылок, если вообще неуничтожать объекты (не использовать delete). Однако это «лекарство» еще хуже «болезни», поскольку приводит к другой ошибке приработе с динамическими объектами — возникновению мусора.Мусор — это неуничтоженные объекты из динамической памяти,на которые отсутствуют ссылки.
Раз нет ссылок на них, то объектыне могут использоваться (они бесполезны), но и не могут быть уничтожены (т. е. зря занимают память) — типичный мусор! Если мусорзаполняет всю доступную память, то размещение новых объектовстановится невозможным, и программа аварийно завершается. Проблема мусора в том, что эта ошибка проявляется при длительной раdelete56боте с программой, идогда уже на стадии эксплуатации. Обнаружитьее еще труднее, чем висячую ссылку.Пример возникновения мусора:int * рХ = new int; // разместили первый объектint * pY = new int; // разместили второй объектpY = рХ; // ошибка!!! Первый объект стал мусорнымКак избавиться от мусора и висячих ссылок? Во-первых, не использовать операцию явного освобождения памяти (избавимся отвисячих ссылок), а во-вторых, добавить в виртуальную машину языка(точнее, в подсистему времени выполнения) компоненту автоматический сборщик мусора.
Диспетчер динамической памяти долженотслеживать (запоминать) все ссылки на объекты из динамическойпамяти. Если все ссылки отслеживаются, то автоматический сборщикмусора может найти неиспользуемые объекты и пометить соответствующие участки памяти как свободные. Как правило, сборщикмусора дополнительно собирает все используемые объекты в непрерывную область памяти (ссылки на них, конечно, изменяются),и этот процесс называется дефрагментацией памяти.
Когда вызывать автоматический сборщик мусора? Во-первых, при выполненииоперации new, когда свободной памяти требуемого размера нет.Во-вторых, современные системы могут выполнять автоматическуюсборку мусора параллельно с выполнением программы.Конечно, автоматическая сборка мусора приводит к повышеннымнакладным расходам, и эффективность программ снижается. Однакоповышение надежности программы компенсирует этот недостаток,поэтому современные языки, такие как Java и С#, используют автоматическую сборку мусора.В языке C++ реализовать эту возможность затруднительно в силутого, что указатели на динамические объекты (адрес получен от new)и нединамические объекты (адрес получен путем адресной операции&) не отличаются друг от друга, поэтому отследить все ссылки в программе практически невозможно.
Эта особенность языка приводитеще к одной причине сбоев в программах: ошибочному вызыву операции delete для адреса нединамического объекта.Интересно, что реализация языка C++ для платформы .NET использует автоматическую сборку мусора (так как любой язык в .NETдолжен ее использовать), однако она реализована не для указателейC++, а для переменных специального нестандартного типа, эквивалентного ссылкам языка C# [33].Использование адресной арифметики.
Адресная арифметика —это применение операций сложения и вычитания к указателям.Например, указатель р содержит адрес объекта целого типа (adr).Тогда операция р+1 возвращает адрес, равный adr+sizeof (int).Аналогично, операция р - l возвращ ает адрес, равный a d r -sizeof (int) . Пусть в общем случае р объявлен как указатель57на тип т (Т*р;), ie — выражение целого типа, Addr — значение(машинный адрес), хранящееся в р. Тогда p+ie — это значение(машинный адрес) типа Т*, которое равно Addr+ie*sizeof (Т).Аналогично p-ie — это значение (машинный адрес) типа Т*, котороеравно Addr— ie*sizeof(Т).Нетрудно увидеть, что разность между двумя указателями pi и р2на тип Т есть целое значение, равное разности значений машинныхадресов из pi и р2, деленной на sizeof (Т).Использование адресной арифметики позволяет очень эффективно работать с последовательностями объектов данных одного типа(т.е.
с массивами).Однако непосредственная работа с адресами не может бытьпроконтролирована ни статически (в момент трансляции), ни динамически (во время выполнения программы). В самом деле, привычислении адреса никак нельзя гарантировать, что полученныйадрес корректен, т. е. указывает на правильный объект требуемоготипа. Поэтому программы, использующие адресную арифметику,могут содержать ошибки, которые трудно обнаружить.Использование адресов подпрограмм.