seem to finish

This commit is contained in:
ivan-igorevich 2022-01-28 15:05:44 +03:00
parent 2a05b8ce3f
commit 6f639f55cd
3 changed files with 64 additions and 72 deletions

Binary file not shown.

132
main.tex
View File

@ -35,98 +35,88 @@
\import{sections/}{12-files}
\section{Распределение памяти}
Этот раздел находится в конце книги, но не по важности. Сильная сторона языка С не только в возможности работать с указателями, но и в возможности самостоятельно управлять выделяемой памятью внутри программы. В языках высокого уровня данная возможность зачастую скрыта от программиста, чтобы по случайности программа не привела к зависанию среды виртуализации, не попыталась воспользоваться всей возможной оперативной памятью или не сломала операционную систему.
Этот раздел находится в конце книги, но не по важности. Сильная сторона языка С (и, как следствие, С++) не только в возможности работать с указателями, но и в возможности самостоятельно управлять выделяемой памятью внутри программы. В языках высокого уровня данная возможность зачастую скрыта от программиста, чтобы по случайности программа не привела к зависанию среды виртуализации, по неосторожности не попыталась воспользоваться всей возможной оперативной памятью или не сломала операционную систему. Итак, как мы уже знаем, все переменные всех типов как-то хранятся в памяти, и до этого момента нас устраивало, как операционная система нам эту память выделяет. Но, пришло время взять бразды правления в свои руки. Процесс \textbf{выделения памяти} для программы называется \textbf{memory allocation}, отсюда и название функции, которая выделяет память и пишет в предложенный идентификатор указатель на начало этой области.
% Итак, как мы уже знаем, все переменные всех типов как-то хранятся в памяти, и до этого момента нас устраивало, как операционная система нам эту память выделяет. Но, пришло время взять бразды правления в свои руки. Процесс выделения памяти для программы называется memory allocation отсюда и название функции, которая выделяет память и пишет в предложенный идентификатор указатель на начало этой области. malloc(size); - она принимает в качестве аргумента размер выделяемой памяти. Как видим, функция возвращает пустоту, то есть область памяти будет зафиксирована, но не размечена. То есть это будет просто некоторая пустая область из n байт.
% СЛАЙД ПРО MALLOC() И АРГУМЕНТЫ
% Чтобы иметь возможность в этой области хранить значения нам нужно её подготовить для хранения этих значений разметить.
% Например, мы уверены, что будем складывать в нашу область памяти какие-то целые числа типа integer. Для этого при вызове функции нам надо использовать синтаксис приведения типа полученной области. Мы знаем, что каждая переменная типа integer хрпнится в памяти в 4 байтах. Например мы создаем указатель на некоторую область состоящую из 123 байт таким образом будет выделена память для 123 байт, но она никак не будет размечена.
% А при помощи оператора приведения типа мы скажем компилятору, что нам необходимо выделить некоторую область памяти, поделить её на ячейки размера int, и каждой такой ячейке дать свой адрес.
\subsection{\code{void* malloc(size);}}
Функция \code{void* malloc(size);} принимает в качестве аргумента размер выделяемой памяти. Как видим, функция возвращает довольно необычный на первый взгляд тип: \code{void*}, то есть область памяти будет зафиксирована, но не размечена. То есть это будет просто некоторая пустая область из \code{size} байт.
Чтобы иметь возможность в этой области хранить значения нам нужно её подготовить для хранения этих значений разметить. Например, мы уверены, что будем складывать в нашу область памяти какие-то целые числа типа \code{int}. Для этого при вызове функции нам надо использовать синтаксис приведения типа полученной области. Мы знаем, что каждая переменная типа \code{int} хранится в памяти в четырёх байтах. Например мы создаем указатель на некоторую область состоящую из 123 байт таким образом будет выделена память для 123 байт, но она никак не будет размечена. А при помощи оператора приведения типа мы скажем компилятору, что нам необходимо не только выделить некоторую область памяти, но и поделить её на ячейки размера \code{int}, и каждой такой ячейке дать свой адрес.
\begin{lstlisting}[language=C,style=CCodeStyle]
void* area = malloc(123);
int *array = (int*) malloc(123);
\end{lstlisting}
Здесь важно отметить, что именно такое выделение памяти, как на второй строке, не имеет особого смысла, поскольку 123 не делится на четыре нацело, поэтому у такой области памяти будет не совсем хорошо определённый хвост (последние три байта). Современные компиляторы действуют немного умнее, чем пишет программист, поэтому, скорее всего, будет выделено чуть больше памяти (обычно, это объём в байтах, равный степеням двойки), то есть для нашего случая, 128. Но расчитывать на это и строить на этом логику программы не стоит, во избежание досадных ошибок. Итак, как узнать, сколько нужно выделить памяти и при этом не запутаться. Для этого не обязательно знать размеры всех типов данных языка, для этого придумали оператор \code{sizeof()}.
\frm{Интересно, что \code{sizeоf()} - это именно оператор, хоть и выглядит как функция.}
Он возвращает размер переменной (типа данных) в байтах. Мы напишем \code{sizeof(int)} и умножим его на десять, таким образом мы выделим память в размере 40 байт, или под 10 целочисленных переменных. Приведением типа мы разметим выделенную область под хранение переменных типа \code{int}. Фактически, мы сделали то же самое что и описание массива типа \code{int} при помощи записи объявления массива с использованием квадратных скобок.
% int *area = (int*) malloc(123);
% Итак, как узнать сколько нужно выделить памяти и при этом не запутаться. Для этого не обязательно знать размеры всех типов данных языка Си, для этого придумали оператор sizeof. Оператор sizeоf возвращает размер переменной (типа данных) в байтах. Мы напишем sizeof (int), умножим его на 10. Таким образом мы выделим память в размере 40 байт и разметим их под хранение переменных типа int. Фактически мы сделали то же самое что и описание массива типа int при помощи записи объявления массива с использованием квадратных скобок.
\begin{lstlisting}[language=C,style=CCodeStyle]
int *area = (int*) malloc(sizeof(int) * 10);
int array[10];
\end{lstlisting}
Помните, мы говорили про арифметику указателей? Вот это - то самое место, где она нам поможет понять, что вообще происходит. Давайте реализуем два массива: привычным нам способом и при помощи динамического выделения памяти. Для реализации массива нам понадобится его размер, определим его как константу \code{SIZE}. Заменим в объявленном массиве \code{10} на \code{SIZE} и заполним этот массив какими-нибудь значениями. И напишем второй цикл для вывода этого массива в консоль.
% int *area = (int*) malloc(sizeof (int) * 10);
\begin{lstlisting}[language=C,style=CCodeStyle]
const int SIZE = 10;
int *area = (int*) malloc(sizeof(int) * SIZE);
int array [SIZE];
int i;
for(i = 0; i < SIZE; i++)
array[i] = i * 10;
for(i = 0; i < SIZE; i++)
printf("%d ", array[i]);
\end{lstlisting}
Добавим пустую строку и проделаем тоже самое со вторым массивом, который мы инициализировали как область памяти. В этом виде данный код явно демонстрирует что мы можем одинаково работать и с массивами, объявленными привычными способами и с динамически выделенной областью памяти. Для более наглядной разницы, при заполнении и выводе в консоль второго массива, воспользуемся арифметикой указателей.
% int array[10];
% Помните, мы говорили про арифметику указателей? Вот это то место, где она нам поможет понять, что вообще происходит. Давайте реализуем два массива привычным нам способом и при помощи динамического выделения памяти. Для реализации массива нам понадобится его размер, определим его как константу SIZE. Заменим в объявленном массиве 10 на SIZE и заполним этот массив какими-нибудь значениями.
% И напишем второй цикл для вывода этого массива в консоль.
\begin{lstlisting}[language=C,style=CCodeStyle]
puts("");
for(i = 0; i < SIZE; i++) area[i] = i*10;
for(i = 0; i < SIZE; i++) printf("%d ", area[i]);
for(i = 0; i < SIZE; i++) *(area + i) = i * 10;
for(i = 0; i < SIZE; i++) printf("%d ", *(area + i));
\end{lstlisting}
% Добавим пустую строку
% И проделаем то же самое со вторым массивом, который мы инициализировали как область памяти. В этом виде данный код демонстрирует что мы можем одинаков работать и с массивами, объявленными привычными способами и с динамически выделенной областью памяти. Для более наглядной разницы, при заполнении и выводе в консоль второго массива, воспользуемся арифметикой указателей.
% Запустим наш проект, все работает.
Напомню, мы реализовали массив area вручную. то есть выполняем почти те же операции, которые выполняет компилятор при реализации массива \code{array}. Практически, разложили синтаксис на базовые операции. Очевидно, что это знание открывает нам возможность распределять память в нашем приложении для любых типов данных, в том числе и сложных, таких как структуры.
\frm{Если применить оператор \code{sizeof()} к указателю на локальный массив (объявленный через квадратные скобки), вернётся размер массива, а если применить этот оператор к динамически выделенному массиву (объявленному через оператор \code{malloc();}) вернётся размер указателя (8 байт для 64-хразрядных операционных систем).}
Для того, чтобы каждый раз не пересчитывать размеры переменных вручную, особенно это актуально для строк и структур, используют оператор \code{sizeof()}, который возвращает целочисленное значение в байтах, которое займёт в памяти та или иная переменная.
\subsection{\code{void* calloc(n, size);}}
Функция \code{malloc();} резервирует память для нашей программы, но делает это весьма просто, вместе со всеми теми случайными переменными, которые могут там храниться. Если в коде из предыдущего подраздела убрать заполнение массива \code{area}, с большой долей вероятности, увидим в консоли непонятные ненулевые значения.
\frm{Важно, что выделенная память «не гарантирует нулевых значений» в ячейках, то есть значения вполне могут оказаться нулевыми, но это не гарантируется.}
Для того чтобы гарантированно очистить вновь выделенную область памяти используют функцию \code{calloc();} - clear allocate, которая не только выделит нам память, но и очистит содержимое. Поскольку функция не только выделяет память но и очищает её, считается, что она работает медленнее, чем \code{malloc()}. Синтаксис её весьма похож, только размеры необходимой области памяти передаются двумя аргументами - первый - сколько элементов, второй - какого размера будут элементы.
% const int SIZE = 10;
% int *area = (int*) malloc(sizeof (int) * SIZE);
% int array [SIZE];
% for(i = 0; i < SIZE; i++)
% array [i] = i * 10;
% for(i = 0; i < SIZE; i++)
% printf("%d ", array [i]);
% puts("");
% for(i = 0; i < SIZE; i++) area[i] = i*10;
% for(i = 0; i < SIZE; i++) printf("%d ", area[i]);
\begin{lstlisting}[language=C,style=CCodeStyle]
int *area = (int*) calloc(SIZE, sizeof (int));
\end{lstlisting}
В остальном же выделенная область памяти ничем не будет отличаться от выделенной с помощью \code{malloc();}
% for(i = 0; i < SIZE; i++) *(area + i) = i * 10;
% for(i = 0; i < SIZE; i++) printf("%d ", *(area + i));
\subsection{\code{void free(ptr); void* realloc(ptr, size);}}
По окончании работы с областью памяти надо её освободить, чтобы операционная система могла её использовать по своему усмотрению. Делается это при помощи функции \code{free()}. Если не освобождать память после использования - велика вероятность того, что мы, например, в каком-то цикле будем выделять себе место под какую-то структуру, и рано или поздно съедим всю память. Неприятно может получиться. Такие ситуации называются «утечками памяти» и могут возникать по огромному количеству причин, особенно во встраиваемых и многопоточных системах.
\begin{lstlisting}[language=C,style=CCodeStyle]
free(area);
\end{lstlisting}
% Напомню, мы реализовали массив area вручную. то есть выполняем ровно те операции, которые выполняет компилятор при реализации массива array. Разложили синтаксис на базовые операции. Очевидно, что это знание открывает нам возможность распределять память в нашем приложении для любых типов данных, в том числе и сложных, таких как структуры. Для того, чтобы каждый раз не пересчитывать размеры переменных вручную, особенно это актуально для строк и структур, используют оператор sizeof(), который возвращает целочисленное значение в байтах, которое займёт в памяти та или иная переменная.
Одной из основных задач программиста является недопущение таких утечек.
И напоследок: довольно часто возникают ситуации, когда нам нужно придать нашей программе какой-то динамики, в этом случае мы можем изменить размеры уже выделенного блока памяти. Например, расширить наш массив, добавив туда пару элементов. Это делается при помощи функции \code{realloc();} в которую мы должны передать указатель на область памяти, которую хотим изменить, и размеры новой области памяти в байтах. При помощи этой функции области памяти можно как увеличивать, так и уменьшать, но в этом процессе есть довольно много особенностей, которые выходят далеко за пределы основ языка.
% Функция malloc резервирует память для нашей программы, но делает это весьма просто, вместе со всеми теми случайными переменными, которые могут там храниться.
% Давайте я продемонстрирую это. Закомментируем заполнение массива area, и увидим в консоли непонятные ненулевые значения.
% Для того чтобы гарантированно очистить вновь выделенную область памяти используют функцию calloc() clear allocate которая не только выделит нам память, но и очистит содержимое. Поскольку функция не только выделяет память но и очищает её, считается, что она работает медленнее, чем malloc. Синтаксис её весьма похож, только размеры необходимой области памяти передаются двумя аргументами - первый - сколько элементов, второй - какого размера будут элементы.
\begin{lstlisting}[language=C,style=CCodeStyle]
int newsize = SIZE + 10;
area = realloc(area, newsize * sizeof(int));
for(i = 0; i < newsize; i++) *(area + i) = i * 10;
for(i = 0; i < newsize; i++) printf("%d ", *(area + i));
\end{lstlisting}
Большинство компиляторов выделяют память блоками, размеры которых обычно равны какой-то из степеней двойки, поэтому при объявлении или изменении области памяти в 20 байт, скорее всего (но это не гарантировано, поскольку не регламентируется стандартом) будет выделена область в 32 байта, или если мы объявим 70 байт, то скорее всего будет выделено 128. То есть при работе с областями памяти не стоит ожидать, что они будут даваться нашей программе подряд. Это приводит нас к беседе о фрагментации памяти и других интересных явлениях, также не являющихся основами языка.
% int *area = (int*) calloc(SIZE, sizeof (int));
% По окончании работы с областью памяти надо её освободить, чтобы ОС могла её использовать по своему усмотрению. Делается это при помощи функции free(). Если не освобождать память после использования - велика вероятность того, что мы, например, в каком-то цикле будем выделять себе место под какую-то структуру, и рано или поздно съедим всю память. Неприятно может получиться.
% free(area);
% И напоследок: довольно часто возникают ситуации, когда нам нужно придать нашей программе какой-то динамики, в этом случае мы можем изменить размеры уже выделенного блока памяти. Например, расширить наш массив, добавив туда пару элементов. Это делается при помощи функции realloc() в которую мы должны передать указатель на область памяти, которую хотим изменить, и размеры новой области памяти. При помощи этой функции области памяти можно как увеличивать, так и уменьшать.
% area = realloc(area, sizeof (int));
% Большинство компиляторов выделяют память блоками, размеры которых обычно равны какой-то из степеней двойки, поэтому при объявлении или изменении области памяти в 20 байт, скорее всего будет выделена область в 32 байта, или если мы объявим 70 байт, то скорее всего будет выделено 128. То есть при работе с областями памяти не стоит ожидать, что они будут даваться нашей программе подряд. Организация памяти это отдельный долгий разговор, явно выходящий за рамки Основ.
% Давайте запустим нашу программу с вновь выделенным измененным блоком памяти и увидим что все прекрасно перевыделилось.
% puts("");
% int newsize = SIZE + 10;
% for(i = 0; i < newsize; i++) *(area + i) = i * 10;
% for(i = 0; i < newsize; i++) printf("%d ", *(area + i));
% Спасибо, за внимание и интерес, проявленный к курсу. За прошедшие 14 уроков мы узнали как устроена практически любая программа изнутри, научились работать с памятью и указателями, узнали основные принципы и механизмы работы программ на уровне операционной системы. Заглянули внутрь привычных синтаксических конструкций, узнали, что делают и что скрывают от программистов среды виртуализации и фреймворки. Я и команда Гикбрейнс желает всем успехов в освоении Ваших профессий.
% (артефакт из видеокурса основ си)
% Удачи и до новых встреч, а ну да, и не забывайте освобождать память!
% СЛАЙД С ИТОГАМИ
% free(area);
\section{Итоги}
Спасибо, за внимание и интерес, проявленный к этой книге. В тринадцати коротких главах мы узнали как устроена практически любая программа изнутри, научились работать с памятью и указателями, узнали основные принципы и механизмы работы программ на уровне операционной системы. Заглянули внутрь привычных синтаксических конструкций, немного заглянули, что делают и что скрывают от программистов среды виртуализации и фреймворки. Продолжайте изучать технологии и не теряйте веру в себя, когда что-то не получается с первого раза.
\appendix
\section*{Приложения}

