C++11 fejlesztőeszközök
Czirkos Zoltán · 2024.08.20.
C++11 fordítók, parancssori fordítás és linkelés.
A mai alkalom a unixos fejlesztőeszközökről szól: parancssorból használható fordítóprogramokról, linkerekről és nyomkövetőkről. Ezért ma minden fordítást szándékosan a parancssorból fogunk elvégezni. Ennek célja az, hogy lássuk, „mi van a motorháztető alatt”, azaz mi az a munka, amelyet egy integrált fejlesztőkörnyezet a háttérben elvégez.
A programunk szegmentálása nem áll meg a függvényekre bontásnál: az összetartozó függvényeket különálló modulokba csoportosítjuk. A könnyű, hatékony kezelhetőség érdekében pedig több forrásfájlból építjük fel a programot. A feldarabolt, külön fordított programrészletet végén a linker segítségével rakjuk össze. Ezen laboralkalom célja a linkelés technikai részleteinek bemutatása is.
A laborokhoz
A laborok mellé minden héten lesz kiírva egy beadandó az admin portálon. Ide óra végén töltsd fel a forráskódokat (*.cpp, *.h) egy ZIP fájlba csomagolva! A feladatokat ezért külön projektben oldd majd meg, ne írd felül a megoldásokat.
Labor otthoni munkában
A feladatok kifejezetten unixos parancssori eszközökről szólnak. A lehetőségeid az alábbiak:
- MinGW-ben dolgozol Windowson, pl. a CodeBlocks mellé telepített verzióval.
- Saját Linuxon dolgozol.
A feladatokban nem programokat kell írni, ettől függetlenül keletkeznek majd általad megírt fájlok, azokat kell beadni.
A teljesítéshez az első négy feladatot meg kell oldani.
Nyiss egy parancssort vagy egy terminálablakot! A fordítóprogramok verziószáma a -v paraméterrel ellenőrizhető:
$ g++ -v gcc version 4.8.2 (Ubuntu 4.8.2-19ubuntu1)
CodeBlocks
C:\> cd C:\Program Files\MinGW-4.8.1\bin C:\Program Files\MinGW-4.8.1\bin> g++ -v gcc version 4.8.1 (tdm-2)
Windowson a telepítés módjától függően a Unix-kompatibilis MinGW fejlesztőkörnyezetek más mappába is települhettek. Az
alapbeállítás C:\MinGW-4.8.1. A fenti példa a HSZK-ban használt C:\Program Files\codes.v52\MinGW-4.8.1
mappát mutatja (ez lehet, hogy változott). A konkrét programok az ezen belüli bin mappában vannak.
Ha a HSZK-ban vagy, a mai laboron végzett munkához használd végig ugyanazt a parancssori ablakot, és állítsd be
abban a PATH környezeti változót, hogy a GCC programjai bármelyik mappából elérhetőek legyenek:
C:\> PATH=%PATH%;C:\Program Files\MinGW-4.8.1\bin
Indítsd el a Geany-t (Linuxon) vagy a CodeBlocks-ot (Windowson), és mentsd el a proba.c fájlba ezt a
programkódot:
#include <stdio.h>
int main(void) {
for (int i = 1; i <= 5; ++i)
printf("Hello %d!\n", i);
}
A Geany-hez érdemes tudni:
- Ctrl+S mentés.
- Shift+nyilak kijelölés, Ctrl-C, Ctrl-V szokásos.
- Ctrl+K sort töröl.
- Ctrl+D sort vagy kijelölést dupláz, Ctrl+E kommentel.
- F9 fordít és linkel, F5 futtat.
A programot alapbeállítás szerint valószínűleg nem fogja lefordítani a GCC fordító, mert annak nem alapértelmezése a C99
nyelv. A for() ciklus fejlécében definiált int i változó viszont C99 nyelvi elem. Használd ezért a
terminál ablakot (parancssort), és fordítsd le kézzel a programot:
$ gcc -std=c99 -Wall -Wdeprecated -pedantic proba.c -o proba
$ ./proba Windowson: proba
Hello 1!
Hello 2!
Hello 3!
Hello 4!
Hello 5!
A fordításhoz Linuxon használható a clang is. A clang paraméterezését úgy találták
ki, hogy teljesen csereszabatos legyen a gcc-vel, így a laboranyagban leírtak érvényesek mindkettőre. A Geany
beállításához lásd a fejlesztőkörnyezetekről szóló írást!
A program futtatásakor Linuxban azért kellett megadni az elérési utat: (./ a név elején), mert
Linux rendszerekben az aktuális könyvtár biztonsági okokból nem része a PATH-nak. Windowson elérési út beírása nélkül is indíthatóak
az aktuális mappa programjai.
A fenti opciók jelentése:
-std=c99: C99 módba kapcsolja a fordítót. Szokásos értékei még:c89,c11.-Wall: bekapcsolja a figyelmeztetéseket. (A-Wmég több figyelmeztetést mutat.)-Wdeprecated: engedélyezi a figyelmeztetéseket az olyan nyelvi elemek használatánál, amelyek elavultnak számítanak, és van helyettük modernebb megoldás.-pedantic: szigorú szabványkövető módba kapcsol. Tanulásnál hasznos!-o proba: megadja a kimeneti fájl, jelen esetben a futtatható (bináris) nevét. Windowson ebből automatikusanproba.exelesz.
A laborban használni kell majd még ezeket is:
-c: csak fordít, a linkelést nem végzi el (lásd lentebb).-g: nyomkövetéshez használható információ elhelyezése a programban (melyik függvény hol van stb.).-O0: optimalizálás nélkül fordítja a programot (nyomkövetéshez).-O2: a legtöbb optimalizálási módszert bekapcsolva fordítja a programot.
Tárgykód fájlok
Írj egy új programot hello.c néven, amelyben a kiírás külön függvényben van!
#include <stdio.h>
static void hello(int i) {
printf("Hello %d!\n", i);
}
int main(void) {
for (int i = 1; i <= 5; ++i)
hello(i);
}
Emlékezz vissza: a fordítás mindig két lépésből áll. Az első lépésben (fordítás, compile) a fordítóprogram (compiler) a forráskódot gépi kóddá alakítja, és ekkor keletkeznek a tárgykód (object) fájlok. Ezek még a globális változók és függvények név szerinti hivatkozásait tartalmazzák. A tárgykódokból lesz a linkelés (összeállítás, összeszerkesztés, „linkelés”, link) művelet után a futtatható program. A linkelés közben a név szerinti hivatkozásokat a linker feloldja. Ennek az egésznek az (is) az értelme, hogy különálló, kicsi, áttekinthető forrásfájlokból lehessen egy nagy programot építeni.
Ezeket a műveleteket a fordító megfelelő paraméterezésével külön is el lehet végezni. A -c paraméterrel azt lehet
jelezni, hogy csak fordítani kell, linkelni nem. Ilyenkor a -o a létrehozandó tárgykód fájl nevét adja meg. Ha
paraméterként tárgykód fájlokat kap a fordító, akkor pedig tudja, hogy azokat lefordítani már nem kell, csak linkelni. (A linkelést
igazából egy külön program, az ld végzi, de azt bonyolultabb közvetlenül paraméterezni.)
Végezd el külön a fenti projekt fájljainak fordítását és linkelését! Előbb a fordítást:
$ gcc -g -std=c11 -O0 main.c -c -o main.o
Ekkor előállt a main.o tárgykód fájl.
A parancssori nm alkalmazás megmutatja, hogy mi van egy tárgykód
fájlban. (Linuxon a man nm beírása után olvashatsz róla részletesebben.)
$ nm main.o
A kimeneten azt látod, hogy a tárgykód fájl tartalmaz két lefordított függvényt
(ezeket t és T betűk jelölik, ami a text
szóra utal). Van benne egy hivatkozás a printf-re, ezt pedig az
U csoportba sorolja a program. Az U undefined-ot,
definiálatlant jelent: ahhoz, hogy ezt a tárgykód fájlt programként használjuk,
elő kell majd kerítenünk a printf() függvény definícióját, lefordított változatát.
Linkelés
A tárgykódot linkelve a fordító ezt a libc-ből (a szabványos C
lefordított függvényeit tartalmazó függvénykönyvtár, C Library) előkeresi a printf()-et, és így
futtatható programot kapunk:
$ gcc main.o -o program
$ ./program Windows: program.exe
Unixon a linkeléshez használható a -lc paraméter is, amely azt jelenti,
hogy linkelni „-l” kell a „c” nevű könyvtárat is.
Ez annyira alapvető, hogy ezt kérés nélkül is meg szokta tenni a linker.
Az ldd paranccsal ellenőrizni lehet, hogy a bináris
mely könyvtárakhoz van linkelve. Ez a parancs csak Unixokon van, a MinGW-ből kihagyták.
Ugyancsak, a printf() a MinGW-ben kicsit másképp néz ki; van némi hókuszpókusz
a háttérben, hogy a MinGW-s kódból a Microsoft Visual C-ben lévő printf() elérhető legyen.
Ezek most nem lényegesek.
A statikus függvény
Emlékezz most vissza arra is Prog1-ből, mit jelentett a static
kulcsszó a függvény neve előtt. Ennek a többmodulos programoknál volt értelme:
a static globális változó vagy függvény esetén egy olyan
tárolási osztályt jelent (linkage), amely csak az adott fordítási egységben látszik
(internal linkage). A nem
statikus nevek, mint pl. a main, a teljes projektben látszanak, így
egyediek kell legyenek. A statikus neveknek azonban elég csak fordítási egységenként
egyedieknek lenniük; egy másik valami.c fájlban lehetne egy másik
hello() függvény is.
A hello() tehát csak a mostani main.c forrásfájlból
hívható függvény. Fordítsd le most a programot optimalizálással együtt (-O0
helyett -O2 kapcsolóval), állítsd
elő így a tárgykódot és vizsgáld meg az nm paranccsal! Látsz valami
különbséget? Mi okozza ezt a különbséget? Magyarázd meg!
$ gcc -g -std=c11 -O2 main.c -c -o main.o $ nm main.o
Megoldás
A függvény definíciója ilyenkor valószínűleg eltűnik a tárgykód fájlból.
Ez azért van, mert kicsi, rövid függvény lévén a fordító úgy dönt az optimalizálás,
gyorsabb futás érdekében kihagyja a függvényhívást, és a függvény törzsét beilleszti
a hívás helyére, megspórolva így az ide-oda ugrást és a paraméterátadás műveleteit.
Mivel a static miatt a fordító látja, hogy sehol máshol nem hívódik
a függvény, teljesen kihagyja azt a tárgykódból.
Több forráskód
Bontsd most két forráskódra a programot! A main.c-ben legyen a
main() függvény, a hello.c-ben a hello()
függvény, és írj az utóbbihoz egy hello.h fejlécfájlt is!
Fordítsd le linkelés nélkül külön a két forrásfájlt, majd vizsgáld meg az
nm paranccsal a kapott tárgykódokat! Linkeld össze a két tárgykódot
teljes programmá! Hogyan kellett mindehhez a hello() függvényt módosítanod?
Azt kell elérni, hogy a tárgykód fájlba úgy kerüljön a hello() függvény,
hogy máshonnan is elérhető legyen (exportált függvény; exported function, function
with external linkage).
Megoldás
Egyszerű: le kell róla szedni a static jelzőt. Különben a
main() nem fogja látni, hiába van deklarálva a hello.h-ban,
és hiába van definiálva a hello.c-ben.
A fordítás lépése:
$ gcc -std=c99 -Wall -Wdeprecated -pedantic -g -c main.c -o main.o $ gcc -std=c99 -Wall -Wdeprecated -pedantic -g -c hello.c -o hello.o $ gcc -g main.o hello.o -o program
Globális változók
Módosítsd újra a projektet! Hozz most létre egy lang nevű globális változót a
hello.c-ben, amelynek a típusa legyen enum Language, a
lehetséges értékei legyenek: language_en és language_hu.
Add ennek a változónak a language_en kezdeti értéket!
Írd át úgy a hello() függvényt, hogy ennek figyelembe vételével
jelenítse meg az üzenetet! Írd át a főprogramot is:
int main(void) {
hello(0); // angol (kezdeti érték)
lang = language_hu;
hello(1); // magyar
lang = language_en;
hello(2); // megint angol
}
Minek, hol kell lennie ehhez? Mi különbözteti meg egy globális változó deklarációját a definíciójától? Mi a különbség egyáltalán a deklaráció és a definíció között? Nézd meg most is a tárgykód fájlokat!
Megoldás
A felsorolt típus definíciója a fejlécfájlban, a hello.h-ban, mert
azt mindkettő fordítási egységnél látnia kell a fordítónak:
typedef enum Language {
language_en, language_hu
} Language;
A változó definíciója a forrásfájlba, hello.c-be kell:
Language lang = language_hu;
És végül, a változó deklarációja a fejlécfájlba. Ehhez az extern
kulcsszót kell használni, ettől válik deklarációvá a sor:
extern Language lang;
Változónál és függvénynél a deklaráció azt jelenti, hogy megadjuk a típusát; a definíció pedig azt, hogy lefoglaljuk hozzá a memóriaterületet. A deklarációra a többi programrész fordítása közben van szükség, a definíció hatására pedig ténylegesen kerül is valami a tárgykódba.
A fordítás és linkelés különválasztásának akkor van igazán értelme, ha nagy
projekten kell dolgozni. Ha nem mindegyik forrásfájl változik az újrafordítások
között, akkor sok tárgykód fájl változatlan maradhat. A mostani projektben
például a main.c változásakor a main.o-t kell újra
előállítani, az hello.o viszont változatlan maradhat. Így rengeteg
idő megspórolható. Az alábbi gráf élei a függőségeket (dependency)
mutatják, vagyis azt, hogy melyik fájl tartalmától melyik másik fájl tartalma
függ.
Ha ismertek a függőségek, akkor a keletkező fájlok előállítása automatizálható:pl. ha egy tárgykód fájl újabb,
mint a belőle linkelt program, akkor a linkelést újra el kell végezni.
Erre képes a make program. A függőségeket ennek egy ún. Makefile írja le, aminek a neve mindig
Makefile. Ez szabályokat (rule) tartalmaz. Minden szabály egy cél fájlt ad meg
és annak függőségeit, továbbá parancsokat, amelyekkel a cél fájl előállítható.
Az egyes szabályok leírása között üres sornak kell szerepelnie. A parancsokat pedig egy tabulátor
vezeti be, alább ezt a piros vonalak jelzik:
célfájl1: függőségfájl1 függőségfájl2 függőségfájl3... _______parancsok... _______parancsok... _______parancsok... célfájl2: függőségfájl1 függőségfájl2... _______parancsok... _______parancsok... _______parancsok...
A helló projektet az alábbi Makefile tudja kezelni:
program: main.o hello.o gcc -g main.o hello.o -o program hello.o: hello.c hello.h gcc -std=c99 -Wall -Wdeprecated -pedantic -g -c hello.c -o hello.o main.o: main.c hello.h gcc -std=c99 -Wall -Wdeprecated -pedantic -g -c main.c -o main.o
Mentsd el ezt Makefile néven, és próbáld ki a make parancsot (Windowson lehet, hogy mingw32-make kell)!
Töröld le külön-külön az egyes előállított fájlokat (pl. main, hello.o),
és figyeld meg, mikor melyik parancsot futtatja a make!
Változók, makrók és minták
A program az alábbi Makefile segítségével is megépíthető. Ez változókat
(mint pl. a BINARY), makrókat (pl. $^ és $@) és mintákat
(pl. a %.o kezdetű szabály) is tartalmaz. Bogarászd ki, vajon melyik mit jelent!
BINARY = program OBJECTS = main.o hello.o HEADERS = hello.h CC = gcc CFLAGS = -std=c11 -O0 -Wall -Wdeprecated -pedantic -g LDFLAGS = -g .PHONY: all clean all: $(BINARY) clean: rm -f $(BINARY) $(OBJECTS) $(BINARY): $(OBJECTS) $(CC) $(LDFLAGS) $^ -o $@ %.o: %.c $(HEADERS) $(CC) $(CFLAGS) -c $< -o $@
Térjünk most át a C++ nyelvre! C-ben minden függvényt egyértelműen azonosított a neve. C++-ban ez már nincs így, több ugyanolyan nevű függvényünk is lehet.
Tekintsük az alábbi, (khm) demonstrációs céllal készült programkódot:
void foo() {} // 1
void foo(int i) {} // 2
class Foo {
public:
void foo(); // 3
};
void Foo::foo() {}
namespace FooNS {
void foo() {} // 4
class Foo {
public:
void foo(); // 5
};
void Foo::foo() {}
}
int main(int argc, char *argv[]) {
/* 1 */ foo();
/* 2 */ foo(1);
/* 3 */ Foo().foo();
/* 4 */ FooNS::foo();
/* 5 */ FooNS::Foo().foo();
}
Az egyforma nevű függvények közül a kiválasztott példányt a hívás helye és módja határozza meg: a választásnál a fordító figyelembe veszi a paraméterek típusát, a névteret, tagfüggvények esetén pedig az objektum típusát is. Csak egy gond van: a fordítás után keletkező tárgykódban már nem léteznek a deklarációk, ott már csak nevek vannak. Csakis és kizárólag a nevek alapján meg kell tudni különböztetni a függvényeket. Ezért találták ki a name mangling nevű módszert (más néven: function name decoration). Ez azt jelenti, hogy a tárgykódba írt függvénynevekbe a fordító szisztematikusan belekódolja a paraméterek típusát, az osztályok és a névterek neveit is.
Az egyes fordítók egészen eltérő „name mangling” módszereket használnak. A
Wikipédián van egy jó
táblázat erről: a void h(int) függvénynek a különféle fordítók
egész változatos neveket adnak, pl.
_Z1hi, h__Fi, ?h@@YAXH@Z,
CXX$__7H__FI0ARG51T és hasonlók.
Próbáld ezt ki! Fordítsd le a fenti programot, állítsd elő a tárgykód
fájlt! A C++ fordítónak használhatod a g++-t vagy Linuxon a clang++-t;
a paraméterezésük megegyezik a C fordítóéval. Írasd ki a keletkező tárgykód
fájl szimbólumait! A -C paraméter hatására az nm
„demangle”-eli a neveket; hasonlítsd össze a kimenetet enélkül és ezzel együtt is!
Mi a helyzet a main() függvénnyel? Mit gondolsz, miért?
Megoldás
Az előállított tárgykód fájl tartalma G++ fordítóval:
0000000000000050 T main 0000000000000050 T main 0000000000000010 T _Z3fooi 0000000000000010 T foo(int) 0000000000000000 T _Z3foov 0000000000000000 T foo() 0000000000000020 T _ZN3Foo3fooEv 0000000000000020 T Foo::foo() 0000000000000040 T _ZN5FooNS3Foo3fooEv 0000000000000040 T FooNS::Foo::foo() 0000000000000030 T _ZN5FooNS3fooEv 0000000000000030 T FooNS::foo()
A main() előtt is van élet: a fordítók további programrészeket
tesznek a lefordított programba, amelyek létrehozzák a globális változókat, átveszik
a paramcssori argumentumokat, és így tovább. Ezek a programrészek hívják meg
a main() függvényt. Mivel ezek lefordítva, készen járnak a
fejlesztőkörnyezethez, bennük a main() függvény már név szerint
van hivatkozva. Ezért annak nevét változatlanul hagyja a fordító. Ebben is különleges
ez a függvény, nem csak abban, hogy kiegészíti a fordító return 0-val,
ha nem lát benne ilyet.
Egészítsd ki az előbbi Makefile-t úgy, hogy C++ programok lefordítására is alkalmas legyen. Ehhez tedd a következőket:
- Hozz létre a
CCmellett egyCXXnevű változót is. Ez fogja megadni a C++ fordító nevét:g++. - Hozz létre egy
CXXFLAGSnevű változót is, a C++ fordító paraméterezéséhez. A szabvány évjáratát a g++-nál is a-std=kapcsoló adja meg; legyen ezc++11. (A szokásos értékei:c++98ésc++11.) - A linkelés helyén írd át a
CCváltozótCXX-re, hogy a lefordított programhoz a C++-specifikus könyvtárak is linkelődjenek. - Hozz létre egy szabály mintát a
%.o: %.cmellett: ez legyen a%.o: %.cppminta. Add meg értelemszerűen a parancsot.
Állítsd be a tetején úgy a változókat, hogy a foo.cpp adja meg
a teljes programot: foo. Próbáld ki egy make clean
után, hogy működik-e a Makefile-od! Ezután jön a varázslat: össze kell majd linkelned
egy C-ben és egy C++-ban írt programrészletet. Erről szól a következő feladat.
Megoldás
BINARY = foo OBJECTS = foo.o HEADERS = CC = gcc CXX = g++ CFLAGS = -std=c11 -O0 -Wall -Wdeprecated -pedantic -g CXXFLAGS = -std=c++11 -O0 -Wall -Wdeprecated -pedantic -g LDFLAGS = -g .PHONY: all clean all: $(BINARY) clean: rm -f $(BINARY) $(OBJECTS) $(BINARY): $(OBJECTS) $(CXX) $(LDFLAGS) $^ -o $@ %.o: %.c $(HEADERS) $(CC) $(CFLAGS) -c $< -o $@ %.o: %.cpp $(HEADERS) $(CXX) $(CXXFLAGS) -c $< -o $@
Hozz létre egy üres mappát a következő projektnek, és másold bele azt a
Makefile-t, amit az előbb okosítottál fel C++-hoz. Hozz létre két forrásfájlt
és egy fejlécfájlt: main.cpp, hello.c és hello.h,
és add meg ezeket a fájlokat a Makefile-ban, ahol kell.
Írj a hello.c-ben egy hello_c() nevű függvényt.
Csináljon ez csak annyit, hogy kiír egy üzenetet a képernyőre. Tegyél bele valami
nagyon C-specifikus dolgot, pl. hozz létre egy class nevű változót
(ilyet C++-ban garantáltan nem lehet).
Írd meg a főprogramot a main.cpp-ben! Hívd meg innen a
hello_c() nevű függvényt, ezen felül pedig csinálj valami nagyon
C++-specifikus dolgot, pl. definiálj egy osztályt a class kulcsszóval!
Próbáld meg lefordítani és linkelni a programot. Mit tapasztalsz? Mi az oka?
Elvileg odáig el kellett jutnia a fordításnak, hogy létrejön a main.o
és a hello.o. Vizsgáld meg ezeket az nm paranccsal!
Megoldás
A hibát az okozza, hogy a C++ fordító használ name mangling-et, a C fordító pedig
nem. A C fordító ezért egy hello_c nevű függvényt tesz a tárgykódba,
a C++ fordító pedig a _Z7hello_cv függvényre hivatkozik:
$ nm main.o
0000000000000000 T main
U _Z7hello_cv nem ugyanaz,
$ nm hello.o
0000000000000000 T hello_c mint ez
U printf
Az extern "C" hívási konvenció
A C-ben írt kódrészletek igen nagy hányada változtatás nélkül C++-ban is
lefordítható, és ugyanúgy működik is. Ez azonban nem azt jelenti, hogy ugyanaz a
tárgykód fájl keletkezik, és a legnyilvánvalóbb különbséget épp a tárgykódban
megjelenő függvénynevek jelentik a C++-os name mangling miatt. Mivel azonban
a C/C++ programok ahol lehet, binárisan is kompatibilisek, a két tárgykódot
össze lehet linkelni, ha szólunk a C++ fordítónak, hogy a hello_c()
függvényt az eredeti nevén keresse. Ezt pedig az extern "C" jelzéssel
lehet megtenni.
Első lépésként töröld ki az #include "hello.h" sort a mainből, és
tedd be helyette ezt a sort:
extern "C" void hello_c(void);
Próbáld meg így lefordítani a programot – így már működnie kell. Vizsgáld meg
a tárgykód fájlokat, látni fogod, mindkét helyen már hello_c néven
látszik a hivatkozás.
Ez a jelzés a fordító számára a függvény ún. hívási konvencióját (calling convention) adja meg. Általában akkor kell ilyet használni, amikor külső, más fordítóval lefordított, esetleg más nyelven íródott függvényt kell hívni. Azt adja meg a fordító számára, hogy a hívott függvény hogyan, milyen sorrendben, milyen elhelyezéssel várja a paramétereket a veremben, esetleg máshol – mindezt bit szinten pontosan. Ebben az esetben arra is utal a C++ fordítónak, hogy ez egy C-ben írt függvény lesz, ezért a nevét pontosan így lehet majd megtalálni a tárgykódban, nem szabad name mangling-et használni.
A hívási konvenciót egyszerre több függvény deklarációjára is lehet alkalmazni, ehhez egyszerűen egy blokkba kell tenni a függvényeket:
extern "C" {
void hello_c(void);
double masik_fuggveny(int, char*);
}
Legjobb lenne ezt ilyen formán betenni a hello.h fejlécfájlba,
de azt nem lehet, mert annak a fájlnak a C fordító által is értelmezhetőnek kell
lennie. Ezt a problémát az előfordító segítségével szokás megoldani. A C++
biztosítja, hogy előfordítás közben definiálva van egy __cplusplus
nevű makró, C-ben pedig nincs ilyen. Ezt a tényt kihasználva lehet megjeleníteni
vagy eltüntetni a hívási konvenció megadását:
#ifdef __cplusplus
extern "C" {
#endif
void hello_c(void);
#ifdef __cplusplus
}
#endif
Ha így írjuk meg a fejlécfájlt, akkor az már azt fogja jelenteni mindkét
nyelven, amit kell. A C++ előfordító beteszi az extern "C"-t, ezért
a name mangling nem lesz érvényes a függvényre; a C fordító pedig az egészből nem
lát semmit, mert az előfordítója kitörölte azt a sort.
Javítsd a fejlécfájlt ilyenre, és fordítsd le a programot! Ellenőrizd ezután újra a tárgykód fájlokat!
Megoldás
#include "hello.h"
class Osztaly {};
int main() {
hello_c();
}
#include <stdio.h>
#include "hello.h"
void hello_c(void) {
int class = 2;
printf("Hello C, class = %d!\n", class);
}
#ifndef HELLO_H
#define HELLO_H
#ifdef __cplusplus
extern "C" {
#endif
void hello_c(void);
#ifdef __cplusplus
}
#endif
#endif
Normál, „statikus” linkelés közben a futtatható programba bemásolódnak a lefordított függvények. Ennek több hátránya van:
- Ha egy függvényt átírunk, nem csak a forrásfájlját kell újrafordítani, hanem újra is kell linkelni a programot.
- Ha több programban is használjuk ugyanazokat a függvényeket, többször lesznek bent a memóriában.
Ezeket a problémákat oldja meg a dinamikus linkelés (dynamic linking). Ez lényegében annyit jelent, hogy némely forrásfájlokat nem a futtatható programfájl előállításakor, hanem csak annak elindításakor linkelünk a programhoz. Ezek Unix operációs rendszeren az SO-k (shared object), Windowson pedig a DLL-ek (dynamic link library).
Az SO-kból (DLL-ekből) csak egy változat töltődik be a memóriába; ha több program használja azokat, akkor osztoznak rajta. Így kisebb a memóriafelhasználás. Könnyebb a cseréjük is: ha az SO-ból új verzió készül, az azt használó programokat nem kell újralinkelni (amíg persze a benne lévő függvények kompatibilisek maradtak). Még az is megoldható, hogy egy program a futása közben töltsön be és dobjon el egy SO-t; így működnek a programok bővítményei (pluginek) és a hardvereszközök meghajtóprogramjai (driver v. kernel module).
Az SO-k létrehozása és programhoz linkelése majdnem ugyanúgy megy, mint a
szokásos fordítás, csak néhány extra paramétert kell adni a fordítónak, amivel
megadjuk, mit szeretnénk. Ezek közül az egyetlen, aminek elméleti jelentősége
van, a -fPIC paraméter. Ezzel kell jelezni fordítás közben azt, hogy
„PIC = position independent code”-ot szeretnénk létrehozni. Az így lefordított
programba a fordító a globális változók és függvények elérését indirekción
keresztül végzi, ami lehetővé teszi azt, hogy ezek elmozdíthatók legyenek
a memóriában.
Próbáld ki a dinamikus linkelést! Hozd létre az alábbi mini-projektet, és utána kövesd az utasításokat!
#include <stdio.h>
#include "test.h"
void test(void) {
printf("Dynamic linking test 1.0\n");
}
#ifndef TEST_H_INCLUDED
#define TEST_H_INCLUDED
void test(void);
#endif
#include "test.h"
int main(void) {
test();
}
Először fordítsd le a test.c fájlt! Az ebben lévő függvény
fog a dinamikusan linkelt részbe kerülni. Ennél a fordítási lépésnél kell megadni
a -fpic paramétert:
$ gcc -fPIC test.c -c -o test.o
Ezután hozd létre az SO (DLL) fájlt! Itt meg lehetne adni több tárgykódot, amik beépülnek az SO-ba, de most csak egy van:
$ gcc -shared test.o -o libtest.so Windowson: -o libtest.dll
Végül fordítsd le a főprogramot, és linkeld hozzá dinamikusan a -l paraméterrel a függvénykönyvtárat! A
-L. paraméter azt adja meg, hogy a .so (.dll) fájlt az aktuális mappában kell keresni; mert alapértelmezetten
csak a rendszerszinten definiált mappákban keresné. Az -ltest paraméternél nem kell a név elején lévő lib-et,
és a név végén lévő .so-t odaírni; azokat automatikusan kezeli. Ezeket megadva a linkelés közben ellenőrzi, hogy
megtalálhatók-e a függvények. Az rpath a linkernek átadott paraméter (-Wl: a linkernek, rpath
: a -rpath paramétert, az értéke: .); azt a mappát adja meg, ahol az SO fájlt futási időben
keresni fogja a program. Most ez is az aktuális mappa lesz:
$ gcc main.c -L. -ltest -Wl,-rpath,. -o prog
Ezután a program elindítható. Az ldd paranccsal ellenőrizni tudod, hogy a létrejött futtatható milyen
könyvtárakhoz kapcsolódik, ezek között látszódnia kell a libtest.so-nak is, és nem lehet mellette „not found”
jelzés:
nincs
ldd$ ldd prog
Próbáld ki, hogy tényleg dinamikus-e a linkelés! Változtasd meg a test()
függvényt, írjon ki ez új verziószámot! Fordítsd utána a test.c-t újra,
és állítsd elő a libtest.so új verzióját is. A ./prog
programot futtatva ezután rögtön az új verziószám kell látszódjon.