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

Язык Си / Директивы препроцессора в Си

 

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

Основные директивы препроцессора

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

Директива #include

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

 
 
#include <stdio.h>
#include "func.c"


Директива #define

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

 
#define Идентификатор Замена

Поля Идентификатор и Замена разделяются одним или несколькими пробелами.
Директива #define указывает компилятору, что нужно подставить строку, определенную аргументом Замена, вместо каждого аргумента Идентификатор в исходном файле. Идентификатор не заменяется, если он находится в комментарии, в строке или как часть более длинного идентификатора.
1
2
3
4
5
6
7
8
#include <stdio.h>
#define A 3
int main()
{
  printf("%d + %d = %d", A, A, A+A); // 3 + 3 = 6
  getchar();
  return 0;
}


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

  • U или u представляет целую константу в беззнаковой форме (unsigned);
  • F (или f) позволяет описать вещественную константу типа float;
  • L (или l) позволяет выделить целой константе 8 байт (long int);
  • L (или l) позволяет описать вещественную константу типа long double

 
 
 
 
 
 
 
#define A 280U   // unsigned int
#define B 280LU  // unsigned long int
#define C 280    // int (long int)
#define D 280L   // long int
#define K 28.0   // double
#define L 28.0F  // float
#define M 28.0L  // long double


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

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

Пример на Си: Вычисление синуса угла
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#define PI 3.14159265
#define SIN(x) sin(PI*x/180)
int main()
{
  int c;
  system("chcp 1251");
  system("cls");
  printf("Введите угол в градусах: ");
  scanf("%d", &c);
  printf("sin(%d)=%lf", c, SIN(c));
  getchar(); getchar();
  return 0;
}

Результат выполнения
Директива define

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

Однако при использовании таких макроопределений следует соблюдать осторожность, например

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#define sum(A,B) A+B
int main()
{
  int a, b, c, d;
  a = 3; b = 5;
  c = (a + b) * 2; // c = (a + b)*2
  d = sum(a, b) * 2; // d = a + b*2;
  printf(" a = %d\n b = %d\n", a, b);
  printf(" c = %d \n d = %d \n", c, d);
  getchar();
  return 0;
}

Результат выполнения:
Использование макроопределений define
По умолчанию текст макроопределения должен размещаться на одной строке. Если требуется перенести текст макроопределения на новую строку, то в конце текущей строки ставится символ "обратный слеш" — \.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#define sum(A,B) A + \
                 B
int main()
{
  int a, b, c, d;
  a = 3; b = 5;
  c = (a + b) * 2; // c = (a + b)*2
  d = sum(a, b) * 2; // d = a + b*2;
  printf(" a = %d\n b = %d\n", a, b);
  printf(" c = %d \n d = %d \n", c, d);
  getchar();
  return 0;
}

Кроме того, директива #define позволяет замещать часть идентификатора. Для указания замещаемой части используется ##.
1
2
3
4
5
6
7
8
9
#include <stdio.h>
#define SUM(x,y) (a##x + a##y)
int main()
{
  int a1 = 5, a2 = 3;
  printf("%d", SUM(1, 2)); // (a1 + a2)
  getchar();
  return 0;
}

Результат выполнения:
Использование ## в директиве #define

Условная компиляция

Директивы #if или #ifdef/#ifndef вместе с директивами #elif, #else и #endif управляют компиляцией частей исходного файла.
Если указанное выражение после #if имеет ненулевое значение, в записи преобразования сохраняется группа строк, следующая сразу за директивой #if. Синтаксис условной директивы следующий:

1
2
3
4
5
6
7
#if константное выражение
   группа операций
#elif константное выражение
   группа операций
#else
   группа операций
#endif

Отличие директив  #ifdef/#ifndef заключается в том, что константное выражение может быть задано только с помощью #define.

У каждой директивы #if в исходном файле должна быть соответствующая закрывающая директива #endif. Между директивами #if и #endif может располагаться любое количество директив #elif, однако допускается не более одной директивы #else. Директива #else, если присутствует, должна быть последней перед директивой #endif.

Пример

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdlib.h>
#define P 2
int main()
{
  system("chcp 1251");
  system("cls");
#if P==1
  printf("Выполняется ветка 1");
#elif P==2
  printf("Выполняется ветка 2, P=%d", P);
#else
  printf("Выполняется другая ветка, P=%d", P);
#endif
  getchar();
  return 0;
}

Результат выполнения
Условная компиляция


Назад: Язык Си

Комментариев к записи: 30

  • Константин Петрович Львов
    Допустима такая конструкция в BCB6
    1
    2
    3
    4
    #define GSW_SAAR_DATA \
    UNUSED int  deli[4] = {0,1,1,0}, delj[4] = {0,0,1,1}, npan = 6; \
    UNUSED double  longs_pan[6] = {260.00, 272.59, 276.50, 278.65, 280.73, 292.0},\
      lats_pan[6] = { 19.55,  13.97,   9.60,   8.10,   9.33,   3.4}

  • Григорий
    Здравствуйте ув. Елена ! Подскажите пожалуйста какой будет результат. (0x0319 && 0x0000)=? . Мне нужно задать координаты а одна из координат всегда 0 . но другая 319 значит пиксели нужно рисовать от Х0 до Х319 . Заранее спасибо!

    • Елена Вставская
      Результат будет 0. И не совсем понятно, при чем тут координаты.

  • Станислав
    Добрый день! Подскажите, пожалуйста, как сделать так, чтобы макроопределение #define объявлялось только в одном файле проекта, а использовалось во всех? Возможно ли это?

  • Григорий
    Уважаемая Елена! Не могли б Вы помочь разобраться в ошибке которая возникает в АтмелСтудии. Я создаю библиотеку "h" компилятор заголовок делает сам, в процессе сборки выдает ошибку unterminated #ifndef SPI_INI_H_. где искать ошибку? Заранее спасибо!

    • Елена Вставская
      Слишком мало информации, чтобы помочь. Кроме того, я не работала с Atmel Studio

      • Григорий
        Попробую уменьшить код, чтоб можно было выложить. Спасибо что оперативно отвечаете!!!

        • Елена Вставская
          Можете прислать в личные сообщения через форму на сайте.

  • добрый день , подскажите пожалуйста зачем использовать директивы #if, #else и.тд, если они по сути не отличаются от обычных операторов ветвления( объясните суть пж)

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


  • Дмитрий
    Помогите, пожалуйста, сделать define! Имеем
    1
    2
    #define PIN_D0 0xF0
    #define PIN_CLK PIN_D0
    Надо
    1
    #define EX_TO_ASM(_pin) /* ???? */
    Чтобы строка EX_TO_ASM(PIN_CLK) превратилась в
    1
    _asm("PIN_CLK: equ 0xF0");
    Из за кавычек не получается, голову сломал, толку ноль.. ((

    • Елена Вставская
      Не совсем понятна суть того, что должен выполнять ассемблер. Действие
      1
      _asm("PIN_CLK: equ 0xF0");
      Подставляем макроопределение для PIN_CLK и PIN_D0, получаем
      0xF0 equ 0xF0
      Зачем?

  • Дмитрий
    Помогите, кто может, разобраться с дефайнами.. Имеется
    1
    2
    #define PIN_D 0x0F
    #define PIN_CLK PIN_D0
    Нужно, чтоб
    1
    #define EXPORT(PIN_CLK) ???
    разворачивался в
    1
    _asm("PIN_CLK: equ 0x0F");
    Пожалуйста!!!!

  • Есть такой кусок кода:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #ifdef ALL
    #define PIN_0
    #define PIN_1
    #define PIN_2
    #define PIN_3
    #define PIN_4
    #define PIN_5
    #define PIN_6
    #define PIN_7
    #define PIN_8
    #define PIN_9
    #define PIN_10
    #define PIN_11
    #define PIN_12
    #define PIN_13
    #define PIN_14
    #define PIN_15
    #define PIN_16
    #define PIN_17
    #define PIN_18
    #define PIN_19
    #endif
    И он рабочий. Я не могу понять его, ведь у #define нет второго аргумента! Кто объяснит?

    • Елена Вставская
      Кода в данном куске нет, только макроопределения. Подобного типа директивы #define могут использоваться с последующим использованием директив условной компиляции. Например,
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      #include <iostream>
      using namespace std;
      #define PIN_0 // если закомментировать эту строчку, вывода не будет
      int main()
      {
        #ifdef PIN_0
        cout << "PIN_0";
        #endif
        cin.get();
        return 0;
      }
      Конечно, константе PIN_0 никакое значение не присвоено, и если мы попробуем присвоить ее какой-то переменной, то получим ошибку. Но подобный синтаксис вполне можно использовать в совокупности с директивами условной компиляции.

  • Добрый день! Не могу понять процессы компиляции и компоновки. Из прочитанной информации запомнил следующее: При директиве #Include < > препроцессор помещает в исходный файл содержимое файла библиотеки указанного в скобках. Дальше идет компиляция - перевод на машинный код и получается объектный файл. Потом компоновка - к объектному файлу добавляется код запуска и библиотечный код . Не могу понять для чего на этапе компоновки добавляется код файла библиотеки если еще до компиляции код файла библиотеки был помещен в исходный код. Подскажите пожалуйста.

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

  • Как работают макросы? Есть такой кусок кода (выжимка из h файлов stm32): (Проверяю в онлайн редакторе https://ideone.com/hehrYD)
    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
    #include <stdint.h>
    #include <stdio.h>
    #include <stdlib.h>

    #define     __IO    volatile             /*!< Defines 'read / write' permissions */
    /** 
      * @brief General Purpose I/O
      */


    typedef struct
    {
      __IO uint32_t CRL;
      __IO uint32_t CRH;
      __IO uint32_t IDR;
      __IO uint32_t ODR;
      __IO uint32_t BSRR;
      __IO uint32_t BRR;
      __IO uint32_t LCKR;
    } GPIO_TypeDef;

    #define PERIPH_BASE         0x40000000UL /*!< Peripheral base address in the alias region */
    #define APB1PERIPH_BASE     PERIPH_BASE
    #define APB2PERIPH_BASE     (PERIPH_BASE + 0x00010000UL)
    #define GPIOA_BASE          (APB2PERIPH_BASE + 0x00000800UL)
    #define GPIOA               ((GPIO_TypeDef *)GPIOA_BASE)

    int main()
    {
      int i = GPIOA;
      printf ("%x",i);  
      getchar();
      return 0;
    }
    результат 40010800 Вопрос: почему так работает
    1
    #define GPIOA               ((GPIO_TypeDef *)GPIOA_BASE)
    а так нет
    1
    #define GPIOA               ((GPIO_TypeDef )*GPIOA_BASE)
    или так тоже нет
    1
    #define GPIOA               ((GPIO_TypeDef )*(GPIOA_BASE))
    подозреваю что в одном случае интерпретирует запись как указатель, а в других как унарную операцию умножения. но ПОЧЕМУ? как понять в каком случае и как будет происходить интепрпретация? как происходить "разворачивание" макроса?

    • Потому что препроцессор - сугубо текстовый процессор. Он может ТОЛЬКО заменять один текст другим. Для того, чтобы понять что и почему так не работает нужно включить в опции компилятора вывод препроцессированного текста (в Keil: Project -> Options for ... -> вкладка Listing -> установить галочку чекбокса "C Preprocessor Listing"), вооружиться книжкой по Си, открыть раздел работы с указателями и внимательно смотреть.

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

    • Елена Вставская
      Ну, например, директивы условной компиляции позволяют по-разному генерировать исполнимый код в зависимости от каких-то параметров.
      Допустим, в зависимости от типа микроконтроллера, для которого пользователь генерирует свой проект, подключаются разные библиотеки с описаниями регистров.
      Или в зависимости от версии аппаратной платформы необходимо скомпилировать тот или иной код программы.
      То есть проверка условий в директивах препроцессора предусматривает проверку каких-то настраиваемых перед компиляцией константных значений.

      • Значит с помощью такой дерективы код будет работать на более низком уровне и от этого его лучшее качество, я правильно понял ???

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

    • // Думаю, понятно будет на примере. // Где-то в начале самого главного исходника среди всех исходников: #define FLAG_DEBUG_INFO 1 // 1=выводить подробную информацию в лог (соединения, набор данных в буферы, разбор команд) // Где-то в коде некой функции: #if FLAG_DEBUG_INFO unsigned char debbuff[32] printf(debbuff, "Command ok, command=%X", mycd); #endif // Если ещё не понятно, то поясню: а зачем нам в финальной, не отладочной версии, выделять буфер на целых 32 байта, тратить процессорное время, память, если в финальной версии не нужно выводить отладочную информацию. Этот код вообще не будет присутствовать в финальной сборке.

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *