Tehát már tudjuk, hogyan deklaráljunk, definiáljunk és használjunk függvényeket a programokban. Ebben a fejezetben a speciális formájukról – a túlterhelt funkciókról – lesz szó. Két függvényt túlterheltnek nevezünk, ha azonos a nevük, ugyanabban a hatókörben vannak deklarálva, de a formális paraméterek listája eltérő. Elmagyarázzuk, hogyan deklarálják ezeket a függvényeket, és miért hasznosak. Majd megfontoljuk a megoldásuk kérdését, i.e. arról, hogy a több túlterhelt függvény közül melyiket hívják meg a program végrehajtása során. Ez a probléma az egyik legnehezebb a C++-ban. Aki szeretne belemenni a részletekbe, annak érdekes lesz elolvasnia a fejezet végén található két részt, ahol az argumentumtípus-konverzió és a túlterhelés-feloldás témája részletesebben foglalkozik.

9.1. Túlterhelt függvénydeklarációk

Most, miután megtanultuk a függvények deklarálását, meghatározását és használatát a programokban, megismerkedünk túlterhelés egy másik szempont a C++-ban. A túlterhelés lehetővé teszi több, azonos nevű függvény használatát, amelyek hasonló műveleteket hajtanak végre különböző típusú argumentumokkal.
Ön már kihasználta az előre meghatározott túlterhelt funkció előnyeit. Például a kifejezés értékeléséhez

az egész összeadás műveletét hívjuk, míg a kifejezés kiértékelését

1.0 + 3.0

lebegőpontos összeadást végez. Ennek vagy annak a műveletnek a kiválasztása a felhasználó számára észrevétlenül történik. Az összeadás operátor túlterhelt a különböző típusú operandusok befogadásához. Nem a programozó, hanem a fordító feladata, hogy felismerje a kontextust és alkalmazza az operandusok típusának megfelelő műveletet.
Ebben a fejezetben bemutatjuk, hogyan határozhatja meg saját túlterhelt funkcióit.

9.1.1. Miért kell túlterhelni a függvény nevét?

A beépített összeadási művelethez hasonlóan szükségünk lehet egy olyan funkciókészletre, amely ugyanazt a műveletet hajtja végre, de különböző típusú paramétereken. Tegyük fel, hogy olyan függvényeket akarunk definiálni, amelyek az átadott paraméterértékek közül a legnagyobbat adják vissza. Ha nem lenne túlterhelés, minden ilyen függvénynek egyedi nevet kellene adni. Például a max() függvénycsalád így nézhet ki:

int i_max(int, int); int vi_max(const vektor &); int matrix_max(const mátrix &);

Mindazonáltal mindegyik ugyanazt csinálja: a paraméterértékek közül a legnagyobbat adják vissza. A felhasználó szemszögéből itt csak egy művelet van - a maximum kiszámítása és megvalósításának részletei kevéssé érdekesek.
A megfigyelt lexikai összetettség a programozási környezet korlátait tükrözi: minden, ugyanabban a hatókörben előforduló névnek egyedi entitásra kell utalnia (objektum, függvény, osztály stb.). Egy ilyen korlátozás a gyakorlatban bizonyos kellemetlenségeket okoz, mivel a programozónak emlékeznie kell vagy valahogy meg kell találnia az összes nevet. A funkció túlterhelése segít ezen a problémán.
Túlterhelést használva a programozó ilyesmit írhat:

int ix = max(j, k); vektor vec; //... int iy = max(vec);

Ez a megközelítés számos helyzetben rendkívül hasznosnak bizonyul.

9.1.2. A függvénynév túlterhelése

A C++ nyelvben két vagy több függvény is kaphat azonos nevet, feltéve, hogy a paraméterlistájuk vagy a paraméterek számában, vagy típusában különbözik. BAN BEN ezt a példát túlterhelt max() függvényt deklarálunk:

intmax(int, int); int max(const vektor &); int max(const mátrix &);

Minden túlterhelt deklarációhoz külön max() függvénydefiníció szükséges, megfelelő paraméterlistával.
Ha egy függvénynév többször van deklarálva valamilyen hatókörben, akkor a második (és az azt követő) deklarációt a fordító a következőképpen értelmezi:

  • ha két függvény paraméterlistája szám szerint eltér, ill paraméter típusok, akkor a függvények túlterheltnek minősülnek: // túlterhelt függvények void print(const string &); void print(vektor &);
  • ha két függvény deklarációjában a visszatérési típus és a paraméterlista megegyezik, akkor a második deklarációt ismétlődőnek tekintjük: // ugyanazon függvény deklarációi void print(const string &str); void print(const string &); A paraméterek neveit nem vesszük figyelembe a deklarációk összehasonlításakor;
    ha két függvény paraméterlistája megegyezik, de a visszatérési típusok eltérőek, akkor a második deklarációt hibásnak tekinti (inkonzisztens az elsővel), és a fordító hibaként jelzi: unsigned int max(int ​​i1, int i2); int max(int ​​i1, int i2);
    // hiba: csak a típusok különböznek
    // értékeket ad vissza

A túlterhelt függvények nem csak visszatérési típusukban különbözhetnek egymástól; ha két függvény paraméterlistája csak az alapértelmezett argumentumértékekben tér el, akkor a második deklarációt megismételtnek tekintjük:

// azonos függvény deklarációi int max (int *ia, int sz); int max (int *ia, int = 10);

A typedef kulcsszó alternatív nevet hoz létre a számára meglévő típus adatok, nem jön létre új típus. Ezért, ha két függvény paraméterlistája csak abban különbözik, hogy az egyik typedef-et használ, a másik pedig olyan típust, amelynek typedef álneve, akkor a listák azonosnak minősülnek, mint a calc() függvény következő két deklarációjában. Ebben az esetben a második deklaráció fordítási hibát ad, mert a visszatérési érték eltér a korábban megadott értéktől:

// typedef nem vezet be új típusú typedef double DOLLAR; // hiba: ugyanazok a paraméterlisták, de különböző // visszatérési típusok extern DOLLAR calc(DOLLAR); extern int calc(double);

A const vagy a volatile specifikátorokat nem veszik figyelembe egy ilyen összehasonlításnál. Így a következő két nyilatkozatot azonosnak tekintjük:

// deklarálja ugyanazt a függvényt void f(int); void f(const int);

A const specifikátor csak a függvénydefiníción belül fontos: azt jelzi, hogy tilos a paraméter értékének megváltoztatása a függvénytörzsben. Az érték által átadott argumentum azonban úgy használható a függvény törzsében, mint egy szabályos aktivált változó: a függvényen kívül a változások nem láthatók. (Az argumentumok átadásának módszereit, különösen az érték átadását a 7.3. szakasz tárgyalja.) Ha egy const specifikátort adunk egy érték által átadott paraméterhez, az nincs hatással annak értelmezésére. Bármely int típusú érték átadható az f(int) függvénynek, ahogy az f(const int) függvény is. Mivel mindkettő ugyanazt az argumentumértéket veszi fel, a fenti deklarációk nem tekinthetők túlterheltnek. f() így definiálható

Érvénytelen f(int i) ( )

Érvénytelen f(konst int i) ( )

Ennek a két definíciónak a jelenléte egy programban hiba, mivel ugyanaz a függvény kétszer van definiálva.
Ha azonban egy const vagy volatile specifikátort alkalmazunk egy mutató vagy referencia típusú paraméterre, akkor azt figyelembe veszi a deklarációk összehasonlításakor.

// különböző függvényeket void deklarálunk f(int*); void f(const int*); // és itt különböző függvények vannak deklarálva
void f(int&);
void f(const int&);

9.1.3. Mikor ne terhelje túl a függvény nevét

Milyen esetekben nem előnyös a név túlterhelés? Például, ha a függvényekhez különböző neveket rendelünk, akkor a program könnyebben olvasható. Íme néhány példa. A következő funkciók ugyanazon absztrakt dátumtípuson működnek. Elsősorban jó jelöltek a túlterhelésre:

void setDate(Dátum&, int, int, int); Dátum &convertDate(const string &); void printDate(const Date&);

Ezek a függvények ugyanazon az adattípuson, a Dátum osztályon működnek, de szemantikailag különböző műveleteket hajtanak végre. Ebben az esetben a különböző nevek használatához kapcsolódó lexikális bonyolultság a programozó azon konvenciójából fakad, hogy az adattípusra és az elnevezési függvényekre vonatkozó műveletkészletet e műveletek szemantikája szerint biztosít. Igaz, a C++ osztályú mechanizmus redundánssá teszi az ilyen konvenciót. Az ilyen függvényeket a Date osztály tagjaivá kell tennünk, de különböző neveket kell hagynunk, amelyek tükrözik a művelet jelentését:

#beleértve class Date ( public: set(int, int, int); Date& convert(const string &); void print(); // ...
};

Vegyünk egy másik példát. A következő öt Képernyőtag-függvény különféle műveleteket hajt végre a képernyőkurzoron, amely ugyanannak az osztálynak a tagja. Ésszerűnek tűnhet ezeket a függvényeket a move() általános név alatt túlterhelni:

Screen& moveHome(); Screen& moveAbs(int, int); Screen& moveRel(int, int, char *irány); Screen& moveX(int); Screen& moveY(int);

Az utolsó két függvényt azonban nem lehet túlterhelni, mivel ugyanazokat a paraméterlistákat tartalmazzák. Az aláírás egyedivé tételéhez egyesítsük őket egyetlen funkcióba:

// függvény, amely egyesíti a moveX() és a moveY() függvényeket Screen& move(int, char xy);

Mostantól minden függvénynek más paraméterlistája van, így a move() név alatt túlterhelhetők. Ezt azonban nem szabad megtenni: a különböző nevek olyan információkat hordoznak, amelyek nélkül a program nehezebb lesz megérteni. Például az e funkciók által végrehajtott kurzormozgatási műveletek eltérőek. Például a moveHome() speciális fajta balra mozgatást hajt végre felső sarok képernyő. Az alábbi két hívás közül melyik a felhasználóbarátabb és könnyebben megjegyezhető?

// melyik hívás világosabb? myScreen.home(); // erre gondolunk! myScreen.move();

Bizonyos esetekben nem kell túlterhelnie a függvény nevét, vagy nem kell különböző neveket hozzárendelnie: az alapértelmezett argumentumértékek használatával több függvényt is összevonhat egybe. Például a kurzorvezérlő funkciókat

MoveAbs(int, int); moveAbs(int, int, char*);

eltérnek egy harmadik char* típusú paraméter jelenlétében. Ha a megvalósításuk hasonló, és ésszerű alapértelmezett érték található a harmadik argumentumhoz, akkor mindkét függvény helyettesíthető eggyel. Ebben az esetben egy 0 értékű mutató alkalmas az alapértelmezett érték szerepére:

Move(int, int, char* = 0);

Használjon bizonyos szolgáltatásokat, ha az alkalmazáslogika megköveteli. Nem szükséges túlterhelt függvényeket beletenni egy programba csak azért, mert léteznek.

9.1.4. Túlterhelés és terjedelem A

Minden túlterhelt funkció ugyanabban a hatókörben van deklarálva. Például egy lokálisan deklarált függvény nem terhel túl, hanem egyszerűen elrejti a globálisat:

#beleértve void print(const string &); voidprint(dupla); // túlterheli a print() void fooBar(int ival)
{
// külön hatókör: elrejti a print() mindkét megvalósítását
extern void print(int); // hiba: a print(const string &) nem látható ezen a területen
print("Érték: ");
print(ival); // helyes: a print(int) látható
}

Mivel minden osztály meghatározza a saját hatókörét, a két különböző osztályba tartozó függvények nem terhelik túl egymást. (Az osztálytagfüggvényekkel a 13. fejezet foglalkozik. Az osztálytagfüggvények túlterhelésének megoldásával a 15. fejezet foglalkozik.)
Az ilyen függvények deklarálása a névtérben is megengedett. Mindegyikhez tartozik saját hatókör is, hogy a különböző hatókörben deklarált függvények ne terheljék túl egymást. Például:

#beleértve névtér IBM ( extern void print(const string &); extern void print(double); // túlterheli a print() ) névtér Disney ( // külön hatókör: // nem terheli túl a print() függvényt az IBM névtérből extern void nyomtatás (int); )

A deklarációk és direktívák használata segít a névtértagok elérhetővé tételében más hatókörökben. Ezek a mechanizmusok némileg befolyásolják a túlterhelt függvények deklarációit. (A deklarációk és a direktívák használatáról a 8.6. fejezetben volt szó.)

Hogyan befolyásolja a deklaráció használata a funkció túlterhelését? Emlékezzünk vissza, hogy bevezet egy álnevet egy névtértaghoz abban a hatókörben, amelyben a deklaráció előfordul. Mit csinálnak az ilyen deklarációk a következő programban?

Névtér libs_R_us ( int max(int, int); int max(double, double); extern void print(int);
külső üres nyomtatás (dupla);
) // nyilatkozatok használatával
a libs_R_us::max;
a libs_R_us::print(double); // hiba void func()
{
max(87, 65); // meghívja a libs_R_us::max(int, int)
max(35,5; 76,6); // meghívja a libs_R_us::max(double, double)

Az első használó deklaráció mindkét libs_R_us::max függvényt a globális hatókörbe hozza. Most a max() függvények bármelyike ​​meghívható a func()-on belül. Az argumentumok típusai határozzák meg, hogy melyik függvényt kell meghívni. A második használó deklaráció egy hiba: nem lehet paraméterlistája. A libs_R_us::print() függvény csak így van deklarálva:

A libs_R_us::print;

A use deklaráció mindig elérhetővé tesz minden túlterhelt függvényt a megadott néven. Ez a korlátozás biztosítja, hogy a libs_R_us névtér interfész ne sérüljön. Egyértelmű, hogy hívás esetén

Nyomtatás(88);

a névtér szerzője a libs_R_us::print(int) függvény meghívását várja. Ha megengedi a felhasználónak, hogy a több túlterhelt függvény közül csak egyet vegyen fel a hatókörbe, akkor a program viselkedése kiszámíthatatlanná válik.
Mi történik, ha a use-deklaráció egy már létező nevű függvényt hoz hatókörbe? Ezek a függvények úgy néznek ki, mintha ott vannak deklarálva, ahol a use deklaráció előfordul. Ezért a bevezetett függvények részt vesznek az adott hatókörben található összes túlterhelt függvény nevének feloldásában:

#beleértve névtér libs_R_us ( extern void print(int); extern void print(double); ) extern void print(const string &); // libs_R_us::print(int) és libs_R_us::print(double)
// túlterhelés nyomtatás(const string &)
a libs_R_us::print; void fooBar (int ival)
{
// print(const string &)
}

A use deklaráció két deklarációt ad a globális hatókörhöz: egyet a print(int) és egyet a print(double) számára. Ezek álnevek a libs_R_us térben, és szerepelnek a print nevű túlterhelt függvényekben, ahol a globális print(const string &) már létezik. A fooBar nyomtatási túlterhelésének feloldásakor mindhárom funkciót figyelembe veszik.
Ha egy using deklaráció bevezet valamilyen függvényt egy olyan hatókörbe, amely már rendelkezik azonos nevű és azonos paraméterlistával rendelkező függvényekkel, az hibának minősül. A using-deklaráció nem lehet print(int) függvény álnéven a libs_R_us névtérben, ha a print(int) már létezik a globális hatókörben. Például:

Névtér libs_R_us ( void print(int); void print(double); ) void print(int); a libs_R_us::print; // hiba: ismételt deklaráció print(int) void fooBar(int ival)
{
print(ival); // melyik nyomtatás? ::print vagy libs_R_us::print
}

Megmutattuk, hogyan kapcsolódnak egymáshoz a használat-deklarációk és a túlterhelt függvények. Most nézzük meg a use-direktíva használatának sajátosságait. A use direktíva hatására a névtértagok ezen a területen kívül deklarálva jelennek meg, és hozzáadják őket egy új hatókörhöz. Ha már van egy ilyen nevű függvény ebben a hatókörben, akkor túlterhelés lép fel. Például:

#beleértve névtér libs_R_us ( extern void print(int); extern void print(double); ) extern void print(const string &); // direktíva használatával
// print(int), print(double) és print(const string &) elemek
// ugyanaz a túlterhelt függvénykészlet
névtér használatával libs_R_us; void fooBar (int ival)
{
print("Érték: "); // meghívja a globális függvényt
// print(const string &)
print(ival); // meghívja a libs_R_us::print(int)
}

Ez akkor is igaz, ha több direktívát használunk. Az azonos nevű függvények, amelyek különböző terek tagjai, ugyanabban a halmazban szerepelnek:

IBM névtér ( int print(int); ) névtér Disney ( double print(double); ) // a-direktíva használatával // számos túlterhelt függvényt generál különböző // névterekből az IBM névterek használatával; Disney névtér használata; hosszú dupla nyomtatás (hosszú dupla); int main() (
nyomtat(1); // IBM::print(int) néven
print(3.1); // Disney felhívása::print(double)
visszatérés 0;
}

A globális hatókörben a print nevű túlterhelt függvénykészlet a print(int), a print(double) és a print(long double) függvényeket tartalmazza. Mindegyiket figyelembe veszi a main() a túlterhelés-feloldás során, bár eredetileg különböző névterekben voltak meghatározva.
Tehát ismét a túlterhelt funkciók ugyanabban a hatókörben vannak. Főleg deklarációk és olyan direktívák használatának eredményeként kerülnek oda, amelyek más hatókörökből származó neveket tesznek elérhetővé.

9.1.5. extern "C" direktíva és túlterhelt A függvények

A 7.7. szakaszban láttuk, hogy a külső "C" bind direktíva használható egy C++ programban annak jelzésére, hogy valamilyen objektum C részben van. Hogyan befolyásolja ez a direktíva a túlterhelt függvénydeklarációkat? A C++ és C nyelven írt függvények lehetnek ugyanabban a halmazban?
A bind direktíva a sok túlterhelt függvény közül csak egyet határozhat meg. Például a következő program hibás:

// hiba: két túlterhelt függvényhez megadott direktíva extern "C" extern "C" void print(const char*); külső "C" void print(int);

A következő példa egy túlterhelt calc() függvényre a külső "C" direktíva tipikus használatát szemlélteti:

ClassSmallInt(/* ... */); osztály BigNum(/* ... */); // a C-ben írt függvény mindkettő meghívható programból,
// C-ben írt vagy C++-ban írt programból.
// A C++ függvények olyan paramétereket kezelnek, amelyek osztályok
extern "C" double calc(double);
extern SmallInt calc(const SmallInt&);
extern BigNum calc(const BigNum&);

A C-ben írt calc() függvény C-ből és C++ programból is meghívható. A másik két függvény egy osztályt vesz fel paraméterként, ezért csak C++ programban használható. A nyilatkozatok sorrendje nem jelentős.
A bind direktíva irreleváns annak eldöntésében, hogy melyik függvényt hívjuk meg; csak a paraméter típusok fontosak. Az átadott argumentumok típusainak leginkább megfelelő függvény kerül kiválasztásra:

Smallint si = 8; int main() ( calc(34); // C függvény hívása calc(double) calc(si); // C++ függvény calc(const SmallInt &) // ... return 0; )

9.1.6. Túlterhelt függvényekre mutatók A

Mutatót deklarálhat a sok túlterhelt függvény egyikére. Például:

külső void ff(vektor ); extern void ff(unsigned int); // milyen függvényre mutat a pf1?
void (*pf1)(unsigned int) =

Mivel az ff() függvény túlterhelt, az &ff inicializáló önmagában nem elegendő a kiválasztáshoz helyes opció. Annak megértéséhez, hogy melyik függvény inicializálja a mutatót, a fordító az összes túlterhelt függvény halmazában megkeresi azt, amelyiknek ugyanaz a visszatérési típusa és paraméterlistája, mint a mutató által hivatkozott függvénynek. Esetünkben az ff(unsigned int) függvény lesz kiválasztva.
De mi van akkor, ha nincs olyan függvény, amely pontosan megfelelne a mutató típusának? Ezután a fordító hibaüzenetet ad:

külső void ff(vektor ); extern void ff(unsigned int); // hiba: egyezés nem található: érvénytelen paraméterlista void (*pf2)(int) = // hiba: egyezés nem található: rossz visszatérési típus double (*pf3)(vektor ) = &ff;

A hozzárendelés hasonló módon működik. Ha a mutató értéke egy túlterhelt függvény címe, akkor a függvényre mutató mutató típusát használjuk a hozzárendelési operátor jobb oldalán található operandus kiválasztásához. Ha pedig a fordító nem talál a kívánt típusnak pontosan megfelelő függvényt, akkor hibaüzenetet ad ki. Így a függvénymutatók közötti típuskonverzió soha nem történik meg.

Mátrix calc(const mátrix &); intcalc(int, int); int (*pc1)(int, int) = 0;
int (*pc2)(int, double) = 0; // ...
// helyes: a calc(int, int) függvény van kiválasztva
pc1 = // hiba: nincs egyezés: érvénytelen második paramétertípus
pc2=

9.1.7. Biztonságos kötés A

Túlterhelés használatakor az a benyomásunk támad, hogy egy programnak több azonos nevű függvénye is lehet, különböző paraméterlistákkal. Ez a lexikális kényelem azonban csak a forrásszöveg szintjén létezik. A legtöbb fordítórendszerben az ezt a szöveget feldolgozó programok futtatható kód létrehozása érdekében megkövetelik, hogy minden név elkülönüljön. A linkszerkesztők általában megengedik Külső linkek lexikálisan. Ha egy ilyen szerkesztő kétszer vagy többször találkozik a print névvel, nem tudja megkülönböztetni őket típusanalízissel (a típusinformáció ekkorra általában elveszik). Tehát csak egy üzenetet nyomtat az újradefiniált karakternyomtatásról, és kilép.
A probléma megoldása érdekében a függvény nevét a paraméterlistával együtt egyedi belső nevet adjuk. A fordító után meghívott programok csak ezt a belső nevet látják. A névfeloldás pontos módja a megvalósítástól függ. Az általános ötlet az, hogy a paraméterek számát és típusát karakterláncként ábrázoljuk, és hozzáfűzzük a függvény nevéhez.
Amint azt a 8.2. szakaszban említettük, az ilyen kódolás különösen azt garantálja, hogy két azonos nevű, különböző paraméterlistával rendelkező, különböző fájlokban található függvénydeklarációt a linker ne észleljen ugyanazon függvény deklarációjaként. Mivel ez a módszer segít megkülönböztetni a túlterhelt funkciókat a hivatkozásszerkesztési fázisban, biztonságos linkelésről beszélünk.
A névdíszítés nem vonatkozik a külső "C" direktívával deklarált függvényekre, mivel a sok túlterhelt függvény közül csak egy írható tiszta C-ben. Két különböző paraméterlistájú, külső "C"-ként deklarált függvényt a linker egyként értelmez. és ugyanaz a karakter.

9.1. gyakorlat

Miért kell deklarálni a túlterhelt függvényeket?

9.2. gyakorlat

Hogyan lehet deklarálni az error() függvény túlterhelt verzióit, hogy a következő hívások helyesek legyenek:

Int index; int felső határ; char selectVal; // ... error("Tömb határokon kívül: ", index, felső határ); error("Osztás nullával"); error("Érvénytelen kijelölés", selectVal);

9.3. gyakorlat

Magyarázza meg a második deklaráció hatását az alábbi példák mindegyikében:

(a) intcalc(int, int); int calc(const int, const int); (b) int get(); double get(); (c) int *reset(int *); double *reset(double *): (d) extern "C" int compute(int *, int); extern "C" double compute(double *, double);

9.4. gyakorlat

Az alábbi inicializálások közül melyik eredményez hibát? Miért?

(a) void reset(int *); void (*pf)(void *) = visszaállítás; (b) intcalc(int, int); int (*pf1)(int, int) = számított; (c) külső "C" int compute(int *, int); int (*pf3)(int*, int) = számítás; (d) érvénytelen (*pf4) (állandó mátrix &) = 0;

9.2. A túlterhelési megoldás három lépése

Funkció túlterhelési felbontás az úgynevezett túlterhelt halmazból a függvény kiválasztásának folyamatát, amelyet meg kell hívni. Ez a folyamat a meghívásakor megadott argumentumokon alapul. Vegyünk egy példát:

T t1, t2; void f(int, int); void f(float, float); int main() (
f(t1, t2);
visszatérés 0;
}

Itt a túlterhelés-feloldási folyamat során a T típusától függően meghatározzák, hogy az f(int,int) vagy f(float,float) függvényt hívjuk-e meg az f(t1,t2) kifejezés feldolgozása során, vagy hiba van. rögzítésre kerül.
A funkciók túlterhelésének feloldása a C++ nyelv egyik legösszetettebb aspektusa. Az összes részlet megértése érdekében a kezdő programozóknak komoly nehézségekkel kell szembenézniük. Ezért be ez a szekció csak bemutatjuk rövid áttekintés a túlterhelési megoldás működését, hogy legalább némi benyomást keltsen a folyamatról. Azok számára, akik többet szeretnének tudni, a következő két rész részletesebb leírást ad.
A funkciótúlterhelés feloldásának folyamata három lépésből áll, amelyeket a következő példában mutatunk be:

void f(); void f(int); void f(double, double = 3,4); void f(char *, char *); void main() (
f(5,6);
visszatérés 0;
}

A funkció túlterhelésének feloldásakor a következő lépéseket kell végrehajtani:

  1. Ki van emelve az adott hívás túlterhelt függvényeinek halmaza, valamint a függvénynek átadott argumentumok listájának tulajdonságai.
  2. A túlterhelt függvények közül azokat választjuk ki, amelyek az adott argumentumokkal hívhatók, számuk és típusuk figyelembevételével.
  3. Megkeresi a hívásnak leginkább megfelelő függvényt.

Vegyük sorra az egyes tételeket.
Az első lépés a túlterhelt függvények készletének azonosítása, amelyet ebben a hívásban figyelembe kell venni. Az ebben a halmazban szereplő függvényeket jelölteknek nevezzük. A jelölt függvény egy olyan függvény, amelynek a neve megegyezik a meghívóval, és deklarációja a hívás pontján látható. Példánkban négy ilyen jelölt van: f(), f(int), f(double, double) és f(char*, char*).
Ezt követően azonosítják az átadott argumentumok listájának tulajdonságait, azaz. számuk és típusuk. Példánkban a lista két kettős argumentumból áll.
A második lépésben a jelöltek halmaza közül kiválasztják az életképeseket - azokat, amelyek az adott argumentumokkal meghívhatók A perzisztens függvénynek vagy annyi formális paramétere van, amennyi a hívott függvénynek átadott tényleges argumentum, vagy több, de majd minden további paraméternél alapértelmezett érték. Ahhoz, hogy egy függvényt perzisztensnek lehessen tekinteni, a hívásban átadott bármely tényleges argumentumnak át kell lennie a deklarációban megadott formális paraméter típusára.

Példánkban két beállított függvény van, amelyek a megadott argumentumokkal hívhatók meg:

  • az f(int) függvény megmaradt, mert csak egy paramétere van, és a tényleges dupla argumentum átalakul formális int paraméterré;
  • az f(double,double) függvény megmaradt, mert a második argumentumnak van egy alapértelmezett értéke, és az első formális paraméter double típusú, ami pontosan az aktuális argumentum típusa.

Ha a második lépés után nem találtunk stabil függvényeket, akkor a hívást hibásnak tekintjük. Ilyenkor azt mondjuk, hogy hiányzik a levelezés.
A harmadik lépés a hívás kontextusának leginkább megfelelő funkció kiválasztása. Az ilyen funkciót a legjobb állásnak (vagy legjobb illeszkedésnek) nevezzük. Ebben a lépésben rangsorolják azokat a konverziókat, amelyekkel a tényleges argumentumok típusait a létrehozott függvény formális paramétereinek típusaira öntötték. A legalkalmasabbnak azt a funkciót tekintjük, amelyhez a következő feltételek teljesülnek:
a tényleges argumentumokra alkalmazott transzformációk nem rosszabbak, mint bármely más jól bevált függvény meghívásához szükséges transzformációk;
egyes argumentumok esetében az alkalmazott konverziók jobbak, mint azok a konverziók, amelyek ugyanazon argumentumok leadásához más jól bevált függvények hívásaiban szükségesek.
A típuskonverziókat és azok rangsorolását részletesebben a 9.3 fejezet tárgyalja. Itt csak röviden tekintjük át példánkban a rangsorolási transzformációkat. A létrehozott f(int) függvényhez az aktuális argumentum dupla int típusú standard leadását kell alkalmazni. A létrehozott f(double,double) függvény esetében az aktuális argumentum double típusa pontosan megegyezik a formális paraméter típusával. Mivel a pontos egyezés jobb, mint egy szabványos konverzió (egyetlen konverzió sem mindig jobb, mint egy konverzió), az f(double,double) a legmegfelelőbb függvény ehhez a híváshoz.
Ha a harmadik lépésben nem lehet megtalálni a beállított függvények közül az egyetlen legjobbat, vagyis nincs olyan beállított függvény, amely jobban illeszkedne, mint az összes többi, akkor a hívást kétértelműnek tekintjük, pl. téves.
(A 9.4. szakasz részletesebben tárgyalja a túlterhelés-feloldás összes lépését. A feloldási folyamatot túlterhelt osztálytagfüggvény és túlterhelt operátor hívásakor is alkalmazzák. A 15.10. szakasz tárgyalja az osztálytagfüggvényekre vonatkozó túlterhelés-feloldási szabályokat, a 15.11. szabályok (túlterhelt operátorok. A túlterhelési felbontásnak figyelembe kell vennie a sablonokból származó funkciókat is. A 10.8. szakasz tárgyalja, hogy a sablonok hogyan befolyásolják ezt a felbontást.)

9.5. gyakorlat

Mi történik a funkció túlterhelés-feloldási folyamatának utolsó (harmadik) lépésében?

9.3. Argumentumtípus-konverziók A

A függvénytúlterhelés-feloldási folyamat második lépésében a fordító azonosítja és rangsorolja azokat a konverziókat, amelyeket a hívott függvény minden tényleges argumentumára alkalmazni kell, hogy azt bármelyik jól bevált függvény megfelelő formális paraméterének típusára konvertálja. A rangsor három lehetséges kimenetel egyikét adhatja:

  • pontos egyezés. A tényleges argumentum típusa pontosan megegyezik a formális paraméter típusával. Például, ha a túlterhelt print() függvények halmaza a következőkkel rendelkezik: void print(unsigned int); void print(const char*); voidprint(char);
  • akkor a következő három hívás mindegyike pontos egyezést ad:
    unsigned int a;
print("a"); // megfelel a print(char); print("a"); // megfelel a print(const char*); nyomtat(a); // egyezik print(unsigned int);
  • típuskonverzióval való egyeztetés. Az aktuális argumentum típusa nem egyezik a formális paraméter típusával, de átalakítható arra: void ff(char); ff(0); // Az int típusú argumentum a char típusba kerül
  • megfelelés hiánya. A tényleges argumentum típusa nem adható át a függvénydeklaráció formális paraméterének típusához, mert a szükséges konverzió nem létezik. A print() függvény következő két hívása nem egyezik:
  • // a print() függvények a fentiek szerint vannak deklarálva int *ip; osztály SmallInt ( /* ... */ ); SmallInt si; print(ip); // hiba: nincs egyezés
    nyomtat(si); // hiba: nincs egyezés
  • A pontos egyezés megállapításához a tényleges argumentum típusának nem kell egyeznie a formális paraméter típusával. Néhány triviális transzformáció alkalmazható az argumentumra, nevezetesen:

    • l-érték átalakítása r-értékké;
    • tömb átalakítása mutatóvá;
    • függvény konvertálása mutatóvá;
    • specifikáló konverziók.

    (Az alábbiakban részletesebben tárgyaljuk.) A típusátalakítással való megfelelés kategóriája a legösszetettebb. Többféle ilyen szereposztást is figyelembe kell venni: típusbővítmények (promóciók), szabványos konverziók és felhasználó által meghatározott konverziók. (A típusbővítéseket és a szabványos konverziókat ebben a fejezetben tárgyaljuk. A felhasználó által definiált konverziókat később, az osztályok részletes tárgyalása után mutatjuk be; ezeket a konverter hajtja végre, egy tagfüggvény, amely lehetővé teszi a saját „standard” készletének meghatározását. ” transzformációkat egy osztályban. A 15. fejezetben megvizsgáljuk az ilyen konvertereket, és azt, hogy ezek hogyan befolyásolják a funkció túlterhelési felbontását.)
    Amikor egy adott híváshoz a legjobban bevált függvényt választjuk, a fordító azt a függvényt keresi, amelynél a tényleges argumentumokra alkalmazott transzformációk a „legjobbak”. A típuskonverziók rangsorolása a következőképpen történik: a pontos egyezés jobb, mint a típusbővítmény, a típusbővítmény jobb, mint a normál konverzió, ez pedig jobb, mint a felhasználó által meghatározott konverzió. A 9.4. szakaszban visszatérünk a rangsoroláshoz, de egyelőre egyszerű példák Mutatjuk, hogyan segít a legmegfelelőbb funkció kiválasztásában.

    9.3.1. További információ a Pontos egyezésről

    A legegyszerűbb eset akkor fordul elő, ha a tényleges argumentumok típusai megegyeznek a formális paraméterek típusaival. Például az alábbiakban két túlterhelt max() függvény látható. Ekkor a max() függvény mindegyik hívása pontosan megegyezik az egyik deklarációval:

    int max(int, int); double max(double, double); int i1; void calc(double d1) (
    max(56, i1); // pontosan megfelel a max(int, int);
    max(d1, 66,9); // pontosan megegyezik a max(double, double);
    }

    Egy felsorolt ​​típus csak a benne meghatározottakkal egyezik meg pontosan. felsorolási elemek, valamint az ilyen típusúnak nyilvánított objektumok:

    Enum Tokenek ( INLINE = 128; VIRTUÁLIS = 129; ); Tokenek curTok = INLINE; enum Stat( Sikertelen, Megfelelt); extern void ff(Tokenek);
    extern void ff(Stat);
    extern void ff(int); int main() (
    ff(pass); // pontosan megegyezik az ff(Stat)
    ff(0); // pontosan megegyezik az ff(int)
    ff(curTok); // pontosan megegyezik az ff(Tokens)
    // ...
    }

    Fentebb már említettük, hogy a tényleges argumentum pontosan megegyezhet a formális paraméterrel, még akkor is, ha valamilyen triviális konverzióra van szükség a típusuk leadásához, amelyek közül az első az l-érték átalakítása r-értékké. Az l-érték olyan objektum, amely megfelel a következő feltételeknek:

    • megkaphatja az objektum címét;
    • megkaphatja egy objektum értékét;
    • ez az érték könnyen módosítható (kivéve, ha az objektumdeklarációban van const specifikáció).

    Ezzel szemben az r-érték olyan kifejezés, amelynek értéke kiértékelődik, vagy olyan kifejezés, amely egy ideiglenes objektumot jelöl, amelyhez nem lehet címet szerezni, és amelynek értéke nem módosítható. Íme egy egyszerű példa:

    intcalc(int); int main() ( int lval, res; lval = 5; // lvalue: lval; rvalue: 5
    res = calc(lval);
    // lvalue:res
    // rvalue: ideiglenes objektum az érték tárolására,
    // a calc() függvény adja vissza
    visszatérés 0;
    }

    Az első hozzárendelési utasításban az lval változó az l-érték, a literál 5 pedig az r-érték. A második hozzárendelési utasításban a res az l-érték, és az ideiglenes objektum, amely a calc() függvény által visszaadott eredményt tárolja, az r-érték.
    Bizonyos helyzetekben, olyan környezetben, ahol egy érték várható, egy l-érték kifejezés használható:

    intobj1; intobj2; int main() (
    // ...
    belső = obj1 + obj2;
    visszatérés 0;
    }

    Itt obj1 és obj2 l-értékek. A main() függvényben történő összeadás végrehajtásához azonban értékeiket az obj1 és obj2 változókból nyerik ki. Az l-érték kifejezéssel reprezentált objektum értékének kinyerését az l-érték r-értékké való konvertálásának nevezzük.
    Ha a függvény érték által átadott argumentumot vár, ha az argumentum l-érték, akkor a rendszer r-értékké alakítja:

    #beleértve stringcolor("lila"); voidprint(karakterlánc); int main() (
    nyomat(szín); // pontos egyezés: értékkonverzió
    // rértékben
    visszatérés 0;
    }

    Mivel a print(color) hívás argumentumát érték adja át, az l-értéket r-értékké alakítja a rendszer, hogy kivonja a szín értékét, és átadja azt a print(string) prototípus függvénynek. Mindazonáltal annak ellenére, hogy megtörtént egy ilyen öntvény, a tényleges színargumentumról azt feltételezzük, hogy pontosan megegyezik a print(string) deklarációval.
    Függvények hívásakor nem mindig szükséges ilyen átalakítást alkalmazni az argumentumokra. A hivatkozás egy l-érték; ha a függvénynek van referencia paramétere, akkor a függvény meghívásakor l-értéket kap. Ezért a leírt transzformációt nem alkalmazzuk arra a tényleges argumentumra, amelynek a formális referenciaparaméter megfelel. Tegyük fel például, hogy a következő függvény deklarálva van:

    #beleértve void print(list &);

    Az alábbi hívásban li a lista objektumot képviselő l-érték , átadva a print() függvénynek:

    Lista li(20); int main() (
    // ...
    print(li); // pontos egyezés: nincs konverzió lvalue értékről
    // rvalue
    visszatérés 0;
    }

    Az li egy referenciaparaméterrel való egyeztetése pontos egyezésnek számít.
    A második konverzió, amely még mindig pontos egyezést rögzít, egy tömb mutatóvá alakítása. Ahogy a 7.3. szakaszban megjegyeztük, a függvényparaméter soha nem tömbtípus, hanem az első elemére mutató mutatóvá alakul át. Hasonlóképpen, egy tényleges tömb típusú argumentum az NT-ből (ahol N a tömb elemeinek száma, T pedig az egyes elemek típusa) mindig egy T-re mutató mutatóba kerül. -mutató átalakítás. Ennek ellenére a tényleges argumentum pontosan megegyezik a "pointer to T" típusú formális paraméterrel. Például:

    int ai; void putValues(int*); int main() (
    // ...
    putValues(ai); // pontos egyezés: konvertálja a tömböt a következőre
    // mutató
    visszatérés 0;
    }

    A putValues() függvény meghívása előtt a tömb mutatóvá alakul, ami azt eredményezi, hogy a tényleges ai argumentum (három egész számból álló tömb) egy int mutatóba kerül. Bár a putValues() függvény formális paramétere egy mutató, és a tényleges argumentum híváskor átalakul, pontos megfelelés jön létre közöttük.
    Pontos egyezés megállapításánál lehetőség van a függvény mutatóvá alakítására is. (A 7.9. fejezetben említettük.) A tömbparaméterekhez hasonlóan a függvényparaméter is függvénymutatóvá válik. A tényleges "function" típusú argumentum szintén automatikusan átkerül a függvénymutató típusára. Az aktuális argumentum ilyen típusú konvertálását függvény-mutató konverziónak nevezik. A transzformáció végrehajtása közben a tényleges argumentum a formális paraméterrel pontosan megegyezőnek tekinthető. Például:

    Int lexicoCompare(const string &, const string &); typedef int (*PFI)(const string &, const string &);
    void sort(karakterlánc *, karakterlánc *, PFI); string as; int main()
    {
    // ...
    sort (mint,
    mint + mérete(mint)/mérete(mint -1),
    lexicoCompare // pontos egyezés
    // függvény konvertálása mutatóvá
    ); visszatérés 0;
    }

    A sort() hívás előtt egy függvény-mutató konverzió kerül alkalmazásra, amely a lexicoCompare argumentumot "function" típusból "pointer to function" típusba dobja. Bár a függvény formális argumentuma egy mutató, a tényleges argumentum pedig a függvény neve, és ezért a függvényt mutatóvá alakították, a tényleges argumentum a sort() függvénynek pontosan a harmadik formális paramétere. .
    A fentiek közül az utolsó a specifikációk átalakítása. Csak a mutatókra vonatkozik, és a const vagy a volatile specifikátorok (vagy mindkettő) hozzáadása az adott mutatót megszólító típushoz:

    Int a = (4454, 7864, 92, 421, 938); int*pi = a; bool is_equal(const int * , const int *); void func(int *parm) ( // pontos egyezés a pi és parm között: a specifikációk átalakítása
    if (egyenlő(pi, parm))
    // ... return 0;
    }

    Az is_equal() függvény meghívása előtt a tényleges pi és parm argumentumok „pointer to int” típusról „pointer to const int” típusra konvertálódnak. Ez a transzformáció abból áll, hogy a megcímzett típushoz adunk egy const specifikátort, ezért a specifikációs transzformációk kategóriájába tartozik. Annak ellenére, hogy a függvény két mutatót vár egy const int-re, és a tényleges argumentumok egy int-re mutatnak, feltételezzük, hogy az is_equal() függvény formális és tényleges paraméterei pontosan egyeznek.
    A specifikus konverzió csak arra a típusra vonatkozik, amelyet a mutató megcímez. Nem használatos, ha a formális paraméternek van const vagy volatile specifikációja, de a tényleges argumentumnak nincs.

    extern void takeCI(const int); int main() (
    int ii = ...;
    vegye CI(ii); // a specifikátor konverzió nincs alkalmazva
    visszatérés 0;
    }

    Bár a takeCI() függvény formális paramétere const int típusú, és int típusú ii argumentummal hívják meg, nincs specifikáló konverzió: pontos egyezés van a tényleges argumentum és a formális paraméter között.
    A fentiek mindegyike igaz arra az esetre is, amikor az argumentum egy mutató, és a const vagy a volatile specifikációk erre a mutatóra hivatkoznak:

    extern void init(int *const); extern int *pi; int main() (
    // ...
    init(pi); // a specifikátor konverzió nincs alkalmazva
    visszatérés 0;
    }

    Az init() függvény formális paraméterében szereplő const specifikátor magára a mutatóra vonatkozik, nem a címzett típusra. Ezért a fordító nem veszi figyelembe ezt a specifikációt az aktuális argumentumra alkalmazandó konverziók elemzésekor. A pi argumentumra nem alkalmazunk specifikáló konverziót: ez az argumentum és a formális paraméter pontosan megegyezőnek tekinthető.
    Az első három transzformációt (l-érték-r-érték, tömb-mutató és függvény-mutató) gyakran l-érték transzformációnak nevezik. (A 9.4. szakaszban látni fogjuk, hogy bár mind az l-értékű transzformációk, mind a specifikáló transzformációk a pontos egyezést nem sértő transzformációk kategóriájába tartoznak, ennek mértéke magasabb, ha csak az első transzformációra van szükség. beszéljünk erről kicsit részletesebben.)
    A pontos egyezés kikényszeríthető explicit típusú öntéssel. Például, ha két funkció túlterhelt:

    extern void ff(int); extern void ff(void *);

    Ff(0xffbc); // ff(int) hívása

    pontosan megegyezik az ff(int) értékkel, még akkor is, ha a literális 0xffbc hexadecimális állandóként van írva. A programozó kényszerítheti a fordítót az ff(void *) függvény meghívására egy cast művelet explicit végrehajtásával:

    Ff(reinterpret_cast (0xffbc)); // ff(void*) hívása

    Ha egy ilyen öntvényt alkalmazunk az aktuális argumentumra, akkor az átveszi azt a típust, amelyre átalakítja. Az explicit típuskonverziók segítik a túlterhelés-feloldási folyamat szabályozását. Például, ha a túlterhelés-feloldás kétértelmű eredményt ad (a tényleges argumentumok két vagy több jól bevált függvénynek egyformán jól egyeznek), akkor egy explicit cast használható a kétértelműség feloldására, és arra kényszeríti a fordítót, hogy válasszon egy adott függvényt.

    9.3.2. További információ a típuskiterjesztésről

    A típusbővítmény a következő konverziók egyike:

    • a char, unsigned char vagy short típusú tényleges argumentum kibővül az int típusra. Az unsigned short típusú tényleges argumentum kibővül az int típusra, ha az int gépmérete nagyobb, mint a short, és egyébként az unsigned int típusra;
    • a float típusú argumentum double típusúra bővül;
    • a felsorolt ​​típusú argumentum a következő típusok közül az elsőre bővül, amely képes az összes enum tagértéket reprezentálni: int, unsigned int, long, unsigned long;
    • A bool argumentum int típusúra bővül.

    Hasonló kiterjesztést alkalmazunk, ha a tényleges argumentum típusa az imént felsorolt ​​típusok egyike, a formális paraméter pedig a megfelelő kiterjesztett típus:

    extern void manip(int); int main() (
    manip("a"); // típusú char int-re bővül
    visszatérés 0;
    }

    A karakterliterál char típusú. Int-re bővül. Mivel a kiterjesztett típus megfelel a manip() függvény formális paraméterének típusának, azt mondjuk, hogy meghívásához az argumentumtípus kiterjesztése szükséges.
    Tekintsük a következő példát:

    extern void print(unsigned int); extern void print(int); extern void print(char); unsigned char uc;
    print(uc); // print(int); uc csak típuskiterjesztésre van szüksége

    Egy olyan hardverplatformon, ahol az előjel nélküli char egy bájt memóriát, az int pedig négy bájtot foglal el, a bővítmény az előjel nélküli karaktert int-vé alakítja, mivel az összes előjel nélküli char értéket képviselheti. Egy ilyen géparchitektúra esetén a példában bemutatott sok túlterhelt függvény közül a print(int) biztosítja a legjobb illeszkedést az előjel nélküli char argumentumhoz. A másik két funkció esetében az illesztéshez szabványos öntvény szükséges.
    A következő példa egy tényleges felsorolt ​​típusú argumentum kiterjesztését szemlélteti:

    Enum Stat(Fail, Pass); extern void ff(int);
    külső void ff(char); int main() (
    // helyes: enum tag A Pass kibővül az int típusra
    ff(pass); // ff(int)
    ff(0); // ff(int)
    }

    Néha az enum bővítése meglepetéseket hoz. A fordítók gyakran az elemeinek értéke alapján választják ki az enum ábrázolását. Tegyük fel, hogy a fenti architektúra (egy bájt a char-hoz és négy bájt az int-hez) a következőhöz hasonló enumot határoz meg:

    e1 szám ( a1, b1, c1 );

    Mivel csak három a1, b1 és c1 elem van 0, 1 és 2 értékkel – és mivel ezek az értékek mindegyike char típussal ábrázolható, a fordító általában a char-ot választja az e1 típus reprezentálására. Tekintsük azonban az e2 felsorolást a következő elemekkel:

    Enum e2 ( a2, b2, c2=0x80000000 );

    Mivel az egyik konstans értéke 0x80000000, a fordítónak úgy kell döntenie, hogy az e2-t olyan típussal reprezentálja, amely elegendő a 0x80000000 érték tárolására, azaz unsigned int.
    Tehát bár az e1 és az e2 is enumok, ábrázolásaik eltérőek. Emiatt az e1 és e2 különböző típusokra bővül:

    #beleértve string format(int);
    karakterlánc formátum (unsigned int); int main() (
    formátum(a1); // hívásformátum(int)
    formátum(a2); // hívásformátum (unsigned int)
    visszatérés 0;
    }

    A format() első alkalommal meghívásra kerül, a tényleges argumentum kibővül int típusúra, mivel a char az e1 típust jelöli, ezért a túlterhelt format(int) függvény meghívásra kerül. A második híváskor a tényleges e2 argumentum típusa unsigned int, és az argumentum unsigned int-re bővül, ami a túlterhelt formátum(unsigned int) függvény meghívását okozza. Ezért ügyeljen arra, hogy két enum viselkedése a túlterhelés-feloldási folyamat tekintetében eltérő lehet, és az elemek értékétől függ, amelyek meghatározzák, hogyan történik a típusbővítés.

    9.3.3. További információ a normál átalakításról

    Ötféle szabványos transzformáció létezik, nevezetesen:

    1. integer típusú konverziók: egy egész szám típusból vagy felsorolásból bármilyen más egész szám típusba adás (kivéve a transzformációkat, amelyeket fentebb típuskiterjesztésként soroltunk be);
    2. lebegőpontos típusú konverziók: bármilyen lebegőpontos típusból bármely más lebegőpontos típusba történő casting (kivéve a transzformációkat, amelyeket fentebb típusbővítésként soroltunk be);
    3. konverziók egész típusú és lebegőpontos típusok között: casting bármilyen lebegőpontos típusból tetszőleges egész típusba vagy fordítva;
    4. mutatókonverziók: a 0 egész érték mutatótípusba öntése vagy bármilyen típusú mutató átalakítása void* típusúvá;
    5. konverzió bool típusba: casting bármely egész típusból, lebegőpontos típusból, felsorolt ​​típusból vagy mutatótípusból bool típusba.

    Íme néhány példa:

    extern void print(void*); külső üres nyomtatás (dupla); int main() (
    int i;
    print(i); // egyezik print(double);
    // átesem a standard konverzión int-ről double-ra
    print(&i); // egyezik print(void*);
    // &i szabványos átalakításon megy keresztül
    // int*-ből érvénytelen*
    visszatérés 0;
    }

    Az 1., 2. és 3. csoportba tartozó konverziók potenciálisan veszélyesek, mert előfordulhat, hogy a céltípus nem képviseli a forrástípus összes értékét. Például a lebegő elemek nem képviselhetik megfelelően az összes int értéket. Ez az oka annak, hogy az ezekbe a csoportokba tartozó transzformációk standard transzformációk, és nem típuskiterjesztések közé tartoznak.

    int i; voidcalc(úszó); int main() ( calc(i); // az egész típus és a lebegőpontos típus közötti standard konverzió potenciálisan veszélyes // az i return 0 értékétől függően; )

    A calc() függvény meghívásakor az int integer típusúról a lebegőpontos típusú lebegőpontosra történő szabványos konverzió kerül alkalmazásra. Az i változó értékétől függően előfordulhat, hogy nem lehet lebegőként tárolni a pontosság elvesztése nélkül.
    Feltételezzük, hogy minden szabványos változtatás ugyanannyi munkát igényel. Például a char-ból előjel nélküli karakterré konvertálásnak nincs nagyobb prioritása, mint a char-ból duplává történő átalakításnak. A típusok közelségét nem veszik figyelembe. Ha két létrehozott függvény az aktuális argumentum szabványos transzformációját igényli, hogy megfeleljen, akkor a hívást kétértelműnek tekinti, és a fordító hibának jelöli meg. Például két túlterhelt funkció esetén:

    extern void manip(hosszú); extern void manip(float);

    akkor a következő hívás nem egyértelmű:

    int main() ( manip(3.14); // hiba: kétértelműség // a manip(float) nem jobb, mint a manip(int) return 0; )

    A 3.14 konstans dupla típusú. Egy-egy szabványos konverzió segítségével bármely túlterhelt funkcióval megfeleltetés létesíthető. Mivel két transzformáció vezet a célhoz, a hívás kétértelműnek tekinthető. Egyik átalakulás sem élvez elsőbbséget a másikkal szemben. A programozó a kétértelműséget explicit típusú öntéssel is feloldhatja:

    Manip(static_cast (3,14)); // manip(hosszú)

    vagy egy utótag használatával, amely jelzi, hogy a konstans float típusú:

    Manip (3,14F)); // manip(float)

    Íme néhány további példa a kétértelmű hívásokra, amelyek hibaként vannak megjelölve, mert több túlterhelt függvénynek felelnek meg:

    extern void farith(unsigned int); extern void farith(float); int main() (
    // a következő hívások mindegyike kétértelmű
    farith("a"); // az argumentum char típusú
    farith(0); // argumentum int típusú
    farith(2ul); // az argumentum unsigned long típusú
    farith(3,14159); // az argumentum double típusú
    farith(igaz); // az argumentum bool típusú
    }

    A szabványos mutatókonverziók néha ellentétesek az intuitív hatásokkal. Konkrétan, a 0 értéket a rendszer bármilyen típusú mutatóra vetíti; az így kapott mutatót nullnak nevezzük. A 0 érték egy egész típusú konstans kifejezésként ábrázolható:

    void set(int*); int main() (
    // mutatókonverzió 0-ról int*-ra, argumentumokra alkalmazva
    // mindkét hívásban
    set(0L);
    set(0x00);
    visszatérés 0;
    }

    A 0L konstans kifejezés (hosszú int típus 0 értéke) és a 0x00 konstans kifejezés (0 hexadecimális egész szám) egész típusú, ezért int* típusú nullmutatóvá alakítható.
    De mivel az enumok nem egész típusúak, a 0-val egyenlő elem nem kerül mutatótípusba:

    Enum EN (zr = 0); set(zr); // hiba: a zr nem konvertálható int* típusúvá

    A set() függvény hívása hiba, mert nincs konverzió a felsorolási elem zr értéke és az int* típusú formális paraméter között, pedig zr 0.
    Vegye figyelembe, hogy a 0 konstans kifejezés int típusú. A mutatótípusra való átküldéshez szabványos átalakítás szükséges. Ha van egy int típusú formális paraméterrel rendelkező függvény a túlterhelt függvények halmazában, akkor a túlterhelés megengedett a javára abban az esetben, ha a tényleges argumentum 0:

    Void print(int); voidprint(void*); void set(const char*);
    void set(char *); int main()(
    nyomtat(0); // a print(int);
    set(0); // kétértelműség
    visszatérés 0;
    }

    A print(int) hívás pontos egyezést jelent, míg a print(void*) híváshoz a 0 értéket kell egy mutatótípusba önteni. Mivel egy egyezés jobb, mint egy transzformáció, ennek a hívásnak a megoldásához az ember választ nyomtatási funkció(int). A set() hívása kétértelmű, mivel a 0 megfelel mindkét túlterhelt függvény formális paramétereinek a szabványos transzformáció alkalmazásával. Mivel mindkét funkció egyformán jó, a kétértelműség rögzítve van.
    Az utolsó lehetséges mutatókonverzió lehetővé teszi, hogy bármilyen típusú mutatót öntsön a void* típusba, mivel a void* egy általános mutató bármely adattípusra. Íme néhány példa:

    #beleértve extern void reset(void *); void func(int *pi, string *ps) (
    // ...
    reset(pi); // mutató átalakítása: int* érvénytelen*
    /// ...
    reset(ps); // mutató átalakítása: string* érvénytelen*
    }

    Csak az adattípusokra mutató mutatókat lehet érvényteleníteni* a szabványos konverzióval, a függvényekre mutató mutatókat így nem lehet:

    Typedef int(*PFV)(); külső PFV tesztesetek; // függvényekre mutatók tömbje extern void reset(void *); int main() (
    // ...
    reset(textcase); // hiba: nincs szabványos átalakítás
    // int(*)() és void* között
    visszatérés 0;
    }

    9.3.4. Linkek

    Egy függvény tényleges argumentuma vagy formális paramétere lehet hivatkozás. Hogyan érinti ez a típuskonverziós szabályokat?
    Fontolja meg, mi történik, ha a hivatkozás tényleges érv. Típusa soha nem referenciatípus. A referencia argumentumot l-értékként kezeli, amelynek típusa megegyezik a megfelelő objektum típusával:

    int i; int& ri = i; voidprint(int); int main() (
    print(i); // argumentum egy int típusú érték
    print(ri); // ugyanaz
    visszatérés 0;
    }

    A tényleges argumentum mindkét hívásban int típusú. A hivatkozás átadása a második hívásban nem befolyásolja magának az argumentumnak a típusát.
    A fordító által figyelembe vett szabványos típuskonverziók és kiterjesztések megegyeznek, ha a tényleges argumentum a T típusra való hivatkozás, és ha maga is az adott típusú. Például:

    int i; int& ri = i; voidcalc(double); int main() (
    calc(i); // szabványos konverzió egész típusok között
    // és lebegőpontos típus
    calc(ri); // ugyanaz
    visszatérés 0;
    }

    És hogyan befolyásolja a formális referenciaparaméter az aktuális argumentumra alkalmazott transzformációkat? Az összehasonlítás a következő eredményeket adja:

    • az aktuális argumentum referenciaparaméter-inicializálónak alkalmas. Ebben az esetben azt mondjuk, hogy pontos egyezés van köztük: void swap(int &, int &); void manip(int i1, int i2) (
      // ...
      csere(i1, i2); // helyes: call swap(int &, int &)
      // ...
      visszatérés 0;
      }
    • az aktuális argumentum nem tudja inicializálni a referenciaparamétert. Ilyen helyzetben nincs pontos egyezés, és az argumentum nem használható a függvény meghívására. Például:
    • int obj; void fred(double &); int main() ( frd(obj); // hiba: a paraméternek const double & return 0 típusúnak kell lennie; )
    • Az frd() függvény meghívása hiba. A tényleges argumentum int típusú, és double típusúvá kell alakítani, hogy megfeleljen a formális referenciaparaméternek. Ennek az átalakításnak az eredménye egy ideiglenes változó. Mivel egy hivatkozásnak nincs const specifikációja, az ilyen változók nem használhatók inicializálására.
      Íme egy másik példa, ahol nincs egyezés a formális referenciaparaméter és a tényleges argumentum között:
    • B osztály; void takeB(B&); BgiveB(); int main() (
      venniB(adniB()); // hiba: a paraméternek const B & típusúnak kell lennie
      visszatérés 0;
      }
    • A takeB() függvény meghívása hiba. A tényleges argumentum a visszatérési érték, azaz. egy ideiglenes változó, amely nem használható hivatkozás inicializálására a const specifikátor nélkül.
      Mindkét esetben azt látjuk, hogy ha a formális referenciaparaméterben szerepel a const specifikátor, akkor pontos egyezés állapítható meg közötte és a tényleges argumentum között.

    Vegye figyelembe, hogy mind az l-érték-r-érték konverzió, mind a referencia inicializálás pontos egyezésnek minősül. Ebben a példában az első függvényhívás hibát eredményez:

    Void print(int); voidprint(int&); intiobj;
    int &ri = iobj; int main() (
    print(iobj); // hiba: kétértelműség
    print(ri); // hiba: kétértelműség
    nyomtatás(86); // helyes: call print(int)
    visszatérés 0;
    }

    Az iobj objektum egy argumentum, amely mindkét print() függvényre leképezhető, azaz a hívás kétértelmű. Ugyanez vonatkozik a következő sorra is, ahol az ri hivatkozás mindkét print() függvénynek megfelelő objektumot jelöli. A harmadik felhívással viszont minden rendben van. Számára a print(int&) nem jól bevált. Az egész konstans egy r-érték, így nem inicializálhat referenciaparamétert. Az egyetlen beépített függvény a print(86) meghívására a print(int), ezért ezt túlterhelési felbontásban választják.
    Röviden, ha a formális argumentum egy hivatkozás, akkor a tényleges argumentum akkor pontos egyezés, ha képes inicializálni a hivatkozást, és nem másként.

    9.6. gyakorlat

    Nevezzen meg két triviális transzformációt, amelyek megengedettek a pontos egyezés megállapítása során.

    9.7. gyakorlat

    Mi a rangja az egyes argumentumkonverzióknak a következő függvényhívásokban:

    (a) void print(int *, int); int arr; print(arr, 6); // függvényhívás (b) void manip(int, int); manip("a", "z"); // függvényhívás (c) int calc(int, int); dupla dobj; double = calc(55.4, dobj) // függvényhívás (d) void set(const int *); int*pi; set(pi); // függvényhívás

    9.8. gyakorlat

    Az alábbi hívások közül melyik a hibás, mert nincs konverzió az aktuális argumentum típusa és a formális paraméter között:

    (a) enum Stat( Fail, Pass ); érvénytelen teszt (Stat); szöveg(0); // függvényhívás (b) void reset(void *); reset(0); // függvényhívás (c) void set(void *); int*pi; set(pi); // függvényhívás (d) #include lista opera(); void print(oper()); // függvényhívás (e) void print(const int); intiobj; print(iobj); // függvényhívás

    9.4. A funkció túlterhelési felbontásának részletei

    A 9.2. szakaszban már említettük, hogy a funkció túlterhelés-feloldási folyamata három lépésből áll:

    1. Állítsa be a jelölt függvények készletét egy adott hívás feloldásához, valamint az aktuális argumentumlista tulajdonságait.
    2. A jelöltek halmazából válassza ki a beállított függvényeket - azokat, amelyek a tényleges argumentumok adott listájával hívhatók, figyelembe véve azok számát és típusát.
    3. Válassza ki a hívásnak leginkább megfelelő függvényt a tényleges argumentumokra alkalmazandó transzformációk rangsorolásával, hogy azok megfeleljenek a létrehozott függvény formális paramétereinek.

    Készen állunk arra, hogy részletesebben megvizsgáljuk ezeket a lépéseket.

    9.4.1. Jelölt funkciók

    A jelölt függvény olyan függvény, amelynek neve megegyezik a meghívottéval. A jelölteket kétféleképpen választják ki:

    • a függvény deklarációja a hívás pontján látható. A következő példában
      void f(); void f(int); void f(double, double = 3,4); void f(char*, char*); int main() (
      f(5,6); // négy jelölt van a felhívás megoldására
      visszatérés 0;
      }
    • mind a négy f() függvény teljesíti ezt a feltételt. Ezért a jelöltek halmaza négy elemet tartalmaz;
    • ha az aktuális argumentum típusa egy névtérben van deklarálva, akkor ennek a térnek a hívott függvényével azonos nevű tagfüggvényei hozzáadódnak a jelölt halmazhoz: namespace NS ( class C ( /* ... */ ); void takeC( C&); ) // A cobj típus az NS névtérben deklarált C osztály
      NS::Cobj; int main() (
      // a takeC() függvények egyike sem látható a hívóponton
      takeC(cobj); // helyes: NS::takeC(C&) meghívva,
      // mert az argumentum NS::C típusú, tehát
      // figyelembe veszi a takeC() függvényt,
      // az NS névtérben deklarálva
      visszatérés 0;
      }

    Így a jelöltek halmaza a szakszervezet függvénykészlet, látható a hívási pontnál, és a tényleges argumentumtípusokkal azonos névtérben deklarált függvénykészlet.
    A hívás pillanatában látható túlterhelt függvények halmazának azonosításakor a korábban már tárgyalt szabályok érvényesek.
    A beágyazott hatókörben deklarált függvény inkább elrejti, semmint túlterheli a külső hatókör azonos nevű függvényét. Ilyen helyzetben csak a beágyazott hatókörön belüli függvények lesznek jelöltek, pl. azokat, amelyek híváskor nincsenek elrejtve. A következő példában a hívóponton látható jelölt függvények a format(double) és a format(char*):

    Char* formátum(int); void g() ( char *formátum(double); char* formátum(char*); formátum(3); // formátum (double) hívása)
    }

    Mivel a globális hatókörben deklarált format(int) rejtett, nem szerepel a jelölt függvények halmazában.
    A jelöltek bemutatása a hívás helyén látható nyilatkozatokkal lehetséges:

    Névtér libs_R_us ( int max(int, int); double max(double, double); ) char max(char, char); void func()
    {
    // a névtér függvényei láthatatlanok
    // mindhárom hívás a max(char, char) globális függvény javára van feloldva
    max(87, 65);
    max(35,5; 76,6);
    max("J", "L");
    }

    A libs_R_us névtérben definiált max() függvények láthatatlanok a hívási pontban. Az egyetlen látható a max() függvény a globális hatókörből; csak ez szerepel a jelölt függvények halmazában, és mindhárom func () hívásnál meghívásra kerül. A use deklaráció segítségével megjeleníthetjük a max() függvényeket a libs_R_us névtérből. Hová kell tenni a használati nyilatkozatot? Ha belefoglalja a globális hatókörbe:

    Char max(char, char); a libs_R_us::max; // use-declaration

    majd a libs_R_us max() függvényei hozzáadódnak a túlterhelt függvények halmazához, amely már tartalmazza a globális hatókörben deklarált max() függvényt. Most már mindhárom függvény látható a func()-ban, és jelöltté válik. Ebben a helyzetben a func() hívások megoldása a következőképpen történik:

    Void func() ( max(87, 65); // meghívva: libs_R_us::max(int, int) max("J", "L"); // hívva::max(char, char) )

    De mi történik, ha a func() függvény helyi hatókörébe beszúrunk egy use deklarációt, amint az ebben a példában látható?

    void func() ( // a-deklaráció használata a libs_R_us::max használatával; // ugyanazok a függvényhívások, mint fent
    }

    A max() függvények közül melyik fog szerepelni a jelölthalmazban? Emlékezzünk arra, hogy a deklarációk használata egymásba van ágyazva. Egy ilyen deklarációval a lokális hatókörben a max(char, char) globális függvény el van rejtve, így csak

    Libs_R_us::max(int, int); libs_R_us::max(double, double);

    Ők a jelöltek. Most a func() hívások a következőképpen vannak megoldva:

    Void func() ( // using-declaration // A max(char, char) globális függvény el van rejtve a libs_R_us::max használatával; max(87, 65); // libs_R_us::max(int, int)
    max(35,5; 76,6); // A libs_R_us::max(double, double) meghívásra kerül
    max("J", "L"); // a libs_R_us::max(int, int) meghívása
    }

    A használó direktívák a függvényjelölt halmaz összetételét is befolyásolják. Tegyük fel, hogy úgy döntünk, hogy a libs_R_us névtér max() függvényeit a func()-ban láthatóvá tesszük. Ha a következő használó direktívát a globális hatókörbe helyezzük, akkor a jelölt függvények halmaza a max(char, char) globális függvényből, valamint a libs_R_us-ban deklarált max(int, int) és max(double, double) függvényekből áll majd:

    Névtér libs_R_us ( int max(int, int); double max(double, double); ) char max(char, char);
    névtér használatával libs_R_us; // void func() direktíva használatával
    {
    max(87, 65); // a libs_R_us::max(int, int) meghívása
    max(35,5; 76,6); // A libs_R_us::max(double, double) meghívásra kerül
    }

    Mi történik, ha a use direktívát a helyi hatókörbe helyezi, mint a következő példában?

    Void func() ( // a-directive használata névtér használatával libs_R_us; // ugyanazok a függvényhívások, mint fent
    }

    A max() függvények közül melyik lesz a jelöltek között? Emlékezzünk vissza, hogy a használó direktíva láthatóvá teszi a névtér tagjait, mintha azon a területen kívül lennének deklarálva, azon a ponton, ahol egy ilyen direktíva el van helyezve. Példánkban a libs_R_us tagjai a func() függvény lokális hatókörében láthatók, mintha téren kívülre lettek volna deklarálva - a globális hatókörben. Ebből következik, hogy a func()-on belül látható túlterhelt függvények halmaza megegyezik az előzővel, azaz. magába foglalja

    Max(char, char); libs_R_us::max(int, int); libs_R_us::max(double, double);

    A use-direktíva megjelenik a lokális vagy globális hatókörben, a func() függvény hívásainak felbontását ez nem érinti:

    Void func() ( a libs_R_us névtér használatával; max(87, 65); // a libs_R_us::max(int, int) meghívása
    max(35,5; 76,6); // A libs_R_us::max(double, double) meghívásra kerül
    max("J", "L"); // hívva::max(int, int)
    }

    Tehát a jelöltek halmaza a hívási ponton látható függvényekből áll, beleértve a use-declarations és use-directive által bevezetetteket, valamint a tényleges argumentumtípusokhoz tartozó névterekben deklarált függvényeket. Például:

    Névtér basicLib ( int print(int); double print(double); ) névtér matrixLib ( osztálymátrix ( /* ... */ ); void print(const maxtrix &); ) void display() ( basicLib::print használatával matrixLib::matrix mObj;
    print(mObj); // maxtrixLib hívása::print(const maxtrix &) print(87); // A basicLib::print(const maxtrix &) meghívásra kerül
    }

    A print(mObj) jelöltek a basicLib::print(int) és basicLib::print(double) függvények display()-en belüli use-deklaratai, mivel ezek láthatók a hívóponton. Mivel a függvény tényleges argumentuma matrixLib::matrix típusú, a matrixLib névtérben deklarált print() függvény is jelölt lenne. Melyek a print(87) jelölt függvényei? Csak basicLib::print(int) és basicLib::print(double) látható a hívóponton. Mivel az argumentum int típusú, a többi jelölt keresése során a rendszer nem veszi figyelembe a további névtereket.

    9.4.2. Kialakult jellemzők

    A jól bevált funkció az egyik jelölt. Formális paramétereinek listája vagy ugyanannyi elemet tartalmaz, mint a hívott függvény tényleges argumentumainak listája, vagy több. Ez utóbbi esetben azért további beállítások alapértelmezett értékek vannak megadva, különben a függvény nem hívható meg a megadott számú argumentummal. Ahhoz, hogy egy függvényt stabilnak lehessen tekinteni, minden tényleges argumentumot át kell alakítani a megfelelő formális paraméter típusára. (Az ilyen átalakításokat a 9.3. pont tárgyalja.)
    A következő példában az f(5.6) függvény hívásának két beállított függvénye van: f(int) és f(double).

    void f(); void f(int); void f(double); void f(char*, char*); int main() (
    f(5,6); // 2 beállított függvény: f(int) és f(double)
    visszatérés 0;
    }

    Az f(int) függvény azért maradt fenn, mert csak egy formális paramétere van, ami megfelel a hívásban lévő tényleges argumentumok számának. Ezenkívül létezik egy double típusú argumentum szabványos átalakítása int-re. Az f(double) függvény is megmaradt; van egy double típusú paramétere is, és ez pontosan megegyezik a tényleges argumentummal. Az f() és f(char*, char*) jelölt függvények kikerülnek a túlélő függvények listájából, mert nem hívhatók meg egyetlen argumentummal.
    A következő példában a format(3) egyetlen beállított függvénye a format(double). Bár a format(char*) jelölt egyetlen argumentummal is meghívható, az aktuális int argumentum típusáról nem történik átalakítás a formális paraméter char* típusára, ezért a függvény nem tekinthető jól bevált.

    Char* formátum(int); void g() ( // a globális függvényformátum(int) rejtett char* format(double); char* formátum(char*); format(3); // egyetlen beállított függvény van: format(double) )

    A következő példában mindhárom jelölt függvény képes lesz meghívni a max() függvényt a func()-on belül. Mindegyik két argumentummal hívható meg. Mivel a tényleges argumentumok int típusúak, pontosan megfelelnek a libs_R_us::max(int, int) függvény formális paramétereinek, és konvertálással a libs_R_us::max(double, double) függvény paramétertípusaira önthetők. egész számok lebegtetése, valamint libs_R_us::max(char, char) függvényparaméterek típusa egész szám típusú konverzióval.


    a libs_R_us::max; char max(char, char);
    void func()
    {
    // mindhárom max() függvény jól bevált
    max(87, 65); // a libs_R_us::max(int, int) használatával hívjuk meg
    }

    Ne feledje, hogy a több paraméterrel rendelkező jelölt függvény azonnal megszűnik, amint azt tapasztalja, hogy a tényleges argumentumok egyike nem önthető át a megfelelő formális paraméter típusára, még akkor is, ha az összes többi argumentum esetében létezik ilyen konverzió. A következő példában a min(char *, int) függvény ki van zárva a túlélők halmazából, mivel nem lehet az első int argumentum típusát a megfelelő char * paraméter típusára konvertálni. És ez annak ellenére történik, hogy a második argumentum pontosan megegyezik a második paraméterrel.

    extern double min(double, double); extern double min(char*, int); void func()
    {
    // egy jelölt függvény min(double, double)
    min (87, 65); // hívás min(double, double)
    }

    Ha a jelöltek halmazából kizárva az összes nem megfelelő paraméterszámú függvényt és azokat, amelyek paramétereihez nem történt megfelelő transzformáció, nincsenek állandóak, akkor a függvényhívás feldolgozása fordítási hibával zárul. Ebben az esetben azt mondják, hogy nem találtak egyezést.

    Érvénytelen nyomtatás (unsigned int); voidprint(char*); voidprint(char); int*ip;
    osztály SmallInt ( /* ... */ );
    SmallInt si; int main() (
    print(ip); // hiba: nincs beállított függvény: nem található egyezés
    nyomtat(si); // hiba: nincs beállított függvény: nem található egyezés
    visszatérés 0;
    }

    9.4.3. Legjobb bevált funkció

    A legjobbnak azok a megállapított függvények tekinthetők, amelyek formális paraméterei leginkább megfelelnek az aktuális argumentumok típusainak. Minden ilyen függvény esetében az egyes argumentumokra alkalmazott típuskonverziókat rangsorolják, hogy meghatározzák, mennyire egyezik a paraméterrel. (A 6.2. szakasz leírja a támogatott típuskonverziókat.) A legjobban bevált függvény egy olyan függvény, amely egyszerre teljesít két feltételt:

    • az argumentumokra alkalmazott transzformációk nem rosszabbak, mint bármely más jól bevált függvény meghívásához szükséges transzformációk;
    • legalább egy argumentum esetében az alkalmazott transzformáció jobb, mint bármely más jól bevált függvény ugyanazon argumentumánál.

    Előfordulhat, hogy ahhoz, hogy a tényleges argumentumot a megfelelő formális paraméter típusára öntsük, több konverziót kell végrehajtani. Tehát a következő példában

    Int arr; void putValues(const int *); int main() (
    putValues(arr); // 2 átalakítás szükséges
    // tömb mutató + specifikáló konverzió
    visszatérés 0;
    }

    az arr argumentum „három int tömb” típusból „pointer to const int” típusba történő átadásához egy konverziós sorozat kerül alkalmazásra:

    1. Tömb mutató konverzió, amely három intből álló tömböt alakít át int mutatóvá.
    2. Specifikus konverzió, amely az int mutatót const int mutatóvá alakítja.

    Ezért helyesebb lenne azt mondani, hogy ahhoz, hogy a tényleges argumentumot a beállított függvény formális paraméterének típusára öntsük, konverziók sorozatára van szükség. Mivel nem egy, hanem több transzformációt alkalmazunk, a függvénytúlterhelés-feloldási folyamat harmadik lépése tulajdonképpen a transzformációk sorozatait rangsorolja.
    Egy ilyen sorozat rangját a benne foglalt transzformációk közül a legrosszabb rangnak tekintjük. A 9.2. szakaszban leírtak szerint a típuskonverziók rangsorolása a következő: a pontos egyezés jobb, mint a típusbővítmény, a típusbővítmény pedig jobb, mint a normál konverzió. Az előző példában mindkét változtatásnak pontos egyezési rangja van. Ezért az egész sorozatnak ugyanaz a rangja.
    Egy ilyen gyűjtemény több átalakításból áll, amelyeket a bemutatott sorrendben alkalmaznak:

    l-érték konverzió -> típusbővítés vagy szabványos konverzió -> specifikáló konverzió

    Az l-érték konverzió kifejezés a 9.2. szakaszban tárgyalt pontos egyezésű átalakítások közül az első háromra vonatkozik: l-érték-r-érték konverzióra, tömb-mutató konverzióra és függvény-mutató konverzióra. Az átalakítások sorozata nulla vagy egy l-érték konverzióból áll, ezt követi a nulla vagy egy típuskiterjesztés vagy szabványos konverzió, végül pedig a nulla vagy egy specifikációs konverzió. Mindegyik fajtából csak egy transzformáció alkalmazható tényleges argumentum leadására egy formális paraméter típusára.

    A leírt sorozatot standard transzformációk sorozatának nevezzük. Van egy sorozat is felhasználó által meghatározott konverziók, amely egy olyan konverter függvényhez van társítva, amely az osztály tagja. (A konvertereket és a felhasználó által meghatározott konverziók sorozatait a 15. fejezet tárgyalja.)

    Melyek azok a sorozatok, amelyekben a tényleges argumentumok megváltoznak a következő példában?

    Névtér libs_R_us ( int max(int, int); double max(double, double); ) // using-declaration
    a libs_R_us::max; void func()
    {
    char c1, c2;
    max(c1, c2); // a libs_R_us::max(int, int) meghívása
    }

    A max() függvényhívás argumentumai char típusúak. Az argumentumtranszformációk sorrendje a libs_R_us::max(int,int) függvény meghívásakor a következő:

    1a. Mivel az argumentumokat érték adja át, az l-érték r-értékké alakítása kivonja a c1 és c2 argumentumok értékeit.

    2a. Az argumentumok típusbővítéssel konvertálódnak char-ból int-be.
    Az argumentumtranszformációk sorrendje a libs_R_us::max(double,double) függvény meghívásakor a következő:
    1b. Az l-érték r-értékké való konvertálásával a c1 és c2 argumentumok értékei kivonásra kerülnek.

    2b. Az integer és a float típusok közötti szabványos konverzió a char típustól a double típusig vetíti az argumentumokat.

    Az első sorozat rangja típusbővítés (a legrosszabb változás alkalmazott), míg a másodiké egy szabványos konverzió. Mivel a típuskiterjesztés jobb, mint a típuskonverzió, a libs_R_us::max(int,int) függvényt választottuk a legjobban ehhez a híváshoz.
    Ha az argumentumtranszformációk rangsoroló sorozatai egyetlen jól bevált függvényt sem tárnak fel, akkor a hívás kétértelműnek minősül. Ebben a példában a calc() mindkét hívása a következő sorrendet igényli:

    1. Konvertálja az l-értéket r-értékké az i és j argumentumértékek kinyeréséhez.
    2. Szabványos konverzió az aktuális argumentumok megfelelő formális paraméterekbe öntéséhez.

    Mivel nem lehet megmondani, hogy ezek közül a sorozatok közül melyik jobb a másiknál, a hívás kétértelmű:

    Int i, j; extern long calc(hosszú, hosszú); extern double calc(double, double); void jj() ( // hiba: kétértelműség, nincs legjobb egyezés
    calc(i,j);
    }

    A specifikus konverzió (const vagy volatile specifikátor hozzáadása a mutatót megszólító típushoz) pontos egyezési ranggal rendelkezik. Ha azonban két transzformációs sorozat csak abban különbözik, hogy az egyiknek a végén van egy további specifikáló transzformációja, akkor az anélküli szekvenciát tekintjük jobbnak. Például:

    void reset(int *); void reset(const int *); int*pi; int main() (
    reset(pi); // a specifikációk konvertálása nélkül jobb:
    // válassza ki a reset(int *)
    visszatérés 0;
    }

    Az első reset(int*)-jelölt függvény tényleges argumentumára alkalmazott szabványos konverziók sorozata pontos egyezés, csak az l-értékről az r-értékre kell lépni az argumentum értékének kinyeréséhez. A második jelölt függvény reset(const int *) esetében szintén az l-érték r-érték transzformációt alkalmazzuk, de ezt követi a specifikátor átalakítás is, amely az eredményül kapott értéket mutatóból intbe mutatóba const int-be adja. Mindkét sorozat pontosan egyezik, de nincs kétértelműség. Mivel a második szekvencia abban különbözik az elsőtől, hogy a végén egy specifikáló transzformáció található, az ilyen transzformáció nélküli szekvencia tekinthető a legjobbnak. Ezért a reset(int*) a legjobb álló funkció.
    Íme egy másik példa, ahol az öntési specifikációk befolyásolják, hogy melyik sorozatot választjuk:

    int kivonat(void*);
    int kivonat(const void *);

    int main() (
    kivonat(pi); // kivonat kiválasztása(void *)
    visszatérés 0;
    }

    Itt két beállított függvény hívható meg: extract(void*) és extract(const void*). Az extrakció(void*) függvény transzformációinak sorozata abból áll, hogy az l-értéket r-értékké alakítjuk a kivonathoz érvértékek, majd a szokásos mutatókonverzió következik: pointer to int to a pointer to void. Az extraktum(const void*) függvény esetében ez a szekvencia abban különbözik az elsőtől, hogy a specifikátorokat további konverzióval alakítja át, hogy az eredménytípust mutatóból void-ba mutatóból const void-ba adja át. Mivel a szekvenciák csak ebben a transzformációban különböznek egymástól, az elsőt választottuk megfelelőbbnek, így az extra (const void*) függvény lesz a legjobban álló.
    A const és a volatile specifikációk szintén befolyásolják a referenciaparaméterek inicializálásának rangsorát. Ha két ilyen inicializálás csak a const és a volatile specifikátor hozzáadásával tér el, akkor az extra specifikátor nélküli inicializálás jobbnak tekinthető a túlterhelési felbontásban:

    #beleértve void manip(vektor &); void manip(const vektor &); vektor f();
    külső vektor vec; int main() (
    manip(vec); // select manip(vector &)
    manip(f()); // select manip(const vector &)
    visszatérés 0;
    }

    Az első hívásban a referencia inicializálása bármely függvényhíváshoz pontosan megegyezik. De ez a kihívás továbbra sem lesz kétértelmű. Mivel mindkét inicializálás ugyanaz, kivéve egy további const specifikáció jelenlétét a második esetben, az ilyen specifikáció nélküli inicializálást jobbnak tartják, így a túlterhelés a jól bevált manip(vektor javára lesz megoldva &).
    A második híváshoz csak egy jól bevált függvény manip(const vektor &). Mivel a tényleges argumentum egy ideiglenes változó, amely tartalmazza az f() által visszaadott eredményt, az ilyen argumentum egy r-érték, amely nem használható a manip(vector) nem állandó formális referenciaparaméterének inicializálására &). Ezért az egyetlen jól bevált manip(const vektor &).
    Természetesen a függvényeknek több tényleges argumentuma is lehet. Az állandóak közül a legjobbat az összes argumentum transzformációs sorrendjének figyelembevételével kell kiválasztani. Vegyünk egy példát:

    extern int ff(char*, int); extern int ff(int, int); int main() ( ff(0, "a"); // ff(int, int)
    visszatérés 0;
    }

    Az ff() függvény, amely két int argumentumot tartalmaz, a legjobb állapotú függvénynek lett kiválasztva a következő okok miatt:

    1. az első érve jobb. A 0 pontosan megegyezik egy formális int paraméterrel, míg a szabványos mutatókonverzió szükséges a char * típusú paraméterhez;
    2. második érve is azonos rangú. A char típusú "a" argumentumhoz a két függvény bármelyikének második formális paraméterével való megfelelés megállapításához egy olyan konverziós sorozatot kell alkalmazni, amelynek típuskiterjesztés rangja van.

    Íme egy másik példa:

    int compute(const int&, short); int compute(int&, double); külső intiobj;
    int main() (
    compute(iobj, "c"); // számítás(int&, double)
    visszatérés 0;
    }

    Mindkét compute(const int&, short) és compute(int&, double) függvény megmaradt. A másodikat választották a legjobbnak a következő okok miatt:

    1. az első érve jobb. Az első létrehozott függvény referencia-inicializálása rosszabb, mert ehhez egy const specifikátort kell hozzáadni, amelyre nincs szükség a második függvényhez;
    2. második érve is azonos rangú. A char típusú "c" argumentumhoz a két függvény bármelyikének második formális paraméterével való megfelelés megállapításához standard transzformáció rangú transzformációsorozatot kell alkalmazni.

    9.4.4. Érvek alapértelmezett értékekkel

    Az alapértelmezett értékekkel rendelkező argumentumok sok jól bevált funkciót kiterjeszthetnek. A maradék függvények, amelyeket a tényleges argumentumok adott listájával hívunk meg. De egy ilyen függvénynek több formális paramétere is lehet, mint a ténylegesen megadott argumentum, abban az esetben, ha minden paraméterhez van valamilyen alapértelmezett érték, amely nincs megadva:

    extern void ff(int); extern void ff(hosszú, int = 0); int main() (
    ff(2L); // egyezik: ff(long, 0); ff(0; 0); // egyezik: ff(long, int);
    ff(0); // egyezik az ff(int);
    ff(3,14); // hiba: kétértelműség
    }

    Az első és a harmadik hívásnál az ff() függvény ki van jelölve, annak ellenére, hogy csak egy argumentumot adtak át. Ennek oka a következő okok:

    1. van egy alapértelmezett érték a második formális paraméterhez;
    2. a long típusú első paraméter pontosan megfelel az első hívás tényleges argumentumának, és a harmadik hívás argumentumának típusára önthető egy szabványos konverzió rangú szekvenciával.

    Az utolsó hívás kétértelmű, mert mindkét beállított függvény kiválasztható a standard transzformáció alkalmazásával az első argumentumra. Az ff(int) függvény csak azért nem előnyös, mert egy paramétere van.

    9.9. gyakorlat

    Magyarázza el, mi történik, ha megoldja a túlterhelést a compute() meghívására a main() függvényen belül. Milyen jellemzői vannak a jelölteknek? Melyikük áll majd meg az első lépés után? Milyen transzformációs sorozatot kell alkalmazni az aktuális argumentumra, hogy az megfeleljen az egyes létrehozott függvények formális paramétereinek? Melyik funkció lesz a legjobb állás?

    Névtér primerLib ( void compute(); void compute(const void *); ) primerLib::compute;
    void compute(int);
    void compute(double, double = 3,4);
    void compute(char*, char* = 0); int main() (
    számítás(0);
    visszatérés 0;
    }

    Mi történik, ha a use deklaráció a main()-ba kerül a compute() meghívása előtt? Válaszoljon ugyanazokra a kérdésekre.

    A függvénytúlterhelés több (két vagy több) függvény definiálása azonos névvel, de eltérő paraméterekkel. A túlterhelt funkciók paraméterkészletei sorrendben, számban és típusban eltérőek lehetnek. Így a funkció túlterhelésére azért van szükség, hogy elkerüljük a hasonló műveleteket végrehajtó, de eltérő programlogikával rendelkező függvények nevének megkettőzését. Vegyük például a areaRectangle() függvényt, amely egy téglalap területét számítja ki.

    Float areaRectangle(float, float) //függvény, amely egy téglalap területét számítja ki két paraméterrel a(cm) és b(cm) ( return a * b; // megszorozzuk a téglalap oldalainak hosszát és visszatérünk a kapott termék)

    Tehát ez egy két float típusú paraméterrel rendelkező függvény, és a függvénynek átadott argumentumoknak centiméterben kell lenniük, a float típusú visszatérési értéke is centiméterben van megadva.

    Tegyük fel, hogy kiindulási adataink (téglalap oldalai) méterben és centiméterben vannak megadva, például: a = 2m 35 cm; b = 1m 86 cm Ebben az esetben célszerű négy paraméterű függvényt használni. Vagyis a téglalap oldalainak minden hosszát két paraméterben adjuk át a függvénynek: méterben és centiméterben.

    Float areaRectangle(float a_m, float a_sm, float b_m, float b_sm) // függvény, amely kiszámolja egy téglalap területét 4 paraméterrel a(m) a(cm); b(m) b(cm) (vissza (a_m * 100 + a_sm) * (b_m * 100 + b_sm); )

    A függvény törzsében a méterben átadott értékeket (a_m és b_m) centiméterekre konvertálják, és hozzáadják az a_sm b_sm értékekhez, majd megszorozzuk az összegeket, és megkapjuk a téglalap területét cm-ben persze lehetett az eredeti adatokat centire konvertálni és az első függvényt használni, de most nem erről van szó.

    Most az a legfontosabb, hogy két függvényünk van, különböző aláírásokkal, de azonos névvel (túlterhelt függvények). Az aláírás egy függvény nevének és paramétereinek kombinációja. Hogyan lehet ezeket a függvényeket hívni? És a túlterhelt függvények hívása nem különbözik a szokásos függvények hívásától, például:

    TerületTéglalap(32, 43); // egy függvényt hívunk meg, amely kiszámolja egy téglalap területét két paraméterrel: a(cm) és b(cm) areaRectangle(4, 43, 2, 12); // egy függvényt hívunk meg, amely kiszámítja egy téglalap területét 4 paraméterrel a(m) a(cm); b(m) b(cm)

    Amint látja, a fordító fogja választani kívánt funkciót, csak a túlterhelt függvények aláírásait elemzi. A függvény túlterhelését megkerülve egyszerűen deklarálhatna egy függvényt más néven, és az jól ellátná a feladatát. De képzelje el, mi történik, ha kettőnél több ilyen funkcióra van szüksége, például 10-re. És mindegyikhez ki kell találnia egy értelmes nevet, és a legnehezebb megjegyezni őket. Pontosan ezért egyszerűbb és jobb a funkciók túlterhelése, hacsak persze nincs rá igény. Forrás programok az alábbiakban láthatók.

    #include "stdafx.h" #include << "S1 = " << areaRectangle(32,43) << endl; // вызов перегруженной функции 1 cout << "S2 = " << areaRectangle(4, 43, 2, 12) << endl; // вызов перегруженной функции 2 return 0; } // перегруженная функция 1 float areaRectangle(float a, float b) //функция, вычисляющая площадь прямоугольника с двумя параметрами a(см) и b(см) { return a * b; // умножаем длинны сторон прямоугольника и возвращаем полученное произведение } // перегруженная функция 2 float areaRectangle(float a_m, float a_sm, float b_m, float b_sm) // функция, вычисляющая площадь прямоугольника с 4-мя параметрами a(м) a(см); b(м) b(cм) { return (a_m * 100 + a_sm) * (b_m * 100 + b_sm); }

    // kód Code::Blocks

    // Dev-C++ kód

    #beleértve névtér használata std; // túlterhelt függvények prototípusai float areaRectangle(float a, float b); float areaRectangle(float a_m, float a_sm, float b_m, float b_sm); int main() ( cout<< "S1 = " << areaRectangle(32,43) << endl; // вызов перегруженной функции 1 cout << "S2 = " << areaRectangle(4, 43, 2, 12) << endl; // вызов перегруженной функции 2 return 0; } // перегруженная функция 1 float areaRectangle(float a, float b) //функция, вычисляющая площадь прямоугольника с двумя параметрами a(см) и b(см) { return a * b; // умножаем длинны сторон прямоугольника и возвращаем полученное произведение } // перегруженная функция 2 float areaRectangle(float a_m, float a_sm, float b_m, float b_sm) // функция, вычисляющая площадь прямоугольника с 4-мя параметрами a(м) a(см); b(м) b(cм) { return (a_m * 100 + a_sm) * (b_m * 100 + b_sm); }

    A program eredménye az 1. ábrán látható.

    Funkció túlterhelés

    A műveletek túlterhelése (operátorok, funkciók, eljárások)- a programozásban - a polimorfizmus megvalósításának egyik módja, amely abban áll, hogy egy művelet (operátor, függvény vagy eljárás) több különböző változatának egy hatókörében egyidejűleg létezhetnek, amelyek azonos nevű, de a paraméterek típusaiban különböznek. amelyre alkalmazzák.

    Terminológia

    A "túlterhelés" kifejezés az angol "overloading" pauszpapírja, amely az 1990-es évek első felében jelent meg a programozási nyelvekről szóló könyvek orosz fordításában. Talán ez nem a legjobb fordítási lehetőség, mivel az orosz "túlterhelés" szónak megvan a maga kialakult jelentése, amely gyökeresen különbözik az újonnan javasolttól, azonban gyökeret vert és meglehetősen elterjedt. A szovjet korszak kiadványaiban a hasonló mechanizmusokat oroszul „műveletek újradefiniálásának” vagy „újradefiniálásának” nevezték, de ez a lehetőség nem vitatható: eltérések és zavarok merülnek fel az angol „override”, „overload” és „ újradefiniál".

    A megjelenés okai

    A legtöbb korai programozási nyelvnek megvolt az a megkötése, hogy egy programban egyszerre legfeljebb egy művelet volt elérhető azonos néven. Ennek megfelelően a program egy adott pontján látható összes függvénynek és eljárásnak más-más nevet kell viselnie. A programozási nyelv részét képező függvények, eljárások és operátorok neveit és megnevezéseit a programozó nem használhatja saját függvényeinek, eljárásainak és operátorainak megnevezésére. Egyes esetekben a programozó létrehozhat saját programobjektumot egy másik, már létező nevével, de ekkor az újonnan létrehozott objektum „átfedésbe kerül” az előzővel, és lehetetlenné válik mindkét opció egyidejű használata.

    Ez a helyzet néhány meglehetősen gyakori esetben kényelmetlen.

    • Néha szükség van olyan műveletek leírására és alkalmazására a programozó által létrehozott adattípusokra, amelyek jelentésükben egyenértékűek a nyelven már elérhetőkkel. Klasszikus példa a komplex számokkal való munkavégzésre szolgáló könyvtár. A közönséges numerikus típusokhoz hasonlóan támogatják az aritmetikai műveleteket, és természetes lenne az ilyen típusú műveletekhez létrehozni a „plusz”, „mínusz”, „szorzás”, „osztás” műveletet, ugyanazokkal a műveleti előjelekkel jelölve őket, mint a többi numerikus esetében. típusok. A nyelvben definiált elemek használatának tilalma számos függvény létrehozását kényszeríti ki olyan néven, mint a ComplexPlusComplex, IntegerPlusComplex, ComplexMinusFloat és így tovább.
    • Ha azonos jelentésű műveleteket alkalmazunk különböző típusú operandusokra, akkor kénytelenek más elnevezést kapni. Ha nem lehet azonos nevű függvényeket használni a különböző típusú függvényekhez, akkor ugyanarra a dologra különböző neveket kell kitalálni, ami zavart okoz, és akár hibákhoz is vezethet. Például a klasszikus C nyelvben a szabványos könyvtárfüggvénynek két változata létezik egy szám modulusának meghatározására: abs() és fabs() – az első egész argumentumhoz, a második valós argumentumhoz való. Ez a helyzet a gyenge C típusú ellenőrzéssel kombinálva nehezen megtalálható hibához vezethet: ha egy programozó abs(x)-et ír a számításba, ahol x egy valós változó, akkor néhány fordító figyelmeztetés nélkül kódot generál, ami alakítsuk át x-et egész számmá a tört részek elvetésével, és számítsuk ki a modulust a kapott egészből!

    Részben a problémát objektumprogramozással oldják meg – amikor új adattípusokat osztályokként deklarálunk, a rajtuk végzett műveletek osztálymetódusokká formalizálhatók, beleértve az azonos nevű osztálymetódusokat is (mivel a különböző osztályok metódusainak nem kell rendelkezniük különböző nevek), de egyrészt a különböző típusú értékekre vonatkozó műveletek ilyen tervezési módja kényelmetlen, másrészt nem oldja meg az új operátorok létrehozásának problémáját.

    A kezelői túlterhelés önmagában csak "szintaktikai cukor", bár még önmagában is hasznos lehet, mert lehetővé teszi a fejlesztő számára, hogy természetesebb módon programozzon, és az egyedi típusok jobban viselkedjenek, mint a beépítettek. Ha általánosabb oldalról közelítjük meg a kérdést, akkor azt láthatjuk, hogy azok az eszközök, amelyek lehetővé teszik a nyelv bővítését, új műveletekkel, szintaktikai konstrukciókkal történő kiegészítését (és a műveletek túlterhelése az objektumok, makrók mellett az egyik ilyen eszköz , funkcionalitások, lezárások) már a metanyelvben is megfordítják – a konkrét feladatokra orientált nyelvek leírásának eszköze. Segítségével minden konkrét feladathoz a számára legmegfelelőbb nyelvi bővítmény építhető, amely lehetővé teszi a megoldás legtermészetesebb, legérthetőbb és legegyszerűbb leírását. Például a műveletek túlterhelésére szolgáló alkalmazásban: összetett matematikai típusok (vektorok, mátrixok) könyvtárának létrehozása és a velük végzett műveletek természetes, „matematikai” formában történő leírása létrehoz egy „vektorműveletek nyelvét”, amelyben a a számítások rejtve vannak, és a feladatok megoldását vektor- és mátrixműveletekkel lehet leírni, nem a technikára, hanem a probléma lényegére koncentrálva. Ezen okok miatt kerültek egykor az Algol-68 nyelvbe ilyen eszközök.

    Túlterhelési mechanizmus

    Végrehajtás

    Az operátorok túlterhelése két, egymással összefüggő tulajdonság bevezetését jelenti a nyelvbe: több eljárás vagy függvény deklarálása azonos néven ugyanabban a hatókörben, valamint a műveletek saját implementációinak leírása (vagyis a műveletek jelei, általában infix jelöléssel, operandusok közé írva). Alapvetően végrehajtásuk meglehetősen egyszerű:

    • Ahhoz, hogy több azonos nevű művelet létezhessen, elegendő egy olyan szabályt bevinni a nyelvbe, amely szerint egy műveletet (eljárást, függvényt vagy operátort) a fordító nemcsak névről (jelölésről) ismer fel, hanem paramétereik típusai szerint is. Tehát abs(i), ahol i egész számként van deklarálva, és abs(x), ahol x valósnak van deklarálva, két különböző művelet. Alapvetően nem okoz nehézséget egy ilyen értelmezés megadása.
    • A műveletek meghatározásához és újradefiniálásához megfelelő szintaktikai konstrukciókat kell bevezetni a nyelvbe. Elég sok lehetőség lehet, de valójában nem különböznek egymástól, elég megjegyezni, hogy az űrlap bevitele "<операнд1> <знакОперации> <операнд2>» alapvetően hasonló a « függvény meghívásához<знакОперации>(<операнд1>,<операнд2>)". Elég, ha a programozó függvények formájában leírja az operátorok viselkedését - és a leírás problémája megoldódik.

    Lehetőségek és problémák

    Az eljárások és funkciók általános elképzelés szintjén történő túlterhelése általában nem nehéz sem megvalósítani, sem megérteni. Azonban még ebben is van néhány "csapdája", amelyeket figyelembe kell venni. Az operátor túlterhelésének engedélyezése sokkal több problémát okoz mind a nyelvi implementátornak, mind az ezen a nyelven dolgozó programozónak.

    Azonosítási probléma

    Az eljárások és funkciók túlterhelését lehetővé tévő nyelvi fordító fejlesztője előtt az első kérdés a következő: hogyan lehet kiválasztani az azonos nevű eljárások közül azt, amelyiket ebben az esetben alkalmazni kell? Minden rendben van, ha van az eljárásnak olyan változata, amelynek formális paramétereinek típusai pontosan megegyeznek a jelen hívásban használt tényleges paraméterek típusaival. Azonban szinte minden nyelvben van bizonyos fokú szabadság a típusok használatában, feltételezve, hogy a fordító bizonyos helyzetekben automatikusan típusbiztos konverziót hajt végre. Például a valós és egész argumentumokkal végzett aritmetikai műveleteknél az egész szám általában automatikusan valós típusúvá alakul, és az eredmény valós. Tegyük fel, hogy az add függvénynek két változata van:

    int add(int a1, int a2); float add(float a1, float a2);

    Hogyan kezelje a fordító az y = add(x, i) kifejezést, ahol x egy float, i pedig int? Nyilvánvaló, hogy nincs pontos egyezés. Két lehetőség van: vagy y=add_int((int)x,i) , vagy y=add_flt(x, (float)i) (itt az add_int és add_float nevek a függvény első és második verzióját jelölik) .

    Felmerül a kérdés: engedélyeznie kell-e a fordítóprogramnak a túlterhelt függvények használatát, és ha igen, mi alapján választja ki az adott változatot? A fenti példában különösen a fordítónak figyelembe kell vennie az y változó típusát a választáskor? Megjegyzendő, hogy a fenti helyzet a legegyszerűbb, ennél sokkal bonyolultabb esetek is lehetségesek, amit súlyosbít, hogy nem csak a beépített típusok konvertálhatók a nyelv szabályai szerint, hanem a programozó által deklarált osztályok is. , ha rokoni kapcsolataik vannak, egymásnak vethetők. A problémára két megoldás létezik:

    • Egyáltalán tiltsák a pontatlan azonosítást. Követelje meg, hogy minden egyes típuspárhoz legyen egy pontosan megfelelő változata a túlterhelt eljárásnak vagy műveletnek. Ha nincs ilyen lehetőség, akkor a fordítónak hibát kell dobnia. A programozónak ebben az esetben explicit konverziót kell alkalmaznia, hogy a tényleges paramétereket a kívánt típuskészletre öntse. Ez a megközelítés kényelmetlen az olyan nyelvekben, mint például a C++, amelyek meglehetősen nagy szabadságot engednek meg a típusok kezelésében, mivel jelentős különbségekhez vezet a beépített és túlterhelt operátorok viselkedésében (az aritmetikai műveletek a közönséges számokra is alkalmazhatók gondolkodás nélkül, hanem más típusokhoz - csak kifejezett konverzióval) vagy a műveletek számos lehetőségének megjelenéséhez.
    • Határozzon meg bizonyos szabályokat a „legközelebbi illeszkedés” kiválasztásához. Általában ennél a változatnál a fordító a változatok közül azokat választja ki, amelyek hívásai csak biztonságos (nem veszteséges információs) típusú konverziókkal érhetők el a forrásból, és ha több is van belőlük, akkor az alapján választhat, hogy melyik változat igényel kevesebbet. ilyen átalakítások. Ha az eredmény egynél több lehetőséget hagy, a fordító hibát dob, és megköveteli a programozótól, hogy kifejezetten adja meg a változatot.

    A működés túlterhelésére vonatkozó speciális szempontok

    Az eljárásoktól és függvényektől eltérően a programozási nyelvek infix műveleteinek két további tulajdonsága van, amelyek jelentősen befolyásolják a funkcionalitásukat: a prioritás és az asszociativitás, amelyek jelenléte az operátorok "lánc" rögzítésének lehetőségéből adódik (hogyan kell megérteni a + b * c: mint (a+b)*c vagy mint a+(b*c) Az a-b+c kifejezés jelentése (a-b)+c vagy a-(b+c) ?).

    A nyelvbe épített műveletek mindig előre meghatározott hagyományos elsőbbséggel és asszociativitással rendelkeznek. Felmerül a kérdés: milyen prioritásokkal és asszociativitással rendelkeznek ezeknek a műveleteknek az újradefiniált változatai, vagy ráadásul a programozó által létrehozott új műveletek? Vannak más finomságok, amelyek pontosítást igényelhetnek. Például a C-ben a ++ és -- - előtag és utótag kétféle növelési és csökkentési operátor létezik, amelyek eltérően viselkednek. Hogyan viselkedjenek az ilyen operátorok túlterhelt verziói?

    A különböző nyelvek eltérő módon kezelik ezeket a kérdéseket. Így a C++-ban az operátorok túlterhelt verzióinak prioritása és asszociativitása ugyanaz marad, mint a nyelvben meghatározottak; lehetőség van a növelő és csökkentő operátorok elő- és utótag formáinak külön-külön túlterhelésére speciális aláírások segítségével:

    Tehát az int az aláírások változására szolgál

    Új műveletek bejelentése

    Az új műveletek bejelentésével még bonyolultabb a helyzet. Egy ilyen nyilatkozat lehetőségének a nyelvbe való belefoglalása nem nehéz, de megvalósítása jelentős nehézségekkel jár. Az új művelet deklarálása tulajdonképpen egy új programozási nyelvi kulcsszó létrehozása, amit bonyolít az a tény, hogy a szöveges műveletek általában határolók nélkül követhetnek más tokeneket. Amikor megjelennek, további nehézségek merülnek fel a lexikális elemző megszervezésében. Például, ha a nyelv már rendelkezik a „+” és az unáris „-” műveletekkel (jelváltás), akkor az a+-b kifejezés pontosan értelmezhető + (-b)-ként, de ha egy új művelet +- deklarálva van a programban, azonnal kétértelműség keletkezik, mert ugyanaz a kifejezés már a (+-) b -ként is értelmezhető. A nyelv fejlesztőjének és megvalósítójának valamilyen módon meg kell küzdenie az ilyen problémákkal. A lehetőségek szintén eltérőek lehetnek: megkövetelik, hogy minden új művelet egykarakteres legyen, feltételezzük, hogy eltérés esetén a művelet „leghosszabb” verzióját választjuk (vagyis a következő karakterkészletig, amelyet a a fordító bármely műveletnek megfelel, továbbra is olvasható), próbálja meg észlelni az ütközéseket a fordítás során, és hibákat generáljon ellentmondásos esetekben ... Így vagy úgy, az új műveletek deklarálását lehetővé tevő nyelvek megoldják ezeket a problémákat.

    Nem szabad elfelejteni, hogy az új műveleteknél az asszociativitás és a prioritás meghatározása is felmerül. Már nincs kész megoldás szabványos nyelvi művelet formájában, és általában csak be kell állítani ezeket a paramétereket a nyelv szabályaival. Például tegyen minden új műveletet balra asszociatívvá, és adja meg nekik ugyanazt, rögzített prioritást, vagy vigye be a nyelvbe mindkettő megadásának eszközét.

    Túlterhelés és polimorf változók

    Ha túlterhelt operátorokat, függvényeket és eljárásokat használnak erősen tipizált nyelvekben, ahol minden változónak van előre deklarált típusa, akkor a fordítónak kell eldöntenie, hogy a túlterhelt operátor melyik verzióját használja az adott esetben, bármilyen bonyolult is legyen. . Ez azt jelenti, hogy a lefordított nyelveknél az operátor túlterhelés alkalmazása nem vezet teljesítményromláshoz - mindenesetre a program objektumkódjában van egy jól definiált művelet vagy függvényhívás. Más a helyzet, ha lehetséges polimorf változók használata a nyelvben, vagyis olyan változók, amelyek különböző időpontokban különböző típusú értékeket tartalmazhatnak.

    Mivel az érték típusa, amelyre a túlterhelt műveletet alkalmazni kell, a kód fordításakor nem ismert, a fordító megfosztja attól a képességétől, hogy előzetesen válassza ki a megfelelő opciót. Ebben az esetben kénytelen beágyazni egy töredéket az objektumkódba, amely közvetlenül a művelet végrehajtása előtt meghatározza az argumentumokban szereplő értékek típusát, és dinamikusan kiválasztja a típuskészletnek megfelelő változatot. Sőt, ezt a definíciót a művelet minden egyes végrehajtásakor meg kell adni, mert akár ugyanazt a kódot másodszor is meghívva máshogyan lehet végrehajtani.

    Így az operátor-túlterhelés polimorf változókkal kombinálva elkerülhetetlenné teszi a meghívandó kód dinamikus meghatározását.

    Kritika

    A túlterhelés alkalmazását nem minden szakértő tekinti áldásnak. Ha a funkciók és eljárások túlterhelése általában nem kifogásolható (részben azért, mert nem vezet néhány tipikus „operátori” problémához, részben azért, mert kevésbé csábító a visszaélésre), akkor az operátor túlterhelése elvileg , és bizonyos nyelvi megvalósításokban, számos programozási teoretikus és gyakorló meglehetősen súlyos kritikának van kitéve.

    A kritikusok rámutatnak, hogy az azonosítás, a prioritás és az asszociativitás fent vázolt problémái gyakran szükségtelenül megnehezítik vagy természetellenesek a túlterhelt operátorok kezelését:

    • Azonosítás. Ha a nyelvnek szigorú azonosítási szabályai vannak, akkor a programozó kénytelen megjegyezni, hogy mely típuskombinációkhoz vannak túlterhelt műveletek, és ezekre manuálisan öntsön operandusokat. Ha a nyelv lehetővé teszi a "hozzávetőleges" azonosítást, soha nem lehet biztos abban, hogy egy meglehetősen bonyolult helyzetben pontosan az a műveleti változat kerül végrehajtásra, amelyre a programozó gondolt.
    • Prioritás és asszociativitás. Ha mereven vannak meghatározva, ez kényelmetlen lehet, és nem releváns a témakör szempontjából (például halmazokkal végzett műveleteknél a prioritások eltérnek az aritmetikai prioritásoktól). Ha ezeket a programozó be tudja állítani, ez további hibaforrássá válik (már csak azért is, mert egy művelet különböző változatai eltérő prioritásúak, vagy akár asszociativitással rendelkeznek).

    Hogy a saját műveletek használatának kényelme mennyivel tudja felülmúlni a program irányíthatóságának romlásával járó kényelmetlenséget, az kérdés, amire nincs egyértelmű válasz.

    A nyelvi megvalósítás szempontjából ugyanazok a problémák vezetnek a fordítók bonyolultságához, hatékonyságának és megbízhatóságának csökkenéséhez. És a túlterhelés polimorf változókkal együtt való alkalmazása is nyilvánvalóan lassabb, mint egy keménykódolt művelet hívása fordítás közben, és kevesebb lehetőséget biztosít az objektumkód optimalizálására. A túlterhelés megvalósításának sajátosságai különböző nyelveken külön kritikának vannak kitéve. A C++-ban tehát a kritika tárgya lehet a túlterhelt függvények nevének belső megjelenítésére vonatkozó megegyezés hiánya, ami a különböző C++ fordítók által összeállított könyvtárak szintjén inkompatibilitást okoz.

    Egyes kritikusok a szoftverfejlesztés elméletének általános elvei és a valós ipari gyakorlat alapján a túlterhelés ellen emelnek szót.

    • A nyelvépítés "puritán" megközelítésének hívei, mint például Wirth vagy Hoare, egyszerűen azért ellenzik a kezelői túlterhelést, mert könnyen eltekinthető tőle. Véleményük szerint az ilyen eszközök csak bonyolítják a nyelvet és a fordítót, anélkül, hogy ennek a bonyodalomnak megfelelő további funkciókat biztosítanának. Véleményük szerint a nyelv feladat-orientált kiterjesztésének létrehozásának ötlete csak vonzónak tűnik. Valójában a nyelvi bővítőeszközök használata csak a szerzője számára teszi érthetővé a programot - aki ezt a bővítményt fejlesztette. A program sokkal nehezebbé válik a többi programozó számára, hogy megértsék és elemezzék, ami megnehezíti a karbantartást, a módosítást és a csapatfejlesztést.
    • Megjegyzendő, hogy a túlterhelés használatának lehetősége gyakran provokatív szerepet játszik: a programozók, ahol csak lehetséges, elkezdik használni, ennek eredményeként a program egyszerűsítésére és egyszerűsítésére szolgáló eszköz a program bonyolításának és zavarának oka.
    • Előfordulhat, hogy a túlterhelt operátorok nem pontosan azt teszik, amit a fajtájuk alapján elvárnak tőlük. Például az a + b általában (de nem mindig) ugyanazt jelenti, mint a b + a , de az "egy" + "kettő" különbözik a "kettő" + "egy" kifejezéstől olyan nyelveken, ahol a + operátor túlterhelt. karakterlánc összefűzés.
    • Az operátor túlterhelése környezetérzékenyebbé teszi a programtöredékeket. A kifejezésekben szereplő operandusok típusának ismerete nélkül lehetetlen megérteni, mit csinál a kifejezés, ha túlterhelt operátorokat használ. Például egy C++ programban az utasítás<< может означать и побитовый сдвиг, и вывод в поток. Выражение a << 1 возвращает результат побитового сдвига значения a на один бит влево, если a - целая переменная, но если a является выходным потоком , то же выражение выведет в этот поток строку «1» .

    Osztályozás

    Az alábbiakban néhány programozási nyelv osztályozása látható aszerint, hogy lehetővé teszik-e a kezelő túlterhelését, és hogy az operátorok egy előre meghatározott készletre korlátozódnak-e:

    Tevékenységek Nincs túlterhelés Túlterhelés van
    Korlátozott műveletsor
    • Célkitűzés-C
    • Piton
    Lehetőség van új műveletek meghatározására
    • PostgreSQL
    • Lásd még

      Wikimédia Alapítvány. 2010 .

      Nézze meg, mi az a „Funkciótúlterhelés” más szótárakban:

        - (operátorok, függvények, eljárások) a programozásban a polimorfizmus megvalósításának egyik módja, amely abban áll, hogy egy hatókörben egy művelet több különböző változata (operátor, függvény vagy ... ... Wikipédia) egyidejűleg létezhet.



    Hogyan érhető el a funkció túlterhelése C-ben? (10)

    Van mód a függvény túlterhelésére C-ben? Olyan egyszerű funkciókat nézek, amiket túl lehet terhelni, pl

    foo (int a) foo (char b) foo (float c , int d)

    Azt hiszem, nincs közvetlen út; Megkerülő megoldásokat keresek, ha vannak.

    Remélem, az alábbi kód segít megérteni a funkció túlterhelését

    #beleértve #beleértve int fun(int a, ...); int main(int argc, char *argv)( fun(1,10); fun(2"questionbank"); return 0; ) int fun(int a, ...)( va_list vl; va_start(vl,a ); if(a==1) printf("%d",va_arg(vl,int)); else printf("\n%s",va_arg(vl,char *)); )

    Úgy értem, úgy érted – nem, nem teheted.

    A va_arg függvényt így deklarálhatjuk

    void my_func(char* formátum, ...);

    De át kell adnia néhány információt a változók számáról és típusairól az első argumentumban – például a printf() .

    Igen tetszik.

    Itt adsz egy példát:

    void printA(int a)( printf("Hello world from printA: %d\n",a; ) void printB(const char *buff)( printf("Hello world from printB: %s\n",buff) ; ) #define Max_ITEMS() 6, 5, 4, 3, 2, 1, 0 #define __VA_ARG_N(_1, _2, _3, _4, _5, _6, N, ...) N #define _Num_ARGS_(... ) __VA_ARG_N(__VA_ARGS__) #define NUM_ARGS(...) (_Num_ARGS_(_0, ## __VA_ARGS__, Max_ITEMS()) - 1) #define CHECK_ARGS_MAX_LIMIT(t) if(NUM_ARGS(args) #define_CHMINKKARG_t) if(NUM_ARGS(args) #define print(x , args ...) \ CHECK_ARGS_MIN_LIMIT(1) printf("hiba");fflush(stdout); \ CHECK_ARGS_MAX_LIMIT(4) printf("hiba");fflush(stdout) ; \ (( \ if (__beépített_típusok_kompatibilis_p ((x), int)) \ printA(x, ##args); \ else \ printB (x,##args); \ )) int main(int argc, char* * argv) ( int a=0; print(a); print("hello"); return (EXIT_SUCCESS); )

    0-t és hello-t ad ki a printA és a printB-ből.

    Ha a fordítóprogramod gcc, és nem bánod, ha minden alkalommal manuálisan frissíted, amikor új túlterhelést adsz hozzá, akkor csinálhatsz egy makrótömeget, és a hívó fél szempontjából a kívánt eredményt kapod, nem olyan szép írni... de lehetséges

    nézd meg a __builtin_types_compatibilis_p-t, majd használd olyan makrók meghatározására, amelyek

    #define foo(a) \ ((__builtin_types_compatibilis_p(int, a)?foo(a):(__builtin_types_compatible_p(float, a)?foo(a):)

    de igen, csúnya, csak nem

    EDIT: A C1X támogatást kap a típuskifejezésekhez, amelyek így néznek ki:

    #define cbrt(X) _Generic((X), long double: cbrtl, \ default: cbrt, \ float: cbrtf)(X)

    Amint már említettük, a túlterhelést abban az értelemben, ahogyan te érted, a C nem támogatja. A probléma megoldásának szokásos idióma az, hogy a függvény egy címkézett uniót vesz fel. Ezt a struct paraméterrel valósítjuk meg, ahol maga a struktúra valamilyen típusú típusjelzőből, például egy enumból és különböző értéktípusok uniójából áll. Példa:

    #beleértve typedef enum ( T_INT, T_FLOAT, T_CHAR, ) my_type; typedef struct ( my_type type; union ( int a; float b; char c; ) my_union; ) my_struct; void set_overload (my_struct *whatever) ( switch (bármi->type) ( T_INT eset: whatever->my_union.a = 1; break; case T_FLOAT: whatever->my_union.b = 2.0; break; case T_CHAR: whatever-> my_union.c = "3"; ) ) void printf_overload (my_struct *whatever) ( switch (bármilyen->type) ( case T_INT: printf("%d\n", whatever->my_union.a); break; case T_FLOAT : printf("%f\n", whatever->my_union.b); break; case T_CHAR: printf("%c\n", whatever->my_union.c); break; ) ) int main (int argc, char* argv) ( my_struct s; s.type=T_INT; set_overload(&s); printf_overload(&s); s.type=T_FLOAT; set_overload(&s); printf_overload(&s); s.type=T_CHAR; set_overload(&s) ; printf_overload(&s); )

    Nem használhatod a C++-t, és nem használhatod az összes többi C++ funkciót ezen kívül?

    Ha eddig nem volt szigorú C, akkor inkább variadic függvényeket javaslok.

    A következő megközelítés hasonló a a2800276, de néhány C99 makróval:

    // szükségünk van a `size_t` #include // argumentumtípusok a sum_arg_types enum elfogadásához ( SUM_LONG, SUM_ULONG, SUM_DOUBLE ); // egy argumentum tárolására szolgáló struktúra struct sum_arg ( enum sum_arg_types type; union ( long as_long; unioned long as_ulong; double as_double; ) érték; ); // egy tömb méretének meghatározása #define count(ARRAY) ((Sizeof (ARRAY))/(sizeof *(ARRAY))) // így lesz a függvényünk neve #define sum(...) _sum( count(sum_args(__VA_ARGS__)), sum_args(__VA_ARGS__)) // `struct sum_arg` tömb létrehozása #define sum_args(...) ((struct sum_arg )( __VA_ARGS__ )) // inicializálók létrehozása a sum argumentumokhoz #define (ÉRTÉK) ( SUM_LONG, ( .as_long = (VALUE) ) ) #define sum_ulong(VALUE) ( SUM_ULONG, ( .as_ulong = (VALUE) ) ) #define sum_double(VALUE) ( SUM_DOUBLE, ( .as_double = (ÉRTÉK) ) ) // polimorf függvényünk long double _sum(size_t count, struct sum_arg * args) ( long double value = 0; for(size_t i = 0; i< count; ++i) { switch(args[i].type) { case SUM_LONG: value += args[i].value.as_long; break; case SUM_ULONG: value += args[i].value.as_ulong; break; case SUM_DOUBLE: value += args[i].value.as_double; break; } } return value; } // let"s see if it works #include int main() ( unsigned long foo = -1; long double value = sum(sum_long(42), sum_ulong(foo), sum_double(1e10)); printf("%Le\n", érték); return 0; )

    Egyelőre az _Általános, mivel a kérdés _Általános, szabványos C (kiterjesztés nélkül) kapott támogatja a funkciók túlterhelését (és nem operátorok), köszönhetően a _Generic szónak _Generic a C11-ben. (a GCC a 4.9-es verzió óta támogatja)

    (A túlterhelés nem igazán "beépült" a kérdésben bemutatott módon, de könnyen elpusztítható valami, ami így működik.)

    A Generic egy fordítási idejű operátor, amely ugyanabba a családba tartozik, mint a sizeof és az _Alignof. Ezt a szabvány 6.5.1.1. szakasza írja le. Két fő paraméter kell hozzá: egy kifejezés (amely nem kerül kiértékelésre futás közben) és egy típus/kifejezés asszociációk listája, ami kicsit olyan, mint egy kapcsolóblokk. A _Generic lekéri a kifejezés általános típusát, majd „átvált” rá, hogy kiválassza a listából a típusának megfelelő végső eredménykifejezést:

    Generic(1, float: 2.0, char *: "2", int: 2, alapértelmezett: get_two_object());

    A fenti kifejezés 2-re értékeli ki - a vezérlőkifejezés típusa int , tehát az int-hez társított kifejezést választja ki értékként. Ezek egyike sem marad futásidőben. (Az alapértelmezett záradék kötelező: ha nem adja meg, és a típus nem egyezik, akkor fordítási hibát okoz.)

    A függvénytúlterhelésnél hasznos technika, hogy a C előfeldolgozó beillesztheti, és kiválaszthatja az eredménykifejezést a vezérlő makrónak átadott argumentumok típusa alapján. Tehát (példa a C szabványból):

    #define cbrt(X) _Általános((X), \ long double: cbrtl, \ default: cbrt, \ float: cbrtf \)(X)

    Ez a makró úgy valósítja meg a túlterhelt cbrt műveletet, hogy átadja az argumentum típusát a makrónak, kiválasztja a megfelelő implementációs függvényt, majd átadja az eredeti makrót annak a függvénynek.

    Tehát az eredeti példa megvalósításához a következőket tehetjük:

    Foo_int (int a) foo_char (char b) foo_float_int (float c , int d) #define foo(_1, ...) _Generic((_1), \ int: foo_int, \ char: foo_char, \ float: _Generic(() ELSŐ(__VA_ARGS__,)), \int: foo_float_int))(_1, __VA_ARGS__) #define FIRST(A, ...) A

    Ebben az esetben használhatjuk az alapértelmezett: kötést a harmadik esetre, de ez nem mutatja be, hogyan lehet kiterjeszteni az elvet több argumentumra. A végeredmény az, hogy használhatja a foo(...)-ot a kódjában anélkül, hogy (sokat) aggódna az argumentumok típusa miatt.

    Bonyolultabb helyzetekben, például olyan függvényeknél, amelyek több argumentumot túlterhelnek vagy számokat módosítanak, segédprogrammakrókat használhat a statikus elküldési struktúrák automatikus generálására:

    void print_ii(int a, int b) ( printf("int, int\n"); ) void print_di(double a, int b) ( printf("double, int\n"); ) void print_iii(int a, int b, int c) ( printf("int, int, int\n"); ) void print_default(void) ( printf("ismeretlen argumentumok\n"); ) #define print(...) OVERLOAD(print, (__VA_ARGS__), \ (print_ii, (int, int)), \ (print_di, (double, int)), \ (print_iii, (int, int, int)) \) #define OVERLOAD_ARG_TYPES (int, double) #define OVERLOAD_FUNCTIONS (print) #include "activate-overloads.h" int main(void) ( print(44, 47); // kiírja az "int, int" parancsot print(4.4, 47); // kiírja a "double, int" print (1, 2, 3); // kiírja az "int, int, int" parancsot print(""); // kiírja az "ismeretlen argumentumokat" )

    (végrehajtás itt). Némi erőfeszítéssel lecsökkentheti a hőcserélőt, hogy a beépített túlterhelés-támogatással rendelkező nyelvhez nagyon hasonlítson.

    Félretéve már lehetett túlterhelni Mennyiség argumentumokat (nem pedig típust) a C99-ben.

    Vegye figyelembe, hogy a C kiértékelésének módja megmozgathat. Ez a foo_int választja ki, ha például literális karaktert próbál átadni neki, és szüksége van némi foo_int-re, ha azt szeretné, hogy a túlterhelések támogassák a string literálokat. Összességében azonban nagyon klassz.

    Leushenko válasza nagyon klassz: csak a foo példa nem fordítja le a GCC-t, ami a foo(7) -n meghiúsul, beleütközik az ELSŐ makróba és a tényleges függvényhívásba ((_1, __VA_ARGS__), plusz vesszővel maradva. problémákba ütközünk, ha további túlterheléseket akarunk biztosítani, például a foo(double) .

    Ezért úgy döntöttem, hogy részletesebben válaszolok erre a kérdésre, beleértve az üres túlterhelés engedélyezését (foo(void) - ami némi gondot okozott...).

    Az ötlet most a következő: definiáljon egynél több általánost különböző makrókban, és válassza ki a megfelelőt az argumentumok számának megfelelően!

    Az érvek száma meglehetősen egyszerű, a következő válasz alapján:

    #define foo(...) SELECT(__VA_ARGS__)(__VA_ARGS__) #define SELECT(...) CONCAT(SELECT_, NARG(__VA_ARGS__))(__VA_ARGS__) #define CONCAT(X, Y) CONCAT_(X, Y) # definiálja a CONCAT_(X, Y) X ## Y

    Ez jó, a SELECT_1 vagy a SELECT_2 mellett döntünk (vagy több argumentum mellett, ha akarod/szükséged van rájuk), tehát csak a megfelelő definíciókra van szükségünk:

    #define SELECT_0() foo_void #define SELECT_1(_1) _Általános ((_1), \ int: foo_int, \ char: foo_char, \ double: foo_double \) #define SELECT_2(_1, _2) _Általános((_1), \ double : _Általános((_2), \ int: foo_double_int \) \)

    Először is, egy üres makróhívás (foo()) továbbra is létrehoz egy tokent, de az üres. Tehát a számláló makró valójában 1-et ad vissza 0 helyett, még akkor is, ha a makrót üresnek nevezik. "Könnyen" megoldhatjuk ezt a problémát, ha __VA_ARGS__ vesszővel a __VA_ARGS__ után feltételesen, attól függően, hogy a lista üres-e vagy sem:

    #define NARG(...) ARG4_(__VA_ARGS__ VESZSŐ(__VA_ARGS__) 4, 3, 2, 1, 0)

    Ez nézett könnyű, de a COMMA makró elég nehéz; szerencsére ezzel a témával már Jens Gustedt blogja is foglalkozik (köszönöm, Jens). A fő trükk az, hogy a függvénymakrók nem bővülnek ki, hacsak nem zárójelek követik őket, további magyarázatért lásd Jens blogját... Csak egy kicsit módosítanunk kell a makrókat az igényeinknek megfelelően (rövidebb neveket és kevesebb érvet használok a rövidség kedvéért) .

    #define ARGN(...) ARGN_(__VA_ARGS__) #define ARGN_(_0, _1, _2, _3, N, ...) N #define HAS_COMMA(...) ARGN(__VA_ARGS__, 1, 1, 1, 0 ) #define SET_COMMA(...) , #define COMMA(...) SELECT_COMMA \ (\ HAS_COMMA(__VA_ARGS__), \ HAS_COMMA(__VA_ARGS__ ()), \ HAS_COMMA(SET_COMMA __VA_ARGS__), \_HAS_COMMA_(_SET_COMMA) \) #define SELECT_COMMA(_0, _1, _2, _3) SELECT_COMMA_(_0, _1, _2, _3) #define SELECT_COMMA_(_0, _1, _2, _3) COMMA_ ## _0 ## _1 ## _3 #2 ## define COMMA_0000 , #define COMMA_0001 #define COMMA_0010 , // ... (az összes többi vesszővel együtt) #define COMMA_1111 ,

    És most jól vagyunk...

    Teljes kód egy blokkban:

    /* * demo.c * * Létrehozva: 2017-09-14 * Szerző: sboehler */ #include void foo_void(void) ( puts("void"); ) void foo_int(int c) ( printf("int: %d\n", c); ) void foo_char(char c) ( printf("char: %c \n", c); ) void foo_double(double c) ( printf("double: %.2f\n", c); ) void foo_double_int(double c, int d) ( printf("double: %.2f, int: %d\n", c, d); ) #define foo(...) SELECT(__VA_ARGS__)(__VA_ARGS__) #define SELECT(...) CONCAT(SELECT_, NARG(__VA_ARGS__))(__VA_ARGS__) # define CONCAT(X, Y) CONCAT_(X, Y) #define CONCAT_(X, Y) X ## Y #define SELECT_0() foo_void #define SELECT_1(_1) _Generic ((_1), \ int: foo_int, \ char : foo_char, \ double: foo_double \) #define SELECT_2(_1, _2) _Generic((_1), \ double: _Generic((_2), \ int: foo_double_int \) \) #define ARGN(...) ARGN_( __VA_ARGS__) #define ARGN_(_0, _1, _2, N, ...) N #define NARG(...) ARGN(__VA_ARGS__ COMMA(__VA_ARGS__) 3, 2, 1, 0) #define HAS_COMMA(...) ARGN(__VA_ARGS__, 1, 1, 0) #define SET_COMMA(...) , #define COMMA(...) SELECT_COMMA \ (\ HAS_COMMA(__VA_ARGS__), \ HAS_COMMA(__VA_ARGS__ ()), \ HAS_COMMA_SET_VAAR__COMMA()_ , \ HAS_COMMA(SET_COMMA __VA_ARGS__ ()) \) #define SELECT_COMMA(_0, _1, _2, _3) SELECT_COMMA_(_0, _1, _2, _3) #define SELECT_COMMA_(_0, _1, _3) COMMA_(_0, _1, _#) COMMA #0 # _1 ## _2 ## _3 #define COMMA_0000 #define COMMA_0001 #define COMMA_0010 _1000 , #define COMMA_1001 , #define COMMA_1010 , #define COMMA_1011 , #define0COMMA_1011 , #MA_1define0 COMMA_1 , #1define0 e COMMA_1110 , #define COMMA_1111 , int main( int argc, char** argv) ( foo(); foo(7); foo(10,12); foo(12,10; 7); foo((char)"s"); visszatérés 0; )

    A C++ nyelv azt a lehetőséget valósítja meg, hogy a különböző adattípusokon eltérő műveleteket végrehajtó függvényekhez ugyanazt az azonosítót használjuk, aminek eredményeként több, azonos nevű, de eltérő paraméterlistával rendelkező függvény használható mind számban, mind típusban.

    Az ilyen függvényeket ún túlterheltés magát a mechanizmust túlterhelésfunkciókat.

    A fordító határozza meg, hogy az azonos nevű függvények közül melyiket kell meghívni, összevetve a tényleges argumentumok típusait az összes függvény fejlécében szereplő formális paraméterek típusaival, azaz. a fordító az argumentumok típusától és számától függően létrehozza a megfelelő függvény hívását.

    A meghívandó függvény megtalálása három külön lépésben történik:

    1. Keressen egy olyan függvényt, amely pontosan egyezik a paraméterekkel, és használja, ha talál.

    2. Keresse meg a megfelelő függvényt a beépített adattípus-konverziókkal.

    3. Keresse meg a megfelelő függvényt a felhasználó által definiált transzformációk segítségével.

    Példa a funkció túlterhelésére

    Adjunk példát a függvényre S 1 két lehetőséggel x,nál nél, amely az átadott argumentumok típusától függően működik, az alábbiak szerint:

    – ha a paraméter típusa egész, akkor a függvény S 1 összeadja az értékeiket, és visszaadja a kapott összeget;

    – ha a paraméter típusa hosszú, funkció S 1 megszorozza az értéküket, és visszaadja a kapott terméket;

    – ha a paraméter típusa valós, akkor a függvény S 1 elosztja értékeiket, és visszaadja a hányadost.

    #beleértve

    int S1 (int x, int y) (

    hosszú S1 (hosszú x, hosszú y) (

    dupla S1 (dupla x, dupla y) (

    int a = 1, b = 2, c;

    hosszú i = 3, j = 4, k;

    kettős x = 10, y = 2, z;

    printf("\n c = %d; k = %ld; z = %lf . \n", c, k, z);

    Ennek eredményeként a következőket kapjuk:

    c = 3; k = 12; z = 5.000000 .

    Változóparaméteres funkciók

    A felhasználó által definiált függvény paraméterlistájában ellipszist használunk, ha az argumentumok száma nem ismert előre. Ebben az esetben korlátlan számú paraméter adható meg a prototípusában az alábbiak szerint:

    érvénytelen f1 (int a , dupla b , …);

    Egy ilyen jelölés azt mondja a fordítónak, hogy a paraméterek szükséges tényleges argumentumai mögött aÉs b követhet vagy nem követhet más argumentumot a függvény meghívásakor.

    Felsoroljuk e mechanizmus használatának főbb jellemzőit.

    1. Számos makróparancsot használnak az ilyen funkciók paramétereinek eléréséhez, ezek a következők:

    va _ lista És va _ Rajt – makrók a paraméterekhez való hozzáférés előkészítésére;

    va _ arg – paraméterek használata;

    va _ vége - kilépés előtti takarítás.

    Ezek a fejlécfájlban vannak deklarálva stdarg . h .

    2. Egy ilyen függvénynek legalább egy (megnevezett) paraméterrel kell rendelkeznie ahhoz, hogy átadja az átadandó argumentumok számát.

    3. Makróhoz va_ Rajt két argumentumot kell átadnia - a paraméterlista nevét, amely meghatározza va_ listaés a számukat.

    4. A makrók meghatározott sorrendjét nem lehet megtörni. Ellenkező esetben beláthatatlan következményekkel járhat.

    5. Makróhoz va_ arg a paraméterlista neve mellett a kívánt típust is át kell adni. Ha a típusok nem egyeznek, hiba történik.

    Az ellipszis használata teljesen letiltja a paramétertípus-ellenőrzést. Az ellipszisre csak akkor van szükség, ha a paraméterek száma és típusa is megváltozott.

    A következő példa ezt a lehetőséget szemlélteti.

    #beleértve

    #beleértve

    void f1(double s, int n ...) (

    va_start(p, n);

    printf(" \n Double S = %lf ", s);

    for(int i=1; i<=n; i++) {

    v = va_arg(p, int);

    printf("\n %d argumentum = %d ", i, v);

    void main(void) (

    f1(1,5, 3, 4, 5, 6);

    Ennek eredményeként a következőket kapjuk:

    Kettős S = 1.500000

    1. érv = 4

    2. érv = 5

    3. érv = 6

    nyomjon meg egy gombot a folytatáshoz