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

Обновлено: 18.05.2024

Спасибо. Мы получили ваш запрос и незамедлительно ответим.

Присоединяйтесь к нам!

  • Общаться с другими участниками
  • Уведомления об ответах
    на ваши сообщения
  • Поиск по ключевым словам
  • Доступ в один клик к вашим
    любимым форумам
  • Автоматические подписи
    на ваших сообщениях
  • Лучше всего то, что это бесплатно!

*Функции Tek-Tips зависят от того, получают ли участники электронную почту. Присоединяясь, вы соглашаетесь на получение электронной почты.

Правила публикации

Реклама, продажа, рекрутинг, размещение курсовых и дипломных работ запрещено.

Динамические массивы, SetLength и проблемы с памятью

Динамические массивы, SetLength и проблемы с памятью

Мне интересно, почему моей программе требуется больше памяти, чем дольше она работает, но не должно. Я предполагаю, что делаю что-то не так с динамическими массивами, которые использую.

У меня есть фиксированный массив динамических массивов, содержащих записи,
например:

TReservation = запись
Directed: Boolean;
Источник, назначение: TStationAddress;
// это только указатели на уже существующие объекты
Priority: 0..15;
TimeOfRequest: Cardinal;
конец;

TReservations = массив TReservation;

TReservationTable = class(TObject)
private
Элементы: array[0..99] TReservations;
конец;

Моя программа начинается с позиции 0 массива Items и присваивает ей определенное количество записей TReservation (обычно от 1 до 4), и так продолжается до позиции 99, очищая предыдущие элементы. Затем он начинает повторно использовать позицию 0 и так далее. как кольцевой буфер.

Я думал, что это может работать таким образом (схематично):

var
i, j, CurrentPosition: Integer;

для i := от 0 до 99999 do
begin
CurrentPosition := i mod 100;
// очистить элемент перед текущим элементом
Finalize(Items[(i-1) mod 100]);
// ИЛИ (какой вариант лучше?)
SetLength(Items[(i-1) mod 100], 0);
// сохраняем некоторые данные в текущий элемент
SetLength(Items[CurrentItem], RandomRange(1,5));
для j := от 0 до High(Items[CurrentItem]) do
begin
Items[CurrentItem][j].Directed := True;
Items[CurrentItem][j].Priority := 12;
// .
конец;
конец;

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

Я предполагаю, что Finalize() или SetLength(x,0) не освобождают память, назначенную динамическому массиву с записями.

Имейте в виду, что я могу назначить 5 записей позиции 0 при первом запуске, а когда я вернусь в позицию 0, я могу назначить ноль записей. Затем следует освободить память для 5 ранее назначенных записей.

Что я делаю не так или все делаю правильно?

Спасибо за любые предложения!

установки длины массива dyn на 0 недостаточно, присвоение ему значения nil фактически освободит память.

Кстати, зачем работать с динамическими массивами, если есть такие вещи, как Tlist и TObjectList?

Думаю, я либо старомодный, либо сумасшедший

Знаете ли вы, насколько больше памяти будет использоваться TObjectLists (по сравнению с динамическими массивами). если бы у меня было 20000 из них? Думаю, это было моим первым намерением, когда я решил не использовать списки. два года назад.

<р>. и, кстати: есть ли инструмент, который может указать программисту на код, который назначает память, но не освобождает ее?

ищите сыщика памяти для поиска утечек памяти.

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

Отправлено предупреждение

Благодарим вас за помощь в защите форумов Tek-Tips от неприемлемых сообщений.
Персонал Tek-Tips проверит это и примет соответствующие меры.

Ответить в этой теме

Размещение сообщений на форумах Tek-Tips доступно только для участников.

Нажмите здесь, чтобы присоединиться к Tek-Tips и пообщаться с другими участниками! Уже участник? Войти

В следующей статье о динамических массивах в Delphi говорится, что динамический массив выделяется с помощью функции SetLength().

Мне кажется, что это утечка памяти:

Вопрос в том, что если SetLength() является эквивалентом C++ MyObject *obs = new MyObject[20] , тогда массивы являются просто указателями, поэтому установка переменной Delphi myObjects в nil аналогична установке obj = NULL в С++? То есть это утечка памяти?

