Общие сведения о языке C. Этапы сборки кода на языке C, используемые утилиты. Препроцессор. Заголовочные файлы. Комментарии
План занятия
- Контрольная по встроенным функциям lua (4 вопроса по типу вопросов последнего ДЗ)
- Разбор домашних заданий
- Глобальная переменная arg (lua)
- Ликбез по типизации в языках программирования
- Язык C: история возникновения, особенности, связь с другими языками
- Установка компилятора (Cygwin, видеоинструкция)
- Написание простой программы на C и её компиляция
- Этапы сборки кода на языке C, используемые утилиты
- Препроцессор, заголовочные файлы
- Комментарии в сишных исходниках
- Обсуждение прогресса по учебным проектам (Github!)
- Составление таблицы посетителей факультатива: имя, логин на кодомо, логин на гитхабе, тема учебного проекта (+URL на гитхабе)
Разбор домашних заданий
Число замыканий
Сколько замыканий создаёт этот код?
local function f(x) return function(y) return x ^ y end end local f2 = f(2) print(f2(10)) print(f2(11)) print(f(3)(40)) print(f(3)(45)) print(f(4)(45))
Правильный ответ: 5:
- f
- f2
- f(3)
- f(3)
- f(4)
Ещё раз о чистых функциях
В тесте lua3hw было следующее задание, которое вызвало затруднения у нескольких студентов. "Может ли функция, не использующая глобальные переменные, возвращать разные значения при разных запусках с тем же набором аргументов?"
Правильный ответ: может. Некоторые выбирали вариант "может, если она использует внешние локальные переменные", который не учитывал возможность изменчивости аргументов. Рассмотрим следующий код в качестве контрпримера:
local function f(x) return x.aaa end local myx = {aaa = 1} print(f(myx)) -- prints 1 myx.aaa = 2 print(f(myx)) -- prints 2
Вызов методов
За что отвечает двоеточие в следующей записи?
cat:eat(mouse)
Правильный ответ: вызов метода eat объекта cat, передача mouse вторым аргументом.
Глобальная переменная arg (lua)
Утилита командной строки lua определяет глобальную переменную arg, в которой хранятся аргументы командной строки.
Пример программы, которая распечатывает свои аргументы командной строки:
for i, a in ipairs(arg) do print(string.format("argument %i is %s", i, a)) end
Запуск программы:
$ lua args.lua 11 1 argument 1 is 11 argument 2 is 1
Ликбез по типизации в языках программирования
Типизированные языки программирования разделяют на следующие категории:
Статическая / динамическая типизация. Статическая определяется тем, что конечные типы переменных и функций устанавливаются на этапе компиляции. Т.е. уже компилятор на 100% уверен, какой тип где находится. В динамической типизации все типы выясняются уже во время выполнения программы.
- Примеры:
- Статическая: C, Java, C#;
Динамическая: Python, JavaScript, Ruby.
Сильная / слабая типизация (также иногда говорят строгая / нестрогая). Сильная типизация выделяется тем, что язык не позволяет смешивать в выражениях различные типы и не выполняет автоматические неявные преобразования, например нельзя вычесть из строки множество. Языки со слабой типизацией выполняют множество неявных преобразований автоматически, даже если может произойти потеря точности или преобразование неоднозначно.
- Примеры:
- Сильная: Java, Python, Haskell, Lisp;
Слабая: C, JavaScript, Visual Basic, PHP.
Явная / неявная типизация. Явно-типизированные языки отличаются тем, что тип новых переменных / функций / их аргументов нужно задавать явно. Соответственно языки с неявной типизацией перекладывают эту задачу на компилятор / интерпретатор.
- Примеры:
- Явная: C++, D, C#
Неявная: PHP, Lua, JavaScript
За основу данного раздела взят следующий материал: "Ликбез по типизации в языках программирования". Заинтересованных прошу ознакомиться с полной версией.
Язык C: история возникновения, особенности, связь с другими языками
Следующая книга рекомендуется к прочтению: The C Programming Language. Ritchie & Kernighan
Лекции по языку со следующего сайта рекомендованы к ознакомлению: http://info.fenster.name/
Язык C — компилируемый статически типизированный язык программирования общего назначения, разработанный в 1969—1973 годах сотрудником Bell Labs Деннисом Ритчи как развитие языка Би. Поддерживает структурное и процедурное программирование и рекурсию. Первоначально был разработан для реализации операционной системы UNIX, но, впоследствии, был перенесён на множество других платформ. Язык C "близок к машине", оперирует типами данных и операциями, близкими к типам данных и операциям, с которыми имеет дело процессор, за что C называют "кроссплатформенным ассемблером".
Язык C - один из самых используемых языков программирования за всю историю. Многие языки, в том числе Lua, реализованы на C. Расширения для таких языков обычно пишут на C.
Типизация C: Статическая, Слабая, Явная
Установка компилятора, Lua, Git и смежных утилит
Linux (Debian/Ubuntu)
$ sudo apt-get install gcc make ssh zip unzip vim luarocks
Windows (Cygwin)
Видеоинструкция: youtube, оригинал
Установить Cygwin. Выбрать следующие пакеты: gcc, lua, ssh, git, make, vim, wget, zip, unzip, libreadline
- Запустить Cygwin ярлыком с рабочего стола.
- Редактировать файлы внутри cygwin можно прямо из этого терминала, с помощью команды vim (см. видеоинструкцию). Если vim вызывает затруднения, можно на первых порах редактировать файлы привычным редактором. Для этого нужно знать, где искать файлы cygwin: найдите на диске C: папку с именем cygwin и внутри неё папку home, и в ней папку с именем вашего пользователя - это и есть папка, которая по умолчанию открыта в терминале cygwin. Если вам, наоборот, хочется из cygwin получить доступ к остальным файлам на компьютере, то можете найти их по адресу /cygdrive/буква_диска/путь. Для перехода между папками используйте cd. Для просмотра списка файлов используйте ls. Создать папку: mkdir.
- Установить luaprompt:
$ git clone https://github.com/starius/luaprompt.git $ cd luaprompt $ wget -O cygwin-build.sh https://tinyurl.com/luap-cygwin-build-sh $ sh cygwin-build.sh $ cp luap.exe /bin
Установить LuaRocks:
$ wget http://luarocks.org/releases/luarocks-2.2.0.tar.gz $ tar -xf luarocks-2.2.0.tar.gz $ cd luarocks-2.2.0 $ ./configure $ make build $ make install
- Добавить пути к файлам luarocks'а в .bashrc:
$ luarocks path >> ~/.bashrc
После этого перезапустите терминал с Cygwin. Данный шаг нужен, чтобы lua видел пакеты, установленные через luarocks.
Установить с помощью LuaRocks пакет luasec и проверить, что его удается подцепить:
$ luarocks install luasocket $ luap > ssl = require "ssl"
Если все шаги прошли успешно, поздравляю - вы завершили установку необходимого софта!
Написание простой программы на C и её компиляция
Создайте файл "test.c":
#include <stdio.h> int main() { printf("Hello!\n"); }
Компилируем и запускаем:
$ gcc test.c -o test.exe $ ./test.exe Hello!
Теперь вы умеете писать helloworld на C!
Этапы сборки кода на языке C, используемые утилиты
Следующий материал рекомендуется к прочтению: Сборка программы на языке C.
Теория
Код на языке C состоит из двух типов файлов: заголовочные (header, расширение .h) и файлы с исходным кодом (source file, расширение .c). В заголовочных файлах хранятся объявления (declaration) структур и функций, а в файлах с исходным кодом - их реализация (definition, implementation). С-файлы могут подключать заголовочные файлы с помощью директивы препроцессора #include.
Не используйте для программ на C расширение .C, так как это расширение распознаётся компилятором (и программистами) как C++, а не как C.
При сборке кода из каждого c-файла создаётся объектный файл (o-файл). В o-файле хранится бинарная реализация одного c-файла. Затем все o-файлы собираются вместе и получается конечный файл (программа или библиотека).
Сборка o-файла из c-файла включает следующие этапы: препроцессинг и компиляция. Препроцессинг включает выполнение директив препроцессора, которые включают, но не ограничиваются включением используемых заголовочным файлов и условной компиляцией. После препроцессинга получется препроцессированный c-файл. Этот файл не имеет внешних зависимостей (в виде используемых заголовочных файлов), так как все они уже включены. Обычно препроцессированный файл не сохраняют на диск и он возникает только как промежуточное представление.
Самым затратным по времени является этап компиляции - перевод препроцессированного c-файла в o-файл. Именно на этом этапе язык C переводится в язык машины или близкий к нему.
Сборка воедино o-файлов называется компоновной (linking). Программа-компоновщик проверяет, что все используемые функции определены. Функции могут быть определены в компонуемых o-файлах или подключаемых библиотеках. На выходе получается итоговый результат сборки: исполняемый файл программы или библиотека (в зависимости от того, что мы собираем: программу или библиотеку).
Библиотеки можно разделить на два класса: статические и динамические. Если программа компонуется со статической библиотекой, то код статической библиотеки сливается с кодом программы в конечном файле приложения. Если программа компонуется с динамической библиотекой, то в конечном файле программы прописывается зависимость от динамической библиотеки. Таким образом, программы, скомпонованные статически, не нуждаются в библиотечных файлах и могу распространяться без библиотек (exe всё-в-одном), а программы, скомпонованные динамически, нуждаются в используемых библиотеках для работы. В зависимости от задач бывает предпочтительно использование того или иного типа компоновки. Большинство библиотек можно собрать как в статическом, так и в динамическом виде. Это контролируется опциями компилятора и компоновщика.
Статические библиотеки представляют собой архив с o-файлами. Динамические библиотеки - более сложный объект.
Исполняемый файлы имеют расширение exe в Windows и не имеют расширения в Linux. Статические библиотеки имеют расширение .a, а динамические библиотеки - .dll под Windows и .so под Linux.
Практика
Мы будем пользоваться программой gcc, с помощью которой можно совершать все этапы сборки. В предыдущем разделе мы компилировали простейшую программу на C. Для этого потребовалась всего одна команда: gcc test.c -o test.exe. За кулисами gcc произвёл препроцессинг, компиляцию и компоновку.
Давайте скомпилируем ту же программу, разделяя этапы компиляции и компоновки:
- компиляция: gcc -c test.c -o test.o
- компоновка: gcc test.o -o test.exe
Теперь посмотрим на препрцессированный вариант файла test.c: gcc -E test.c -o test.i. Откройте файл test.i текстовым редактором. На месте подключения системного заголовочного файла stdio.h вы увидите содержимое этого файла.
Напишем программу из двух c-файлов и одного заголовочного файла. В заголовочном файле будет объявлена только одна функция: factorial. В одном из c-файлов мы определим эту функцию (то есть, напишем её тело), а в другом c-файле будем функция main, которая будет спрашивать у пользователя число, считать его факториал с помощью функции factorial и выводить это число на экран.
// factorial.h #ifndef FACTORIAL_H_ #define FACTORIAL_H_ int factorial(int n); #endif // factorial.c #include "factorial.h" int factorial(int n) { int value = 1; int i; for (i = 1; i <= n; i++) { value = value * i; } return value; } // main.c #include <stdio.h> #include "factorial.h" int main() { printf("Enter the number: "); int n; scanf("%i", &n); int f = factorial(n); printf("%i! = %i\n", n, f); return 0; }
Создайте эти 3 файла. Не будем пока вдаваться в детали того, что там происходит.
Соберем программу:
$ gcc -c factorial.c -o factorial.o $ gcc -c main.c -o main.o $ gcc main.o factorial.o -o prog.exe
Мы собрали программу из нескольких файлов!
Давайте её запустим:
$ ./prog.exe Enter the number: 5 5! = 120
Препроцессор, заголовочные файлы
Строки, которые начинаются с решетки (#), называются директивами препроцессора. Выше говорилось, что препроцессор часто используется для включения заголовочных файлов и для условной компиляции.
Для включения содержимого заголовочного файла используется директива #include. После неё пишется имя подключаемого заголовочного файла. Если заголовочный файл локальный, то его имя берут в кавычки, а если системный - то в треугольные скобочки < и > (меньше и больше). Выше мы использовали оба варианта в файле factorial.c.
Важно заметить, что заголовочный файл является сущностью препроцессора, а не компилятора. То есть в языке C нет такого объекта: "заголовочный файл". Для языка C содержимое заголовчного файла - это часть содержимого файла с исходным файлом, как будто его просто взяли и подставили на место директивы include. По сути, для языка C есть только один файл - тот, который в данный момент компилируется. Про остальные файлы он ничего не знает!
Обсудим понятие условной компиляции. Так называется включение или исключение определенных частей кода во время компиляции. Частый пример использования: код, работающий с различными несовместимыми версиями чего-либо. Такому коду в зависимости от окружения, в которое он попал, надо компилировать ту или иные свои части. Часто такое бывает при написании программ, работающих и под Linux, и под Windows. Многие функции таких программ реализованы дважды: под Linux и под Windows. С помощью условной компиляции выбирается нужная версия кода.
Для обеспечения условной компиляции используется семейство директив #if. После неё пишется условие, код и #endif. Если условие верно, то код остается, а если неверно, то исключается. Самый простой пример: #ifndef. Она проверяет в качестве своего условия, что макрос (её аргумент) не объявлен. Для объявления макроса служит директива #define или опция gcc -D.
Посмотрим ещё раз на код заголовочного файла factorial.h:
// factorial.h #ifndef FACTORIAL_H_ #define FACTORIAL_H_ int factorial(int n); #endif
Все эти директивы служат для защиты от двойного включения этого заголовочного файла. При первом включении макрос FACTORIAL_H_ ещё не определен и последующий код включается, в том числе выполняется #define, который определеяет макрос FACTORIAL_H_. Если вдруг этот заголовочный файл включат повторно в том же файле с исходным кодом (это часто случается, когда один заголовочный файл включает другой заголовочный файл...), то #ifndef "увидит", что макрос FACTORIAL_H_ уже определён и пропустит оставшуюся часть файла.
Более простой способ добиться того же: #pragma once. Говорят, что он не везде работает. В любом случае, в ваших заголовочных файлах должна быть одна из защит от множественного подключения: или #ifndef или #pragma once
Комментарии в сишных исходниках
Однострочные комментарии пишутся после //
Многострочные комментарии пишутся между /* и */
Пример:
// comment /* comment comment comment */
Домашнее задание
Установить компилятор и смежное ПО (см. выше). Создать файлы программы, считающей факториал: factorial.h, factorial.c, main.c. Собрать и запустить программу. Файлы и команды сборки см. выше.
- Создать репозиторий для своего учебного проекта на Github. Желательно создавать репозиторий в организации LuaAndC, чтобы все ваши репозитории были в одном месте. В README.md запишите цели и задачи вашего учебного проекта. Можете приступать к работе над проектом!
Убедиться в том, что вы есть в списке слушателей факультатива и ваши данные указаны верно. Если ваше имя отсутствует в списке, сделайте форк, допишите себя в список и сделайте пулл-реквест.
- Добавьте свой учебный проект в список. Процедура такая же: форк + пулл-реквест.
(*) Подтвердить членство в гитхабовской организации LuaAndC (кто ещё не). Дополнительный бонус, который даёт членство в группе - возможность напрямую push'ить в репозитории организации (кроме административных, в т.ч. LuaAndC), минуя pull-request'ы.
- Выучите значения понятий, которые обсуждались на этом занятии (выделены жирным на этой странице)
Выполните домашнее задание с1 на http://kodomoquiz.tk
Задания "на подумать"
- Выше обсуждали типизацию языка C. А какая типизация в Lua?
- Выше было сказано, что в языке C нет отдельной сущности "модуль" или "файл исходного кода", так как по сути язык C всегда имеет дело только с одним файлом исходного кода. А что с модулями в Lua? Являются ли они сущностями языка или представлены в виде других сущностей?
- Выше заходила речь о статических и динамических библиотеках. В чем преимущества и недостатки обоих подходов?
Задания на поиск информации
- В Lua тоже есть понятие "компиляция", но на выходе получается не код для процессора (машинный код), а код для самого же Lua (байт-код). Для компиляции служит команда luac. Разберитесь, как с её помощью получить файлы с байт-кодом из Lua-файлов. Как исполнять полученные файлы? Представляется ли возможным восстановить из них исходные файлы Lua?
- Мы обсудили, как работает #ifndef. Найдите другие директивы семейства #if (условная компиляция) и приведите примеры их использования. Какие директивы вы бы использовали, если нужно поддерживать три реализации одного кода (к примеру, варианты для Windows, для Linux и для Mac)?
Напоминаю, что решения надо выкладывать на гитхаб. Задания "на подумать" и на поиск информации можно выполнить в форме эссе, в формате TXT или MarkDown.