\documentclass[fontsize=14bp]{article} \input{common-preamble} \input{book-preamble} \begin{document} \maketitle \thispagestyle{empty} \newpage \thispagestyle{empty} \tableofcontents \newpage % 01 intro \import{sections/}{01-intro.tex} % 02 basics \import{sections/}{02-basics} % 03 io \import{sections/}{03-io} % 04 variables \import{sections/}{04-variables} % 05 conditions \import{sections/}{05-conditions} % 06 cycles \import{sections/}{06-cycles} % 07 functions \import{sections/}{07-functions} % 08 pointers \import{sections/}{08-pointers} % 09 arrays \import{sections/}{09-arrays} % 10 strings \import{sections/}{10-strings} % 11 structs \import{sections/}{11-structs} % 12 files \import{sections/}{12-files} \section{Распределение памяти} Этот раздел находится в конце документа, но не по важности. Сильная сторона языка С (и, как следствие, С++) не только в возможности работать с указателями, но и в возможности самостоятельно управлять выделяемой памятью внутри программы. В языках высокого уровня данная возможность зачастую скрыта от программиста, чтобы по случайности программа не привела к зависанию среды виртуализации, по неосторожности не попыталась воспользоваться всей возможной оперативной памятью или не сломала операционную систему. Итак, как мы уже знаем, все переменные всех типов как-то хранятся в памяти, и до этого момента нас устраивало, как операционная система нам эту память выделяет. Но, пришло время взять бразды правления в свои руки. Процесс \textbf{выделения памяти} для программы называется \textbf{memory allocation}, отсюда и название функции, которая выделяет память и пишет в предложенный идентификатор указатель на начало этой области. \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} при помощи записи объявления массива с использованием квадратных скобок. \begin{lstlisting}[language=C,style=CCodeStyle] int *area = (int*) malloc(sizeof(int) * 10); int array[10]; \end{lstlisting} Помните, мы говорили про арифметику указателей? Вот это - то самое место, где она нам поможет понять, что вообще происходит. Давайте реализуем два массива: привычным нам способом и при помощи динамического выделения памяти. Для реализации массива нам понадобится его размер, определим его как константу \code{SIZE}. Заменим в объявленном массиве \code{10} на \code{SIZE} и заполним этот массив какими-нибудь значениями. И напишем второй цикл для вывода этого массива в консоль. \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} Добавим пустую строку и проделаем тоже самое со вторым массивом, который мы инициализировали как область памяти. В этом виде данный код явно демонстрирует что мы можем одинаково работать и с массивами, объявленными привычными способами и с динамически выделенной областью памяти. Для более наглядной разницы, при заполнении и выводе в консоль второго массива, воспользуемся арифметикой указателей. \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()}. Синтаксис её весьма похож, только размеры необходимой области памяти передаются двумя аргументами - первый - сколько элементов, второй - какого размера будут элементы. \begin{lstlisting}[language=C,style=CCodeStyle] int *area = (int*) calloc(SIZE, sizeof (int)); \end{lstlisting} В остальном же выделенная область памяти ничем не будет отличаться от выделенной с помощью \code{malloc();} \subsection{\code{void free(ptr); void* realloc(ptr, size);}} По окончании работы с областью памяти надо её освободить, чтобы операционная система могла её использовать по своему усмотрению. Делается это при помощи функции \code{free()}. Если не освобождать память после использования - велика вероятность того, что мы, например, в каком-то цикле будем выделять себе место под какую-то структуру, и рано или поздно съедим всю память. Неприятно может получиться. Такие ситуации называются «утечками памяти» и могут возникать по огромному количеству причин, особенно во встраиваемых и многопоточных системах. \begin{lstlisting}[language=C,style=CCodeStyle] free(area); \end{lstlisting} Одной из основных задач программиста является недопущение таких утечек. И напоследок: довольно часто возникают ситуации, когда нам нужно придать нашей программе какой-то динамики, в этом случае мы можем изменить размеры уже выделенного блока памяти. Например, расширить наш массив, добавив туда пару элементов. Это делается при помощи функции \code{realloc();} в которую мы должны передать указатель на область памяти, которую хотим изменить, и размеры новой области памяти в байтах. При помощи этой функции области памяти можно как увеличивать, так и уменьшать, но в этом процессе есть довольно много особенностей, которые выходят далеко за пределы основ языка. \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. То есть при работе с областями памяти не стоит ожидать, что они будут даваться нашей программе подряд. Это приводит нас к беседе о фрагментации памяти и других интересных явлениях, также не являющихся основами языка. \section{Итоги} Спасибо, за внимание и интерес, проявленный к этой книге. В тринадцати коротких главах мы узнали как устроена практически любая программа изнутри, научились работать с памятью и указателями, узнали основные принципы и механизмы работы программ на уровне операционной системы. Заглянули внутрь привычных синтаксических конструкций, немного заглянули, что делают и что скрывают от программистов среды виртуализации и фреймворки. Продолжайте изучать технологии и не теряйте веру в себя, когда что-то не получается с первого раза. \appendix \section*{Приложения} \addcontentsline{toc}{section}{Приложения} \renewcommand{\thesubsection}{\Alph{subsection}} \subsection{Полный листинг программы программы терминального калькулятора} \label{appendix:calc} \lstinputlisting[language=C,style=CCodeStyle]{../sources/calculator.c} \subsection{Полный листинг программы вычисления среднего арифметического} \label{appendix:arrayaverage} \lstinputlisting[language=C,style=CCodeStyle]{../sources/arrayaverage.c} \subsection{Полный листинг программы умножения дробей} \label{appendix:fractions} \lstinputlisting[language=C,style=CCodeStyle]{../sources/fractions.c} \end{document}