EDIT: из ответа Дэвида я понял, что компилятор управляет памятью для динамически выделяемых массивов. Из его ответа я также понимаю, что компилятор управляет памятью для обычных экземпляров класса (отсюда и использование myObj := MyObject.Create и myObj.Free , myObj := nil и т. д.).Кроме того, поскольку классы Delphi (а не записи) всегда размещаются в куче (Delphi использует своего рода систему ссылок/указателей), означает ли это, что все объекты в динамическом массиве (автоматически управляемом памятью) по-прежнему должны быть размещены в памяти? управляется мной? Например, вызывает ли следующая ошибка двойное освобождение результата?

2 ответа 2

Динамические массивы управляются компилятором. Это делается путем ведения подсчета всех ссылок на массив. Когда последняя ссылка на массив отсоединяется, массив освобождается.

Переменные динамического массива неявно являются указателями и управляются тем же методом подсчета ссылок, что и для длинных строк. Чтобы освободить динамический массив, присвойте nil переменной, которая ссылается на массив, или передайте переменную в Finalize; любой из этих методов удаляет массив, если на него нет других ссылок. Динамические массивы автоматически освобождаются, когда их счетчик ссылок падает до нуля. Динамические массивы длины 0 имеют значение nil. Не применяйте оператор разыменования (^) к переменной динамического массива и не передавайте его в процедуру New или Dispose.

В вашем примере присвоение nil вашей переменной отсоединяет единственную ссылку и приводит к освобождению массива. Так что утечки нет.

Динамические массивы Delphi сильно отличаются от новых C++. Ближайшим аналогом в Delphi является выделение необработанной памяти с помощью GetMem или New .

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

Я просто предполагаю, что динамический массив не освободится
при закрытии приложения??

Я должен освободить его самостоятельно в коде, да?

Сообщение Deamond
Я просто предполагаю, что динамический массив не освободится
когда приложение закроется??
Я должен освободить его самостоятельно в коде да ?

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

Думаю, вы пропустили чтение файла справки по динамическим массивам:

"Чтобы освободить динамический массив, присвойте nil переменной, которая ссылается
на массив, или передайте переменную в Finalize; любой из этих методов
удаляет массив, если нет других ссылок на это."

*Всегда* уничтожайте то, что вы создаете, и всегда используйте соответствующий
вызов освобождения памяти (New --> Dispose; GetMem --> FreeMem; GlobalAlloc -->
GlobalFree и т. д.).

Удалите -not из сообщения электронной почты

Сообщение Deamond
Я просто предполагаю, что динамический массив не освободится
когда приложение закроется??
Я должен освободить его самостоятельно в коде да ?

Если приложение не освобождается при закрытии, значит, происходит что-то
необычное.

Delphi очень эффективно избавится от "простых" переменных

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

Думайте о динамическом массиве как о подскочившем целом числе
(конечно, это не так, но он следует тем же правилам)

Сообщение Deamond
Я просто предполагаю, что динамический массив не освободится
когда приложение закроется??
Я должен освободить его самостоятельно в коде да ?

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

Сообщение Deamond
Я просто предполагаю, что динамический массив не освободится
когда приложение закроется??
Я должен освободить его самостоятельно в коде да ?

Кто-нибудь, пожалуйста, поправьте меня, если я ошибаюсь, но я всегда предполагал,
что любая память, назначенная одному приложению, автоматически освобождается
когда это приложение завершает работу, если только ОС специально не указано
>сохранить его после расторжения?

Сообщение Deamond
Я просто предполагаю, что динамический массив не освободится
когда приложение закроется??
Я должен освободить его самостоятельно в коде да ?

Кто-нибудь, пожалуйста, поправьте меня, если я ошибаюсь, но я всегда предполагал,
что любая память, назначенная одному приложению, автоматически освобождается
когда это приложение завершает работу, если только ОС специально не указано
>сохранить его после увольнения?

Удалите -not из сообщения электронной почты

Сообщение от Bram Bos
Кто-нибудь, пожалуйста, поправьте меня, если я ошибаюсь, но я всегда предполагал,
что любая память, назначенная одному приложению, автоматически освобождается
когда это приложение завершает работу, если ОС не указано
о сохранении его после завершения?

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

