Programujeme v C++ (3) - Preprocesor

Programujeme v C++ (3) - Preprocesor
02.09.2008 09:45 | Články | Miroslav Bendík

V tejto časti seriálu sa naučíme používať preprocesor. Ukážeme si pár príkladov využitia preprocesora v spolupráci s CMake. Na záver si ukážeme skutočný príklad s použitím ncurses.

Preprocesor má za úlohu upraviť zdrojový kód pred samotnou kompiláciou. Kompilátor ho spúšťa väčšinou automaticky. V prípade, že používame gcc môžeme spustiť preprocesor bez toho aby sme museli program kompilovať. Po spracovaní zdrojového kódu preprocesorom neostanú v zdrojovom kóde direktívy pre preprocesor, ani komentáre.

Direktívy pre preprocesor začínajú znakom # (sharp). Pred znakom # nesmú byť žiadne znaky, ani medzery. Direktívy končia novým riadkom. Ak chceme v písaní direktívy pokračovať na ďalšom riadku musíme na konci napísať spätné lomítko (\);

Makrá

Makrá bez argumentov sú často nazývané symbolickými konštantami. Preprocesor vyhľadá názvy makier v programe a nahradí ich hodnotou makra. Definícia symbolickej konštanty môže vyzerať nasledovne:

#define PI 3.14

V kóde sa môžeme na konštantu odvolávať rovnakým spôsobom ako keby to bola obyčajná premenná. Preprocesor v skutočnosti nahradí všetky výskyty reťazca PI v súbore, ktoré nie sú súčasťou reťazcov alebo ktoré nie sú v komentároch výrazom 3.14.

Hodnota konštanty je nepovinná a nemusíme ju písať v prípade, že ju nepotrebujeme. Typickým príkladom je:

#ifndef HLAVIČKA_H
#define HLAVIČKA_H#endif HLAVIČKA_H

Na zrušenie definície symbolickej konštanty alebo makra použijeme direktívu undef.

#undef názov_konštanty

Podmienený preklad

Veľmi často sa stáva, že pri kompilácii potrebujeme za určitej podmienky zmeniť alebo vypustiť časť zdrojového kódu. Časti kódu vo vetve, ktorá nie je splnená sa skutočne odstránia z kódu a nebudú vôbec skompilované. Vďaka tomu je program z ktorého odstránime napríklad ladiace výpisy menší, ale poskytuje menej informácií o svojom behu.

Direktíva #if vyhodnocuje výraz, ktorý za ňou nasleduje. Podmienka končí direktívou #endif. Za #if môže nasledovať aj ďalšia podmienka #elif ktorá funguje rovnako ako #if. Direktíva #elif sa vyhodnotí len v prípade, že predchádzajúca podmienka nebola splnená.

#define LIB_VER 2
#define DEBUG

#if LIB_VER == 2
// kód pre verziu 2
#elif LIB_VER == 3
// kód pre verziu 3
#else
// knižnica nebola nájdená
#endif

#if defined(DEBUG)
printf("Verzia : %d\n", LIB_VER);
#endif

Vo výrazoch je možné používať aj logické operátory. Ako podmienku môžeme použiť napríklad #if !defined DEBUG && LIB_VER > 1.

Zápis #if defined sa dá skrátiť na #ifdef a #if !defined sa dá skrátiť na #ifndef.

Makrá s argumentami

Definícia makra s argumentami sa podobá na funkciu. Argumenty makra nasledujú po názve makra a sú uzavreté v okrúhlych zátvorkách. Argumenty oddeľujeme čiarkou.

#define MAX(a,b) a>b ? a:b

Pozor treba dávať hlavne na to, že medzi názvom makra a okrúhlymi zátvorkami nesmú byť medzery pretože v tom prípade by preprocesor vyhodnotil makro MAX ako makro bez argumentov.

Keď v programe napíšeme výraz z = MAX(x, y); preprocesor ho zmení na z = x>y ? x:y;.

Pri použití makier treba dávať pozor hlavne na to, že fungujú len na textovej úrovni. Ak máme napríklad takéto makro:

#define PODIEL(a,b) a/b
int x = PODIEL(2-1, 1+1);

Kompilátor to nebude chápať ako podiel 1/2, ale pretransformuje to na int a = 2-1/1+1;. C++ používa prioritu operátorov a preto najskôť vyráta podiel a až potom sčítanie a odčítanie. Výsledok celého výrazu nebude teda 0.5 ale 2.

Štandardné makrá

Tieto makrá sú automaticky definované kompilátorom. Nie je možné ich zmeniť.

