basic-c/main.tex

226 lines
22 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-09-29 14:11:47 +03:00
% 09 arrays
\import{sections/}{09-arrays}
% 10 strings
\import{sections/}{10-strings}
2021-09-30 16:01:28 +03:00
% 11 structs
\import{sections/}{11-structs}
2021-08-23 07:19:35 +03:00
\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-09-30 16:01:28 +03:00
\appendix
\section*{Приложения}
\addcontentsline{toc}{section}{Приложения}
\renewcommand{\thesubsection}{\Alph{subsection}}
\subsection{Полный листинг программы программы терминального калькулятора}
\label{appendix:calc}
\lstinputlisting[language=C,style=CCodeStyle]{../sources/calculator.c}
2021-08-23 07:19:35 +03:00
2021-09-30 16:01:28 +03:00
\subsection{Полный листинг программы вычисления среднего арифметического}
\label{appendix:arrayaverage}
\lstinputlisting[language=C,style=CCodeStyle]{../sources/arrayaverage.c}
2021-08-20 15:57:50 +03:00
2021-09-30 16:01:28 +03:00
\subsection{Полный листинг программы умножения дробей}
\label{appendix:fractions}
\lstinputlisting[language=C,style=CCodeStyle]{../sources/fractions.c}
2021-08-20 15:57:50 +03:00
\end{document}