Директивы препроцессора define error include

В языке программирования C++ препроцессор – это часть компилятора, управляющая формированием исходного кода в объектный. Препроцессор C++ унаследован из языка программирования C. Препроцессор имеет набор команд, называемых директивами препроцессора. С помощью этих директив препроцессор управляет изменениями в трансляции исходного кода в объектный.

Препроцессор. Общие сведения. Директивы препроцессора. Обзор.
Директивы #define, #error, #include, #undef, #import, #using, #pragma, #line

1. Препроцессор. Назначение. Директивы

В языке программирования C++ препроцессор – это часть компилятора, управляющая формированием исходного кода в объектный. Препроцессор C++ унаследован из языка программирования C. Препроцессор имеет набор команд, называемых директивами препроцессора. С помощью этих директив препроцессор управляет изменениями в трансляции исходного кода в объектный.

Любая директива препроцессора начинается с символа #. В C++ препроцессор содержит следующие директивы:

  • #define;
  • #if;
  • #endif;
  • #undef;
  • #error;
  • #else;
  • #ifdef;
  • #line;
  • #include;
  • #elif;
  • #ifndef;
  • #pragma;
  • #using;
  • #line;
  • # або NULL.
2. Директива #define. Определение имени макроса

Директива #define определяет идентификатор и последовательность символов, которые будут заменять этот идентификатор в программе. Более подробно о директиве #define и примерах ее использования можно прочитать здесь.

Общий вид использования директивы следующий

#define macro_name character_sequence

здесь

  • macro_name – имя, которое будет использовано в тексте программы;
  • character_sequence – последовательность символов, которая будет заменять имя macro_name каждый раз, когда оно встретится в программе.

Пример.

#include <iostream>
using namespace std;

// Возвести x в степень 4
#define ABC(x) x*x*x*x

// Вычислить длину линии по двум точкам
#define LENGTH_LINE(x1, y1, x2, y2) std::sqrt((x1-x2)*(x1-x2) + (y1-y2)*(y1-y2))

void main()
{
  // 1. Использовать макрос ABC
  int t = 3;
  int x = ABC(t); // x = 81
  cout << "x = " << x << endl;

  // 2. Использовать макрос LENGTH_LINE
  double x1, y1, x2, y2;
  x1 = 3.8;
  y1 = 2.7;
  x2 = -1.4;
  y2 = 5.5;
  double len = LENGTH_LINE(x1, y1, x2, y2);
  cout << "len = " << len << endl;
}
3. Директива #error. Отображение сообщения об ошибке

Использование директивы #error заставляет компилятор приостановить компиляцию. Эта директива используется для отладки программы.

Общая форма использования директивы #error имеет вид

#error error_message

где

  • error_message – текст сообщения об ошибке.

Пример.

В примере компиляция программы будет выполняться до директивы #error. Далее компиляция программы остановится и выдаст сообщение об ошибке

#error: Compilation error

Таким образом *.exe-файл создан не будет.

#include <iostream>
using namespace std;

void main()
{
  // Выполнение вычислений
  double a = 3, b = 10, c;

  c = a * b + 5;

  // Вывести сообщение об ошибке
#error Compilation error.
}
4. Директива #include. Включение заголовка или иного исходного файла

С помощью директивы #include можно подключать к исходному тексту текущего файла другие внешние файлы. Текст этих внешних файлов будет использоваться при формировании исполнительного модуля исходного файла.

В свою очередь, включаемые файлы в своем коде также могут содержать директивы #include. Эти директивы называются вложенными. Стандарт языка C++ допускает до 256 уровней вложения.

В наиболее общем случае подключение файла с именем filename.h может производиться одним из двух способов:

#include "filename.h"

или

#include <filename.h>

Наличие кавычек «» или угловых ограничителей <> определяет способ поиска файла для его подключения.
При первом способе («filename.h») поиск файла производится в каталоге с текущей программой (рабочем каталоге). Если файл не найден, используется второй способ поиска файла (с помощью ограничителей <>).
При втором способе (ограничителе <>) поиск файла осуществляется в каталоге, указанном компилятором. Как правило, это каталог со всеми заголовочными файлами под названием INCLUDE.

При подключении файлов собственно разработанные библиотеки файлов включаются в двойные кавычки «», а файлы стандартной библиотеки включаются в ограничители <>.

При проектировании программы, размещаемой в одном файле, хорошей практикой считается разбивка программного кода на два файла:

  • файл с расширением *.h. Здесь содержатся объявления (только объявления) функций, которые предоставляются в общий доступ (доступны для клиента);
  • файл с расширением *.cpp. Здесь содержатся реализации всех разработанных функций. Сюда могут входить не только функции, предоставленные для общего доступа, но и любые другие дополнительные внутренние функции проекта.

Пример.

// Подключение стандартных библиотек time.h, iostream
#include <time.h>
#include <iostream>

// Подключение пространства имен
using namespace std;

// Подключение пользовательского файла my_file.h
#include "my_file.h"

void main()
{

}
7. Директива #undef

Директива #undef используется для удаления имени макроса, определенного директивой #define. После использования директивы, имя удаляемого макроса становится неопределенным.

Общая форма директивы #undef следующая

#undef macros_name

здесь

  • macros_name – имя макроса, которое нужно удалить (сделать неопределенным).

Пример.

#include <iostream>
using namespace std;

// Директива #undef

// Макрос, умножающий два числа между собой
#define Mult2(x, y) (x * y)

// Макрос, додающий два числа
#define Add2(x, y) (x + y)

void main()
{
  // Проверка, существует ли имя Mult2
#ifdef Mult2
  double res = Mult2(2.5, 7.8);
  cout << "2.5 * 7.8 = " << res << endl;
#endif

  // Отменить имя Add2 - директива #undef
#undef Add2

// Проверка, существует ли имя Add2
#ifdef Add2
  int a = 8, b = 9;
  int resAdd = Add2(a, b);
  cout << "a + b = " << resAdd << endl;
#else
  cout << "The Add2 name is undefined." << endl;
#endif
}

Результат

2.5 * 7.8 = 19.5
The Add2 name is undefined.
5. Директива #import. Включение сведений из библиотеки типов

С помощью директивы #import можно включать сведения из библиотеки типов (TLB – type library). В данном контексте библиотека типов представляет собой иерархическое хранилище информации о возможностях Active-X сервера, которая хранится как файл с расширением *.tlb или *.olb.

Общая форма объявления директивы #import может быть следующей

#import "filename" [attributes]
#import <filename> [attributes]

здесь

  • filename – имя файла, содержащее библиотеку типов. Это может быть файл с расширением *.tlb, *.olb, *.dll или *.exe. Это может быть также любой другой формат файла, понятный для функции API LoadTypeLib();
  • attributes – атрибуты директивы. Эти атрибуты указывают на то, что компилятор изменяет содержимое заголовка библиотеки типов. Более подробную информацию об атрибутах #import можно найти в документации на сайте learn.microsoft.com.

Пример.

#import "MyTypeLib"
6. Директива #using. Включение *.dll, *.obj, *.exe, *.net module файлов

Директива #using выполняет импорт метаданных в программу, которая была скомпилирована с параметром /clr. Это означает, что использование директивы возможно только в режиме C++/CLI.

Общая форма использования директивы #using следующая

#using filename [as_friend]

здесь

  • filename – имя файла с расширением *.dll, *.exe, *.net module или *.obj.

К примеру, чтобы подключить динамическую библиотеку с именем «MyLibrary.dll» нужно выполнить такой код

#using <MyLibrary.dll>
7. Директива #pragma. Задание компилятору функций выполнения

Директива #pragma позволяет задавать функции для их выполнения компилятором. Использование тех или иных функций зависит от операционной системы и текущего компьютера, который называют хост-компьютером. Характеристики функций компилятора определяются при установке компилятора C/C++ на компьютер. С помощью директивы #pragma можно предлагать компилятору различные функции для их обработки с обеспечением полной совместимости с языками C или C++.

Директива #pragma имеет множество особенностей использования и несколько реализаций. Общая форма одной из распространенных реализаций следующая

#pragma token_string

здесь

  • token_string – так называемая строка токена, которая может быть разных значений. Эта строка представляет собой набор символов, определяющих набор инструкций и их аргументов. К токенам относятся, например, once, alloc_text, component, auto_inline, endregion, loop и т.д.

Подробное рассмотрение особенностей реализации директивы в сочетании с тем или иным токеном не является предметом данной темы.

Пример.

В примере указано указание компилятору включить текущий файл заголовка только один раз в случае, когда будет происходить компиляция файла с исходным кодом.
К примеру, файл заголовка – это файл с именем «MyClass.h», файл с исходным кодом – это файл «MyClass.cpp».

// Файл "MyClass.h"
#pragma once
8. Директива #line. Установить строку на название файла в сообщениях об ошибке

Использование директивы #line дает указание компилятору задать (изменить) номера строки и название файла, которые выводятся в сообщениях об ошибке.
При возникновении ошибки компилятор запоминает номер строки ошибки и название файла соответственно в макросах __LINE__ и __FILE__. После вызова директивы #line значение этих макросов изменяется на заданные в этой директиве.
Директива #line может быть использована в одной из двух возможных общих форм:

#line digit_sequence

или

#line digit_sequence [filename]

где

  • digit_sequence – целочисленная константа в диапазоне от 0 до 2147483647 включительно. Эта константа задает номер строки и присваивается макросу __LINE__ после вызова директивы;
  • filename – необязательный параметр, являющийся именем файла, который выводится в сообщении об ошибке и записывается в макросе __FILE__. Если filename опущен, то предыдущее имя файла остается без изменений.

Директива #line обычно используется генераторами программ. В этих программах-генераторах на основе выполнения (не выполнения) некоторого утверждения нужно задавать текст сообщения об ошибке и ссылаться на соответствующий исходный файл.

Пример.

#include <iostream>
using namespace std;

int main()
{
  // Вывести значения макросов __LINE__ и __FILE__
  cout << "The value of __LINE__: " << __LINE__ << endl;
  cout << "The value of __FILE__: " << __FILE__ << endl;

  // Установить новое значение макроса __LINE__
#line 12
  cout << "The value of __LINE__ after changes: " << __LINE__ << endl;
  cout << "The value of __FILE__ after changes: " << __FILE__ << endl;

  // Установить новое значение макросов __LINE__ и __FILE__
#line 122 "source2.cpp"
  cout << "The value of __LINE__ after changes 2: " << __LINE__ << endl;
  cout << "The value of __FILE__ after changes 2: " << __FILE__ << endl;
}

Результат

The value of __LINE__: 7
The value of __FILE__: D:ProgramsC++Project10Source.cpp
The value of __LINE__ after changes: 12
The value of __FILE__ after changes: D:ProgramsC++Project10Source.cpp
The value of __LINE__ after changes 2: 122
The value of __FILE__ after changes 2: D:ProgramsC++Project10source2.cpp


Директивы препроцессора. Макроопределения.

Препроцессор

Препроцессор — это программа, которая преобразует исходный код в код понятный компилятору. В языке С/С++ препроцессор удаляет комментарии, преобразует код в соответствии с макросами и выполняет директивы препроцессора.

Директивы препроцессора

Директива препроцессора include

Директива препроцессора include тупо копирует содержимое файла в место, где была прописана эта директива.

a.h

a.c до препроцессинга

