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-W
mé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.exe
lesz.
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
CC
mellett egyCXX
nevű változót is. Ez fogja megadni a C++ fordító nevét:g++
. - Hozz létre egy
CXXFLAGS
nevű 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
CC
vá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: %.c
mellett: ez legyen a%.o: %.cpp
minta. 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.