11.02 Препроцессор
Классическая схема компиляции
- Трансляция: текст на ЯП → объектный файл (результат трансляции — машинный код — и ссылки на требуемые внешние функции/переменные)
- Компоновка: объектные файлы (в т. ч. взятые из библиотек) → исполняемый файл
Си:
- Препроцессор
- Трансляция в язык Ассемблера
- Трансляция в объектный код
- Компоновка
Вычисления периода компиляции
int main(void) {
int two = 13 + 14;
}gcc -E …
gcc -S -o- …
- 13 + 14 превращается в 27
gcc -C … (объектный файл ….o)
Атомарные макроопределения в роли констант
Борьба с «магическими числами»:
Вычисления периода компиляции (например, char message[12+1]) — не имеют отношения к препроцессору
Макроподстановка — имеет (gcc -E …)
Написать программу, которая вводит 7 чисел, умножает каждое на 7 и выводит; и это разные 7, определяемые двумя #define-ами (скажем, SIZE и MULT)
Условная компиляция
∃? макрос: cc -E …; cc -DVERBOSE -E …
cc/cc -DVERBOSE
Модифицировать предыдущее упражнение так, чтобы обе константы можно было задавать (или не задавать) через -D cc -DMULT=100 решение.c
общий вид #if…
1 #define ABCD 2 2 #include <stdio.h> 3 4 int main(void) 5 { 6 7 #ifdef ABCD 8 printf("1: yes\n"); 9 #else 10 printf("1: no\n"); 11 #endif 12 13 #ifndef ABCD 14 printf("2: no1\n"); 15 #elif ABCD == 2 16 printf("2: yes\n"); 17 #else 18 printf("2: no2\n"); 19 #endif 20 21 #if !defined(DCBA) && (ABCD < 2 * 4 - 3) 22 printf("3: yes\n"); 23 #endif 24 25 // C23 directives #elifdef/#elifndef 26 #ifdef CPU 27 printf("4: no1\n"); 28 #elifdef GPU 29 printf("4: no2\n"); 30 #elifndef RAM 31 printf("4: yes\n"); // selected in C23 mode, may be selected in pre-C23 mode 32 #else 33 printf("4: no3\n"); // may be selected in pre-C23 mode 34 #endif 35 }
Встроенные макросы
Используются для кроссплатформенной условной компиляции
$ touch пустой_файл.h $ gcc -E -dM пустой_файл.h
Модифицировать с помощью #if предыдущее упражнение так, чтобы вводилось не больше 10 чисел Определить новую константу для границы цикла — 10 или SIZE в зависимости от значения SIZE
Параметрические макросы
Есть два способа source code reuse:
- На уровне кода — функции (скомпилированная именованная часть кода + специальная инструкция «вызова»)
- На уровне текста — макросы (один фрагмент текста заменяется другим перед компиляцией)
На что из начала семестра похожи макросы?
Написать макрос sign(x), который раскрывается в тернарное выражение, равное 1, если x ⩾ 0, иначе -1 - Плохой пример!
Написать пример использования, где этот макрос работает возведением в куб, и пример, где он не работает! Улучшим: #define cube(x) (x)*(x)*(x)
Всё ещё можно понять неправильно! Как?
Ещё улучшим #define cube(x) ((x)*(x)*(x))
Написать макрос printel(указатель, индекс), который выводит элемент целочисленного массива
Как можно обмануть этот макрос, если не ставить индекс в круглые скобки?
Если успеем
Макрос — не обязательно одно выражение, это может быть любая строка (несколько операторов, например)
Пример из лекции: чем плох такой макрос? подсказка: тем, что это блочный оператор вместо атомарного
Переписать этот макрос по аналогии из лекции с помощью do … while(0). Написать программу, в которой проверяется, что макрос можно использовать в условном операторе if (…) swap(…); else …;
Д/З (предварительное)
TODO Предполагается, что в некоторых задачах надо будет писать только макроопределения. Во время тестирования к файлу с решением будет приписан (сначала или в конце) текст функции main() с макроподстановками, и полученный генерат уже пойдёт на компиляцию и тестирование. Такие задачи будут начинаться словами «Написать макроопределение …». В этих задачах разрешается определять функции, а в макрос класть вызов этих функций, если так удобнее.
Написать макроопределения SCREEN(имя, ширина, высота), DOT(x, y, c) и DISPLAY(имя). SCREEN(имя, ширина, высота) задаёт «текстовый экран» — двумерный массив имя типа char (заполненный точками). DOT(x, y, c) помещает на «экран» по координатам x, y символ c (координаты начинаются с 0 в левом нижнем углу). DISPLAY(имя) выводит экран построчно.
Подсказка. В DOT(x, y) как бы не хватает параметров «ширина» и «высота». Откуда их взять? Поскольку мы заводим локальный двумерный массив, его sizeof — это его площадь, а sizeof его нулевой строки — ширина.
Написать макроопределения INIT(имя, количество), INPUT(имя) и FREE(имя). INIT(имя, количество) заводит локальный массив строк имя на количество элементов (изначально содержащий NULL-ы); INPUT(имя) вводит не более количества строк, а FREE(имя) — освобождает память, занятую этими строками. Тестирующая программа при этом можем выглядеть так:
Подсказка. В INPUT(имя) как бы не хватает параметра «количество»? Откуда его взять? Поскольку мы заводим ''локальный массив'' строк (а не char **), от него можно брать sizeof (не забываем, что это количество байтов, а не количество элементов типа char *`)
Подсказка 2. В free() можно передавать NULL
Написать программу, при компиляции которой будет задан макрос TYPENUM — чиcло от 1 до 4. 1 соответствует типу int, 2 — char, 3 — double и 4 — float. Программа должна будет ввести два значения соответствующего типа, сложить их и вывести результат. char вводится/выводится с помощью "%c"
Написать программу, при компиляции которой будет задан макрос FORMULA(x) — некоторое выражение над переменной x типа double. Написать программу, которая в цикле вводит число y и выводит double x такое, что y == FORMULA(x). Гарантируется, что FORMULA(x) — монотонно возрастающая функция от x (например, x²). Конец ввода — EOF.
Указание 1. Если взять a<x и b>x, то для монотонной функции f() годится метод половинного деления: если f((a+b)/2) меньше y, то сделать a равным (a+b)/2, иначе b, и так до равенства
Указание 2. Для вещественных чисел не работает a==b, но функция nextafter() покажет, что между a и b никаких других чисел уже нет.
Указание 3. В float.h есть константа DBL_MAX — максимальное хранимое число типа double (минимальное — -DBL_MAX). Вы не поверите, но половинное деление сходится очень быстро.