#include "a.h"

int g() {
    return f();
}

a.c после препроцессинга

int f() {
    return 1;
}

int g() {
    return f();
}

Директива препроцессора define и макросы

Директива препроцессора define создает макрос, который представляет из себя правило конвертации идентификатора в указанный токен.

Макросы бывают двух типов: макросы-объекты и макросы-функции

Макросы-объекты

Макросы-объекты можно создать двумя способами:

#define <идентификатор>
#define <идентификатор> <заменяющий текст>

Обратите внимание: макросы не заканчиваются точкой с запятой, так как это директивы препроцессора, а не выражения языка Си.

Макросы с заменяющим текстом

Если присутствует макрос с заменяющим текстом, то любое появление идентификатора заменяется заменяющим текстом.

Пример 1:

a.c до препроцессинга

#define PI 3.141592

double circle_area(double r) {
    return PI * r * r;
}

a.c до препроцессинга

double circle_area(double r) {
    return 3.141592 * r * r;
}

Пример 2:
a.c до препроцессинга

#define CINT_PTR const int *

void f (CINT_PTR p);

a.c после препроцессинга

Макросы без заменяющего текста

Макросы без заменяющего текста работают так же как и макросы с заменяющим текстом:
когда препроцессор встречает идентификтор, то он заменит его НИЧЕМ. Может показаться, что такие макросы бесполезны, но их основная задача заключается в другом. Зачем? Будет понятно позже (спойлер: htader-guard и условная компиляция).

Макросы-функции

Макросы-функции ведут себя так же как функции и используются как функции))) Они имеют следующий вид:

#define <идентификатор>(аргумент1, аргумент2, ...) <тело>

Вообще, считается, что использование макрос-функций — это плохой тон, так как их использование может быть опасным и код с кучей макрос-функций сложнее дебажить.

Пример:

a.c до препроцессинга

#define PI 3.141592
#define AREA(R) PI * R * R

int f() {
    return AREA(3);
}

a.c после препроцессинга

int f() {
    return 3.141592 * 3 * 3;
}

Если макрос очень большой, то можно сделать его в несколько строк, но придется каждую строчку заменять символом ««:

#define SUM (A, B) do {
    return A + B;
} while(0)

int sum(int a, int b) {
    SUM(a, b);
}

Директива undef

Директива препроцессора undef удаляет макрос, созданный до этого с помощью директивы define

Директивы препроцессорa if, ifdef и прочие

Директива препроцессора if работает так: если выражение стоящее сразу после директивы if имеет ненулевое значение, то группа строк до директивы endif сохраняется препроцессором, иначе строчки между if и endif сохранены не будут, и в код скопмилированной программы их не будет.

Пример:

a.c до препроцессинга

#define DEBUG 1

int f(int x) {
#if DEBUG > 0
    printf("%i", x);
#endif    
    return (x + 2) * 2;
}

a.c после препроцессинга

int f(int x) {
    printf("%i", x); 
    return (x + 2) * 2;
}

a.c после препроцессинга, если убрать строчку #define DEBUG 1

int f(int x) {
    return (x + 2) * 2;
}

Данный код можно упростить с помощью директив препроцессора ifdef, ifndef
Они проверяют, был и до этого определен макрос с таким идентификатором.

Пример:

a.c до препроцессинга

#define DEBUG
#define WIN64


int f(int x) {
#ifdef DEBUG
    printf("%i", x);
#endif

#ifdef WIN64    
    return win64func(x);
#endif

#ifdef WIN32
    return win32func(x);
#endif   
}

a.c после препроцессинга

int f(int x) {
    printf("%i", x); 
    return win64func(x);
}

Данный подход называется условной компиляцией. Хорошим примерном условной компиляции является такая штука, как header guard. Когда в проекте очень много заголовочных файлов, может случится такое, что один заголовочный файл будет подключен в один файл .c несколько раз, это приведет к дублированию определений.

Пример:

math.h

double circle_area(double r) {
    return PI * r * r;
}

geometry.h

#include "math.h"
double cylinder_volume(double r, double h) {
    return circle_area(r) * h;
}

a.c

#include <stdio.h>
#include "math.h"
#include "geometry.h"

void print_cylinder_data(double r, double h) {
    printf("Площадь основания: %lf", сircle_area(r));
    printf("Объем: %lf", cylinder_volume(r, h));
}

a.c после препроцессинга

#include <stdio.h>
double circle_area(double r) { // из math.h
    return PI * r * r;
}

double circle_area(double r) { // из geometry.h
    return PI * r * r;
}

double cylinder_volume(double r, double h) {
    return circle_area(r) * h;
}

void print_cylinder_data(double x) {
    printf("Площадь основания: %lf", сircle_area(x));
    printf("Объем: %lf", cylinder_volume(x));
}

Как можно заметить, функция circle_area была определена дважды, что привело к ошибки компиляции — компилятор не знает какую функцию из них надо юзать. Даную проблему можно решить с помощью header_guard:

math.h

#ifndef MATH_H
#define MATH_H
double circle_area(double r) {
    return PI * r * r;
}
#endif

geometry.h

#ifndef GEOMETRY_H
#define GEOMETRY_H
#include "math.h"
double cylinder_volume(double r, double h) {
    return circle_area(r) * h;
}
#endif

a.c

#include <stdio.h>
#include "math.h"
#include "geometry.h"

void print_cylinder_data(double r, double h) {
    printf("Площадь основания: %lf", сircle_area(r));
    printf("Объем: %lf", cylinder_volume(r, h));
}

a.c после препроцессинга

#include <stdio.h>
double circle_area(double r) { // из math.h
    return PI * r * r;
}

double cylinder_volume(double r, double h) { // из geometry.h
    return circle_area(r) * h;
}

void print_cylinder_data(double x) {
    printf("Площадь основания: %lf", сircle_area(x));
    printf("Объем: %lf", cylinder_volume(x));
}

Как это работает? Когда препроцессор подключает заголовочный файл он проверит: был ли определен идентификатор MATH_H. Если такой идентификатор препроцессору встретился впервые, то он определит этот идентификатор, а дальше вставит содержимой файла math.h. Если такой идентификатор ранее встречался препроцессору, то содержимое файла будет проигнорировано.

В настоящее время большинство компиляторов поддерживает альтернативу header guards — директиву #pragma once. Вместо того чтобы писать аж три строчки кода (!) можно написать только одну строчку #pragma once в начале заголовочного файла. Однако #pragma once не является официальной частью языка С и не все компиляторы ее поддерживают (хотя большинство поддерживает), поэтому чтобы сохранить переносимость вашего кода, лучше использовать header guards.

Директива препроцессора error

Директива препроцессора error выдает указанную ошибку компиляции и завершает компиляцию

Пример:

#ifndef SOME_IMPORTANT_MACRO
#error Need an important macro!
#endif

Аннотация: В лекции рассматриваются практически важные свойства препроцессора языка С и примеры типовых препроцессорных директив и конструкций.

Теоретическая часть

Препроцессор (англ. preprocessor ) – программа, выполняющая предварительную обработку входных данных для другой программы [19.1]. Препроцессор языка программирования С просматривает программу до компилятора и заменяет в программе определенные сочетания символов (символические аббревиатуры) на соответствующие директивы. Он отыскивает и подключает к программе необходимые файлы и может изменить условия компиляции [19.1]. Препроцессор имеет тот же смысл, что и буферный процессор.

Препроцессор языка С выполняет макроподстановку, условную компиляцию и включение именованных файлов. Строки, начинающиеся со знака # (перед которыми разрешены символы пустого пространства), задают препроцессору инструкции-директивы. Их синтаксис не зависит от остальной части языка; они могут фигурировать где угодно и оказывать влияние (независимо от области действия) вплоть до конца единицы трансляции. Границы строк принимаются во внимание: каждая строка анализируется отдельно (но есть возможность и сцеплять строки). Лексемами для препроцессора являются все лексемы языка и последовательность символов, задающие имена файлов. Кроме того, любой символ, не определенный каким-либо другим способом, также воспринимается как лексема [19.2]. Влияние символов пустого пространства, отличающихся от пробелов и горизонтальных табуляций, внутри строк препроцессора не определено.

В предыдущих лабораторных работах уже встречались строки с начальным символом #. Это #include и #define. Первая директива (инструкция) использовалась для подключения заголовочных файлов, в первую очередь из библиотеки языка С, а вторая – для подстановки символов или чисел в определенные места программного кода.

Имеются следующие директивы препроцессора:

#define #endif #ifdef #line
#elif #error #ifndef #pragma
#else #if #include #undef

Каждая директива препроцессора должна занимать отдельную строку. Например, строка

#include <stdio.h>    #include <stdlib.h>

рассматривается как недопустимая [19.3].

Директива #define

Директива #define определяет идентификатор и последовательность символов, которая будет подставляться вместо идентификатора каждый раз, когда он встретится в исходном файле. Идентификатор называется именем макроса, а сам процесс замены – макрозаменой (макрорасширением, макрогенерацией, макроподстановкой) [19.3]. В общем виде директива #define выглядит следующим образом (должно быть задано буквами латинского алфавита):

#define имя_макроса  последовательность_символов

В определении, как правило, в конце последовательности символов не ставится точки с запятой. Между идентификатором и последовательностью символов последовательность_символов может быть любое количество пробелов, но признаком конца последовательности символов может быть только разделитель строк [19.3].

Имена макросов обычно задаются с помощью букв верхнего регистра.

У директивы #define имя макроса может определяться с формальными параметрами. Тогда каждый раз, когда в программе встречается имя макроса, то используемые в его определении формальные параметры заменяются теми аргументами, которые встретились в программе. Такого рода макросы называются макросами с формальными параметрами (макроопределениями с параметрами и макросами, напоминающими функции ) [19.3]. Ключевым элементом макроса с параметрами являются круглые скобки, внутри которых находятся собственно формальные параметры. Рассмотрим пример макроса с тремя параметрами для определения следующего условия: будет ли остаток от деления случайной функции на правую границу интервала больше, чем половина этого интервала.

Программный код решения примера

#include <stdio.h>
#include <conio.h>
#include <stdlib.h>
#include <time.h>

#define MAX(a,b,c) ((1+rand()%(b)) > ((a)+(b))/2 ) ? (c):(b)

int main (void) {
	int a, b, c;
	srand((unsigned) time(NULL));

	printf("n Enter a, b, c: ");
	scanf_s("%d%d%d", &a, &b, &c);
	printf("n MAX(a,b,c): %dn", MAX(a,b,c));	

	printf("nn ... Press any key: ");
	_getch();
	return 0;
}

В общем случае рекомендуется заключать в круглые скобки формальные параметры, над которыми выполняются те или иные действия.

Использование вместо настоящих функций макросов с формальными параметрами (т. е. a, b, с ) дает следующее преимущество: увеличивается скорость выполнения кода, потому что в таких случаях не надо тратить ресурсы на вызов функций. Кроме того, макрос не чувствителен к типу данных, т. е. в нем отсутствует проверка типов аргументов. Однако если у макроса с формальными параметрами очень большие размеры, то тогда из-за дублирования кода увеличение скорости достигается за счет увеличения размеров программы [19.3]. И еще, в конструировании макроса следует быть очень внимательным. Как правило, макросы используются для небольших пользовательских функций [19.4].

Директива #error