__LINE__intRiadok ktorého spracovanie práve prebieha
__FILE__stringAktuálny súbor
__DATE__stringDátum spustenia preprocesora
__TIME__stringČas spustenia preprocesora
__STDC__int (0 alebo 1)Ak kompilátor podporuje ANSI C je __STDC__ 1, inak 0
__cplusplusstring/int(1)Toto makro je definované, ak používame C++ kompilátor. Malo by obsahovať verziu štandardu C++ (napr. 199711L). Nie všetky kompilátory dodržiavajú štandard. Ak používame kompilátor g++ bude toto makro namiesto verzie obsahovať číslo 1.

Okrem štandardných makier existujú makrá špecifické pre preprocesor, ktorý používame.

Operátory # a ##

Na miesto operátora # vloží preprocesor hodnotu argumentu ako string (aj s úvodzovkami). Operátorom ## môžeme spájať 2 reťazce. Výsledok nie je v úvodzovkách.

#include <cstdio>

#define mkstr(a) #a
#define premenna_j(cis) j_ ## cis
#define vypis_vyraz(vyraz) printf("Hodnota vyrazu \"%s\" je %d\n", #vyraz, (vyraz))

using namespace std;

int main(int argc, char *argv[])
{
	int j_2 = 5;
	printf("%s\n", mkstr(Hello));
	printf("%d\n", premenna_j(2));
	vypis_vyraz(2 + 3);
	return 0;
}

Tento program transformuje preprocesor nasledovne:

using namespace std;

int main(int argc, char *argv[])
{
 int j_2 = 5;
 printf("%s\n", "Hello");
 printf("%d\n", j_2);
 printf("Hodnota vyrazu \"%s\" je %d\n", "2 + 3", (2 + 3));
 return 0;
}

Prečíslovanie zdrojového kódu

Direktívou #line číslo riadku je možné zmeniť číslovanie od nasledujúceho riadku. Ak máme teda #line 200, nasledujúci riadok bude mať poradové číslo 200, riadok za ním 201 …

Vyvolanie chyby

Na vyvolanie chyby podporuje preprocesor direktívu #error správa. Ako sa chyba prejaví záleží od implementácie kompilátora. Kompilátor g++ vypíše chybu na štandardný chybový výstup (stderr).

Voľby pre preprocesor

Preprocesor integrovaný v kompilátore g++ je možné riadiť parametrami z príkazového riadku.

Spustenie preprocesora bez kompilácie

Ak si chcete vyskúšať, čo presne robí preprocesor s kódom stačí pridať kompilátoru voľbu -E. Príkaz g++ -E subor.cpp vypíše na štandardný výstup súbor spracovaný preprocesorom.

Definícia makra

Makrá sa dajú pridávať priamo voľbami gcc. Makrá sa pridávajú voľbou -D názov_makra. V tomto prípade bude hodnota makra 1. Ak chceme definovať aj hodnotu makra za názov makra pridáme =hodnota_makra naprílad -D LIB_VER=2. Takto definované makrá sa dajú elegantne využiť na podmienenú kompiláciu.

#include <cstdio>

using namespace std;

int main(int argc, char *argv[])
{
#if (LIB_VER == 3)
	printf("Verzia 3\n");
#else
	printf("Verzia nebola rozpoznana\n");
#endif
	return 0;
}

Ak tento súbor skompilujeme príkazom g++ subor.cpp -o subor a spustíme výsledný súbor, program vypíše "Verzia nebola rozpoznana". Ak k voľbám kompilátora pridáme -D LIB_VER=3 podmienka LIB_VER == 3 bude spustená a program vypíše "Verzia 3".

Využitie preprocesora v cmake

Väčšina linuxových programov má niektoré závislosti voliteľné. V týchto prípadoch je najlepšia možnosť vôbec nekompilovať časti kódu závislé na nesplnených závislostiach. Na tento účel sa dá využiť podmienená kompilácia. Prvý problém, ktorý musíme vyriešiť je to, ako odovzdáme informáciu preprocesoru o tom čo kompilovať a čo nie. Na výber máme buď pridanie volieb pre preprocesor, čo je ale dosť nevhodné riešenie ak chceme vytvoriť väčšie množstvo makier. Omnoho vhodnejšie riešenie je vytvoriť súbor config.h v ktorom budú požadované definície.

Definovanie makier voľbami gcc

Najskôr si ukážeme prvú metódu. Projekt sa bude skladať z 2 súborov - main.cpp a CMakeLists.txt.

// main.cpp
#include <cstdio>

using namespace std;

int main(int argc, char *argv[])
{
#ifdef DEFINOVANE
	printf("Definovane\n");
#else
	printf("Nedefinovane\n");
#endif
	return 0;
}
# CMakeLists.txt
PROJECT(main)

ADD_DEFINITIONS(-DDEFINOVANE=1)

ADD_EXECUTABLE(main main.cpp)

Program skompilujeme a spustíme obvyklým spôsobom, teda príkazmi cmake ., make a ./main. Príkaz ADD_DEFINITIONS pridá k voľbám pre preprocesor definíciu makra DEFINOVANE=1. Po spustení teda program vypíše "Definovane".

Definícia môže byť samozrejme aj podmienená. Je možné napríklad ponechať výber voliteľných komponentov na užívateľovi. Ak chceme dať používateľovi možnosti na výber, použijeme cmake príkaz OPTION. Tento príkaz má 2 povinné parametre - premennú do ktorej sa uloží užívateľom zadaná hodnota a popis voľby. Posledným nepovinným parametrom je počiatočná hodnota. S použitím príkazu OPTION upravíme CMakeLists.txt nasledovne:

PROJECT(main)

OPTION(DEFINOVANE "Definovane, alebo nie, snad jasne" ON)

IF (DEFINOVANE)
	ADD_DEFINITIONS(-DDEFINOVANE=1)
ENDIF (DEFINOVANE)

ADD_EXECUTABLE(main main.cpp)

Ak chceme teraz skompilovať bez voľby -DDEFINOVANE=1 spustíme cmake s voľbou -DDEFINOVANE=OFF. Výsledný príkaz bude teda cmake . -DDEFINOVANE=OFF. V prípade, že neuvedieme akú hodnotu DEFINOVANE chceme použiť, bude použitá štandardná hodnota teda v našom prípade ON.

CMake podporuje ešte jeden spôsob ako nastaviť užívateľské voľby. Je ním jednoduché terminálové rozhranie. Na platforme Windows je možné využiť gui, ktoré zatiaľ na linuxe nie je dostupné. Ak chceme konfigurovať voľby terminálovom rozhraní spustíme príkaz ccmake .. Po chvíľke sa zobrazí konfiguračný dialóg.

Po stlačení c prebehne konfigurácia. Po konfigurácii je možné zmeniť voľby. Pozor treba dať na to, že po zmene nastavení je nutné znovu spustiť konfiguráciu (c).

Nástroj ccmake robí to isté čo cmake, preto po ukončení konfiguračného dialógu (q) nie je potrebné znovu spúšťať cmake.

Definovanie makier v samostatnom súbore

Nástroje ako automake a CMake dokážu zo vstupného súboru (tzv. šablóny) vytvoriť hlavičkový súbor s definíciami, ktorý môžeme využiť v zdrojovom kóde. Šablóny majú štandardne príponu *.h.in. V tomto súbore sa všetky výskyty ${PREMENNA} alebo @PREMENNA@ nahradia hodnotou premennej PREMENNA. Ak táto premenná nie je definovaná, nahradenie prebehne rovnako, ako keby bola premenná prázdny reťazec. Okrem toho môžeme vo vstupnom súbore použiť #cmakedefine PREMENNA, ktoré sa nahradí reťazcom #define PREMENNA ak je premenná definovaná, alebo /* #undef PREMENNA */ v prípade, že premená nie je definovaná, alebo jej hodnota je nepravdivá.

