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


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


лекция № 3 - Директивы препроцессора. Часть 1.

лекция № 3 - Директивы препроцессора. Часть 2.

Директивы препроцессора играют важную роль для языков программирования в которых есть препроцессор :-) 
Директивы начинаются с символа #
Смотрим ниже директивы препроцессора в языке Си.
#if
#ifdef
#ifndef
#elif
#else
#endif
#include
#define
#undef
#line
#error
#pragma 
Начнем с директивы #include, которая включает в текст программы содержимое указанного файла:
файл hello.h
int a;
файл main.c
#include "hello.h"
int main()
{
  return 0;
}
После обработки пропроцессора, компилятору уйдет следующий код
int a;
int main()
{
  return 0;
}
Как вы видите, препроцессор вставил содержимое файла hello.h в тот файл в котором было указано это сделать, в файл main.c
Проверить какой текст программы уйдет компилятору, можно с помощью gcc или cpp(эта программа по идее идет вместе с компилятором), нужно выполнить в терминале или консоле:
user@user:~/tmp/c_lng$ gcc -E main.c 
или
user@user:~/tmp/c_lng$ cpp main.c
на экране мы увидим
# 1 "main.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "main.c"
# 1 "hello.h" 1
 
int a;
# 2 "main.c" 2
int main()
{
  return 0;
}

результаты можно перенаправить в файл:
user@user:~/tmp/c_lng$ gcc -E main.c > mainafterpreproc.c  
или
user@user:~/tmp/c_lng$ cpp main.c > mainafterpreproc.c
Будет создан файл с именем mainafterpreproc.c в котором будет интересующая нас информация.
Если имя файла обрамлено треугольными скобками <a_file.h>, то поиск этого файла происходит по стандартным путям для операционной системы, по которым располагаются различные библиотеки.
Если имя файла обрамлено кавычками "a_file.h", то поиск этого начинается с пути по которому находится собственно говоря исходный код, то есть в локальной директории, если же там этого файла нет, значит поиск будет по стандартным путям.
Введите себе правило, если это стандартная библиотка то <>, если это ваша библиотка то "".
Стандартная - <stdio.h>
Ваша           - "hello.h"
Можно включать любые файлы с любым расширением, самое главное чтобы содержимое файла соответствовало правилам языка. 
Но есть принятые правила, .h заголовочные файлы, значит их и включаем.
Это не мешает вам создать файлы с константами, назвать его superhide.mp4 и включить его в свою программу :-) . Я не рекомендую так делать, потому что с такого начинается бардак.
С #include разобрались.

#define - определяет макрос или символьную константу.
#undef - отменяет ранее объявленный макрос или константу
#include <stdio.h>
#define DEBUG
#define HELLO_STR "hello!"
#define MIN_AGE 18
#define MAX_AGE 50
int main()
{
  printf("%s\n", HELLO_STR);
  printf("%i - %i\n", MIN_AGE, MAX_AGE);
#undef HELLO_STR
  printf("%s\n", HELLO_STR);              // compilation error
  return 0;
} 
Чтобы данный код собрался, закомментируйте строку с комментарием 
// compilation error
#define DEBUG - объявляет символьную константу без значения, просто вводит слово DEBUG так сказать в программу, и в коде программы, используя #if мы можем проверять, есть ли DEBUG, если есть то можем выводить отладочные сообщения на экран, если же нету(просто закомментировать объявление), то отладочные сообщения не нужны. Об этом ниже.
#define HELLO_STR "hello!" - объявляет строковую константу имя которой HELLO_STR а значение "hello!".
#define MIN_AGE 18
#define MAX_AGE 50 - аналогично объявляет целочисленные константы, для минимального возраста и максимального.
Препроцессор когда видит в коде использование таких констант, просто вместо них подставляет их значения. Вы можете в этом убедиться когда посмотрите на результат работы препроцессора, об этом было выше.
#undef HELLO_STR - отменяет объявленную ранее константу HELLO_STR, поэтому и ошибка.
Поэксперементируйте с этим кодом.