View File

@ -6,7 +6,9 @@ typedef int boolean;
\end{lstlisting}
Обратите внимание, что в отличие от директивы \code{\#define} это оператор языка С, а не препроцессора, поэтому в конце обязательно ставится точка с запятой. Написав такой псевдоним мы в любом месте программы можем писать \code{boolean} вместо \code{int}, что должно повысить понятность кода для людей, которые будут с ним работать.
\subsection{Структуры данных}
Несмотря на то что язык С создавался в незапамятные времена, уже тогда программисты понимали, что примитивных типов данных недостаточно для комфортного программирования. Мир вокруг можно моделировать различными способами. Самым естественным из них является представление о нём, как о наборе объектовно важно помнить, что С - это процедурный язык, в нём не существует объектно-ориентированного программирования. Тем не менее, у каждого объекта в мире есть свои свойства. Например, для человека это возраст, пол, рост, вес и т.д. Для велосипеда тип, размер колёс, вес, материал, изготовитель и прочие. Для товара в магазине артикул, название, группа, вес, цена, скидка и т.д. У объектов одного типа набор этих свойств одинаковый: все, например, собаки или коты могут быть описаны, с той или иной точностью, одинаковым набором свойств, но значения этих свойств будут разные.
Несмотря на то что язык С создавался в незапамятные времена, уже тогда программисты понимали, что примитивных типов данных недостаточно для комфортного программирования. Мир вокруг можно моделировать различными способами. Самым естественным из них является представление о нём, как о наборе объектовно важно помнить, что С - это процедурный язык, в нём не существует объектно-ориентированного программирования.
\frm{Существует шутка, что нельзя задавать вопрос программисту на С, является ли структура объетом. Думаю, дело в том, что это введёт С-программиста в бесконечное обдумывание такого вопроса.}
Тем не менее, у каждого объекта в мире есть свои свойства. Например, для человека это возраст, пол, рост, вес и т.д. Для велосипеда тип, размер колёс, вес, материал, изготовитель и прочие. Для товара в магазине артикул, название, группа, вес, цена, скидка и т.д. У объектов одного типа набор этих свойств одинаковый: все, например, собаки или коты могут быть описаны, с той или иной точностью, одинаковым набором свойств, но значения этих свойств будут разные.
\frm{
Сразу небольшое отступление, для тех кто изучал высокоуровневые языки, такие как Java или С\#, в С отсутствуют классы в том виде в котором вы привыкли их видеть. Для объектно-ориентированного программирования был разработан язык С++, который изначально называли Си-с-классами.
}