Исследование подробностей на языке С — КиберПедия 

Семя – орган полового размножения и расселения растений: наружи у семян имеется плотный покров – кожура...

Биохимия спиртового брожения: Основу технологии получения пива составляет спиртовое брожение, - при котором сахар превращается...

Исследование подробностей на языке С

2021-01-29 105
Исследование подробностей на языке С 0.00 из 5.00 0 оценок
Заказать работу

 

Вот объявления функций из темы справки GNU/Linux malloc (3):

 

 

Функции выделения памяти возвращают тип. Это бестиповый или общий указатель, все, что с ним можно делать – это привести его к другому типу и назначить типизированному указателю. Примеры впереди.

Тип является беззнаковым целым типом, который представляет размер памяти. Он используется для динамического выделения памяти, и далее в книге мы увидим множество примеров его использования. На большинстве современных систем _t является, но лучше явно использовать вместо простого целого типа.

Тип используется для вычисления адреса в арифметике указателей, как в случае вычисления указателя в массиве:

 

 

Заголовочный файл объявляет множество стандартных библиотечных функций С и типов (таких, как), он определяет также константу препроцессора, которая представляет «нуль» или недействительный указатель. (Это нулевое значение, такое, как 0 или ''. Явное использование 0 относится к стилю С++; в С, однако, является предпочтительным, мы находим его гораздо более читабельным для кода С.)

 

Начальное выделение памяти:

 

Сначала память выделяется с помощью. Передаваемое функции значение является общим числом затребованных байтов. Возвращаемое значение является указателем на вновь выделенную область памяти или, если память выделить невозможно. В последнем случае для обозначения ошибки будет установлен. (errno является специальной переменной, которую системные вызовы и библиотечные функции устанавливают для указания произошедшей ошибки. Она описывается в разделе 4.3 «Определение ошибок».) Например, предположим, что мы хотим выделить переменное число некоторых структур. Код выглядит примерно так:

 

 

Представленные здесь шаги являются стереотипными. Порядок следующий:

1. Объявить указатель соответствующего типа для выделенной памяти.

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

При выделении массивов для строк символов или других данных типа нет необходимости умножения на, поскольку последнее по определению всегда равно 1. Но в любом случае это не повредит.

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

Обратите внимание, что на C++ присвоение знамения указателя одного типа указателю другого типа требует приведения типов, какой бы ни был контекст. Для управления динамической памятью программы C++ должны использовать и, а не и, чтобы избежать проблем с типами.

4. Проверить возвращенное значение. Никогда не предполагайте, что выделение памяти было успешным. Если выделение памяти завершилось неудачей, возвращает. Если вы используете значение без проверки, ваша программа может быть немедленно завершена из‑за нарушения сегментации (segmentation violation), которое является попыткой использования памяти за пределами своего адресного пространства.

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

Выделив блок памяти и установив в указатель на него, мы можем затем интерпретировать как массив, хотя он в действительности указатель:

 

 

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

 

ЗАМЕЧАНИЕ. Блок памяти, возвращенный, не инициализирован. Он может содержать любой случайный мусор. Необходимо сразу же инициализировать память нужными значениями или хотя бы нулями. В последнем случае используйте функцию (которая обсуждается в разделе 12.2 «Низкоуровневая память, функции):

 

Другой возможностью является использование, которая вскоре будет описана.

 

Джефф Колье (Geoff Collyer) рекомендует следующую методику для выделения памяти:

 

 

Этот подход гарантирует, что выделит правильное количество памяти без необходимости смотреть объявление pointer. Если тип впоследствии изменится, оператор автоматически гарантирует, что выделяемое число байтов остается правильным. (Методика Джеффа опускает приведение типов, которое мы только что обсуждали. Наличие там приведения типов также гарантирует диагностику, если тип изменится, а вызов не будет обновлен.)

 

Освобождение памяти:

 

Когда вы завершили использование памяти, «верните ее обратно», используя функцию. Единственный аргумент является указателем, предварительно полученным с использованием другой функции выделения. Можно (хотя это бесполезно) передать функции пустой указатель:

 

 

После вызова f доступ к памяти, на которую указывает, запрещен. Она теперь «принадлежит» процедурам выделения, и они могут поступать с ней как сочтут нужным. Они могут изменить содержимое памяти или даже удалить ее из адресного пространства процесса! Таким образом, есть несколько типичных ошибок, которых нужно остерегаться при использовании:

Доступ к освобожденной памяти

Если она не была освобождена, переменная продолжает указывать на блок памяти, который больше не принадлежит приложению. Это называется зависшим указателем (dangling pointer). На многих системах вы можете уйти от наказания, продолжая использовать эту память, по крайней мере до следующего выделения или освобождения памяти. На других системах, однако, такой доступ не будет работать. В общем, доступ к освобожденной памяти является плохой мыслью: это непереносимо и ненадежно, и GNU Coding Standards отвергает его. По этой причине неплохо сразу же установить в указателе программы значение. Если затем вы случайно попытаетесь получить доступ к освобожденной памяти, программа немедленно завершится с ошибкой нарушения сегментации (надеемся, до того, как вы успели вашу программу выпустить в свет).

Освобождение одного и того же указателя дважды

Это создает «неопределенное поведение». После передачи блока памяти обратно выделяющим процедурам они могут объединить освобожденный блок с другой свободной памятью, которая есть в их распоряжении. Освобождение чего‑то уже освобожденного ведет к неразберихе и в лучшем случае к крушению; известно, что так называемые двойные освобождения приводили к проблемам безопасности.

