basic-c/main.tex

709 lines
84 KiB
TeX
Raw Normal View History

\documentclass[a4paper]{article}
\usepackage[russian]{babel}
2021-08-20 15:57:50 +03:00
\include{formatting}
\begin{document}
\maketitle
\thispagestyle{empty}
\newpage
\thispagestyle{empty}
2021-08-20 15:57:50 +03:00
\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}
2021-08-25 08:42:08 +03:00
% 07 functions
\import{sections/}{07-functions}
2021-08-25 16:52:36 +03:00
% 08 pointers
\import{sections/}{08-pointers}
2021-08-25 16:52:36 +03:00
\section{Массивы}
В этом разделе нас с вами ждут массивы. Много массивов. И ещё пара слов о директивах компилятору, иногда также называемых директивами препроцессора. С них и начнём.
\subsection{Директива \code{\#define}}
Помимо уже хорошо знакомой вам директивы \code{\#include}, частично описанной в разделе \hyperref[text:directive]{\ref{text:directive}}, естественно, существуют и другие. Некоторые из них ограничивают импорт описанных в заголовочном файле функций, некоторые <<\textbf{описывают}>> какие-то константы и даже действия. Вот, директиву \textbf{описать} мы и рассмотрим подробнее. Она не зря называется директивой препроцессора, поскольку даёт указание не процессору во время выполнения программы выделить память, присвоить значения, а непосредственно компилятору: заменить в тексте программы одни слова на другие. Таким образом можно задавать константы проекта, и даже делать сокращённые записи целых действий. Например, написав \code{\#define ARRAY\_LENGTH 50} мы предпишем компилятору, перед запуском трансляции нашего кода заменить все слова \code{ARRAY\_LENGTH} на цифру 50. Весьма удобно, но этим можно не ограничиваться, мы можем попросить компилятор заменить вызовы функций и операторы на короткие, удобные нам слова. Важно помнить, что директивы препроцессора работают с текстом программы, поэтому не осуществляют никаких дополнительных проверок. Это сложный и мощный инструмент, который чаще всего используется для решения нетривиальных задач, например, выбор кода, который попадёт в компиляцию в зависимости от операционной системы. Иногда в программах можно встретить описание недостающего но такого привычного булева типа при помощи директив препроцессора:
\begin{lstlisting}[language=C,style=CCodeStyle]
#define bool int
#define true 1
#define false 0
\end{lstlisting}
Но нам пока что достаточно умения создать глобальную именованную константу. Код ниже демонстрирует, что директивы не обязательно группировать именно в начале файла, а можно использовать там, где это удобно и уместно, так мы можем объявить константу с длиной массива в начале файла, а можем прямо внутри функции \code{int main (int argc, char *argv[])}.
\begin{figure}[h!]
2021-08-25 16:52:36 +03:00
\begin{lstlisting}[language=C,style=CCodeStyle]
int main(int argc, char* argv[]) {
#define ARRAY_LENGTH 50
int a = ARRAY_LENGTH;
printf("a = %d", a);
return 0;
}
\end{lstlisting}
\end{figure}
2021-08-25 16:52:36 +03:00
\subsection{Массивы}
Вступление про директивы препроцессора напрямую не связано с темой массивов, но директива \code{\#define} для объявления размера массива применяется чрезвычайно часто. Рассмотрим природу этого явления чуть позже.
\frm{Массив это множество данных одного типа, расположенных в памяти подряд.}
Язык С строго типизирован, поэтому невозможно создать массив из разных типов данных. На данном этапе мы рассматриваем только простые типы данных, поэтому и массивы будем рассматривать статические. Статическим массивом называют массив, количество элементов которого заранее известно и не изменяется за время работы программы. Альтернативой статическому массиву является динамический, таких массивов в языке С не существует, но всегда можно самостоятельно описать такую структуру данных, которая будет хранить значения, динамически расширяясь и сужаясь. Также для начала ограничим нашу беседу одномерными массивами, то есть такими, которые можно записать в виде значений через запятую. Статические одномерные массивы принято объявлять двумя способами:
\begin{itemize}
\item простое объявление с указанием размера;
\item объявление, совмещённое с инициализацией
\end{itemize}
Для примера объявим массив, содержащий элементы типа \code{int}, дадим ему идентификатор или имя массива \code{arr} (сокращённо от англ array), укажем максимальное количество элементов которые может вместить в себя массив, например, пять.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
2021-08-25 16:52:36 +03:00
int arr[5];
2021-08-25 16:52:36 +03:00
arr[0] = 10;
arr[1] = 20;
arr[2] = 30;
\end{lstlisting}
\end{figure}
2021-08-25 16:52:36 +03:00
Как уже говорилось массив это множество данных или элементов. К каждому элементу массива можно обратиться по его номеру, который принято называть индексом. Индексация элементов начинается с нуля. Давайте заполним наш массив значениями типа \code{int}. Для этого последовательно обратимся к каждому элементу и присвоим значение. Обратите внимание, что язык С не гарантирует что инициализационное значение элементов массива будет равно нулю, если это не указано явно, поэтому выведя на экран содержимое массива, мы можем гарантировать значения только первых трёх элементов, которые мы указали в коде. Второй способ объявления, совмещённый с инициализацией массива используют, если массив сравнительно небольшой и его значения заранее известны, например:
\begin{lstlisting}[language=C,style=CCodeStyle]
2021-08-25 16:52:36 +03:00
int arr[6] = {1, 1, 2, 3, 5, 8};
\end{lstlisting}
2021-08-25 16:52:36 +03:00
При этом, если сразу заполняются все элементы, размерность можно не указывать. Итак, мы научились создавать и заполнять значениями массивы. Теперь общее правило объявления массивов в С: при объявлении массива нужно указать его имя, тип элементов, количество элементов, опционально - указать сами эти элементы. Количество элементов есть натуральное число, то есть целое положительное, ноль не может быть количеством элементов. Нельзя задавать переменное количество элементов массива.
2021-08-23 07:19:35 +03:00
2021-08-25 16:52:36 +03:00
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
int nArr[100]; // An array for 100 int's;
float fArr[5]; // An array for 1 float's;
char cArr[2]; // An array for 2 char's;
int varElem;
int nArr[varElem]; // Compile error! Number of elements must be constant;
\end{lstlisting}
\end{figure}
Так мы обязаны создавать массивы только с точно указанным числом элементов. Для языка С это позволено сделать объявлением константы времени исполнения \code{const int elements; int arr[elements]}, но, например, в С++ такая запись вызовет ошибку компиляции, поэтому там необходимо строго указывать размер числовым литералом или объявив его директивой \code{\#define}, что, фактически, одно и тоже.
\frm{В более поздних стандартах С++ появилось ключевое слово \code{constexpr}, позволяющее объявлять константы времени компиляции и отказаться от объявления размеров массива только литералом}
Теперь давайте научимся получать доступ к элементам массива. Нет ничего проще, тем более, что мы это уже делали объявляли массив и для примера его заполняли. Для доступа к конкретному элементу массива нужно указать имя массива и индекс элемента в квадратных скобках. Квадратные скобки - это тоже оператор языка, он называется оператором индексного доступа:
2021-08-23 07:19:35 +03:00
2021-08-25 16:52:36 +03:00
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
int a = arr[0];
printf("lets see whats in 0-th element: %d", a)
\end{lstlisting}
\end{figure}
2021-08-23 07:19:35 +03:00
% При помощи массивов решают множество задач, таких как поиск, сортировка, составление таблиц соответствия, создание частотных диаграмм. На основе массивов создают более сложные структуры данных. Для примера давайте напишем программу, которая будет печатать наш массив в консоль.
% #include <stdio.h>
% int main( int argc, char *argv[])
% {
% int i;
% printf("Your array is: ");
% for (i = 0; i < 5; i++)
% {
% printf("%d ", arr[i]); // получаем значение i элемента массива и выводим в консоль.
% }
% return 0;
% }
% Мы научились создавать, инициализировать массивы и обращаться к его элементам. Теперь решим задачу посложнее - напишем программу, которая проверит насколько статистически хорош описанный в стандартной библиотеке генератор псевдо-случайных чисел. Для такой статистической проверки нам понадобится сформировать так называемый частотный массив. Массив, в котором будет содержаться информация о том, сколько раз то или иное число появилось во множестве, полученном при помощи генератора псевдослучайных чисел.
% Данная программа наглядно демонстрирует не только работу с массивами, но и то, что генератор псевдослучайных чисел в языке С генерирует статистически верную последовательность случайных чисел.
% #include <stdio.h>
% #include <stdlib.h>
% #include <time.h>
% #define ARRAY_LENGTH 10
% #define NUMBERS_AMOUNT 1000000;
% int main( int argc, char *argv[]){
% srand(time(NULL)); // зададим начальное значение ГПСЧ
% int frequency[ARRAY_LENGTH] = {0}; // объявим и обнулим
% int a;
% int i;
% for (i = 0; i < NUMBERS_AMOUNT; i++) // заполним
% {
% a = rand() % ARRAY_LENGTH; // сгенерируем
% frequency[a]++; // для числа 0 увеличим значение в 0-й ячейке массива, для числа 1 - в 1-й, и т.д.
% }
% for (i = 0; i < ARRAY_LENGTH; i++) // выведем
% {
% printf("Number %d generated %6d (%5.2f%%) times\n", i, frequency[i], ((float)frequency[i] / NUMBERS_AMOUNT * 100)); // количество вхождений и подсчёт процентов
% }
% return 0;
% }
% На этом занятии мы познакомились с понятием массива, научились создавать и заполнять массивы. Применили массив по назначению. До встречи на следующем занятии.
\section{Массивы 2}
% Здравствуйте, коллеги. На предыдущем занятии мы начали говорить о массивах, давайте продолжим. Это будет непросто, но мы поговорим о том, что из себя представляет идентификатор массива, чем чреват выход за пределы массива, затронем тему арифметики указателей и научимся передавать ссылки на массивы в функции.
% Как упоминалось ранее - массив - это ссылочный тип данных. То есть в идентификаторе хранится адрес, ссылка на первый байт первого элемента массива, дальнейший доступ к элементам осуществляется посредством смещения относительно этого байта. Таким образом запись вида array[0] говорит нам о том, что нужно взять адрес массива и сместить указатель на 0 элементов того типа, из которых состоит массив. Отсюда становится ясно, ПОЧЕМУ индексирование массивов начинается с нуля.
% int arr[ARRAY_LENGTH];
% arr[0] = 20;
% Давайте я попробую в комментариях к коду визуализировать положение вещей в массивах. Вот в квадратных скобках наш массив, квадратики это элементы нашего массива. Соответственно, если мы берем элемент с нулевым смещением, то есть по нулевому индексу, это будет вот этот вот индекс, квадратными скобочками я его нарисую. Все остальные индексы мы просто игнорируем. Далее, берем следующий элемент массива. Для этого возьмем смещение в единицу, допустим, это будет число 50. Соответственно в комментариях опять рисуем наш массив, и уже смещение будет на единицу. То есть мы игнорируем нулевой элемент и берем первый. Надеюсь, стало понятнее.
% int arr[ARRAY_LENGTH]; // [ [][][][][][][][][][] ]
% arr[0] = 20; // [ [] ]
% arr[1] = 50; // [ [] ]
% МОЖЕТ ПЕРЕДЕЛАТЬ НА СЛАЙДЫ?
% Относительно выхода за пределы массива надо сказать, что ни компилятор, ни тем более операционная система никаких проверок не делают, поэтому это полностью ложится на плечи программиста. Никаких сред виртуализации, никаких исключений, только случайные данные, которые могут попасться нашей программе. Надо сказать, что всё-таки бОльшая часть значений за пределами массива будет равна нулю, но всё равно лишний раз экспериментировать не стоит.
% arr[10] = 60; // [ ] []
% И ЭТО ТОЖЕ
% Как мы уже знаем в идентификаторе массива хранится ссылка на первый байт первого элемента массива, т.е. идентификатор является по сути - указателем. Но существует несколько отличий: указатель - это переменная, к ней применимы, например, операции инкремента и декремента, чего конечно нельзя делать с идентификатором массива без должной подготовки, о которой и поговорим. Обратившись к идентификатору массива мы можем получить доступ к элементам массива не только при помощи записи индекса в квадратных скобках, но и при помощи так называемой арифметики указателей. Мы знаем, что массив - это единая область памяти, и значения в нём располагаются подряд по очереди, значит, отсчитав от указателя на первый индекс нужное количество байт - мы получим указатель на второй индекс. Давайте для примера подсчитаем среднее арифметическое всех чисел в массиве, с использованием арифметики указателей. Заполним этот массив с клавиатуры.
% Создадим вспомогательную переменную float result, для хранения результата и в цикле будем запрашивать у пользователя цифры. Количество введенных цифр должно соответствовать количеству элементов массива, поэтому условием выхода из цикла будет равенство итератора и длины массива1, т.к. индексация массива начинается с 0, длина массива на 1 больше последнего индекса. Выведем в консоль надпись «введите значение», при помощи функции scanf считаем его и сразу положим в массив. Здесь мы к указателю на первый элемент массива прибавляем значение итератора и получаем индекс массива, в который будет положено значение. Выведем в консоль получившийся массив при помощи цикла for и привычной нам функции printf. Следом напишем ещё один цикл в котором подсчитаем среднее арифметическое. Для этого к результату будем прибавлять существующий результат и значение массива на которую указывает конструкция *(average + i).Как вы видите, некоторые подсчеты программа выполняет за нас - мы прибавляем к указателю единицу, двойку, тройку и т.д, а программа понимает, что надо взять не следующий по счёту байт, а следующий указатель. Т.к. в данном примере мы используем массив в котором хранятся значения типа int, а как вы помните int = 4 байта, то при увеличении указателя на 1 мы обратимся к области памяти находящейся на 4 байта дальше идентификатора, при увеличении на 2 на 8 байт и т.д. Подсчитать среднее арифметическое не составит труда.
% Далее при помощи функции printf выведем в консоль среднее арифметическое. Запустим, повводим цифры и убедимся что все работает.
% int i = 0;
% float result = 0;
% while (i < ARRAY_LENGTH){
% printf("Enter value %d:", i);
% scanf("%d", arr + i);
% i++;
% }
% printf("Your array is: ");
% for (i = 0; i < ARRAY_LENGTH; i++)
% printf("%d ", arr[i]);
% printf("\nAnd the average is: ");
% for (i = 0; i < ARRAY_LENGTH; i++)
% result += *(arr + i);
% printf("%f \n", result/ARRAY_LENGTH);
% Как мы уже говорили, идентификатор массива - это не обычный указатель. Обычный указатель хранит в себе адрес какой-то другой переменной, и сам где-то хранится. Указатель на начало массива хранит в себе адрес массива, то есть адрес его нулевого элемента, и сам этот указатель находится в этом самом месте. На первый взгляд сложновато? Но пусть Вас это не сбивает с толку, на деле всё не так жутко. На деле это означает, что при передаче массива в функцию мы не должны использовать оператор взятия адреса, поскольку идентификатор массива сам по себе является указателем на собственное начало.
% В только что написанной нами программе оформим вывод массива на экран и поиск среднего арифметического в виде функции.
% Опишем функции printArray и average в которые передадим указатель на массив и его длину, т.к. в массиве не содержится сведений о его размере. Т.к. мы передаем в функцию указатель, то все действия которые описаны в этой функции будут происходить с массивом который мы создали в основной части программы через этот указатель, который мы передали, никакого копирования значений или чего то подобного. Для корректной работы наших функций объявим в них итератор и изменим названия переменных на название аргументов.
% void printArray(int* array, int length){
% int i;
% for (i = 0; i < length; i++)
% printf("%d ", array[i]);
% }
% float average(int* array, int length){
% float result=0;
% int i;
% for (i = 0; i < length; i++)
% result += *(array + i);
% return result/length;
% }
% Напишем вызов наших функций в основной части программы. Запустим и убедимся, что все также работает корректно. Вот, собственно, и вся магия передачи массивов в функции.
% printf(“Our array is: ”);
% printArray(arr, ARRAY_LENGTH);
% printf(“\n And the average is: ”);;
% printf(“%f \n”, average(arr, ARRAY_LENGTH));
% На этом уроке мы познакомились с арифметикой указателей и способом изменить массив в сторонней функции, это открыло нам массу возможностей для написания функций, реализующих те или иные алгоритмы работы с массивами. До встречи на следующем уроке
\section{Массивы 3}
% Коллеги, здравствуйте. Рад, вас приветствовать на десятом, юбилейном уроке курса основы языка С. На этом уроке нас ждёт продолжение разговора о массивах.
% Массив в языке С может иметь сколько угодно измерений. Самые распространённые это двумерные и трёхмерные, которые легко себе представить в виде таблицы или куба соответственно. Итак, массив это структура, содержащая элементы. Двумерный массив - это массив из массивов, содержащих элементы. Трёхмерный - это массив из массивов, содержащих массивы. И так далее.
% В массиве могут находиться любые типы данных, мы, для удобства, будем рассматривать работу массивов с числами.
% Давайте для удобства попробуем визуализировать двумерный массив. Создадим двумерный массив, например 5*5, и вот здесь в комментариях я попробую его нарисовать с помощью псевдографики. Массив 5*5 это 5 столбцов и 5 строчек. Соответственно, каждая строчка это будет у нас младший индекс, а каждый столбец старший индекс.
% Трехмерный массив… Боюсь, что псевдографики в комментариях для этого не хватит. Но он может быть 3*3*3 это всем известный кубик Рубика. У него есть 6 граней, в каждой из которых 3*3 квадратика.
% СЛАЙД О МАССИВЕ, ПОКАЗАТЬ ЧТО ЭТО МАССИВ МАССИВОВ
% int twoDimensional[5][5];
% /*
% * [][][][][]
% * [][][][][]
% * [][][][][]
% * [][][][][]
% * [][][][][]
% */
% int threeDimensional[3][3][3]; // Rubiks cube
% Как работать с многомерными массивами мы рассмотрим на примере двумерного массива. Поставим для себя задачу - сформировать таблицу Пифагора (раньше такие на тетрадях в клетку печатали на обратной стороне). Таблица подразумевает наличие двух измерений - строк и колонок. Для этого объявим константы rows и cols и присвоим им значения 10, rows это количество строк, а cols - соответственно столбцов Создадим двумерный массив, table[rows][cols]. Итак, мы создали массив размером rows, в каждом элементе которого содержится ссылка на массив размером cols - т.е. массив массивов содержащих непосредственные значения.
% Таблица Пифагора представляет собой таблицу, где строки и столбцы озаглавлены множителями, а в ячейках таблицы находится их произведение. Вот это самое произведение мы и будем выводить.
% Заполнение таких массивов значениями ничем не отличается от заполнения одномерных массивов. Заполним нашу матрицу поэлементно: напишем двойной цикл который будет заполнять нашу таблицу.
% Объявим переменные итераторы. И с помощью внешнего цикла фор пройдемся по всем строкам массива, а с помощью вложенного по всем столбцам массива, при этом будем записывать в каждый элемент массива результат умножения. Формула (r + 1) * (c + 1) позволяет исключить 0 из нашей таблицы. Давайте разберем более подробно работу такой конструкции. Внешний цикл при каждой итерации перемещает нас на одну строчку вниз. Вложенный, при каждой итерации, перемещает нас на одно значение вправо. Важно понять, что на одну итерацию внешнего цикла приходится cols итераций вложенного. Т.е. с помощью такой конструкции мы поочередно перебираем все элементы массива.
% Также есть способ заполнять многомерные массивы посредством конструкции в фигурных скобках, этот подход используется когда мы заранее знаем все значения. Например, вот так:
% int arr[3][4] = {{0,1,2,3}, {4,5,6,7}, {8,9,10,11}} - объявляем и инициализируем двумерный массив размером 3х4.
% Далее напишем такой же двойной цикл, который будет выводить на экран наши двумерные массивы в удобном для нас виде.
% Запустим нашу программу и увидим, что все отлично работает.
% void advancedArraysMain() {
% printf(“Multi-dimensional Arrays! \n”);
% const int rows = 10, cols = 10;
% int table[rows][cols];
% // int arr[3][4] = {{0,1,2,3}, {4,5,6,7}, {8,9,10,11}}
% int r, c;
% for(r = 0; r < rows; r++) {
% for(c = 0; c < cols; c++)
% table[r][c] = (r+1)*(c+1);
% }
% for(r = 0; r < rows; r++) {
% for(c = 0; c < cols; c++)
% printf("%3d ", table[r][c]);
% printf("\n");
% }
% Как уже говорилось, массивы могут содержать данные любых типов, в том числе и указатели. Именно это позволяет массиву хранить другие массивы, строки и прочие ссылочные типы данных. Используя массивы указателей, мы можем создать, например, массив строк.
% Давайте запишем char* stringArray[3] = {"Hello", "C", "World"};
% Это указатели на строки, такой тип данных является указателем. И мы можем создать из этих указателей массив. Используя массивы указателей, мы можем создать, например, двумерный массив, где каждый элемент не обязан быть того же размера, что и остальные.
% Но строки и сложно-составленные указатели - это темы, которые очень сильно выходят за рамки Основ языка, но это не помешает нам разобраться со строками на следующих уроках
% char* stringArray[3] = {"Hello", "C", "World"};
% for(r = 0; r < 3; r++)
% printf("%s ", stringArray[r]);
% На этом уроке мы познакомились с многомерными массивами и узнали о том, что массив может быть не только из примитивов, но и из указателей. До новых встреч на следующих уроках, коллеги.
% СЛАЙД С ИТОГАМИ
\section{Строки}
% Здравствуйте, уважаемые коллеги. Рад всех приветствовать на очередном видео курса Основы языка С. Получив на предыдущих уроках представление об указателях и массивах, и вскользь упомянув строки, пришла пора изучить их подробнее.
% Итак, что же такое строка. В повседневной жизни строка это набор или последовательность символов. Так вот в языке Си строка это тоже последовательность символов. А последовательности в языке Си представлены, вы, наверное, уже догадались, массивами и указателями. Никакого примитива Стринг в языке Си нет. Как бы нам этого не хотелось - его нет. Но есть одна хорошая новость, примитива Стринг не существует и в других языках (я имею ввиду, конечно, си-подобные языки, то есть примерно треть вообще всех языков программирования). Раз строка - это массив или указатель - это всегда ссылочный тип данных.
% СЛАЙД ПРО Си-СТРОКИ
% Раз строка - это набор символов, давайте немного вспомним что такое сиволы и как с ними работать. Как вам уже известно, символьная переменная это переменная типа char. В отличие от строки это примитивный числовой тип данных, и к нему применимы все операции допустимые для примитивов, такие как присваивание, +, -, *, / - хотя не все имеют смысл (я например с трудом могу представить ситуацию в которой необходимо умножать или делить коды символов).
% Здесь значением sym является 65 в кодовой таблице ASCII. Т.е. запись sym = 65 и sym = 'A' - абсолютно эквивалентны. Однако для улучшения читаемости кода лучше использовать вариант sym = 'A'.
% СЛАЙД ПРО ТО ЧТО СИМВОЛ ЭТО ЦИФРА. ТАБЛИЦА АСКИИ
% Ну что, немного вспомнили, что такое символы, теперь переходим к строкам. Строками мы пользуемся с самого первого занятия: написав в двойных кавычках «Привет Мир», мы использовали строку. Строки иногда называют типом данных, но в языке С строка это массив, массив символов, поэтому и работать с ней можно, как с массивами. Строки в языке С можно описать двумя способами - как указатель и как массив из переменных типа char. Давайте создадим строку в виде массива из char назовем ее string1 и запишем This is a string!" это строка. Также создадим указатель, назовем его string2 и запишем This is also a string! это тоже строка.
% У каждого из способов есть свои особенности. Так, например в массиве из переменных типа char мы можем изменять символы. Давайте выведем наши строки в консоль и убедимся, что я не ошибся и их действительно можно так объявлять. Все работает. Теперь давайте изменим какой-нибудь символ в одной из строк. Например, string1[5] = X; и символ с пятым индексом изменится.
% int main (int argc, const char* argv[]) {
% char string1[256] = "This is a string!";
% char* string2 = "This is also a string!";
% printf("%s \n", string1);
% printf("%s \n", string2);
% string1[5] = 'X';
% printf("%s \n", string1);
% Указатель на тип char нам такой возможности не даёт и, получается, представляет собой immutable string неизменяемую строку. Зато указатели на строки мы можем возвращать из функций. Т.е. мы можем объявить функцию как указатель на char вернуть из нее строку и вывести в консоль.
% И это открывает перед нами широчайшие возможности по работе с текстами.
% char* helloFunction () {
% return "Hello!";
% }
% printf("%s \n",* helloFunction ());
% Параллельно с написанием нашей функции, приветствующей мир, давайте изучим некоторые стандартные возможности языка С для работы со строками. Например, специальную функцию, которая призвана выводить строки в консоль puts(); работает она очень похожим на printf() образом, но может выводить только строки, без каких-то других параметров, и добавляет символ конца строки. А также изучим специальную функцию gets() которая призвана считывать строки из консоли и записывать их в переменные.
% Создадим изменяемую строку типа массив из char, назовём её name, передадим эту строку в функцию gets(), и выведем на экран результат, полученный из консоли. Запустим нашу программу, напишем имя и увидим его в консоли.
% char name[255];
% gets(name);
% puts(name);
% Теперь, мы можем поприветствовать пользователя нашей программы как следует, по имени. В нашей существовавшей функции приветствия внесём небольшие изменения. Создадим строку, в которой будем хранить приветственное слово, и в которую будет дописываться имя пользователя. Применим функцию склеивания строк. Поскольку склеивание - ненаучный термин, будем использовать слово конкатенация. И это слово подсказывает нам название функции, которую мы будем использовать : strcat(). Функция принимает на вход два параметра - строку, к которой нужно что-то прибавить, и строку, которую нужно прибавить. Логично предположить, что первая строка должна быть изменяемой (то есть являться массивом символов). Функция прибавит все символы второй строки в первую и вернёт указатель на изменённую строку. Очень удобно. Запустим наш проект, введем имя и убедимся что все сломалось.
% char* helloFunction(char* name) {
% char welcome[255] = "Hello, ";
% return strcat(welcome, name);
% }
% int main(int argc, comst char* argv[]) {
% char name[256];
% gets(name);
% puts(helloFunction(name));
% return 0;
% }
% Что же случилось? Мы можем возвращать из функции только фиксированные строки, как в предыдущем примере. То есть, получается, нужно писать кейс, в котором содержатся все возможные имена, и оператором switch перебирать все возможные варианты ввода, иначе мы устраиваем утечку памяти, создавая болтающийся в воздухе указатель? Нет, нас это не устраивает. Что же делать? Какой бы мы ни создали указатель в функции - он перестанет существовать, как только мы выйдем из функции. Выход очень простой - раз указатель не идёт в мэйн, надо чтобы мэйн дал нам указатель.
% Добавим в аргументы функции указатель на выходную строку, и напишем что для начала сложить строки и положить в локальный массив strcat(welcome, name). Добавим в основную функцию массив char result [], который будет хранить результат и передадим в функцию helloFunction char name и char result. Запускаем, и не работает. А все дело в том, что мы не используем библиотечные функции, есть функция strcpy, которая не просто перекладывает указатель в определенную переменную, а копирует строку, теперь все работает.
% char* helloFunction (char* name, char* out) {
% char welcome[255] = “Hello, ”;
% strcat(welcome, name);
% strcpy(out, welcome);
% }
% int main(int argc, comst char* argv[]) {
% char name[256];
% char result[256];
% gets(name);
% helloFunction(name, result)
% puts(result);
% return 0;
% }
% Если присмотреться - то все функции работающие со строками, именно так и делают - запрашивают источник данных и конечную точку, куда данные нужно положить. А кто мы такие, чтобы спорить с библиотечными функциями? Обратите внимание на то, что функции strcat() и strcpy() возвращает указатель на получившуюся строку. Мы также возвращаем указатель на строку, и тут встаёт вопрос о вызывающем функцию контексте, нужен ли этот указатель вызывающему. Работа со строками в Си до сих пор является очень и очень актуальной темой на программистских форумах, можете удостовериться.
% Давайте рассмотрим ещё пару функций. Например, сравнение строк, функция strcmp, допустим, пусть именно меня программа приветствует как-то иначе. Функция возвращает отрицательные значения, если первая строка меньше второй, положительные, если первая больше второй, и 0 если строки равны. Это функция, которую удобно применять в условиях. И копирование строк, функция strcpy, также принимающую на вход две переменных - куда копировать, и что копировать.
% Если строки будут действительно равны, мы скопируем в строку с именем слово Master!. Скомпилируем, запустим, введем имя и все работает.
% void helloFunction (char* name, char* out) {
% char welcome[255] = “Hello, ”;
% if (strcmp("Ivan", name) == 0)
% strcpy(name, "Master!");
% strcat(welcome, name);
% strcpy(out, welcome);
% }
% Из всех функций для работы со строками чрезвычайно часто используются atoi(); atof(); переводящие написанные в строке цифры в численные переменные внутри программы. atoi() переводит в int, atof() во float.
% Объявим переменную num, предложим пользователю ввести цифру, естественно в виде строки. Будем принимать ее при помощи функции gets, хотя как мы помним, могли бы и scanf() Заведем переменную int number для хранения результата работы функции atoi. Затем давайте умножим результат сам на себя и выведем окончательное число в консоль. Запустим проект и убедимся, что все работает.
% Полный список функций для работы со строками можно посмотреть в заголовочном файле string.h. Описание и механика их работы легко гуглится, документации по языку очень много.
% char num[64];
% puts("Enter a number: ");
% gets(num);
% int number = atoi(num);
% number *= number;
% printf("We powered your number to %d", number);
% И напоследок еще пару слов об обработке символов. Функции для работы с символами содержатся в заголовочном файле stdlib.h. Естественно, наша программа может получить какие-то значения в виде строк. Не всегда же есть возможность использовать scanf(); - например, считывание из графических полей ввода даёт нашей программе значения в виде строк. В языке С есть функции для работы с каждым символом строки, например:
% isalpha() возвращает истину или ложь если символ является символом из алфавита;
% isdigit() если символ является цифрой;
% isspace() является ли символ пробелом;
% isupper(), islower() находится ли символ в верхнем или нижнем регистре; toupper(), tolower() переводят символ в верхний или нижний регистр.
% Можем использовать одну из них соответственно нашей задаче, допустим, пользователь может вводить своё имя как с заглавной буквы, так и всеми строчными. Уравняем оба варианта для нашей проверки одной строкой name[0] = tolower(name[0]); а после проверки вернём заглавную букву на место name[0] = toupper(name[0]); и удостоверимся что даже если мы напишем своё имя с маленькой буквы - программа напишет его с большой. Запустим
% СЛАЙД С ФУНКЦИЯМИ STDLIB.H
% void helloFunction (char* name, char* out) {
% char welcome[255] = “Hello, ”;
% name[0] = tolower(name[0]);
% if (strcmp("ivan", name) == 0)
% strcpy(name, "Master!");
% name[0] = toupper(name[0]);
% strcat(welcome, name);
% strcpy(out, welcome);
% }
% На этом уроке мы познакомились со строками, способами работы с ними и немного улучшили понимание механизма работы программ. Попробовали в деле некоторые функции из стандартной библиотеки. До встречи на следующем уроке.
\section{Структуры}
% Коллеги, здравствуйте. Рад вас приветствовать на 12м уроке Основы Языка С. На этом занятии мы поговорим о структурах данных в языке С.
% Несмотря на то что язык Си создавался в незапамятные времена, уже тогда программисты понимали, что примитивных типов данных недостаточно для комфортного программирования. Мир вокруг можно моделировать различными способами. Самым естественным из них является представление о нём, как о наборе объектов.
% У каждого объекта есть свои свойства. Например, для человека это возраст, пол, рост, вес и т.д. Для велосипеда тип, размер колёс, вес, материал, изготовитель и пр. Для товара в магазине идентификационный номер, название, группа, вес, цена, скидка и т.д. У объектов одного типа набор этих свойств одинаковый: все собаки могут быть описаны, с той или иной точностью, одинаковым набором свойств, но значения этих свойств будут разные.
% СЛАЙД ПРО ТО ЧТО СТРУКТУРЫ МОГУТ БЫТЬ С ОДИНАКОВЫМИ ЗНАЧЕНИЯМИ
% Сразу небольшое отступление, для тех кто изучал высокоуровневые языки, такие как Java или С#, в Си отсутствуют классы в том виде в котором вы привыкли их видеть. Так вот, для работы с такими объектом нам необходима конструкция, которая бы могла агрегировать различные типы данных под одним именем так появились структуры. Т.е. структура данных - это такая сущность, которая объединяет в себе несколько примитивов. Для примера, создадим такую структуру, как простая дробь. В программировании существуют дробные числа и представлены они типами float и double. Но это десятичные дроби. Мы же будем описывать обычную дробь.
% Для описания структуры используется ключевое слово struct и название структуры. Далее в фигурных скобках описываются переменные, входящие в структуру. В нашем примере это будут целая часть, числитель и знаменатель. У этих переменных не гарантируются инициализационные значения, т.е. мы ничего не присваиваем им изначально, это просто описание, которое говорит компилятору о том, что когда в коде встретится инициализация нашей структуры, для её хранения понадобится вот столько памяти, которую нужно разметить для хранения вот этих переменных.
% #include <stdio.h>
% struct fraction {
% int integer;
% int divisible;
% int divisor;
% };
% Для сокращения записи опишем новый тип данных, назовём его ДРОБЬ. Это делается при помощи ключевого слова typedef. Его синтаксис прост, пишем typedef название старого типа данных название нового типа, т.е. как мы будем называть его в дальнейшем.
% typedef struct fraction Fraction;
% Доступ к переменным внутри структуры осуществляется привычным для высокоуровневых языков способом - через точку. Создадим три переменных для хранения двух структур типа дробь с которыми будем совершать операции, и одну для хранения результата. Инициализируем переменные какими-нибудь значениями. Опишем целочисленные значения, опишем делимое для обеих дробей и опишем делитель для обеих дробей. Для простоты будем использовать простые дроби типа 1/5.
% В комментарии к каждой дроби я напишу, как бы она выглядела на бумаге.
% Внутрь функций структуры данных можно передавать как по значению, так и по ссылке.
% int main(int argc, const char* argv[]){
% Fraction f1, f2, result;
% f1.integer = -1;
% f1.divisible = 1; //-1 | 1 /5
% f1.divider = 5;
% f2.integer = 1;
% f2.divisible = 1; ; //1 | 1 /5
% f2.divider = 5;
% result.divisible = 0;
% result.divider = 0;
% }
% Опишем функцию, которая будет выводить нашу дробь на экран. В эту функцию мы можем передать нашу структуру по значению. Т.е. внутри каждой функции мы будем создавать копию структуры типа дробь, и заполнять её теми значениями, которые передадим в аргументе. Вывод каждой дроби на экран будет зависеть от ряда условий. Именно эти условия мы и опишем внутри нашей функции.
% Если делимое равно 0, то у дроби надо вывести только целую часть, если же делимое не равно 0 и целая часть равно 0 выводим только дробную часть. Пишем: если делимое не равно 0 то вступает в силу следующее условие если целая часть равна 0, то печатаем дробь следующим образом: число, значок дроби, число числа это делимое и делитель.
% printf("%d / %d", f.divisible, f.divider);
% В противном случае, если целая часть и делимое не равны 0, то выводим всю дробь целую часть, делимое и делитель
% printf("%d %d/%d",f.integer,f.divisible,f.divider);
% И еще один else для общего if если делимое равно 0 то выводим только целую чать:
% printf("%d", f.integer);
% void frPrint(Fraction f) {
% if (f.divisible != 0)
% if (f.integer == 0)
% printf("%d / %d", f.divisible, f.divider);
% else
% printf("%d %d/%d",f.integer,f.divisible,f.divider);
% else
% printf("%d", f.integer);
% }
% Проверим, насколько хорошо мы написали нашу функцию? Для этого вызовем ее и передадим туда значения наших дробей.
% Добавим пустую строчку и запустим.
% frPrint(f1);
% puts(“”);
% frPrint(f2);
% Выглядит неплохо, для полноты картины не хватает только научиться выполнять с этими дробями какие-нибудь действия. Для примера возьмём что-то простое, вроде умножения. Передадим в эту функцию значения наших двух дробей и указатель на структуру, в которую будем складывать результат вычислений.
% Назовем нашу функцию frMul, передадим туда необходимые аргументы и немного вспомним математику. Для того чтобы перемножить две дроби нам надо привести их к простому виду, т.е. лишить целой части а затем перемножить числители и знаменатели. Для перевода в простой вид опишем функцию frDesinteger, в которую будем передавать адрес первой и второй дроби.
% Чтобы не перепутать локальные структуры функции и указатели на внешние структуры, доступ к полям внутри указателей на структуры получают не при помощи точки, а при помощи вот такой стрелки. Т.е. поскольку result для функции frMul является указателем, то мы будем записывать результат не в локальную структуру, а непосредственно в ту структуру, которую мы объявили в в функции Мэйн и передали ссылку на нее нашей функции.
% Ну а далее напишем операции умножения для числителя и знаменателя:
% result->divisible = f1.divisible * f2.divisible;
% result->divider = f1.divider * f2.divider;
% void frMul(Fraction f1, Fraction f2, Fraction *result) {
% frDesinteger(&f1);
% frDesinteger(&f2);
% result->divisible = f1.divisible * f2.divisible;
% result->divider = f1.divider * f2.divider;
% }
% void frDesinteger(Fraction *f) {
% int sign = (f->integer < 0) ? -1 : 1;
% if (f->integer < 0)
% f->integer = -f->integer;
% f->divisible = f->divisible + (f->integer * f->divisor);
% f->divisible *= sign;
% f->integer = 0;
% }
% Теперь можем выводить результат умножения на экран. Для этого вызовем нашу функцию frMul в которую передадим дробь№1, дробь №2 и адрес на результирующую дробь. Затем вызовем функцию печати frPrint и передадим туда нашу результирующую дробь. Запустим нашу программу и убедимся что все работает корректно.
% puts(“”);
% frMul(f1, f2, &result);
% frPrint(result);
% Полученных знаний нам хватит практически для любых операций со структурами. До встречи на следующем уроке, коллеги.
\section{Файлы}
% Коллеги здравствуйте.
% За предыдущие занятия мы с вами познакомились почти со всеми существующими в языке С типами данных, как примитивными, так и ссылочными. Довольно подробно рассмотрели работу почти всех операторов языка. Пришло время поговорить о взаимодействии программы с операционной системой, а именно - о чтении и записи в файловую систему компьютера.
% Файловая система любого компьютера - это структура. Для языка С файл - это тоже структура. Структура, хранящая данные о положении курсора в файле, его название, буферы, флажки и прочие свойства. Файлы делятся на два основных типа - текстовые и бинарные. Мы рассмотрим работу с текстовыми.
% СЛАЙД О ФАЙЛОВОЙ СИСТЕМЕ
% Опишем переменную, хранящую указатель на нашу структуру. Вся основная работа будет проходить через неё. Для того, чтобы присвоить этой переменной указатель на какой-то реальный файл воспользуемся функцией fopen, которая возвращает указатель на адрес в памяти.
% FILE *f;
% Функция принимает в качестве аргументов имя файла в двойных кавычках и режим его открытия.
% Основных используемых режимов шесть - чтение, запись, добавление, двоичное
% чтение, двоичную запись и двоичное добавление. Функции записи и добавления создают файл в случае его отсутствия. А функция записи стирает файл, если он существует и не пустой.
% СЛАЙД О ВОЗМОЖНОСТЯХ И РЕЖИМАХ FOPEN
% Итак создадим текстовый файл с каким-то неожиданным названием вроде filename.txt, и скажем нашей программе, что нужно будет его создать, если его не существует, перезаписать, если существует, а дальше мы будем в него записывать данные.
% Имя файла в аргументе может быть как полным, вроде C:\FILE.TXT тогда файл будет создан в корне диска C, так и относительным, каким мы его указали сейчас. Это значит, что файл будет создан в той папке, в которой запускается наша программа.
% f = fopen(“filename.txt”, “w”);
% В случае, если файл не найден или по какой-то причине не создался, в переменную file запишется нулевой указатель, поэтому перед тем, как начать работу с файлом, нужно проверить, смогла-ли программа его открыть, для этого запишем условие если в наш указатель записался нулевой указатель, то дальнейшее выполнение функции Мэйн не имеет смысла.
% if(file == NULL) return 1;
% Если всё хорошо, можем записывать в файл данные. Для записи в файл есть несколько функций, мы воспользуемся самой простой и очевидной
% fprintf(); . В неё в качестве первого аргумента обязательно нужно передать указатель на файл, в который мы собираемся писать, а дальше можно использовать как знакомый нам printf() со всеми его удобствами, заполнителями, экранированными последовательностями и дополнительными аргументами. После того как мы закончили запись в файл его необходимо
% закрыть, вызвав функцию fclose();
% fprintf(f, “Hello, files! %s”, “we did it! \n”);
% fclose(f);
% Запустим наш проект и посмотрим что у нас получилось. Перейдем в проводник и увидим что в папке проекта появился файл filename.txt, в котором написано наше содержимое, откроем его с помощью блокнота.
% Теперь давайте рассмотрим не менее важную тему, а именно - чтение из файла. Для этого нужно его открыть в режиме чтения. Далее мы можем воспользоваться неожиданно похожей функцией - fscanf() чтобы прочитать форматированные значения из файла. Создадим массив из переменных типа char, назовем его word и, при помощи функции fscanf() считаем из файла некоторую строку, которую положим в этот массив. Далее выведем в консоль строку которую прочитали, для этого воспользуемся привычной нам функцией printf, а затем выведем пустую строку. Запустим нашу программу и увидим, что в консоль вывелось слово Hello, - т.е. до пробела, функция fscanf отлично отработала.
% char word[256];
% f = fopen(“filename.txt”, “r”);
% fscanf(f, “%s”, &word);
% printf(“%s”, word);
% puts(“”);
% Но сколько данных читать? Как узнать, что достигнут конец файла? Для этого придумали функцию feof() (FILE END OF FILE) возвращающую ноль, если конец файла не достигнут, и единицу если достигнут.
% Опишем цикл, который выведет в консоль все полученные сканом строки из нашего файла. Для этого мы циклически пройдемся по всему файлу пока не будет достигнут конец и будем выводить считанные строки в консоль
% Запустим наш проект и убедимся, что вывод в консоль полностью соответствует содержимому файла, и это было не так уж сложно.
% Не забудем в конце закрыть файл.
% char word[256];
% f = fopen(“filename.txt”, “r”);
% while(!feof(file)){
% fscanf(f, “%s”, &word);
% printf(“%s”, word);
% }
% fclose(f);
% puts(“”);
% На следующем уроке поговорим о распределении памяти. До скорой встречи!
\section{Распределение памяти}
% Коллеги, здравствуйте.
% Это занятие находится в конце курса, но не по важности. Сильная сторона языка С не только в возможности работать с указателями, но и в возможности самостоятельно управлять выделяемой памятью внутри программы. В языках высокого уровня данная возможность зачастую скрыта от программиста, чтобы по случайности не подвесить среду виртуализации или не сломать операционную систему.
% Итак, как мы уже знаем, все переменные всех типов как-то хранятся в памяти, и до этого момента нас устраивало, как операционная система нам эту память выделяет. Но, пришло время взять бразды правления в свои руки. Процесс выделения памяти для программы называется memory allocation отсюда и название функции, которая выделяет память и пишет в предложенный идентификатор указатель на начало этой области. malloc(size); - она принимает в качестве аргумента размер выделяемой памяти. Как видим, функция возвращает пустоту, то есть область памяти будет зафиксирована, но не размечена. То есть это будет просто некоторая пустая область из n байт.
% СЛАЙД ПРО MALLOC() И АРГУМЕНТЫ
% Чтобы иметь возможность в этой области хранить значения нам нужно её подготовить для хранения этих значений разметить.
% Например, мы уверены, что будем складывать в нашу область памяти какие-то целые числа типа integer. Для этого при вызове функции нам надо использовать синтаксис приведения типа полученной области. Мы знаем, что каждая переменная типа integer хрпнится в памяти в 4 байтах. Например мы создаем указатель на некоторую область состоящую из 123 байт таким образом будет выделена память для 123 байт, но она никак не будет размечена.
% А при помощи оператора приведения типа мы скажем компилятору, что нам необходимо выделить некоторую область памяти, поделить её на ячейки размера int, и каждой такой ячейке дать свой адрес.
% int *area = (int*) malloc(123);
% Итак, как узнать сколько нужно выделить памяти и при этом не запутаться. Для этого не обязательно знать размеры всех типов данных языка Си, для этого придумали оператор sizeof. Оператор sizeоf возвращает размер переменной (типа данных) в байтах. Мы напишем sizeof (int), умножим его на 10. Таким образом мы выделим память в размере 40 байт и разметим их под хранение переменных типа int. Фактически мы сделали то же самое что и описание массива типа int при помощи записи объявления массива с использованием квадратных скобок.
% int *area = (int*) malloc(sizeof (int) * 10);
% int array[10];
% Помните, мы говорили про арифметику указателей? Вот это то место, где она нам поможет понять, что вообще происходит. Давайте реализуем два массива привычным нам способом и при помощи динамического выделения памяти. Для реализации массива нам понадобится его размер, определим его как константу SIZE. Заменим в объявленном массиве 10 на SIZE и заполним этот массив какими-нибудь значениями.
% И напишем второй цикл для вывода этого массива в консоль.
% Добавим пустую строку
% И проделаем то же самое со вторым массивом, который мы инициализировали как область памяти. В этом виде данный код демонстрирует что мы можем одинаков работать и с массивами, объявленными привычными способами и с динамически выделенной областью памяти. Для более наглядной разницы, при заполнении и выводе в консоль второго массива, воспользуемся арифметикой указателей.
% Запустим наш проект, все работает.
% 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]);
% for(i = 0; i < SIZE; i++) *(area + i) = i * 10;
% for(i = 0; i < SIZE; i++) printf("%d ", *(area + i));
% Напомню, мы реализовали массив area вручную. то есть выполняем ровно те операции, которые выполняет компилятор при реализации массива array. Разложили синтаксис на базовые операции. Очевидно, что это знание открывает нам возможность распределять память в нашем приложении для любых типов данных, в том числе и сложных, таких как структуры. Для того, чтобы каждый раз не пересчитывать размеры переменных вручную, особенно это актуально для строк и структур, используют оператор sizeof(), который возвращает целочисленное значение в байтах, которое займёт в памяти та или иная переменная.
% Функция malloc резервирует память для нашей программы, но делает это весьма просто, вместе со всеми теми случайными переменными, которые могут там храниться.
% Давайте я продемонстрирую это. Закомментируем заполнение массива area, и увидим в консоли непонятные ненулевые значения.
% Для того чтобы гарантированно очистить вновь выделенную область памяти используют функцию calloc() clear allocate которая не только выделит нам память, но и очистит содержимое. Поскольку функция не только выделяет память но и очищает её, считается, что она работает медленнее, чем malloc. Синтаксис её весьма похож, только размеры необходимой области памяти передаются двумя аргументами - первый - сколько элементов, второй - какого размера будут элементы.
% 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);
2021-08-20 15:57:50 +03:00
\end{document}