basic-c/main.tex

295 lines
32 KiB
TeX
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

\documentclass[a4paper]{article}
\usepackage[russian]{babel}
\include{formatting}
\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}
\section{Структуры}
Несмотря на то что язык С создавался в незапамятные времена, уже тогда программисты понимали, что примитивных типов данных недостаточно для комфортного программирования. Мир вокруг можно моделировать различными способами. Самым естественным из них является представление о нём, как о наборе объектовно важно помнить, что С - это процедурный язык, в нём не существует объектно-ориентированного программирования. Тем не менее, у каждого объекта в мире есть свои свойства. Например, для человека это возраст, пол, рост, вес и т.д. Для велосипеда тип, размер колёс, вес, материал, изготовитель и прочие. Для товара в магазине артикул, название, группа, вес, цена, скидка и т.д. У объектов одного типа набор этих свойств одинаковый: все, например, собаки или коты могут быть описаны, с той или иной точностью, одинаковым набором свойств, но значения этих свойств будут разные.
% Сразу небольшое отступление, для тех кто изучал высокоуровневые языки, такие как 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);
\end{document}