Сообщение Deamond
Я просто предполагаю, что динамический массив не освободится
когда приложение закроется??
Я должен освободить его самостоятельно в коде да ?

Когда переменная динамического массива выходит за пределы области видимости или освобождается объект,
которому она принадлежит, массив также освобождается.

Если ваш динамический массив является полем объекта, то массив будет
освобожден при вызове метода FreeInstance объекта. FreeInstance
автоматически вызывается компилятором после вызова деструктора.

Если ваш динамический массив является переменной в подпрограмме, то этот массив будет
освобожден непосредственно перед тем, как подпрограмма вернется к вызывающей программе. Представьте себе скрытый
блок try-finally, обертывающий вашу процедуру, а внутри части finally
содержится код для освобождения массива.

Если ваш динамический массив является переменной с областью действия на уровне модуля, то он будет
освобожден в конце раздела финализации модуля. Код в этом разделе
вызывается при завершении работы вашей программы через код в
System.FInitUnits.

Как уже упоминалось, Windows автоматически восстанавливает всю память,
выделенную вашей программе, когда ваша программа завершает работу. Однако это
не всегда безопасно. То, что память
освобождена, не означает, что были вызваны деструкторы. Например, если у вас есть массив
ссылок на интерфейсы, их методы _Release
не будут вызываться -- счетчики ссылок не будут уменьшаться.

Вы уже видели новинки в действии, так что давайте повторим. Как обсуждалось ранее, вы можете выделять память локально (из стека) или динамически (из кучи). В следующем фрагменте кода показаны примеры размещения двух массивов символов. Один выделяется из стека (локальное выделение), а другой — из кучи (динамическое выделение):

char* bigBuff = новый символ[4096];

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

strcpy(buff, "Крыса Рики");

strcpy(bigBuff, "Очень длинная строка, которая продолжается и продолжается."); // позже. strcpy(bigBuff, бафф);

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

Если новый оператор не может выделить запрошенную память, он возвращается

НОЛЬ. Теоретически вы должны проверять указатель после вызова new, чтобы убедиться, что он содержит ненулевое значение:

if (buff) strcpy(buff, "Buteo Regalis");

иначе ReportError(); // что-то пошло не так

На самом деле, если новый оператор дает сбой в 32-разрядной программе Windows, вся система оказывается в беде, и ни ваша программа, ни какая-либо другая не будут работать долго.

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

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


Освобождение памяти, выделенной с помощью new, выполняется с помощью оператора удаления.

Предупреждение

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

Использовать оператор удаления ужасно просто:

НекоторыйОбъект* мойОбъект = новый НекоторыйОбъект; // сделать кучу вещей с myObject delete myObject; // пока!

На этом все! В операторе удаления не так много всего, но есть несколько вещей, связанных с указателями и удалением, о которых вам следует знать. Во-первых, вы не должны удалять указатель, который уже был удален, иначе вы получите нарушение прав доступа и прочие забавные вещи. Во-вторых, можно удалить указатель, для которого установлено значение 0. Так что же это означает в реальном мире? Позвольте мне объяснить.

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

Ранее я говорил, что рекомендуется инициализировать указатели равными 0, если вы не используете их сразу. Это хорошая идея по двум причинам. Первую причину я объяснил ранее (неинициализированные указатели содержат случайные значения, что нежелательно). Вторая причина заключается в том, что можно удалить указатель NULL — вы можете вызвать удаление для этого указателя и не беспокоиться о том, использовался ли он когда-либо:

Монстр* swampThing = 0;

// позже, когда пора выйти из программы. удалить болото; // пока, сосунок!

В этом случае вам все равно, была ли когда-либо выделена память для объекта, потому что вызов удаления безопасен независимо от того, указывает ли указатель на объект или имеет значение NULL.

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

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

Монстр* борг = новый монстр;

Теперь, если для объекта дважды вызывается метод удаления, это не имеет значения, потому что можно удалить указатель NULL.

Другой способ обойти проблему двойного удаления – проверить указатель на ненулевое значение перед вызовом удаления:

если (swampThing) удалить swampThing;

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

Если вы используете ссылку при динамическом создании объекта, синтаксис удаления требует изменения. Вот пример, иллюстрирующий этот момент:

MyStruct& ref = *new MyStruct; исх.Х = 100; // позже. удалить &ref;

Обратите внимание, что вам нужен оператор адреса для удаления указателя в случае ссылки. Помните, что ссылка не может быть установлена ​​на 0, поэтому вы должны быть осторожны, чтобы не удалить ссылку дважды.


Похожие статьи:

проблема с динамическим массивом динамического массива
var a:массив массива целых чисел; начать набор длины (а, 2); установить длину (а [0], 1); установить длину (а [1], 2); а[0,0]:=1; // все в порядке a[0,1]:=2; // в моем понимании это неправильно a[0,1]:=2 не подходит, потому что a[0] имеет только один элемент, как я объявил setlength(a[0],1), поэтому я не понимаю, почему этот пример кода компилируется и работает нормально? > > в моем понимании a[0,1]:=2 не в порядке, потому что a[0] имеет только один элемент, поскольку я объявил setlength(a[0],1), поэтому я не понимаю, почему этот пример кода компилируется и запускается просто хорошо ? По умолчанию доступ.

Память не освобождается динамическими массивами
У меня возникают проблемы с использованием памяти динамическими массивами, особенно когда я пытаюсь освободить память. Я использую API ODBC для получения определений таблиц и столбцов из большой базы данных. Когда я отключаюсь от базы данных, я хочу освободить память, чтобы я мог сделать то же самое для следующей базы данных, к которой я подключаюсь. Процесс использует много памяти (около 75 МБ в этой симуляции). Проблема возникает в конце первого запуска - память остается выделенной. Во второй и последующие разы через код использование памяти возрастает до 150 МБ, но дополнительные 75 МБ составляют .

D7: Динамический массив, нехватка памяти с SetLength [Изменить]
Я играю с очень большими динамическими массивами.В массив добавляется неопределенное количество элементов, и, сводя к минимуму перераспределение памяти всякий раз, когда выполняется «SetLength», я играю с алгоритмом «Grow». Реализация для этого очень похожа на внутренности TList. Интересно то, что разные значения для роста массива приводят к разным максимальным размерам до того, как произойдет нехватка памяти. Это нехватка памяти происходит довольно рано, прежде чем будет достигнут предел в 2 ГБ или что-то в этом размере. Я приложил демонстрационную программу, которая s.

Delphi 2010 RTTI Установить длину динамического массива [Изменить]
Добрый день, у меня проблема, я искал ее в Google и EDN, но не нашел хорошего источника или полного. переменная x,y:TValue; a:массив [0..100] целых чисел; начало x:=TValue.From(v); y:=TValue.From(а); у.SetArrayElement(0,2); у.SetArrayElement(1,2); конец; Оно работает. потому что компилятор выделил 100 * 32 бита в памяти кучи. Как я буду работать с динамическим массивом a:array of integer? Я хочу: var y:TValue; а: массив целых чисел; начало y:=TValue.From(a); y.SetArrayElement(y.GetArrayLength+1); Большое спасибо и хорошего дня. .

ANN: Выпущена kbmMW v 4.83.00 CodeGear Edition Delphi XE8 Upd 1 для Win32!
Мы рады объявить о выпуске kbmMW v. 4.83.00 CodeGear Edition для Delphi XE8 Update 1 / Win32. kbmMW продолжает устанавливать планку того, на что должен быть способен n-уровневый продукт в реальном мире! Ключевые слова для этого выпуска: - Добавление новых функций регистрации в CodeGear Edition. — Добавлена ​​поддержка JSON в CodeGear Edition. - Исправление ошибок kbmMW CodeGear Edition доступен бесплатно для Delphi XE6/Win32, XE7/Win32, XE8/Win32 и включает kbmMemTable CodeGear Edition. Он может использоваться для коммерческой работы и не требует отчислений или платы за распространение для скомпилированного пользователя.