Директивы #if, #ifdef, #ifndef, вместе с директивами #elif, #else , #endif обычно управляют сборкой исходного файла.
#if - проверка истинности какого-либо условия, если выражение истинно(объявлена ли константа или нет),  код включаетсяв сборку. Зачастую проверяют объявлены ли константы или нет с помощью слова defined( an expression ).
#include <stdio.h>
#define DEBUG
int main()
{
#if defined(DEBUG)
  printf("%s:%i:%s - debug message\n", __FILE__, __LINE__, __FUNCTION__);
#endif
  return 0;
}
В этом коде мы вводим константу DEBUG у которой нет значения, просто строковый литерал, если так можно выразиться. И с помощью директивы
#if defined(DEBUG) - проверяем, версия программы для отладки? Если да, то выводим отладочное сообщение.
Константы __FILE__, __LINE__, __FUNCTION__  означают что во время предкомпиляции(во время обработки кода препроцессором), вместо этих констант будут подставлены - имя файла, номер линии, имя функции.
Эти константы поддрерживаются всеми популярными компиляторами.
Вы могли уже видеть в заголовочных файлах следующую конструкцию:
#ifndef _FILE_H_
#define _FILE_H_
...
#endif
Это защита от повторного включения файла. Если перевести на человеческий язык, то получится следующее -
если нет объявления _FILE_H_
объявить _FILE_H_
 ...
блок  if/ifndef/ifdef закончен.
файл main.c
#include "hello.h"
#include "hello.h"
int main()
{
  return 0;
}
файл hello.h
int a = 0;
При попытке собрать данный код, будет ошибка сборки, так как содержимое файла hello.h будет включено дважды. Значит будет попытка повторно объявить и инициализировать переменную a. Попробуйте.
Но! если мы, изменим наш hello.h следующим образом:
#ifndef HELLO_H
#define HELLO_H
int a = 0;
#endif // HELLO_H 
То все будет в порядке, так как при повторной попытке включения, будет проверка #ifndef HELLO_H - что означает, не было ли объявления HELLO_H, что на самом деле было при первом включении, значит код между #ifndef и #endif повторно включен не будет.
Также с помощью директив, если ваш код имеет заявку на кроссплатформенность(одного языка мало, обычно для программного обеспечения нужны еще и сторонние библиотеки), можно проверять под какой платформой, или каким компилятором код собирается, и включать нужные для этой платформы заголовочные файлы.  
Макросы по которым можно определить платформу, можно посмотреть тут или тут, макросы для определения компилятора тут, но это один из вариантов, поищите в интернетах больше информации по этому поводу.
Теперь посмотрите на код ниже, запустите его у себя, подкорректируйте его, если нужно. Думаю разобраться с кодом труда не составит.
#include <stdio.h>
int main()
{
#if defined(__GNUC__)
  printf("GNU Compiler detected!\n");
  #if defined(__gnu_linux__)
    printf("Linux OS detected!\n");
   #elif defined(__WIN32__) || defined(__WIN64__)
      printf("Windows OS detected!\n");
   #else
    printf("Unknown OS detected!\n");
  #endif
#endif
 
#if defined(__MINGW32__) || defined(__MINGW64__)
  printf("MINGW Compiler detected!\n");
#endif
 
#if defined(_MSC_VER)
  printf("Microsoft Compiler detected!\n");
#endif
  return 0;
}

Директива #line - указание имени файла и номера текущей строки для компилятора, в нормальной жизни программиста обычно не используется
 :-)

#error  - вывод сообщения в консоль сборки и остановка сборки
int main()
{
#if !defined(__GNUC__)
  #error "only GNU Compiler"
#endif
  return 0;
} 
В этом коде мы проверяем, если макрос __GNUC__ отсутствует, то мы выводим сообщение о том что нужен только GNU компилятор. Соответственно сборка останавливается, и в консоль выводится наше сообщение.

#warning - вывод сообщения без остановки сборки, в основном для вывода информационных сообщений в консоль сборки
int main()
{
  #warning "Need refactoring"
  #warning "This version not tested"
#if !defined(__GNUC__)
  #warrning "tested only on GNU Compiler"
#endif
  return 0;
}
#pragma - указание действия, зависящего от реализации, для препроцессора или компилятора. У каждого компилятора может быть свой набор действий. Нужно читать справку к компилятору.
К примеру у компилятора в поставке с Microsoft Visual Studio есть возможность отключать для определенных файлов, предупреждения компилятора:
#pragma warning (disable:4700) - отключает вывод предупреждения 
компилятора(с номером 4700) в консоль сборки, для того файла в начале 
которого указана эта инструкция.
у Microsof для запрета повторной сборки была такая инструкция 
#pragma once 
 вместо православного  
#ifndef _H_ 
#define _H_ 
#endif
Позднее компиляторы GCC стали тоже поддерживать эту инструкцию, поэтому в начале заголовочных файлов можно также писать
#pragma once

Кажется всё! Если что забыл, пишите!
Спасибо за внимание!

Комментарии