Директива #error заставляет компилятор прекратить компиляцию [19.3]. Эта директива используется в основном для отладки. В общем виде директива #error выглядит следующим образом:

#error  сообщение – об – ошибке

Заданное сообщение об ошибке ( сообщение – об – ошибке ) в двойные кавычки не заключается. Когда встречается данная директива, то выводится сообщение об ошибке – возможно, вместе с другой информацией, определяемой компилятором [19.3].

Директива #include

Директива #include дает указание компилятору читать еще один исходный файл – в дополнение к тому файлу, в котором находится сама эта директива [19.3]. Имя исходного файла (подключаемого файла) должно быть заключено в двойные кавычки или в угловые скобки. Обычно имена стандартных заголовочных файлов заключают в угловые скобки. А использование кавычек обычно приберегается для имен специальных файлов, относящихся к конкретной программе. Способ поиска файла зависит от того, заключено ли его имя в двойные кавычки или же в угловые скобки. Если имя заключено в угловые скобки, то поиск файла проводится тем способом, который определен в компиляторе. Часто это означает поиск определенного каталога, специально предназначенного для хранения таких файлов. Если имя заключено в кавычки, то поиск файла проводится другим способом. Во многих компиляторах это означает поиск файла в текущем рабочем каталоге. Если же файл не найден, то поиск повторяется уже так, как будто имя файла заключено в угловые скобки [19.3].

Файлы, имена которых находятся в директивах #include, могут в свою очередь содержать другие директивы #include. Они называются вложенными директивами #include. Количество допустимых уровней вложенности у разных компиляторов может быть разным. Однако в стандарте С89 предусмотрено, что компиляторы должны допускать не менее 8 таких уровней [19.3].

Директивы условной компиляции

Директивы условной компиляции (будут рассмотрены ниже) дают возможность выборочно компилировать части исходного кода программы. Этот процесс называется условной компиляцией [19.3].

Директива условной компиляции #if выглядит следующим образом:

#if  константное_выражение
	последовательность операторов программного кода
#endif

Если находящееся за директивой #if константное выражение истинно, то компилируется код, который находится между этим выражением и #endif, которая обозначает конец блока #if. Константное выражение может быть задано через директиву #define. При этом, если, например, задано число, не равное нулю, то такое константное выражение будет истинно. Если же заданное число есть нуль, то константное выражение будет ложным. В частности константное выражение может быть задано макросом с формальными параметрами, которые должны быть в свою очередь также константными параметрами.

Директива условной компиляции #else используется практически также как в обычном условном операторе языка С: if – else. То есть логика действия позволяет перенаправить выполнение программы. Дополнительная директива условной компиляции #else в общем случае имеет вид

#if  константное_выражение
	последовательность операторов программного кода
#else
альтернативная последовательность операторов программного кода
#endif

Аналогично используются директивы #elif (else if), которые в общем случае имеют следующий вид:

#if  константное_выражение
	последовательность операторов программного кода
#elif  2_ константное_выражение
2_ я_последовательность операторов программного кода
#elif  3_ константное_выражение
3_ я_последовательность операторов программного кода
.
.
.
#elif  N_ константное_выражение
N_ я_последовательность операторов программного кода
#else
альтернативная последовательность операторов программного кода
#endif

Если константное выражение в директиве #elif истинно (не нулевое, например), то будет компилироваться соответствующая последовательность операторов программного кода. При этом другие выражения в директивах #elif проверяться уже не будут, в том числе и директива #else.

Особенностью рассмотренных конструкций является то, что проверка выражений осуществляется внутри директив #if и #endif.

В соответствии со стандартом С89 у директив #if и #elif может быть не менее 8 уровней вложенности. При вложенности каждая директива #endif, #else или #elif относится к ближайшей директиве #if или #elif [19.3].

Каждая директива #if сопровождается директивой #endif.

Директива условной компиляции #ifdef в общем виде выглядит следующим образом:

#ifdef  имя_макроса
последовательность операторов
#endif

Директива условной компиляции #ifdef означает «if defined» (если определено) [19.3]. Последовательность операторов будет компилироваться, если имя макроса было определено ранее с помощью директивы #define.

Директива условной компиляции #ifndef означает «if not defined» (если не определено) в общем виде выглядит следующим образом:

#ifndef  имя_макроса
последовательность операторов
#endif

Последовательность операторов будет компилироваться, если имя макроса еще не определено директивой #define. В директивах #ifdef и #ifndef можно использовать #else или #elif.

Согласно стандарту С89 допускается не менее 8 уровней #ifdef и #ifndef.

Директива #undef удаляет заданное определение имени макроса, то есть «аннулирует» его определение; само имя макроса должно находиться после директивы [19.3].

В общем случае директива #undef выглядит следующим образом:

Директива #undef используется в основном для того, чтобы локализовать имена макросов в тех участках кода, где они нужны.

Для того чтобы узнать определено ли имя макроса, можно использовать директиву #if в сочетании с оператором времени компиляции defined [19.3].

Оператор defined выглядит следующим образом:

Если имя_макроса определено, то выражение считается истинным; в противном случае – ложным.

Единственная причина, по которой используется оператор defined, состоит в том, что с его помощью в #elif можно узнать, определено ли имя макроса [19.3].

Директива #line

Директива #line изменяет содержимое __LINE__ и __FILE__, которые являются зарезервированными идентификаторами (макросами) в компиляторе. В первом из них содержится номер компилируемой в данный момент строки программного кода программы [19.3]. А второй идентификатор – это строка, содержащая имя компилируемого исходного файла.

Директива #line выглядит следующим образом:

В определении директивы #line обязательным является номер строки, относительно которой будет выполняться подсчет следующих строк. Второй параметр «имя_файла» является не обязательным. Если его не будет, то идентификатор __FILE__ будет содержать путь и имя программы. Если указать в качестве параметра новое имя файла – «имя_файла», то __FILE__ будет содержать это новое имя файла.

Директива #line в основном используется для отладки и специальных применений [19.3].

Операторы препроцессора # и ##

Операторы # и ## применяются в сочетании с директивой #define [19.3].
Оператор #, который обычно называют оператором превращения в строку (stringize), превращает аргумент, перед которым стоит, в строку, заключенную в кавычки. Оператор # должен использоваться в макросах с аргументами, поскольку операнд после # ссылается на аргумент макроса [19.5].

Оператор ##, который называют оператором склеивания (pasting), или конкатенации конкатенирует две лексемы. Операция ## должна иметь два операнда [19.5].

Операторы # и ## предусмотрены для работы препроцессора в некоторых особых случаях [19.3,19.5].

Директива #pragma

Директива #pragma – это определяемая реализацией директива, которая позволяет передавать компилятору различные инструкции [19.3]. Она позволяет помещать инструкции компилятору в исходный код [19.4]. Возможности этой директивы следует изучать по документации по компилятору.

Предопределенные символические константы

В языке С определены пять встроенных, предопределенных имен макрокоманд [19.2-19.3-19.4-19.5], которые представлены в табл. 19.1.

Таблица
19.1.

Предопределенные символические константы
Символическая
константа
Объяснение
__LINE__ Возвращает целую константу для номера текущей обрабатываемой строки исходного кода программы
__FILE__ По умолчанию возвращает в виде строки символов имя компилируемого исходного файла
__DATE__ Возвращает в виде строки символов дату (мм дд гг) начала компиляции текущего исходного файла
__TIME__ Возвращает в виде строки символов время (чч:мм:сс) начала компиляции текущего исходного файла
__STDC__ Возвращает целую константу 1, которая указывает на то, что данная реализация совместима со стандартом ANSI

Препроцессор входит в любой компилятор программ на Си++ и любую среду разработки, рассчитаную на этот язык. Препроцессор обрабатывает исходный код программ до их компиляции. Препроцессорные команды, или директивы, управляют работой препроцессора.

Таких команд немного, они все начинаются со знака решётки (#) и должны быть в начале строки исходного кода:

#define
эта директива предусматривает определение макросов или препроцессорных идентификаторов, простейшее применение это замены в тексте программы.
#include
позволяет включать текст других файлов в текст вашей программы.
#undef
отменяет действие директивы #define
#if
организация условной обработки директив.
#ifdef
организация условной обработки директив.
#else
организация условной обработки директив.
#endif
организация условной обработки директив.
#elif
организация условной обработки директив.
#line
управление нумерацией строк в тексте программы.
#error
задаёт текст диагностического сообщения, выводящиеся при наличии ошибок.
#pragma
зависит от среды разработки.
#
нулевая, или пустая, директива, бездейственно пропускается.

Директива #define[править]

Директива #define служит для замены часто использующихся констант, ключевых слов, операторов или выражений некоторыми идентификаторами. Идентификаторы, заменяющие текстовые или числовые константы, называют именованными константами. Идентификаторы, заменяющие фрагменты программ, называют макроопределениями, причём макроопределения могут иметь аргументы.

Основная форма синтаксиса директивы #define:

#define  идентификатор текст

Так например, в программе

#define N 5
int main()
{
    int a;
    a = N;
    return 0;
}

Переменная а примет значение 5.

Директива #include[править]

#include «спецификация-пути»

#include <спецификация-пути>


Директива #include добавляет содержимое заданного файла в другой файл. Можно организовать определения констант и макро в отдельном файле, а затем вставить его директивой #include в любой другой файл. Вставка файлов также очень удобна для объединения объявлений внешних переменных и сложных типов данных. Нужно определить и задать имена этих типов только один раз в созданный для этих целей файл.

Директива #include информирует препроцессор о том, что содержание файла с заданным именем следует обрабатывать так, как будто оно присутствует в исходной программе в месте расположения этой директивы. Новый текст также может содержать директивы препроцессора. Препроцессор выполняет директивы в новом тексте, а затем продолжает обработку текста исходного файла.

Спецификация пути это имя файла, которому может предшествовать имя каталога. Это должно быть имя существующего файла. Синтаксис спецификации файла зависит от операционной системы, в которой компилируется программа.

При поиске файлов препроцессор использует концепцию «стандартного» каталога. Расположение стандартных каталогов для файлов зависит от реализации и операционной системы. Определение стандартного каталога можно найти в руководстве по компилятору.

Препроцессор останавливает поиск сразу же после обнаружения файла с заданным именем. Если задать полную спецификацию файла, заключенную в двойные кавычки (" "), то препроцессор использует её для поиска и игнорирует стандартный каталог.

Если заключенная в двойные кавычки спецификация файла является неполной, то препроцессор сначала ищет каталог «родительского» файла. Родительский файл это файл, содержащий директиву #include. Например, если файл f2 вставляется в файл f1, то f1 будет родительским файлом.

Вставка файлов может быть вложенной. Т. е. директива #include может появляться в файле, который сам вставляется директивой #include. Файл f2 может вызывать файл f3. В этом случае f1 все еще будет родительским для f2, но «дедушкой» для f3.

При вложенной вставке файлов поиск каталогов начинается с родительского файла, затем проходит по дедушкиным файлам. Следовательно, поиск начинается в каталоге, который содержит обрабатываемый исходный файл. Если файл не найден, то поиск продолжается в каталогах, заданных в командной строке компилятора. И, наконец, производится поиск в стандартном каталоге.

Если спецификация файла заключена в угловые скобки (< >), то препроцессор не проводит поиска в текущем рабочем каталоге. Поиск файла начинается в каталогах, заданных в командной строке компилятора, а затем в стандартном каталоге.

Допускается вложение вставки файлов до 10 уровней. При обработке вложенных #include препроцессор всегда будет осуществлять вставку в первоначальный исходный файл.

Директива #undef[править]

Директива #undef удаляет текущее определение идентификатора. Поэтому все встречающиеся появления идентификатора будут игнорироваться предпроцессором. Для удаления определения макро с использованием #undef, нужно задать только идентификатор макро, не задавая список параметров.

Можно применить директиву #undef к идентификатору, у которого нет определения. Тем самым пользователь получает дополнительную гарантию того, что данный идентификатор не определён.

Директива #undef обычно используется в паре с директивой #define для задания области исходной программы, в которой идентификатор имеет специальное значение. Например, некоторая функция исходной программы может иметь объявленные константы, которые задают значения среды работы, которые не влияют на остальную часть программы. Директива #undef также работает с директивой #if для управления условной компиляцией исходной программы.

Условные директивы #if, #ifdef, #else, #endif, #elif[править]

Эти директивы позволяют подавить компиляцию части исходного файла, проверяя постоянное выражение или идентификатор. Результат проверки определяет, какие блоки текста будут переданы в компилятор и какие блоки текста будут удалены из исходного файла при предпроцессорной обработке.

Условная компиляция[править]

Директива #line[править]

Изменяет внутренний номер строки и имя файла компилятора. Если имя файла опущено, оно остается прежним. Cинтаксис директивы:

#line константа <"имя_файла">

к примеру #line 1000 "file.сpp" устанавливается имя исходного файла file.сpp и текущий номер строки 1000.
Текущий номер строки и имя файла доступны через константы препроцессора __LINE__ и __FILE__.

Директива #error[править]

Директива #error создаёт заданное пользователем сообщение об ошибке во время компиляции, а затем завершает компиляцию.

Синтаксис:

#errortoken-string

Примечание: Сообщение об ошибке, создаваемое этой директивой, содержит параметр token-string. Параметр token-string не подлежит расширению макроса. Эта директива наиболее полезна в ходе предварительной обработки и позволяет уведомлять разработчика о противоречиях в программе или о нарушении ограничений. В следующем примере демонстрируется обработка ошибки во время предварительной обработки.

Пример использования:

#if !defined(__cplusplus)
#error C++ compiler required.
#endif

Директива #pragma[править]

#pragma это инструкция компилятору, которая определяется реализацией.
Конструкция #pragma в языке Си/Си++ используется для задания дополнительных указаний компилятору. С помощью этих конструкций можно указать как осуществлять выравнивание данных в структурах, запретить выдавать определённые предупреждения и так далее.

Замены в тексте[править]

Помогите написанию данной статьи

Макросы[править]

Предопределенные макроимена[править]

ВНИМАНИЕ! Вопросы по существу обсуждаемого вопроса просьба задавать здесь или создать тему на форуме и кинуть на неё ссылку в блог или мне в личку.

Объясняю почему

Причин для этого несколько.

Я, как и любой другой автор, всегда могу упустить интересный момент обсуждаемой темы (что подтвердилось на практике). А потому задаваемый вопрос может закрывать пробел в статье. Ответ на конкретный вопрос, как правило, дать несложно. Сложнее его аккуратно сформулировать так, чтобы ответ являлся законченной частью статьи. Поэтому, как правило, на первых порах я ограничиваюсь конкретным ответом на конкретный вопрос, а в статью временно вставляю ссылку на пост, где был дан ответ. А когда дойдут руки, то вместо ссылки пишу нормальное пояснение. Технические возможности блога не позволяют в комментариях пользоваться широкими возможностями, доступными на форуме (то как выделение текста жирным, вставка фрагментов исходников в удобном для чтения виде и т.п.), поэтому будет удобнее, если вопрос и ответ будут опубликованы на форуме

Любая статья является изложением знаний в общем случае. У многих людей мышление устроено так, что прочтя на форуме конкретный вопрос и конкретный ответ на этот вопрос, у них появится бОльшее понимание, чем после прочтения теоретических выкладок (даже если они подкреплены конкретными примерами). Ссылки на такие обсуждения я, как правило, включаю в последний раздел статьи.

Начинающие, как правило, поиск ответов на свои вопросы ведут именно в форуме, а не в блогах. А потому конкретный вопрос и конкретный ответ для них будет более удобным и полезным именно на форуме. Многие люди умеют работать методом тыка, лишь бы был конкретный пример в качестве образца. А потому такое обсуждение будет им полезным даже без прочтения статьи

Исторически сложилось, что раньше (когда ещё не было блога) статьи располагались на форуме и представлены были в виде двух тем. Первая тема создавалась в специально отведённой свалке и представляла собой черновик, который со временем дорабатывался до законченной статьи. После этого статья переезжала во вторую тему в тематическом разделе. А первая тема оставалась дополнительной свалкой для замечаний и мелких вопросов по теме. Ссылку на старое местоположение данной свалки я помещаю в начале статьи. Вопросы, по возможности, прошу создавать в отдельных темах, но если вопрос действительно мелкий, то можно его задать и в указанной свалке.


  • 1. Что такое препроцессор
    1.1. Общие сведения
    1.2. Как посмотреть результат работы препроцессирования
    1.2.1 Компилятор gcc (он же mingw под windows)
    1.2.2 Microsoft Visual C
    1.2.3 Borland Builder
  • 2. Директива #include
    2.1. Общие сведения
    2.2. Различия между угловыми скобками и кавычками
    2.3. Где компилятор ищет файлы для подключения через #inlcude
  • 3. Директивы #define и #undef
    3.1. Директива #define
    3.2. Директива #undef
    3.3. Директива #define с параметрами
    3.3.1. Общие сведения
    3.3.2. Злосчастный пробел
    3.3.3. Макрос — это совсем не функция
    3.3.4. Оператор # для параметров макроса
    3.3.5. Оператор ## в теле макроса
    3.3.6. Директива #define с переменным числом параметров
    3.3.7. Круглые скобки или запятая среди параметров макроса
    3.4. Директива #define, растянутая на несколько строк
    3.5. Использование в директиве #define имён других макросов
  • 4. Директивы условной компиляции
    4.1. Общие сведения об условной компиляции
    4.2. Директивы #if, #else, #elif, #endif
    4.3. Директивы #ifdef, #ifndef
  • 5. Директивы #error и #warning
    5.1. Общие сведения
    5.2. Директива #error
    5.3. Директива #warning
  • 6. Директива #line
  • 7. Директивы, но не относящиеся к препроцессору (#pragma, #import и т.п.)
  • 8. Примеры использования препроцессорных директив на практике
  • 9. Когда следует использовать макросы, а когда конструкции языка программирования
  • 10. Ссылки на темы, где обсуждались подобные вопросы

1. Что такое препроцессор

1.1. Общие сведения

Во всех компиляторах с языков C/C++ есть некая фаза, называемая препроцессированием. Фаза эта запускается автоматически и по большому счёту является прозрачной для пользователя (программиста). Т.е. пользователь в большинстве случаев препроцессор самостоятельно НЕ запускает.

Препроцессирование — это процесс, на вход которого подаётся текст (текстовый файл) и на выходе формируется текст. Во время работы препроцессор занимается тем, что видоизменяет исходный текстовый файл. И только после этого изменённый текстовый файл в дальнейшем попадает в компиляцию. Команды препроцессора (их называют директивами) начинаются на символ #, который должен первым непробельным символом в строке. Первыми директивами препроцессора, с которыми сталкиваются начинающие, являются директивы #include и #define

Замечу также, что на этапе препроцессирования удаляются из текста все комментарии

1.2. Как посмотреть результат работы препроцессирования

Все вменяемые компиляторы предоставляют возможность для того, чтобы посмотреть результат работы препроцессирования. Поэтому начинающим будет полезно узнать о том, как это делается — так проще будет проводить эксперименты

1.2.1 Компилятор gcc (он же mingw под windows)

В компиляторе gcc есть опция -E, которая печатает в терминале результат работы препроцессора и завершает компиляцию — т.е. код формироваться не будет. Если выдача оказывается слишком большой, то её можно перенаправить в файл через «>» в командной строке, либо подать опцию «-o <file>» в запуск gcc

C

/* Файл a.c */
int x = 10;

Код:

$ gcc a.c -E     
# 1 "a.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "a.c"

int x = 10;

Что означают первые 4 строки в препроцессорной выдаче, будет рассказано в разделе 6

Посмотреть список файлов, подключенных через директиву #include (см. раздел 2), можно по опции -H. В этом случае компиляция НЕ останавливается

C

/* Файл a.c */
#include <stdio.h>

Код:

$ gcc a.c -H
. /usr/include/stdio.h
.. /usr/include/features.h
... /usr/include/sys/cdefs.h
.... /usr/include/bits/wordsize.h
... /usr/include/gnu/stubs.h
.... /usr/include/bits/wordsize.h
.... /usr/include/gnu/stubs-32.h
.. /usr/lib/gcc/i486-linux-gnu/4.2.4/include/stddef.h
.. /usr/include/bits/types.h
... /usr/include/bits/wordsize.h
... /usr/include/bits/typesizes.h
.. /usr/include/libio.h
... /usr/include/_G_config.h
.... /usr/lib/gcc/i486-linux-gnu/4.2.4/include/stddef.h
.... /usr/include/wchar.h
... /usr/lib/gcc/i486-linux-gnu/4.2.4/include/stdarg.h
.. /usr/include/bits/stdio_lim.h
.. /usr/include/bits/sys_errlist.h
Multiple include guards may be useful for:
/usr/include/bits/stdio_lim.h
/usr/lib/gcc/i486-linux-gnu/4.2.4/../../../../lib/crt1.o: In function `_start':
(.text+0x18): undefined reference to `main'
collect2: ld returned 1 exit status

1.2.2 Microsoft Visual C

См. тут и тут.

FIXME написать по человечески

1.2.3 Borland Builder

В C++ Builder 2007 на файле в списке исходников можно нажать правую кнопку и далее выбрать «Preprocess». Потарахтев несколько секунд, в списке исходников родится ещё один файл с расширением .i — это и есть препроцессированный текст

FIXME написать по человечески

Люди, кто знает, есть ли возможность посмотреть препроцессированный текст непосредственно из Qt-Creator’а (не запуская ручками g++ в командной строке)


2. Директива #include

2.1. Общие сведения

Директива #include ничего умного не делает, она просто целиком подставляет файл, который передан параметром директиве. Допустим, мы имеем следующие исходники:

C

/* Файл t1.c */
#include "t.h"
 
int main (void)
{
  struct str s;
  s.x = 1;
  s.y = 2;
  return func (&s);
}

C

/* Файл t2.c */
#include "t.h"
 
int func (struct str *p)
{
  return p->x + p->y;
}

C

/* Файл t.h */
 
/* Комментарий к нашей структуре */
struct str
{
  int x, y;
};
 
extern int func (struct str *p);

После препроцессирования файла t1.c получим следуюший текст. То, что начинается со стрелки «<—» в файле нет, просто надо каким-то образом подписывать (комментариями неправильно, т.к. после препроцессирования их не остаётся). Несколько подряд идущих пустых строк (получаемых, например, от комментариев) для наглядности опускаю

C

<-- комментарии удаляются на этапе препроцессирования
    (здесь было "/* Файл t1.c */")
<-- В этом месте находилась директива "#include", с этой
    точки начинается подстановка файла t.h
struct str
{
  int x, y;
};
 
extern int func (struct str *p);
<-- Закончили подстановку t.h, продолжаем подстановку
    файла t1.c
 
int main (void)
{
  struct str s;
  s.x = 1;
  s.y = 2;
  return func (&s);
}

А после препроцессирования t2.c получается вот такой текст:

C

struct str
{
  int x, y;
};
 
extern int func (struct str *p);
 
int func (struct str *p)
{
  return p->x + p->y;
}

В таком препроцессированном виде оба файла в конечном итоге попадают на компиляцию. Что в конечном итоге мы получили?

Структуру «struct str» мы описывали только один раз, но её описание попало в оба файла (t1.c и t2.c), причём описание одно и то же. Без файла t.h нам бы пришлось структуру описывать два раза, причём при каждом изменении следить, чтобы эти изменения в обоих файлах были одинаковые. А файлов могло быть на два, а сто или тысяча.

Описание функции func так же попало в оба файла, что даёт дополнительную проверку со стороны компилятора, если мы поменяем прототип функции func в файле t2.c, но при этом не поменяем в файле t.h, то компилятор схватит нас за руку и выдаст сообщение об ошибке. Если бы мы не пользовались файлом t.h, то могло быть так, что в файле t1.c внешняя функция func описана с одним прототипом, а в файле t2.c реализована с другим прототипом. Компилятор бы в этом месте ошибку не выдал, т.к. каждый файл *.c компилируется по отдельности

То, что подключаемый файл имеет расширение *.h — это обычная условность. Через директиву #include можно подключать абсолютно любой текстовый файл

2.2. Различия между угловыми скобками и кавычками

Есть некие соглашения, что имена системных файлов пишутся в угловых скобках, а пользовательских — в кавычках. Сильно принципиальных отличий между системными и пользовательскими файлами нет, и отличия эти варьируют в зависимости от компилятора. Обычно компилятор не выдаёт warning’и в местах, возникших в системных файлах (из тех соображений, что разработчик компилятора может накосячить, и у пользователя не будет возможности обойти эти warning’и). Ещё одно отличие файла в кавычках от файла в угловых скобках может заключаться в том, что файл в кавычках ищется сначала в текущем каталоге (а точнее в каталоге, где лежит компилируемый файл), а потом в прочих каталогах, а вот файл в угловых скобках ищется только в прочих каталогах — так, например, поступает компилятор от MSVS

2.3. Где компилятор ищет файлы для подключения через #inlcude

У компилятора, как и у большинства системных программ, есть некие заданные пути поиска файлов. Когда компилятор запускает препроцессор, то через опции он подаёт пути поиска для файлов *.h. Препроцессор, встретив директиву #include сначала ищет файл в текущем каталоге (это может быть пропущено для файла в угловых скобках — см. главу 10.1), и если не находит, то ищет по тем путям, которые переданы ему компилятором. На разных платформах файлы с include’ами находятся в разных местах. На unix’ах это как правило каталог /usr/include, на виндузовых компиляторах includ’ы обычно находятся внутри каталога, куда установлен компилятор


3. Директивы #define и #undef

3.1. Директива #define

Директива #define определяет так называемые макросы. Грубо говоря, если мы напишем «#define TRAM 10», то в процессе работы препроцессора все вхождения буквосочетания «TRAM» будут в текстовом виде заменены на «10». При этом надо отметить, что «TRAM» должно быть отдельным токеном (т.е. вокруг должны быть либо пробельные символы, либо знаки препинания). Т.е. «TRAM+TRAM» будет заменено на «10+10», а вот «TRAMPAMPAM» останется без изменений

Для следующего примера

C
1
2
3
4
5
6
7
8
9
10
11
#define N 20
 
int a[N];
int x;
 
void func (void)
{
  int i;
  for (i = 0; i < N; i++)
    x += a[i];
}

во время препроцессирования все вхождения «N» будут заменены на «20». Т.е. после препроцессирования мы будем иметь следующий текст

C
1
2
3
4
5
6
7
8
9
int a[20];
int x;
 
void func (void)
{
  int i;
  for (i = 0; i < 20; i++)
    x += a[i];
}

и именно этот текст попадёт в компилятор, который даже не будет знать о том, что в исходнике написано «N», он будет видеть только «20». Данное свойство иногда запутывает начинающих, когда в момент выдачи ошибки компилятор говорит, к примеру, что у тебя мол в тексте синтаксическая ошибка перед «20», но при этом в исходнике у пользователя вообще нет буквосочетания «20», т.к. оно подцепилось из системных include’ов

Что в итоге мы имеем?. В конкретно данном примере этим макросом N я задал размер массива, а потом везде работал именно через макрос. Можно сразу написать 20, с точки зрения конкретно данного примера ничего не поменяется. НО. Если мне нужно поменять размер массива с 20 на 30, то я просто меняю значение define’а, а во всех остальных местах это фактически изменится автоматически. Если же писать непосредственно 20, то потом во всех местах надо менять 20 на 30, а таких мест может быть много

Работать с define’ами надо аккуратно. Если написать такой код:

C
1
2
3
4
5
6
7
8
9
10
#define N 20
 
int a[N];
...
 
void func (void)
{
  int N;
  N++;
}

то в функции func НЕ будет заведена локальная переменная, как это вроде бы ожидалось. После препроцессирования текст будет такой:

C
1
2
3
4
5
6
7
8
int a[20];
...
 
void func (void)
{
  int 20;
  20++;
}

что на этапе компиляции выдаст ошибку. Поэтому по возможности надо избегать коротких имён макросов. Негласные соглашения заключаются в том, что имена макросов (и элементов enum’ов) пишут целиком заглавными буквами, а имена переменных — комбинацией заглавных и прописных букв

3.2. Директива #undef

Для того, чтобы отменить макрос, существует директива #undef. Как только препроцессор встречает такую директиву, он «забывает» опеределённый ранее макрос и больше не заменяет его. Для демонстрации этого свойства опять вернёмся к предыдущему примеру и добавим туда макрос #undef

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define N 20 <-- в этой точке препроцессор "запомнит" макрос "N"
                 и с этого момента будет заменять "N" на "20"
 
int a[N];
...
 
#undef N <-- в этой точке препроцессор "забудет" макрос "N"
             и больше заменять его не будет
 
void func (void)
{
  int N;
  N++;
}

В результате чего препроцессированный текст будет таким:

C
1
2
3
4
5
6
7
8
9
10
11
12
<-- здесь было "#define"
 
int a[20]; <-- здесь ещё работает замена "N" на "20"
...
 
<-- здесь было "#undef"
 
void func (void)
{
  int N; <-- в местах после #undef замена не производится
  N++;
}

3.3. Директива #define с параметрами

3.3.1. Общие сведения

Определение макроса, при котором одно буквосочетание заменяется на другое, является простейшей макроподстановкой. Однако препроцессор понимает и более сложные макросы — макросы, содержащие параметры. Смысл таких макросов примерно такой же, как и для функций в языка программирования — выделить в одно место часто повторяющиеся действия и задать эти действия параметрами. При этом надо не забывать и чётко себе представлять, что макрос — это не функция, а текстовая замена

Пояснить проще всего на примере. Зададим макрос, в результате подстановки которого мы будем получать квадрат значения параметра, подаваемого в макрос

C
1
2
3
4
5
6
7
8
9
#define SQUARE(val) val * val
 
void func (int x, int y)
{
  int a, b;
 
  a = SQUARE (x);
  b = SQUARE (y);
}

Во время препроцессирования в случае, описанном в предыдущем разделе, была простая замена «N» на «20». В этом же примере заменяться будет текстовые вхождения «SQUARE (<набор символов>)» на «val * val», а в качестве val будет подставляться «<набор символов>». В результате чего после препроцессирования получим вот такой текст:

C
1
2
3
4
5
6
7
void func (int x, int y)
{
  int a, b;
 
  a = x * x; <-- здесь произошла текстовая замена "SQUARE(x)" на "x * x"
  b = y * y; <-- здесь произошла текстовая замена "SQUARE(y)" на "y * y"
}

Макрос можно определять с произвольным количеством параметров, при этом они должны иметь разные имена и разделяться запятыми. Пример макроса для вычисления суммы двух значений

C
1
#define SUM(x1,x2) x1 + x2

3.3.2. Злосчастный пробел

При написании макроса с параметрами одной из распространённых ошибок начинающих, является наличие пробела между именем макроса и открывающей скобкой при описании директивы #define. Это неправильно, открывающая скобка должна идти впритык к имени макроса

C
1
2
3
4
5
6
7
// Правильно. Трактуется как макрос "SQUARE" с одним параметром "val"
// и телом "val * val"
#define SQUARE(val) val * val
 
// Не правильно. Трактуется как макрос "SQUARE" без параметров
// и телом "(val) val * val"
#define SQUARE (val) val * val

Т.е. при неправильном описании макроса

C
1
2
3
4
5
6
#define SQUARE (val) val * val
void func (int x)
{
  int a;
  a = SQUARE (x);
}

препроцессированный текст будет выглядеть

C
1
2
3
4
5
void func (int x)
{
  int a;
  a = (val) val * val (x); <-- произошла текстовая замена "SQUARE" на "(val) val * val"
}

В результате чего во-первых получили совсем не то, что хотели, а во-вторых выдача ошибки компилятора на строку кода «a = SQUARE (x);» скорее всего начинающего введёт в ступор

При этом критичным является лишь то, что между именем макроса и открывающей скобкой не должно быть пробелов. Во всех остальных местах пробелы можно ставить в произвольном количестве

3.3.3. Макрос — это совсем не функция

Ещё одна распространённая ошибка демонстрируется следующим примером

C
1
2
3
4
5
6
#define SQUARE(val) val * val
void func (int x)
{
  int a;
  a = SQUARE (x+1);
}

текст программы выглядит так, как будто бы в переменную a должен записаться квадрат значения «x+1». Однако это не так. По результату работы препроцессора текст будет выглядеть следующим образом:

C
1
2
3
4
5
void func (int x)
{
  int a;
  a = x+1 * x+1;
}

и с учётом того, что приоритет операции умножения выше, чем приоритет операции сложения, то в переменную a запишется значение «x + 1*x + 1» что эквивалентно «2*x + 1», но никак не «(x+1)*(x+1)». Чтобы избежать таких проблем при написании макросов с параметрами во всех местах использования параметров их надо заключать в круглые скобки. Таким образом правильным вариантом будет следующее:

C
1
2
3
4
5
6
#define SQUARE(val) (val) * (val)
void func (int x)
{
  int a;
  a = SQUARE (x+1);
}

что после препроцессирования превратится

C
1
2
3
4
5
void func (int x)
{
  int a;
  a = (x+1) * (x+1);
}

Но это ещё не всё. Если рассмотреть немного изменённый пример

C
1
2
3
4
#define DOUBLE(val) (val) + (val)
...
  x = DOUBLE(y) * DOUBLE(y);
...

то после препроцессирования

C
1
x = (y) + (y) * (y) + (y);

опять получим неверный код. Дабы избежать этого, нужно ещё и всё тело макроса заключить в круглые скобки:

C
1
2
3
4
#define DOUBLE(val) ((val) + (val))
...
  x = DOUBLE(y) * DOUBLE(y);
...
C
1
x = ((y) + (y)) * ((y) + (y));

И если подвести краткий итог, то рекомендация будет следующая: при написании макросов, которые пишутся в качестве быстрых реализаций вместо функции, лучше всего сразу же заключить в круглые скобки все параметры и само тело макроса — меньше проблем будет в будущем

Однако даже такой вариант не сможет отработать корректно в 100% случаев. Макрос, в теле которого параметр используется более одного раза в общем случае работает некорректно (в том смысле, что отработает не так, как от него ожидали). Если в качестве параметра подать конструкцию, значение которой меняется при каждом обращении, то получим некорректный код. Например, глядя на текст

C
1
a = SQUARE (fgetc(fp));

кажется, что мы прочитаем один символ из файла и возведём его значение в квадрат, однако после препроцессирования получим следующее

C
1
a = ((fgetc(fp)) * (fgetc(fp)));

И таким образом за одно обращение к макросу будет прочитано два байта из файла и их значения будут перемножены, что опять-таки не соответствует тому, чего мы ожидали. Точно так же будет проблема, если в качестве параметра подать выражение типа «i++». О таких моментах надо всегда помнить, а потому пользоваться макросами с осторожностью. Применительно к данному примеру в языке C++ более правильным было бы реализовать inline-функцию, но это уже выходит за рамки данной статьи (если дойдут руки, то напишу отдельную)

3.3.4. Оператор # для параметров макроса

Препроцессор работает с файлом строго как с текстом. То же самое касается и параметров директивы #define. Следовательно, препроцессору ничего не стоит делать с ними некоторые простые преобразования. Одним из них является символ ‘#’. Если его поставить перед параметром макроса, то в результате подстановки этот параметр будет взят в кавычки

C
1
2
3
#define MACRO(x,y) x #y
...
MACRO (10, 20)

будет раскрыто вА в качестве более-менее живого примера можно написать макрос для отладочной печати целочисленной переменной

C
1
2
3
4
5
6
#define PRINT_INT_VAR(x) printf (#x "=%dn", x)
...
int a, b[10];
...
PRINT_INT_VAR (a);
PRINT_INT_VAR (b[2]);

что раскроется в

C
1
2
printf ("a" "=%dn", a);
printf ("b[2]" "=%dn", b[2]);

Начинающим наверно это покажется несколько мутным, но тем не менее данный код рабочий. В языках C/C++ запись строковой константы можно распиливать на несколько строковых констант, между которыми находятся знаки пробела, табуляции, переноса строк или вообще ничего не находится. Т.е. записькомпиляторы воспринимают какИсходя из этого, данный макрос раскроется в код, эквивалентный

C
1
2
printf ("a=%dn", a);
printf ("b[2]=%dn", b[2]);

При этом в качестве параметра макроса совсем необязательно подавать только переменную. Можно подавать и выражениечто раскроется в код, эквивалентный

C
1
printf ("a+b=%dn", a+b);

Правда такой вариант макроса отладочной печати неудобен тем, что им можно распечатать только int’ы. Для тех, кто работает на Си++, эта проблема легко устраняется переписыванием макроса в виде

C++
1
2
3
4
5
6
7
#define PRINT_VAR(var) std::cout << #var "=" << (var) << std::endl
...
int i;
float f;
...
PRINT_VAR (i);
PRINT_VAR (f);

А вот тем, кто работает на C, придётся добавить дополнительный параметр в макрос для правильного формата printf’а. При этом программист должен будет сам контролировать, что для каждой переменной подан правильный формат

C
1
2
3
4
5
6
7
#define PRINT_VAR(var,format) printf (#var "=" format "n", (var))
...
int i;
float f;
...
PRINT_VAR (i, "%d");
PRINT_VAR (f, "%f");

FIXME Написать про оператор #, применённый к строковому литералу: https://www.cyberforum.ru/faq/… ost4465754

3.3.5. Оператор ## в теле макроса

Ещё одним оператором для параметров макроса является ##. Если его поставить между двумя «словами» в теле макроса, то эти два «слова» будут склеены в одно (так называемый оператор конкатенации):

C
1
2
3
4
5
#define MACRO1(x,y) x##y
#define MACRO2(x,y) x##y##trampampam
 
MACRO1 (abc, def)
MACRO2 (abc, def)

раскроется в

C
1
2
abcdef
abcdeftrampampam

На практике эта конструкция применяемтся, как правило, для формирования имён функций, переменных, элементов enum’а и т.п. В unix’ах есть так называемые системные вызовы — это обращение пользовательской программы к операционной системе. Каждый вызов кодируется своим номером, а вся номенклатура номеров обычно представлена в виде enum’а (или константы через define). Но на разных ОС это сделано в разных стилях. Например, в linux’е имена для системных вызовов «read» и «write» называются __NR_read и __NR_write. А в solaris’е и hpux’е — SYS_read и SYS_write. В каких-то системах может быть ещё с какими-то другими префиксами. Поэтому в программе можно, например, писать в лоб:

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void func1 (void)
{
  ...
#if defined __linux__
  num = __NR_read;
#elif defined __sun__
  num = SYS_read;
#else
#error "unknown OS"
#endif
  ...
}
 
void func2 (void)
{
  ...
#if defined __linux__
  num = __NR_write;
#elif defined __sun__
  num = SYS_write;
#else
#error "unknown OS"
#endif
  ...
}

и в кажом месте использования писать такую портянку. Но это ещё полбеды. Теперь если вдруг окажется, что программа должна работать ещё на какой-то ОС с каким-то новым префиксом, то все подобные места надо будет исправлять ручками. Поэтому лучше поступать следующим образом

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#if defined __linux__
  #define SYS_NUM(num) __NR_##num
#elif defined __sun__
  #define SYS_NUM(num) SYS_##num
#else
#error "unknown OS"
#endif
 
void func1 (void)
{
  ...
  num = SYS_NUM (read);
  ...
}
 
void func2 (void)
{
  ...
  num = SYS_NUM (write);
  ...
}

в этом случае после препроцессирования linux’овыми компиляторами ны выходе будем иметь текст

C
1
2
3
4
5
6
7
8
9
10
11
12
13
void func1 (void)
{
  ...
  num = __NR_read;
  ...
}
 
void func2 (void)
{
  ...
  num = __NR_write;
  ...
}

а после препроцессирования solaris’овым компилятором будем иметь вот такой текст:

C
1
2
3
4
5
6
7
8
9
10
11
12
13
void func1 (void)
{
  ...
  num = SYS_read;
  ...
}
 
void func2 (void)
{
  ...
  num = SYS_write;
  ...
}

Достоинство этого метода более чем очевидно: макрос SYS_NUM реализуется только в одном месте (в общем заголовочном файле). Если нужно добавить новую ОС, то правка делается только в одном месте (а не в миллионах мест, как это было бы при реализации «в лоб»)

3.3.6. Директива #define с переменным числом параметров

FIXME Написать, а пока ссылка на пример использования

enum

В качестве примера использовать синтетический тест. Создаём макрос, который вычисляет сумму своих аргументов. Типа SUM(2,x,y), SUM(5,x,y,z,a,b). В качестве реального примера сослаться на реализацию syscall’ов в linux’овом unustd.h

3.3.7. Круглые скобки или запятая среди параметров макроса

FIXME Написать, а пока ссылка на пример использования

Использование строк в макросах С++
Как добавить запятую в аргументы макроса?

3.4. Директива #define, растянутая на несколько строк

При написании макроса, его тело может оказаться довольно длинным. В этом случае для удобства можно его разбивать на несколько строк, но при этом в конце каждой строки, кроме самой последней, надо будет ставить символ обратного слэша, который по сути дела говорит препроцессору, что последующий символ перевода строки надо будет проигнорировать и считать, что вся следующая строка как бы является продолжением текущей

C
1
2
3
4
5
6
7
#define FUNC(x,y,z) 
  ( (x)*(x)*(x) 
    + (y)*(y)*(y) 
    + (z)*(z)*(z) )
...
  a = FUNC (10, x+1, t-d);
...

Что после препроцессирования раскроется в

C
1
2
3
...
  a =   ( (10)*(10)*(10)     + (x+1)*(x+1)*(x+1)     + (t-d)*(t-d)*(t-d) );
...

Заметим, что при этом в препроцессированном тексте вся эта конструкция выразится в одну строку (т.е. знаки перевода строки пропадут), что несколько неудобно для просмотра препроцессорной выдачи глазами. Дополнительно появившиеся пробелы являются следствием аккуратного форматирования текста (отступы в начале каждой строки), что так же затрудняет просмотр глазами

3.5. Использование в директиве #define имён других макросов

Пока написано тут и тут
FIXME перенести сюда и аккуратно переписать (хотя вроде бы там написал всё как надо)

Ещё один интересный момент тут
FIXME перенести сюда и аккуратно переписать


4. Директивы условной компиляции

4.1. Общие сведения об условной компиляции

В любой программе есть условные ветки исполнения кода. Точно так же препроцессор позволяет условно включать те или иные фрагменты исходника программы. Для этого используется препроцессорная директива «#if <условное выражение>». Прежде, чем делать подробные объяснения, попробую сначала показать на простом примере, как это использовать.

Допустим мы пишем программу, которая содержит отладочные печати. Эти печати имеет смысл включать только тогда, когда разработчик программы занимается её отладкой. Версия, которую программист отдаёт пользователю, этих печатей не должна содержать. Для этого, например, можно в программе завести переменную и ставить все печати под условие:

C
1
2
3
4
5
6
7
8
9
const int debug = 1;
...
void func (void)
{
  if (debug)
    printf ("entering 'func'n");
  ...
}
...

В простых случаях это действительно будет выходом из ситуации. Если переменную debug установить в 0, то компилятор, видя условие «if (debug)», а также то, что переменная имеет модификатор const т равна нулю, скорее всего вообще удалит вызов printf’а, как мёртвый код, в который программа никогда не попадёт. Но если программа состоит из нескольких исходных файлов, то такой фокус не пройдёт, потому как переменная должна быть определена только в одном модуле, а остальные модули не будут видеть значения переменной, а потому не смогут удалить мёртвый код. При этом вызов printf’а в коде программы останется, хоть и будет стоять под условием, которое никогда не будет равно true. По большому счёту и это тоже терпимо, т.к. десяток или сотня вызовов printf’а принципиально размер бинарного фала не увеличат (т.е. увеличение будет составлять единицы процентов, но не в разы). Хуже обстоит дело, когда мы вызваем не printf, а какую-то «нашу» функцию, которая нужна только для отладочных печатей.

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const int debug = 1;
...
void debug_print (...)
{
  ...
}
 
void func (void)
{
  if (debug)
    debug_print (...);
  ...
}
...

В этом случае в код программы попадут все такие функции, которые никогда не будут запускаться, но место в бинарном фале занимать. И когда таких функций много, то программа может ощутимо «потяжелеть».

В качестве примера я привёл отладочную печать, но вместо отладочной печати может вообще стоять какая-то функциональность. Многие программные продукты распространяются в том виде, что полная версия делается платной, а свободных (бесплатных) версиях отключается часть функциональности. Если это делать приведёным выше способом, то можно попросту покопаться в бинарнике и на месте переменной debug воткнуть единицу, после чего урезанная программа превращается в полную (условно говоря).

Чтобы избежать этих проблем, ненужный код надо физически вырезать из программы. И для этих целей удобно использовать директиву условной компиляции «#if»

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define DEBUG 1
 
#if DEBUG == 1
 
void debug_print (...)
{
  ...
}
 
#endif
 
void func (void)
{
#if DEBUG == 1
  debug_print (...);
#endif
 
  ...
}
...

Эта директива на этапе препроцессирования управляет тем, что попадёт в препроцессорную выдачу, а что нет. Для данного примера после препроцессора будем иметь вот такой текст:

C
1
2
3
4
5
6
7
8
9
10
11
void debug_print (...)
{
  ...
}
 
void func (void)
{
  debug_print (...);
  ...
}
...

Теперь если мы значение макроса DEBUG поменяем на 0, то получим совсем другую выдачу из-под препроцессора:

C
1
2
3
4
5
6
7
8
9
10
11
12
<-- то место, которое вырезалось на препроцессировании из-за невыполнения
    условия в директиве #if. На самом деле каждая строка исходного текста
    будет заменена пустой строкой, но я эти пустые строки рисовать не буду,
    чтобы не загромождать текст. В данном случае здесь вырезался текст
    функции debug_print
 
void func (void)
{
<-- здесь вырезался вызов функции debug_print
  ...
}
...

Мы видим, что после препроцессирования ненужный нам текст физически вырезался и в компиляцию в принципе не попадает. В случае работы с несколькими файлами определение макроса DEBUG следует поместить в один из файлов *.h, который подключается во всех исходных файлах. В противном случае может оказаться так, что если в одном из файлов мы не подцепили описание макроса, то он там и не будет определён, а потому какие-то куски кода окажутся не включенными (даже если в *.h файле значение макроса выставлено в единицу). Поэтому работа с макросом в данном случае (да и не только в данном) требует аккуратности. Что важно в данном случае — управление наличием или отсутствием кода осуществляется путём замены единственного символа и не требует каких-то постоянных усилий по закомментированию и раскомментированию кода

Немного забегая вперёд, скажу, что конкретно это место я бы переписал чуть-чуть по другому. Об этом более подробно расскажу в разделе 8, т.к. здесь описываю только принцип условной компиляции

Аналогично следует поступать, когда из программы нужно вырезать некоторую функциональность, только получится здесь чуточку сложнее. Допустим, у нас есть программа, которую следует собирать в трёх конфигурациях: lite, medium, full. Схематично выглядеть будет примерно так:

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#define BUILD_LITE 1
#define BUILD_MEDIUM 2
#define BUILD_FULL 3
 
/* При сборке должен быть включен только один из этих вариантов */
/* #define BUILD BUILD_LITE */
/* #define BUILD BUILD_MEDIUM */
#define BUILD BUILD_FULL
...
#if BUILD >= BUILD_LITE
/* Данный код подключится при любой конфигурации, но для порядку его
 * надо "подсветить" макросом BUILD. Так проще будет поиском найти все места,
 * которые теоретически зависят от типа сборки, мало ли в будущем мы вместо
 * трёх типов будем использовать пять. */
...
#endif
...
#if BUILD >= BUILD_MEDIUM
/* Данный код подключится в конфигурациях medium и full */
...
#endif
...
#if BUILD == BUILD_FULL
/* Данный код подключится в только в конфигурации full */
...
#endif

4.2. Директивы #if, #else, #elif, #endif

Синтаксис директивы «#if» очень простой и по большому счёту совпадает с операциями условного исполнения в языке. Как-то особенно на этом останавливаться не буду, просто покажу все варианты использования. Единственное, что нужно отметить — обязательно должен быть «#endif», потому как без него препроцессор не сможет понять, в каком месте заканчивается директива «#if»

C
1
2
3
#if <условие>
...
#endif
C
1
2
3
4
5
#if <условие>
...
#else
...
#endif
C
1
2
3
4
5
6
7
8
9
#if <условие>
...
#elif <условие>
...
#elif <условие>
...
#else
...
#endif

Что касается условия, то хочется ещё раз отметить, что препроцессирование делается отдельно от компиляции, а потому в условии директивы «#if» НЕ могут использоваться никакие переменные из программы. В условии могут использоваться только целочисленные константные значения (которые могут быть значениями других макросов). Над этими константами можно выполнять операции сравнения «==», «!=», «<«, «<=», «>», «>=». В условии могут использоваться логические операции «&&», «||», «!», круглые скобки, а так же некая конструкция «defined <macro_name>», значение которой истинно, если макрос <macro_name> определён, в противном случае значение ложно. Препроцессорные «#if’ы», так же, как и языковые, могут быть вложены друг в друга. В строках с директивами можно использовать комментарии

Если обратиться к примерам из предыдущего раздела и скомбинировать их, то можно продемонстрировать один из примеров использования. Ситуация следующая: в программе была найдена ошибка, но аккуратное её исправление требует много времени, а исправить надо быстро. Поэтому пока делаем в виде затычки.

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/* Пользовательская ошибка N1234. На текущий момент она не исправлена, т.к.
 * это требует много времени, а потому под этим макросом пока сделаем затычки
 * для обхода ошибки */
#define BUG1234
...
 
void func1 (void)
{
  ...
  /* Пишем затычку. В случае наличия или отсутствия отладочных печатей код
   * затычки выглядит по разному */
  #if defined BUG1234
  #if DEBUG == 1
  /* Код затычки в отладочной сборке */
  ...
  #else /* DEBUG */
  /* Код затычки в пользовательской сборке */
  ...
  #endif /* DEBUG */
  #endif /* BUG1234 */
  ...
}
...
void func2 (void)
{
  ...
  /* Здесь ошибка проявляется только в MEDIUM сборке */
  #if (defined BUG1234) && (BUILD == BUILD_MEDIUM)
  /* Код затычки */
  ...
  #endif
  ...
}

Таким образом мы исправляем программу, тестируем и отдаём пользователю. Далее спокойно исправляем ошибку. После чего закомментируем макрос BUG1234 и тестируем в режиме с выкинутыми затычками. Важно, что это управляется всего в одной строке. Если что-то не срослось — макрос возвращаем на место. Если всё нормально, то работаем с отключенным макросом, но код какое-то время оставляем, чтобы всё устаканилось и более тщательно оттестировалось. После чего ищем все места, где использовался макрос BUG1234 и удаляем их

Жизнь показала, что не всем понятно, что такое «затычка» по своей сути. Здесь приведён хороший пример с пояснением. Пример основан на реальной ошибке в коде

4.3. Директивы #ifdef, #ifndef

В примере из предыдущего раздела мы ввели макрос BUG1234, у которого не было никакого значения. Такое используют в случаях, когда некоторый настроечный макрос может принимать только два значения (условно 0 и 1). В этих случаях вместо значений 0 и 1 зачастую используют макрос без значения, а проверку делают на основании того, установлен макрос или нет — т.е. через «if defined …». Для таких случаев были придуманы сокращённые варианты «#ifdef <macro_name>» и «ifndef <macro_name>». Параметрами этих директив является не логическое выражение, а один-единственный макрос. Конструкция «#ifdef BUG1234» эквивалентна конструкции «#if defined BUG1234», «#ifndef BUG1234» эквивалентно «#if (! defined BUG1234)». Принципиально новых возможностей эти две конструкции не вносят, просто сокращённая форма записи и не более того.

Каким принципом пользоваться (т.е. «иметь макрос со значениями 0 и 1» или «иметь или не иметь макрос») — это чисто дело вкуса. Я, как правило, пользуюсь философскими соображениями на тему «включено/выключено» или «присутствует/отсутствует». В случае макроса DEBUG я считаю, что отладочный код он всегда есть, только где-то он включен, а где-то выключен, а потому использую макрос со значением. В случае BUG1234 я считаю затычку временным явлением, а потому она либо присутствует, либо отсутствует, поэтому использую макрос без значения и проверки на то, взведён макрос или нет.


5. Директивы #error и #warning

5.1. Общие сведения

Основная область применений данных директив — исходники программ, предназначенные для широкого пользования: open source проекты или программы, которые пишут одновременно большое количество людей. Директивы не делают ничего умного, кроме как заставляют препроцессор (или компилятор, возможно, зависит от конкретной реализации) выдать сообщение с ошибкой или предупреждением. Директивы применяют совместно с директивами условной компиляции, чтобы отсечь некоторые недопустимые или неподдерживаемые комбинации настроек.

5.2. Директива #error

В качестве примера можно взять следующее. Допустим мы пишем программу, которая может работать на разных платформах. При этом в момент компиляции нужно знать, наша платформа big endian или little endian. На разных платформах работают компиляторы от разных разработчиков. Как правило, каждый компилятор выставляет некие предопределённые макросы, в том числе и макросы, определяющие тип процессора. Например, большинство компиляторов под intel’овскую архитектуру выставляют макрос __i386__, компиляторы под sparc’овскую архитектуру выставляют макрос __sparc__ и т.д. Но каких-то более-менее единых макросов, относящихся к endian’у архитектуры нет. Поэтому наиболее надёжным способом будет самим взвести какой-то макрос в зависимости от архитектуры. Можно это сделать следующим образом:

C
1
2
3
4
5
6
7
8
9
#if (defined __i386__) || (defined __alpha__)
/* Архитектуры с little endian */
#define LITTLE_ENDIAN
#elif (defined __sparc__)
/* Архитектуры с big endian */
#define BIG_ENDIAN
#else
#error "unknown architecture"
#endif

Подобные коды как правило помещаются в некоторый настроечный файл *.h. При этом, если мы компилируем код на intel’е или aplha’е, то у нас взведётся макрос LITTLE_ENDIAN, а при компиляции на sparc’е взведётся макрос BIG_ENDIAN. А уже во всей остальной программе мы используем именно макросы LITTLE_ENDIAN и BIG_ENDIAN для выбора нужной ветки компиляции. Теперь, предположим, мы пытаемся скомпилировать код на MIPS’е. Ни один из указанных макросов (__i386__ и т.д.) у нас не будет взведённым, поэтому мы попадаем в ветку «#else», где у нас произойдёт слом на компиляции (с привязкой к файлу исходника и номеру строки). Поглядев на этот код программисту останется только добавить проверку макроса __mips__ в ветку для little endian, после чего программа начнёт собираться.

Смысл директивы #error именно в том, чтобы обнаруживать такие неподдерживаемые или недопустимые комбинации настроечных макросов и уже на этапе компиляции программы выдавать ошибку.

5.3. Директива #warning

Директива «#warning» работает аналогичным образом, но выдаёт не ошибку, а предупреждение. Как правило делается это в тех случаях, когда хотят что-то поменять, но не сразу, а плавно: в какой-то период времени будут работать «старый» и «новый» варианты, но со временем старый вариант будет удалён.

Например, мы имеем в программе макрос DEBUG, который либо включен, либо выключен. В какой-то момент мы решили, что количество отладочных печатей стало слишком большим и в них уже сложно разобраться. Вместо макроса DEBUG мы решили завести макрос DEBUG_LEVEL с цифровым значением от 0 до 10. Значение 0 означает отсутствие каких-либо печатей (и эквивалентно отсутствию макроса DEBUG в нынешней реализации), значение 1 означает самое минимально необходимое количество печатей, значение 2 — уже побольше печатей, значение 10 соответствует полному количеству печатей (и эквивалентно наличию макроса DEBUG в нынешней реализации).

Если программу пишет сто человек, то такой резкий переход с ходу сделать не получится. Поэтому нужен какой-то временный период, в течение которого все люди смогут постепенно перейти от использования макроса DEBUG к макросу DEBUG_LEVEL. Проще всего в точке, где в нынешней реализации определяется макрос DEBUG, поступить следующим образом:

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* Наша старая реализация. Условно пишу так, но включение макроса
 * в общем случае может быть под #ifdef */
#define DEBUG
 
...
 
/* Плавный переход на новую реализацию */
#ifndef DEBUG
/* Отсутствие старого макроса означает новый макрос со значением 0 */
#define DEBUG_LEVEL 0
#else
/* Во время переходного периода наличие старого макроса будет означать
 * новый макрос со значением 10. В этом месте дополнительно выдадим
 * предупреждение. Его будут видеть только те, кто активно работает
 * с включенным макросом DEBUG */
#define DEBUG_LEVEL 10
#warning "Макрос DEBUG устаревший, надо заменять его на DEBUG_LEVEL"
#endif
 
/* В новой реализации макрос определён всегда. Поэтому для внутреннего контроля
 * сделаем проверку */
#ifndef DEBUG_LEVEL
#error "Макрос DEBUG_LEVEL не определён"
#endif

После этого в течение какого-то времени люди избавляются от макроса DEBUG. Практика показывает, что в данных случаях нужно включать выдачу предупреждения, чтобы оно постоянно мозолило глаза и те, кто откладывает это дело на «потом», не забыли. Как вариант предупреждение можно делать многострочным, чтобы бросалось в глаза. После того, как все поправят свои коды, весь наш промежуточный код можно будет удалить и оставить только определения макроса DEBUG_LEVEL. На какой-то период можно будет добавить для контроля такой код (потому что опять-таки практика показывает, что всегда найдутся те, кто в танке):

C
1
2
3
#ifdef DEBUG
#error "Макрос DEBUG больше не поддерживается"
#endif

С директивой #warning есть небольшая засада: она не является стандартной, а потому её поддерживают не все компиляторы. В частности, компилятор gcc с опцией -pedantic начинает на директиву #warning ругаться (поскольку в стандартном Си её не подразумевается). А MSVS её не поддерживает в принципе. Микрософтеры рекомендуют пользоваться директивой #pragma message. Сама #pragma message выдаёт просто сообщение (без привязки к строке исходника). Пляска с бубном в виде трёх дополнительных макросов по указанной ссылке необходима для включения в текст сообщения имени файла и номера строки, на которой находится директива #pragma message


6. Директива #lineFIXME Написать


7. Директивы, но не относящиеся к препроцессору (#pragma, #import и т.п.)FIXME Написать


8. Примеры использования препроцессорных директив на практикеFIXME Написать

  • Внутренняя отладка программ (ASSERT, FATAL и т.п.): см. статью Статическая отладка программ
    Подобным конструкциям альтернатив средствами языка по сути дела нет. Т.е. их можно реализовывать только через макросы. Из принципа, можно конечно реализовать и через конструкции языка, но в конечном итоге окажешься сам себе злобным буратино
  • Добавить раздел 3.5 «Предопределённые макросы». Туда про __FILE__, __LINE__, __DATE__ (в старых версиях это были предопределённые макросы, а теперь — псевдопеременные языка), а так же про системо-зависимые макросы типа __linux__, __i386__ и т.п.
    https://www.cyberforum.ru/cpp-… 20969.html
    Имя парамтера макроса
    Зарезервированные лексемы gcc
  • __header_h__ (краткое описание: https://www.cyberforum.ru/faq/… ost4134774)
  • Set’ы Get’ы для полей структуры
  • DEBUG — сюда ссылка из 4.1
  • *.def (https://www.cyberforum.ru/cpp/thread47863.html)
  • case 1: var1 = 10 ; break; case 2: var2 = 10; break;
  • Save-Load на примере конфигурации окон: Сохранение в файл геометрии окна
  • Печать параметра в виде строки, если параметр является другим макросом (немного описал в Можно ли использвовать диррективу препроцессора #warning в Visual Studio 2008)
  • Плавная реализация какой-то дополнительной функциональности, которую сначала делают под макросом
  • Через макрос форматная строка для printf рядом с typedef’ом
  • Нелепые define-ы
  • Вычисление смещения поля структуры (OFFSETOF)
  • На «стандартном Си» некоторые макросы полноценно невозможно реализовать из-за проблем, описанных в разделе 3.3.3. Однако конструкция typeof языка GNU C во многих случаях придёт на помощь
    Есть ли в Си аналог std::fill() в C++?
  • Удобный интерфейс для отладочной печати
    Вспомогательный макрос для вывода строки и значения
  • Имитация генерации текста в цикле через препроцессор
    «Циклы» на препроцессоре
  • Макросы-пустышки, выполняющие роль подсказки программисту
    непонятня смысловая нагрузка (#define _In_)
  • Аккуратное приклеивание
    Директива #define: из ANSI в UNICODE
  • Технические приёмы с использованием препроцессора
    https://github.com/pfultz2/Clo… and-idioms

9. Когда следует использовать макросы, а когда конструкции языка программированияFIXME Написать

  • Set’ы и Get’ы
  • Примеры с const переменными для Си++. const в качестве размерностей массива
    В чем отличие const и define ?
  • Пример с inline-функцией для Си++
  • Через макрос можно реализовать функцию, похожую на шаблонную за счёт применения typeof, но оно есть только в gnu-расширениях

10. Ссылки на темы, где обсуждались подобные вопросы

По данным ссылкам нет ничего, что выходило бы за рамки статьи. По этим ссылкам просто обсуждались какие-то отдельно взятые аспекты препроцессирования. Прочитать эти темы будет проще, чем читать большую статью

  • Для чего нужен define?
  • Подключить библиотеку в зависимости от ОС
  • [C++] define как определение константы vs. макроса — на всякий случай предупреждаю, что это почти холивар
  • Объявление глобальных переменных для всего проекта
  • https://www.cyberforum.ru/faq/… ost4134774

Циклом
называется последовательность операторов,
которая выполняется несколько раз в
процессе выполнения программы при
различных значениях некоторой переменной
или при выполнении какого-то условия.

Оператор
цикла
while

while
(выражение) оператор

Оператор
while
определяет операции, которые циклически
выполняются до тех пор, пока проверяемое
«выражение» не станет ложным, или
равным нулю, т.е. если «выражение»
истинно (или а общем случае не равно
нулю), то «оператор» (или «тело
цикла») выполняется один раз, а затем
«выражение» проверяется снова. Эта
последовательность действий, состоящая
из проверки и выполнения тела цикла,
периодически выполняется до тех пор,
пока «выражение» не станет ложным.
Каждый такой шаг называется «итерация».

Структура оператора:
for (инициализация; проверка условия;
коррекция) оператор

В операторе for
используются
три выражения, управляющие работой
цикла. Инициализирующее выражение
вычисляется только один раз до начала
выполнения какого-нибудь из операторов
цикла. Если проверяемое выражение
оказывается истинным (или не равным
нулю), тело цикла выполняется один раз.
Затем вычисляется величина корректируемого
выражения, и значение проверяемого
выражения определяется вновь. Таким
образом, тело цикла выполняется до тех
пор, пока проверяемое условие не станет
ложным, или равным нулю.

Структура оператора
do
while:

Do
оператор while (выражение);

Оператор do
while определяет
действия, которые циклически выполняются
до тех пор, пока проверяемое выражение
не станет ложным, или равным нулю.
Оператор do
while
– это
цикл с постусловием;
решением, выполнять или нет в очередной
раз тело цикла, принимается после его
прохождения. Поэтому тело цикла будет
выполнено по крайней мере один раз
.

5 Операторы continue, break языка си.

Оператор break
можно
использовать внутри любой из трех форм
цикла и конструкции switch.
Его выполнение приводит к тому, что
управление программой, минуя оставшуюся
часть тела цикла или конструкцию switch,
содержащую данный оператор, передается
на следующую (за этим циклом или за
конструкцией switch)
команду.

Оператор continue
может
использоваться в любой из трех форм
цикла, но не в операторе switch.
Его выполнение приводит к такому
изменению логики программы, что остальные
операторы тела цикла пропускаются. Для
циклов for
или while
вслед за этим начинается новый шаг, а
для цикла do
while
проверяется
условие на выходе, и затем, если оно
оказывается истинным, выполняется
следующая итерация.

Пример:
for ( i=0; i<n; i++){ if ( i==j ) continue; x[i] = i*i; a =
a + x[i]*3;}

6 Что такое препроцессор. Директивы препроцессора (define, error, условной компиляции) языка си.

Препроцессор-
программа, выполняющая обработку данных
для другой программы. Препроцессор
языка си рассматривает программу до
компиляции.

Директива #include
включает в текст программы содержимое
указанного файла. Эта директива имеет
две формы: #include
«имя файла» #include
<имя файла>

Если имя файла
указано в кавычках, то поиск файла
осуществляется в соответствии с заданным
маршрутом, а при его отсутствии в текущем
каталоге. Если имя файла задано в угловых
скобках, то поиск файла производится в
стандартных директориях операционной
системы, задаваемых командой PATH.

Директива #include
может быть вложенной, т.е. во включаемом
файле тоже может содержаться директива
#include, которая замещается после включения
файла, содержащего эту директиву.

Директива #define
служит для замены часто использующихся
констант, ключевых слов, операторов или
выражений некоторыми идентификаторами.
Идентификаторы, заменяющие текстовые
или числовые константы, называют
именованными константами. Директива
#define имеет две синтаксические формы:
#define идентификатор текст #define
идентификатор (список параметров) текст

Эта директива
заменяет все последующие вхождения
идентификатора на текст. Такой процесс
называется макроподстановкой. Текст
может представлять собой любой фрагмент
программы на СИ, а также может и
отсутствовать. В последнем случае все
экземпляры идентификатора удаляются
из программы.

Пример:
#define WIDTH 80 #define LENGTH (WIDTH+10)

Во второй
синтаксической форме в директиве #define
имеется список формальных параметров,
который может содержать один или
несколько идентификаторов, разделенных
запятыми. Формальные параметры в тексте
макроопределения отмечают позиции на
которые должны быть подставлены
фактические аргументы макровызова.
Каждый формальный параметр может
появиться в тексте макроопределения
несколько раз.

Директива #undef
используется для отмены действия
директивы #define. Синтаксис этой директивы
следующий #undef идентификатор Директива
отменяет действие текущего определения
#define для указанного идентификатора. Не
является ошибкой использование директивы
#undef для идентификатора, который не был
определен директивой #define.

Пример:
#undef
WIDTH
#undef
MAX

Эти директивы
отменяют определение именованной
константы WIDTH и макроопределения MAX.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]

  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #

Понравилась статья? Поделить с друзьями:

Читайте также:

  • Директ икс ошибка варзон
  • Директ 11 ошибка фортнайт
  • Дипломная работа должна быть переплетенная ошибка
  • Дипломант успешно защитил работу исправить ошибку
  • Дипломант конкурса лексическая ошибка

  • 0 0 голоса
    Рейтинг статьи
    Подписаться
    Уведомить о
    guest

    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии