\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} \section{Функции} \subsection{Понятие функции, параметры и аргументы} Функция - это такая обособленная часть кода, которую можно выполнять любое количество раз. У функций обязательно в таком порядке должны быть описаны: тип возвращаемого значения, название, параметры и так называемое тело, то есть собственно исполняемый код. Рассмотрим более детально функцию \code{int main (int argc, char *argv[])}: \code{int} - это \textit{тип возвращаемого значения}, то есть на том месте, откуда будет вызвана эта функция, в результате её работы по выполнении оператора \code{return;}, появится некое целое число. Возвращаемые значения могут быть любых типов. В случае же когда функция не должна возвращать результат своей работы, или никакого возвращаемого результата не предполагается, указывается ключевое слово \code{void} (англ. - пустота). То есть на месте вызова функции в результате её выполнения ничего не появится. Оператор \code{return;} обязателен для не-void функций, а в \code{void} функциях может присутствовать или нет, но никогда не содержит возвращаемого значения. \code{main} - это \textit{название функции}. Функция именно с таким названием, написанным с маленькой буквы, всегда является точкой входа в программу (\hyperref[text:main]{\ref{text:main}}). Операционная система ищет именно эту функцию, когда получает команду на выполнение программы. \frm{Названия функций в рамках одной программы не должны повторяться и не должны начинаться с цифр или спецсимволов, также, как и названия переменных (\hyperref[text:naming]{\ref{text:naming}}) никаких других ограничений на название функций не накладывается.} Конструкция в круглых скобках \code{(int argc, char *argv[])} - это \textit{параметры функции}. Параметры функции - это такие переменные, которые создаются при вызове функции и существуют только внутри неё. С их помощью можно передать в функцию какие-то аргументы и исходные данные для работы. Параметры пишутся в круглых скобках сразу после названия функции. В случае если функция не принимает параметров необходимо поставить после названия пустые круглые скобки. Весь код, содержащийся в фигурных скобках после параметров функции называется \textit{телом функции}. Это те операторы и команды, которые будут последовательно выполнены при вызове функции. В теле функции мы можем \textbf{вызывать} другие функции, но \textbf{никогда не можем создавать в теле функции другие функции}. Никаких других ограничений на написание тела функции язык не накладывает. \begin{verbatim} ТипВозвращаемогоЗначения Имя (СписокАргументов) { ТелоФункции return ВозвращаемоеЗначение; } \end{verbatim} Далее приведём небольшой пример, который призван продемонстрировать, как выглядит простейшее \textit{объявление} и \textit{описпание} функций (function declaration and definition). \begin{lstlisting}[language=C,style=CCodeStyle] void somefunction() { // <-- this is a function printf("some function\n"); // some useful things } int anotherFunction() { printf("another function\n"); // more useful things happened return 10; } int main (int argc, const char* argv[]) { printf("main function\n"); // more useful things somefunction(); // <-- this is invocation int x = anotherFunction(); printf("x = %d\n", 10); return 0; } \end{lstlisting} Так, на тринадцатой строке кода выше мы видим, что \textbf{вернувшееся} из функции, объявленной на пятой строке целое число \code{10} будет присвоено переменной \code{x}. \begin{verbatim} $ ./program main function some function another function x = 10 $ \end{verbatim} Функции принято разделять на проверяющие, считающие и выводящие, и каждая из вышеописанных функций не должна нести дополнительной нагрузки. То есть, функция не должна знать откуда в программе появились её параметры, и где будет использован результат её работы. То есть сам язык таких ограничений не накладывает, но такой подход к написанию функций делает их значительно более гибкими и даёт им возможность быть переиспользованными. Без применения такого подхода было бы невозможно писать абстрактные библиотеки и фреймворки. \frm{\textbf{Параметры функции} - это те переменные, которые указываются в круглых скобках при определении или описании функции. Параметры функции существуют как локальные переменные в кодовом блоке тела функции.\textbf{Аргументы функции} - это те значения переменных или литералов, которые указываются в круглых скобках при выхове функции.} Для примера опишем функцию, суммирующую два числа. Для простоты, в качестве аргументов она будет принимать целые числа и возвращать целочисленный результат. Обратите внимание что функция не <<знает>> откуда взялись эти числа, мы можем их прочитать из консоли, можем задать в виде констант или получить в результате работы какой-то другой функции. Внутри функции \code{int main (int argc, char *argv[])} мы вызываем нашу функцию \code{sum(int x, int y)} суммирующую два числа и передаём в качестве аргументов эти числа. \begin{lstlisting}[language=C,style=CCodeStyle] int sum(int x, int y) { int result = x + y; return result; } int main (int argc, const char* argv[]) { int a; scanf("%d", &a); int x = sum(50, a); printf("x = %d\n", 10); return 0; } \end{lstlisting} Обратите внимание, что в качестве аргументов мы можем передавать константные значения, а также переменные. Значения переменных мы можем получить например из консоли, либо в результате выполнения какой-нибудь другой функции. \begin{verbatim} $ ./program x = 110 $ \end{verbatim} Как уже было сказано, параметры - это переменные, которые хранят в себе некоторые начальные значения вызова функции. Параметризация позволяет использовать одни и те же функции с разными исходными данными. Приглядимся повнимательнее к хорошо знакомой нам функции \code{printf();}. Строка, которую мы пишем в круглых скобках в двойных кавычках - это аргумент функции. То есть мы знаем, что функция умеет выводить на экран строки, как именно - нам нет дела, а какие именно строки - мы указываем в качестве аргумента. Функция \code{printf();} примечательна еще и тем, что она может принимать в себя нефиксированное количество аргументов. Описание работы таких функций, а также их написание выходит далеко за пределы основ языка, нам важно помнить что мы можем это использовать. В аргументе функции \code{printf()} мы можем написать заполнитель соответствующего типа и, например, вызвать нашу функцию \code{sum}. \subsection{Оформление функций. Понятие рефакторинга} Теперь мы без проблем можем оформить уже существующие у нас программы в виде функций. Например, оформим в виде функции программу проверки простоты числа. Для этого опишем функцию которая возвращает целое число, назовем ее \code{isPrime()}, в качестве параметра она будет принимать целое число, назовем его \code{number}. Найдем в предыдущих разделах (стр. \hyperref[code:isPrime]{\pageref{code:isPrime}}) программу определения простоты числа и скопируем в тело функции. Внесем небольшие правки, уберем вывод так как это будет, можно сказать, классическая проверяющая функция, вывод оставим для функции \code{int main (int argc, char *argv[])}, пусть о наличии у нас терминала <<знает>> только она. \frm{Такой процесс, перенос участков кода между функциями, выделение участков кода в функции, синтаксические, стилистические и другие улучшения, называетя \textbf{рефакторингом}. Обычно, рефакторингом занимаются сами разработчики в свободное от основной деятельности времени, в периоды код ревью или по необходимости улучшить читаемость/повторяемость собственного кода.} Следовательно, допишем условия: если делителей два, то число простое, возвращаем \code{ИСТИНУ}, то есть любое ненулевое значение, в нашем примере - единицу. Если же делителей больше – число не простое, возвращаем \code{ЛОЖЬ}, в нашем случае, это ноль. Такой вывод можно записать и другим способом, \code{return (d == 2)} – это выражение в случае истины вернет единицу в случае лжи ноль. Или можно воспользоваться тернарным оператором, то есть, написать \code{return (d == 2) ? 1 : 0}: если условие в скобках истинно вернется единица, ложно – ноль. Также важно, что выйти из функции мы можем на любом этапе ее выполнения, например если делителей уже три, то нам нужно не завершать цикл, а вернуть \code{ЛОЖЬ} из функции. \begin{multicols}{2} \begin{lstlisting}[language=C,style=CCodeStyle] int isPrime(int number){ int d = 0, i = 1; while(i <= number){ if(number % i++ ==0) d++; else continue; if (d == 3) return 0; } //if (d == 2) return 1; //else return 0; //return (d == 2) ? 1 : 0; return (d == 2) } \end{lstlisting} \columnbreak \begin{lstlisting}[language=C,style=CCodeStyle] int main(int argc, char *argv[]) { int number; int dividers = 0, i = 1; printf("Enter number: "); scanf("%d", &number); while (i <= number) { if (number++ % i == 0) { dividers++; } else { continue; } if (dividers == 3) break; } printf("Number %d is%s prime", number, (dividers == 2) ? "" : " not" ); } \end{lstlisting} \end{multicols} % Немного подправим вывод, внесем в него вызов функции isPrime и объявим переменную int num, которую будем передавать в качестве аргумента в функцию isPrime. % Запустим нашу программу и убедимся что все работает – число 71 действительно является простым. % Теперь мы можем написать программы любой сложности, содержащие функции isPrime. sum. О том, что мы работаем с консолью, в нашем случае должна знать только функция main, поэтому ввод значений и вывод на экран мы оставим в ней, а подсчёты значений положим в функции. % int main (int argc, const char* argv[]) { % int a; % scanf(“%d, &a”); % printf(“%d”, sum(50, a)); % int num = 71; % printf(“Введенное число %d %s является простым \n”, number, isPrime(num) ? “” : “не”); % return 0; % } % Пришло время поговорить про прототипы. % Зачастую возникают ситуации, когда функция не описана до точки входа в программу, или вовсе лежит в другом файле. В этом случае мы должны сообщить компилятору, что такую функцию придётся дополнительно поискать. Для этого необходимо указать всю информацию о функции, кроме её тела. Такой оператор называется прототип функции. % Опишем прототип функции isPrime, описав сигнатуру этой функции. % int isPrime(int number); % И пара слов о заголовочных файлах. Заголовочные файлы это мощный инструмент модульной разработки. Мы уже неоднократно видели подключение заголовочного файла stdio.h, давайте посмотрим, что же скрывает и как именно работает эта строка. Обнаружив данный файл на диске мы видим, что в нём содержатся другие подключения библиотек, директивы препроцессора (о которых более подробно мы будем говорить на следующих занятиях) и прототипы функций (например, так часто используемой нами % printf()). (Где-то вот здесь… на 259 строке) % // Здесь показ содержимого stdio.h \section{Указатели} % Коллеги, здравствуйте. Вот и пришла пора поговорить о серьёзном низкоуровневом программировании. О том, от чего стараются оградить программистов языки высокого уровня и современные фреймворки. Об указателях. % На этом уроке мы поговорим о том, что такое указатели и как они соотносятся с остальными переменными, что такое передача аргумента по значению и по ссылке. % Как мы наверняка помним, все переменные и константы, используемые в программе, хранятся в оперативной памяти. У каждой переменной и константы в памяти есть свой собственный адрес. Адреса принято показывать на экране в виде шестнадцатиричных чисел. Этот адрес выдаётся нашей программе операционной системой, а язык Си позволяет использовать его на усмотрение программиста. Иными словами в языке С есть возможность получить доступ к переменной не только по имени, но и по адресу. Получение доступа к переменной по адресу называется разыменовыванием. Давайте выведем в консоль всю имеющуюся информацию о переменной «а». Мы знаем, что это целочисленная переменная значением 50, которая хранится по какому-то адресу. % Адрес переменной может храниться в специальной переменной, которая называется указатель. % Для объявления указателя пишут тип переменной, адрес которой будет храниться в указателе, знак звёздочки и имя указателя. Такому указателю можно присвоить значение адреса существующей переменной, также как мы делали это раньше с другими типами данных % Для наглядности снова выведем всю имеющуюся у нас на данный момент информацию на экран. Напомню, для вывода адреса используется заполнитель %p. Выведем в консоль десятичное значение переменной pointer и адрес переменной pointer % Увидим, что значение переменной pointer является совершенно случайным числом. % Если мы представим это в виде адреса то всё встанет на свои места. Адрес «а» - это значение переменной pointer. % СЛАЙД ГДЕ БУКВЫ И ЦИФРЫ ТОРЧАТ ИЗ КОРОБОК С «АДРЕСАМИ» % СЛАЙД С ПРИМЕРОМ АДРЕСА % РАЗЫМЕНОВЫВАНИЕ (ВОЗМОЖНО КАРТИНКА) % int main (int argc, const char** argv) { % int a = 50; % printf("value of a is %d \n", a); % printf("address of a is %p \n", &a); % int * pointer; % pointer = &a; % printf("value of pointer is %d \n", pointer); % printf("address of pointer is %p \n", &pointer); % printf("value of pointer is %d %p \n", pointer); % Пока что ничего необычного, все эти операции мы выполняли на предыдущих уроках. Но поскольку pointer это необычная переменная, а указатель, то мы можем получить не только её значение, но и значение переменной, на которую она указывает. Давайте запишем, вывести в консоль «переменная pointer указывает на такое-то значение» и разыменуем pointer. Т.е. получим доступ к переменной, на которую ссылается указатель pointer. % Таким образом, получается, что в указателе хранится ссылка на значение некоторой переменной. То есть указатель - это простейший ссылочный тип данных. Без указателей невозможно себе представить создание классов, и всеми любимого объектно-ориентированного программирования, даже массивов. % СЛАЙД СО СТРЕЛОЧКАМИ ЗНАЧЕНИЕ-ПЕРЕМЕННАЯ-АДРЕС-УКАЗАТЕЛЬ-И-ОБРАТНО % printf(“variable ‘pointer’ points at: %d”, * pointer); % Давайте изменим значение переменной «а», не на прямую, а с использованием указателя. Как видим, значение переменной изменилось. % *pointer = 70; % printf("value of a is %d \n", a); % Теперь, когда мы знаем об указателях, и умеем получать значения переменных, на которые они указывают, а также изменять их, перед нами открываются невообразимые ранее перспективы. Мы можем писать функции не создавая в них копии переменных, а передавать в них ссылки на уже существующие переменные, тем самым экономя память, и ускоряя выполнение программы. % Например, не составит труда написать ПРОГРАММУ, которая бы меняла местами значения двух переменных. Но написать ФУНКЦИЮ, которая бы проделывала тоже самое невозможно без применения указателей. Почему? очень просто - внутри функции создаются свои собственные переменные, значения которых меняются местами, и даже если мы вернём одну из них – как быть со второй? А получить доступ ко второй переменной мы не можем, поскольку, помним, она находится в области видимости функции. Такая передача аргументов называется передачей по значению (мы берём значение некоторой переменной и передаём внутрь функции). Т.е. мы берем значения некоторых переменных в функции Мэйн и передаем их в функцию, где создаём новые переменные с этими, переданными, значениями. % Как решить эту проблему? Передавать не значения переменных, а их адрес, тем самым сообщив функции, что нужно не создавать новые копии переменных, а сделать что-то с уже существующими, и, естественно указать адрес, с какими именно. Передача в качестве аргумента адреса, и создание в теле функции нового указателя называется передачей по % ссылке. То есть функция будет ссылаться на переменные, на которые мы укажем и оперировать их значениями. % Давайте немного модифицируем нашу функцию – передадим ей адрес переменных. Внутри функции у нас будут создаваться не переменные, а указатели на переменные, т.е. мы будем ссылаться на те же самые значения – т.е. мы будем изменять переменные, которые создали в функции Мэйн. Теперь нам не нужно ничего возвращать, потому что в функции ничего не создавалось, поэтому заменим int на void. % int swap_variables(int x, int y) { % return x; % } % int first = 50; % int second = 40; % swap_variables(40, 50); % СЛАЙД ПЕРЕДАЧА ПО ЗНАЧЕНИЮ И ПО ССЫЛКЕ % swap_variables(&first, &second); % void swap_variables(int *x, int *y) { % int temp z = *x; % *x = *y; % *y = *z; % } % Удалим из функции все лишнее и выведем в консоль переменные first и second до их смены и после. Передадим в функцию swap переменные first и second и убедимся, что все работает корректно. % int first = 50; % int second = 40; % printf("first = %d, second = %d \n", first, second); % swap_variables(&first, &second); % printf("first = %d, second = %d \n", first, second); % Применение такого подхода открывает перед нами широкие возможности, которые мы рассмотрим в следующих видео. % В этом видео мы познакомились с таким и понятиями, как указатели, передача по ссылке и передача по значению. До новых встреч. % СЛАЙД С ИТОГАМИ \section{Массивы} % Здравствуйте, коллеги, рад всех приветствовать на очередном занятии по Основам языка С. В этом видео нас с вами ждут массивы и ещё пара слов о директивах компилятору, иногда также называемых директивами препроцессора. С них и начнём. % Помимо уже хорошо знакомой вам директивы #include естественно, существуют и другие. Некоторые из них ограничивают импорт описанных в заголовочном файле функций, некоторые описывают какие то константы и даже действия. % СЛАЙД С ОПРЕДЕЛЕНИЕМ ДИРЕКТИВЫ ПРЕПРОЦЕССОРА, ОБЩИЙ ВИД, ПРИМЕРЫ % Вот, директиву ОПИСАТЬ мы и рассмотрим подробнее. Она не зря называется директивой препроцессора, поскольку даёт указание не процессору во время выполнения программы выделить память, присвоить значения, а непосредственно компилятору - заменить в тексте программы одни слова на другие. Например, таким образом можно задавать константы проекта, и даже целые действия. Например, напишем #define ARRAY_LENGTH 50 и это будет означать, что компилятор, перед тем как запустить нашу программу заменит все слова ARRAY_LENGTH на цифру 50. Весьма удобно, но этим можно не ограничиваться, мы можем попросить компилятор заменить целые вызовы функций и операторы на короткие, удобные нам слова. Иногда в программах можно встретить описание недостающего но такого привычного булевого типа при помощи директив препроцессора #define boolean int #define true 1 #define false 0. Но нам с вами пока что достаточно умения создать глобальную именованную константу. % #define ARRAY_LENGTH 50 % int a = ARRAY_LENGTH; % #define boolean int % #define true 1 % #define false 0 % Перейдем к обещанным массивам. Массив – это множество данных одного типа. Язык С строго типизирован, поэтому невозможно создать массив из разных типов данных. На данном этапе мы рассматриваем только простые типы данных, поэтому и массивы будем рассматривать статические. Статическим массивом называют массив, количество элементов которого заранее известно и не изменяется за время работы программы. % СЛАЙД ПРО МАССИВЫ % Статические массивы принято объявлять двумя способами: здесь мы объявляем массив, содержащий элементы типа int, идентификатор или имя массива arr, максимальное количество элементов которые может вместить в себя массив 3. % Как уже говорилось массив это множество данных или элементов. К каждому элементу массива можно обратиться по его номеру, который принято называть индексом. Индексация элементов начинается с нуля. % int arr[3]; % Давайте заполним наш массив значениями типа int. Для этого последовательно обратимся к каждому элементу и присвоим значение: % arr[0] = 10; % arr[1] = 20; % arr[2] = 30; % Обратите внимание, что язык С не гарантирует что инициализационное значение элементов массива будет равно нулю, если это не указано явно. % Второй способ объявления и инициализации массива используют, если массив сравнительно небольшой и его значения заранее известны, например: % int arr[6] = {1, 1, 2, 3, 5, 8}; % Итак, мы научились создавать и заполнять значениями массивы. Теперь общее правило объявления массивов в Си: при объявлении массива нужно указать его имя, тип элементов, количество элементов. Количество элементов есть натуральное число, т.е. целое положительное. Ноль не может быть количеством элементов. Нельзя задавать переменное количество элементов массива. % int nArr[100]; // Объявлен массив, предназначенный для хранения ста целых чисел; % float fArr[5]; // Объявлен массив, предназначенный для хранения 5-ти чисел типа float; % char cArr[2]; // Объявлен массив, предназначенный для хранения двух символов; % int varElem; % int nArr[varElem]; // Ошибка! Количество элементов нельзя задавать переменной; % Теперь давайте научимся получать доступ к элементам массива. Нет ничего проще, тем более, что мы это уже делали когда заполняли массив. для доступа к конкретному элементу массива нужно указать имя массива и индекс элемента: % int a = arr[0] получить значение 0-го элемента массива arr и присвоить его переменной а. % printf(“let’s see whats in 0-th element: %d”, a) % При помощи массивов решают множество задач, таких как поиск, сортировка, составление таблиц соответствия, создание частотных диаграмм. На основе массивов создают более сложные структуры данных. Для примера давайте напишем программу, которая будет печатать наш массив в консоль. % #include % 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 % #include % #include % #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]; // Rubik’s 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 % 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}