Передача указателя, полученного не от функций malloc(), calloc() или realloc()

Это кажется очевидным, но тем не менее важно. Плоха даже передача указателя на адрес где‑то в середине динамически выделенной памяти:

 

 

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

Выход за пределы буфера

Доступ к памяти за пределами выделенного блока также ведет к неопределенному поведению, опять из‑за того, что она может содержать учетную информацию или, возможно, вообще не принадлежать адресному пространству процесса. Запись в такой участок памяти гораздо хуже, поскольку это может уничтожить учетные данные.

Отказ в освобождении памяти

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

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

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

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

Обсуждение ряда полезных инструментов для отладки динамической памяти см в разделе 15.5.2 «Отладчики выделения памяти».

 

Изменение размера:

 

Динамическая память имеет существенное преимущество перед статически объявленными массивами, поскольку это позволяет использовать столько памяти, сколько нужно, и не больше. Не нужно объявлять глобальный, статический или локальный массив фиксированного размера и надеяться, что он: (а) достаточно большой и (б) не слишком большой. Вместо этого можно выделить ровно столько, сколько нужно, не больше и не меньше.

Вдобавок, можно изменять размер динамически выделенной области памяти. Хотя можно сократить размер блока памяти, обычно его увеличивают. Изменение размера осуществляется с помощью. Продолжая пример с, типичный код выглядит следующим образом:

 

 

Как и в случае с, шаги стереотипны по природе и сходны по идее.

1. Вычислить новый выделяемый размер в байтах.

2. Вызвать с оригинальным указателем, полученным от (или от или предыдущего вызова) и с новым размером.

3. Привести тип и присвоить возвращенное значение. Подробнее обсудим дальше.

4. Как и для, проверить возвращенное значение, чтобы убедиться, что оно не равно NULL. Вызов любой функции выделения памяти может завершиться неудачей.

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

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

Возможно, вы заметили, что в нашем примере для указания на измененный блок памяти использовалась отдельная переменная. Можно было бы (хотя это плохая идея) использовать ту же самую переменную, как здесь:

 

Это плохо по следующей причине. Когда возвращает, первоначальный указатель все еще действителен; можно безопасно продолжить использовать эту память. Но если вы повторно используете ту же самую переменную и возвращает, вы теряете указатель на первоначальную память. Эту память больше нельзя использовать. Что еще важнее, эту память невозможно освободить! Это создает утечку памяти, которую нужно избежать.

Для версии в стандартном С есть некоторые особые случаи: когда аргумент равен, действует подобно и выделяет свежий блок памяти. Когда аргумент равен 0, действует подобно и освобождает память, на которую указывает. Поскольку (а) это может сбивать с толку и (б) более старые системы не реализуют эту возможность, мы рекомендуем использовать, когда вы имеете в виду, и, когда вы имеете в виду.

Вот другой довольно тонкий момент[42]. Рассмотрим процедуру, которая содержит статический указатель на динамически выделяемые данные, которые время от времени должны расти. Процедура может содержать также автоматические (т.е. локальные) указатели на эти данные. (Для краткости, мы опустим проверки ошибок. В коде продукта не делайте этого.) Например:

 

Это выглядит просто; размешает данные, использует их, изменяет размер и т.д. Но есть кое‑какие проблемы, которые не выходят за рамки страницы (или экрана), когда вы смотрите на этот код.

В строке, помеченной '', указатель cur используется для обновления элемента таблицы. Однако, был инициализирован начальным значением. Если некоторое условие верно и вернула другой блок памяти, теперь указывает на первоначальный, освобожденный участок памяти! Каждый раз, когда меняется, нужно обновить также все указатели на этот участок памяти. Здесь после вызова и переназначения недостает строки ''.

Две строки, помеченные '', еще более тонкие. В частности, предположим, что делает рекурсивный вызов. Переменная снова может быть изменена совершенно незаметно! После возвращения из значение cur может снова стать недействительным.

Можно подумать (что мы вначале и сделали), что единственным решением является знать это и добавить после вызова функции переназначение с соответствующим комментарием. Однако, Брайан Керниган (Brian Kernighan) любезно нас поправил. Если мы используем индексирование, проблема поддержки указателя даже не возникает:

 

 

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

 

ЗАМЕЧАНИЕ. Как и в случае с, когда вы увеличиваете размер памяти, вновь выделенная после память не инициализируется нулями. Вы сами при необходимости должны очистить память с помощью, поскольку лишь выделяет новую память и больше ничего не делает.

 

 


Поделиться с друзьями:

Биохимия спиртового брожения: Основу технологии получения пива составляет спиртовое брожение, - при котором сахар превращается...

Состав сооружений: решетки и песколовки: Решетки – это первое устройство в схеме очистных сооружений. Они представляют...

Адаптации растений и животных к жизни в горах: Большое значение для жизни организмов в горах имеют степень расчленения, крутизна и экспозиционные различия склонов...

История создания датчика движения: Первый прибор для обнаружения движения был изобретен немецким физиком Генрихом Герцем...



© cyberpedia.su 2017-2024 - Не является автором материалов. Исключительное право сохранено за автором текста.
Если вы не хотите, чтобы данный материал был у нас на сайте, перейдите по ссылке: Нарушение авторских прав. Мы поможем в написании вашей работы!

0.041 с.