Po vytvorení šablóny stačí jediný príkaz v CMakeLists.txt na to, aby sa vygeneroval výstupný hlavičkový súbor. Príkaz, ktorým určíme vstupný a výstupný súbor je configure_file(vstupný_súbor výstupný_súbor). Pri spustení cmake sa zo vstupného súboru (napr. config.h.in) vytvorí výstupný súbor (napr. config.h). Aby sme mohli definície vo výstupnom súbore využiť pri kompilácii, musíme vzniknutý hlavičkový súbor včleniť do zdrojových kódov (napr. #include "config.h").

Aby sme si ukázali ako to funguje, upravíme si projekt tak, aby boli definície v súbore config.h. Šablónou bude súbor config.h.in. Najskôr si upravíme súbor CMakeLists.txt nasledovne:

PROJECT(main)

INCLUDE_DIRECTORIES ("${PROJECT_BINARY_DIR}")

OPTION(DEFINOVANE "Definovane, alebo nie, snad jasne" ON)
SET(VERZIA 3)

CONFIGURE_FILE (
  "${PROJECT_SOURCE_DIR}/config.h.in"
  "${PROJECT_BINARY_DIR}/config.h"
  )

ADD_EXECUTABLE(main main.cpp)

Na začiatku súboru main.cpp včleníme konfiguračný súbor config.h.

// main.cpp
#include "config.h"

Do súboru config.h.in vložíme definíciu makier DEFINOVANE a VERZIA. Makro VERZIA sa bude vo výslednom súbore config.h vyskytovať vždy aj keby premenná VERZIA nebola definovaná v súbore CMakeLists.txt. Makro DEFINOVANE sa bude vo výslednom súbore vyskytovať len v prípade, že bude definovaná premenná DEFINOVANE a bude mať pravdivú hodnotu (ON).

// config.h.in
#cmakedefine DEFINOVANE
#define VERZIA @VERZIA@

Projekt stačí už len skompilovať štandardným spôsobom a spustiť výsledný súbor main;

Využívanie knižníc v programoch

Pri programovaní v C++ sa budeme stretávať s 2 druhmi knižníc. Sú to statické a dynamické knižnice. Statické knižnice majú príponu *.a. Sú to štandardné ar (archiver) archívy. Tieto knižnice sa pri kompilácii stávajú súčasťou programu. Vďaka nim teda bude náš program fungovať aj na počítačoch, kde nie je táto knižnica dostupná pretože ju má program priamo v sebe. Nevýhodou je to, že výsledný program je väčší.

Dynamické knižnice majú príponu *.so. Nedajú sa včleniť priamo do spustiteľného súboru. Pri spustení súboru dochádza k tzv. dynamickému linkovaniu. Dynamické knižnice sa načítavajú do RAM jediný krát aj v prípade, že sú využívané niekoľkými aplikáciami súčasne. Statická knižnica sa načítava do RAM pre každý program zvlášť.

Statické knižnice

Na vytvorenie statickej knižnice pomocou cmake nám stačí vedieť jediný príkaz - add_library(knižnica súbor1.cpp súbor2.cpp …). Názov knižnice sa píše bez prípony. Statická knižnica sa linkuje k programu príkazom target_link_libraries(cieľový_súbor knižnica1 knižnica2 …).

Príkladom sa vrátime k predchádzajúcej časti. Využijeme zdrojové kódy hello_main.cpp, hello_lib.cpp a hello_lib.h z predchádzajúcej časti a vytvoríme z nich adresár s nasledujúcou štruktúrou.

`- CMakeLists.txt
`- hello_main.cpp
`- lib
   `- CMakeLists.txt
   `- hello_lib.cpp
   `- hello_lib.h

Súbor CMakeLists.txt v hlavnom adresári projektu bude mať nasledujúci obsah:

project (hello)

include_directories ("${PROJECT_BINARY_DIR}/lib")

add_subdirectory (lib)
set (EXTRA_LIBS ${EXTRA_LIBS} HelloKniznica)

add_executable (hello hello_main.cpp)
target_link_libraries (hello ${EXTRA_LIBS})

Príkazom add_subdirectory(adresár) sa pridá do kompilačného procesu podadresár v našom projekte. V podadresári musí existovať súbor CMakeLists.txt, ktorý zaistí kompiláciu súborov v podadresári.

Príkazom set(premenná hodnota1 hodnota2 …) nastavujeme hodnotu premennej. V tomto príklade vkladáme do premennej EXTRA_LIBS hodnotu EXTRA_LIBS a "HelloKniznica". Dalo by sa povedať, že v tomto prípade sa pridáva k hodnote premennej EXTRA_LIBS názov knižnice "HelloKniznica". Linkovať takýmto spôsobom je veľmi výhodné v prípade, že linkujeme niekoľko knižníc. V tomto príklade som vôbec nemusel názov knižnice dávať do premennej. Úplne by postačovalo keby som posledný riadok prepísal na target_link_libraries (hello HelloKniznica) a všetko by fungovalo rovnako ako teraz.

Súbor CMakeLists.txt v adresári lib bude mať jediný riadok:

add_library(HelloKniznica hello_lib.cpp)

Pre kompiláciu projektu spustíme v hlavnom adresári cmake . && make.

Dynamické knižnice

Pre vytvorenie programu s dynamicky linkovanou knižnicou stačí pridať slovo SHARED do lib/CMakeLists.txt.

add_library(HelloKniznica SHARED hello_lib.cpp)

Kompilácia prebieha rovnako, ako v predchádzajúcom prípade. Výsledný program by mal po spustení načítať súbor HelloKniznica.so. Ak tento súbor neexistuje, linker vypíše chybu.

Okrem vlastných knižníc je možné linkovať aj systémové knižnice. Na to, aby sme ich mohli využiť musíme mať nainštalované odpovedajúce devel balíčky. Pri nasledujúcom príklade využijem knižnicu ncurses.

Náš projekt sa bude skladať len zo súborov main.cpp a CMakeLists.txt. Súbor main.cpp bude vyzerať nasledovne:

#include <cstdio>
#include <curses.h>
#include <signal.h>
#include <cstdlib>

using namespace std;

static void finish(int sig);

int main(int argc, char *argv)
{
	signal(SIGINT, finish);
	initscr();
	addstr("Hello world!");
	getch();
	finish(0);
	return 0;
}


static void finish(int sig)
{
	endwin();
	exit(0);
}

V programe si najskôr deklarujeme funkciu finish. V hlavnom programe zaistíme, že program po obdržaní sitnálu SIGINT korektne ukončí funkciu curses. Funkcii signal posielame 2 parametre a to signál, pri ktorom sa funkcia vykoná a referenciu na funkciu, ktorá sa má vykonať. Ak by sme to neurobili, po ukončení programu (napríklad klávesovou skratkou Ctrl + C) by sa nastavenia terminálu nevrátili na pôvodnú hodnotu. Funkcia initscr inicializuje terminál. Funkciou addstr pridáme text "Hello world!" na aktuálnu pozíciu kurzoru. Potom čakáme na stlačenie klávesy a ukončíme program.

project(curses)

add_definitions(-Wall -O2 -g)

include(FindCurses)

IF(NOT CURSES_LIBRARIES)
	message (FATAL_ERROR "Kniznica curses nebola najdena")
ENDIF(NOT CURSES_LIBRARIES)

message("Kompilujeme s curses")

LINK_LIBRARIES(${CURSES_LIBRARIES})
INCLUDE_DIRECTORIES(${CURSES_INCLUDE_DIR})

ADD_EXECUTABLE(curses main.cpp)

V súbore CMakeLists.txt si príkazom include vyžiadame modul FindCurses. Ak v systéme nenájdeme knižnicu curses vyvoláme príkazom message chybu. Ak sa knižnica nachádza v systéme, vypíšeme informáciu o tom, že kompilácia prebehne s podporou curses. Príkaz link_libraries funguje rovnako ako target_link_libraries. Rozdiel je len v tom, že link_libraries funguje globálne pre všetky ciele. Program teda zlinkujeme s podporou curses a pridáme k include adresárom adresár, kde máme nainštalované hlavičkové súbory kničnice curses.

Generátory

Doteraz sme pomocou cmake generovali len Unix Makefile súbory. Okrem nich môžeme pomocou CMake generovať ďalšie typy projektov. Ak chceme vygenerovať napríklad KDevelop3 projekt použijeme voľbu -G KDevelop3. Po spustení cmake -G KDevelop3 . sa vygeneruje súbor s projektom, ktorý sa dá otvoriť pomocou KDevelop-u. Kompletný zoznam generátorov je dostupný v oficiálnej dokumentácii k CMake.

Dnes sme sa naučili používať preprocesor. Okrem toho sme si ukázali pokročilejšie techniky používania CMake. V budúcej časti sa už budeme venovať operátorom new, delete, preťažovaniu funkcií, implicitným hodnotám parametrov. Okrem toho sa v budúcej časti konečne začneme venovať triedam a objektom v C++.

    • Suprový článok 03.09.2008 | 00:02
      Avatar Izidor Matušov Archlinux  Používateľ
      Tento článok sa autorovi vydaril. Už sa teším na ďalší diel!
    • Diki 11.09.2008 | 14:20
      Avatar Castler OpenSuse 12.1 KDE4.7  Používateľ
      Chcem sa podakovat autorovi, za clanok, dufam ze sa neopakujem, ale je to poucne, pisane pristupnou formou (aspon mne a to niesom vobec nejaky vedator), a je to nanajvys aktualne. Super aj ja sa tesim na pokracovanie.
      PS: prosim pis dalej take dobre clanky a co najviac :o)
      Viem, že nič neviem.
    • diky 27.11.2008 | 22:40
      Avatar Martin Ďurec Ubuntu (hardy)  Používateľ
      suhlasim...ani ako zaciatocnik s c++ som pri citani clanku nestratil prehlad...napisane vystizne a k veci
      ...a tak som ho teda vyhodil von oknom