Kodomo

Пользователь

Общие сведения о языке C. Этапы сборки кода на языке C, используемые утилиты. Препроцессор. Заголовочные файлы. Комментарии

План занятия

Разбор домашних заданий

Число замыканий

Сколько замыканий создаёт этот код?

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:

Ещё раз о чистых функциях

В тесте 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

Ликбез по типизации в языках программирования

Типизированные языки программирования разделяют на следующие категории:

  1. Статическая / динамическая типизация. Статическая определяется тем, что конечные типы переменных и функций устанавливаются на этапе компиляции. Т.е. уже компилятор на 100% уверен, какой тип где находится. В динамической типизации все типы выясняются уже во время выполнения программы.

    • Примеры:
    • Статическая: C, Java, C#;
    • Динамическая: Python, JavaScript, Ruby.

  2. Сильная / слабая типизация (также иногда говорят строгая / нестрогая). Сильная типизация выделяется тем, что язык не позволяет смешивать в выражениях различные типы и не выполняет автоматические неявные преобразования, например нельзя вычесть из строки множество. Языки со слабой типизацией выполняют множество неявных преобразований автоматически, даже если может произойти потеря точности или преобразование неоднозначно.

    • Примеры:
    • Сильная: Java, Python, Haskell, Lisp;
    • Слабая: C, JavaScript, Visual Basic, PHP.

  3. Явная / неявная типизация. Явно-типизированные языки отличаются тем, что тип новых переменных / функций / их аргументов нужно задавать явно. Соответственно языки с неявной типизацией перекладывают эту задачу на компилятор / интерпретатор.

    • Примеры:
    • Явная: 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, оригинал

$ 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

$ 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 path >> ~/.bashrc

После этого перезапустите терминал с Cygwin. Данный шаг нужен, чтобы lua видел пакеты, установленные через luarocks.

$ 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 произвёл препроцессинг, компиляцию и компоновку.

Давайте скомпилируем ту же программу, разделяя этапы компиляции и компоновки:

Теперь посмотрим на препрцессированный вариант файла 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 */

Домашнее задание

Задания "на подумать"

Задания на поиск информации

Напоминаю, что решения надо выкладывать на гитхаб. Задания "на подумать" и на поиск информации можно выполнить в форме эссе, в формате TXT или MarkDown.