ANN: Выпущены версии kbmMemTable Standard и Professional Edition v. 7.05 для Delphi XE2 Win32/Win64/OSX32!
Мы рады сообщить о немедленном выходе новейших и лучших версий kbmMemTable v. 7.05 Standard и Professional Edition. Этот выпуск поддерживает 32-разрядные версии Win32, Win64 и Mac OSX и содержит дополнительные функции, обновления и улучшения производительности. Ранее мы успешно протестировали kbmMemTable на Win32 с 1 миллионом записей. Теперь мы успешно протестировали его с 10 миллионами записей, состоящих из строк, даты, валюты, заметок и вычисляемых полей, а также нескольких определенных индексов на Win64. Учитывая достаточно памяти и разумное (ограниченное) использование поддерживаемых индексов.

память delphi 2010 не освобождается при закрытии проекта delphi
каждый раз, когда я запускаю delphi 2010, используемая память не освобождается после закрытия проекта, память не перестает расти, а просмотр файлов становится медленным Есть идеи ? Спасибо, Пьер Оже написал: > каждый раз, когда я запускаю delphi 2010, используемая память не > освобождается после закрытия проекта, и память не перестает расти, и > просмотр файлов становится медленным > > есть идеи? Я предполагаю, что вы используете некоторые сторонние компоненты, которые неправильно освобождают память в своих пакетах времени разработки. Пакет времени разработки остается l.

Проблема с Delphi 2007
Хорошо, я уже некоторое время использую Delphi 2007. У меня также есть Delphi 2009 на той же машине некоторое время. Я использую оба из них на регулярной основе. У меня есть журнал Eureka - и установлен Castalia - каждый на пару месяцев. Несколько дней назад я установил сайт Raize Code (полная версия), но пока не использую его в своих проектах. Сегодня со мной происходит самое странное. Когда я печатаю в редакторе Delphi (каждые 5 символов), использование памяти увеличивается на 0,01 ГБ. (Да - 5 введенных символов занимают 10 МБ памяти. ХА!!) Я перезагрузил машину - выключил и перезапустил.

Проблема с Delphi 2006 на Delphi XE Crystal BPL [Правка]
Я нахожусь в процессе переноса нашего приложения с Delphi 2006 на Delphi XE, и для простоты использования в моей разработке установлены и 2006, и XE. компьютер. Я получаю доступ к Crystal Reports XI RDC в нашем приложении, поэтому я перестроил BPL в XE, чтобы добавить новый пакет в XE. Однако, когда я пытаюсь добавить пакет в XE, я получаю сообщение об ошибке: «Процедура регистрации, Craxddrt_tbl.Register в пакете c:\Documents and Settings\All Uers\Documents\RADS Studio\8.0\BPL\Crystal.bpl поднял класс исключения EFilerError: Компонент TDatabase не может быть зарегистрирован пакетом crystal.bpl be.

Проблема при использовании InstallAware 7 CodeGear Special Edition с Delphi 2010 [Изменить]
Приветствую всех! Я пытаюсь создать установочный диск для моего приложения delphi 2010 с помощью installAware, который поставляется с Delphi 2010. Когда я пытаюсь чтобы построить его, он продолжает выдавать мне сообщение об ошибке: - Нет файлов, соответствующих шаблону "C:\Windows\system32\\*120.bpl", и когда я смотрю в свою папку windows\system32, там файлы с расширением *.bpl заканчиваться *140.bpl, а не *120.bpl, как ожидалось. Может ли кто-нибудь сказать мне, если я делаю что-то неправильно или как я могу обойти это. Заранее спасибо.Отредактировано: Тат Хон Чу, 3 декабря 2009 г., 00:55 >

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

Динамический массив как? [Edit]
У нас есть статический массив в коде, например, char *urls [] = ; // это статический массив. Как создать динамический массив из файла? как это. file.txt (содержимое) mama meme end file.txt dynamic_array.cpp char *fr; // файл ifstream; строка ф; форум.открыть("файл.txt"); количество счетчиков = 0; в то время как (!file.eof()) < getline (файл, f); fr[count] = новый символ[strlen(f.c_str())+1];; //fr это массив //что-то не так в массиве при компиляции strcpy (fr[count], f.c_str()); количество += 1; >Но это не работает. _ [ссылка=.

Читайте также: