Utolsó frissítés: 12.08.2018

A metódusokkal együtt az operátorokat is túlterhelhetjük. Tegyük fel például, hogy a következő Counter osztályunk van:

Osztályszámláló ( nyilvános int érték ( get; set; ) )

Ez az osztály valamilyen számlálót képvisel, amelynek értéke a Value tulajdonságban van tárolva.

És tegyük fel, hogy van két Counter osztályú objektumunk – két számláló, amelyeket össze akarunk hasonlítani vagy hozzáadni a Value tulajdonságuk alapján szabványos összehasonlítási és összeadási műveletekkel:

Számláló c1 = új számláló (Érték = 23); Számláló c2 = new Számláló( Érték = 45 ); bool eredmény = c1 > c2; Számláló c3 = c1 + c2;

De tovább Ebben a pillanatban sem az összehasonlítás, sem az összeadás művelet nem érhető el a Counter objektumokhoz. Ezek a műveletek számos primitív típuson használhatók. Például alapértelmezés szerint numerikus értékeket adhatunk hozzá, de a fordító nem tudja, hogyan adjon hozzá összetett típusú objektumokat - osztályokat és struktúrákat. És ehhez túl kell terhelnünk a szükséges kezelőket.

Az operátor túlterhelése abból áll, hogy egy speciális metódust definiálunk abban az osztályban, amelynek objektumaihoz operátort szeretnénk definiálni:

nyilvános statikus visszatérési_típus operátor(paraméterek) ( )

Ennek a metódusnak nyilvános statikus módosítókkal kell rendelkeznie, mivel a túlterhelt operátor az osztály összes objektumánál használatos. Ezután következik a visszatérési típus neve. A visszatérési típus azt a típust jelöli, amelynek objektumait le akarjuk kérni. Például két Counter objektum hozzáadásával egy új Counter objektumot kapunk. A kettő összehasonlítása eredményeként pedig egy bool típusú objektumot szeretnénk kapni, amely jelzi, hogy a feltételes kifejezés igaz vagy hamis. De a feladattól függően a visszatérési típusok bármiek lehetnek.

Ekkor a metódus neve helyett ott van a kulcsszó operátor és maga az operátor. Ezután a paraméterek zárójelben vannak felsorolva. A bináris operátorok két paramétert vesznek fel, az unáris operátorok egy paramétert. Mindenesetre az egyik paraméternek azt a típust - az osztályt vagy struktúrát - kell képviselnie, amelyben az operátor definiálva van.

Töltsük túl például a Számláló osztály több operátorát:

Osztályszámláló ( public int Érték ( get; set; ) public static Számláló operátor + (C1 számláló, c2 számláló) ( új számláló ( Érték = c1.Érték + c2.Érték ); ) public static bool operátor > (C1 számláló, Számláló c2) ( visszatér c1.Érték > c2.Érték; ) nyilvános statikus logikai operátor<(Counter c1, Counter c2) { return c1.Value < c2.Value; } }

Mivel minden túlterhelt operátor bináris - azaz két objektumon hajtják végre, minden túlterheléshez két paraméter tartozik.

Mivel az összeadás művelet esetén a Counter osztály két objektumát szeretnénk hozzáadni, az operátor két objektumot fogad el ebből az osztályból. És mivel az összeadás eredményeként egy új Counter objektumot szeretnénk kapni, ez az osztály is visszatérési típusként használatos. Ennek az operátornak minden művelete egy új objektum létrehozásához vezet, amelynek Value tulajdonsága egyesíti mindkét paraméter Value tulajdonságának értékeit:

Nyilvános statikus számláló operátor +(C1 számláló, c2 számláló) ( új számláló visszatérése ( Érték = c1.Érték + c2.Érték ); )

Két összehasonlító operátor is újra lett definiálva. Ha újradefiniáljuk az egyik összehasonlító operátort, akkor a másodikat is újra kell definiálnunk. Az összehasonlító operátorok maguk hasonlítják össze a Value tulajdonságok értékeit, és az összehasonlítás eredményétől függően igaz vagy hamis értéket adnak vissza.

Most túlterhelt operátorokat használunk a programban:

Static void Main(string args) ( számláló c1 = új számláló ( érték = 23 ); számláló c2 = új számláló ( érték = 45 ); bool eredmény = c1 > c2; Console.WriteLine(eredmény); // false számláló c3 = c1 + c2;Console.WriteLine(c3.Value); // 23 + 45 = 68 Console.ReadKey(); )

Érdemes megjegyezni, hogy mivel az operátordefiníció lényegében egy metódus, ezt a metódust is túlterhelhetjük, vagyis készíthetünk hozzá egy másik verziót. Például adjunk hozzá egy másik operátort a Számláló osztályhoz:

Nyilvános statikus int operátor +(C1 számláló, int érték) ( return c1.Value + érték; )

Ez a módszer hozzáadja a Value tulajdonság értékét és néhány számot, és visszaadja az összegüket. És ezt az operátort is alkalmazhatjuk:

Számláló c1 = új számláló (Érték = 23); int d = c1 + 27; // 50 Console.WriteLine(d);

Ne feledje, hogy a túlterhelés nem változtathatja meg azokat az objektumokat, amelyeket paramétereken keresztül adnak át az operátornak. Például definiálhatunk egy növekmény operátort a Számláló osztályhoz:

Nyilvános statikus Számláló operátor ++(C1 számláló) ( c1.Érték += 10; visszatér c1; )

Mivel az operátor unáris, csak egy paramétert vesz igénybe - annak az osztálynak az objektumát, amelyben ez az operátor definiálva van. De ez a növekmény helytelen meghatározása, mivel az operátornak nem szabad megváltoztatnia a paraméterek értékeit.

És a növekmény operátor pontosabb túlterhelése így néz ki:

Nyilvános statikus számláló operátor ++(C1 számláló) (új számlálót ad vissza (Érték = c1.Érték + 10); )

Ez azt jelenti, hogy a rendszer egy új objektumot ad vissza, amely tartalmazza a Value tulajdonságban megnövelt értéket.

Ugyanakkor nem kell külön operátort definiálnunk az előtag és az utótag növeléséhez (és dekrementálásához), mivel mindkét esetben egy implementáció működik.

Például az előtag növelési műveletet használjuk:

Számláló számláló = new Számláló() ( Érték = 10 ); Console.WriteLine($"(számláló.Érték)"); // 10 Console.WriteLine($"((++counter).Value)"); // 20 Console.WriteLine($"(számláló.Érték)"); 20

Konzol kimenet:

Most a postfix növekményt használjuk:

Számláló számláló = new Számláló() ( Érték = 10 ); Console.WriteLine($"(számláló.Érték)"); // 10 Console.WriteLine($"((számláló++).Érték)"); // 10 Console.WriteLine($"(számláló.Érték)"); 20

Konzol kimenet:

Azt is érdemes megjegyezni, hogy felülírhatjuk az igaz és hamis operátorokat. Például definiáljuk őket a Számláló osztályban:

Osztályszámláló ( public int Érték ( get; set; ) public static bool operátor true(Counter c1) ( return c1.Value != 0; ) public static bool operátor false(Counter c1) ( return c1.Value == 0; ) // az osztály többi része )

Ezek az operátorok túlterheltek, ha egy ilyen típusú objektumot akarunk feltételként használni. Például:

Számláló számláló = new Számláló() ( Érték = 0 ); if (számláló) Console.WriteLine(true); else Console.WriteLine(false);

A kezelők túlterhelésekor ne feledje, hogy nem minden operátort lehet túlterhelni. Különösen a következő operátorokat terhelhetjük túl:

    unáris operátorok +, -, !, ~, ++, --

    bináris operátorok +, -, *, /, %

    összehasonlító műveletek ==, !=,<, >, <=, >=

    logikai operátorok &&, ||

    hozzárendelési operátorok +=, -=, *=, /=, %=

És vannak olyan operátorok, amelyeket nem lehet túlterhelni, mint például az egyenlőség operátor = vagy a hármas operátor ?: és még sok más.

A túlterhelt operátorok teljes listája az msdn dokumentációjában található

Az operátorok túlterhelésekor ne feledjük azt is, hogy nem változtathatjuk meg az operátorok prioritását vagy asszociativitását, nem hozhatunk létre új operátort, nem módosíthatjuk az operátorok logikáját a típusokban, ami a .NET-ben az alapértelmezett.

Jó nap!

A cikk megírásának vágya a bejegyzés elolvasása után jelentkezett, mert sok fontos téma nem került nyilvánosságra benne.

A legfontosabb, hogy ne feledje, hogy a kezelő túlterhelése csak egy kényelmesebb módja a funkciók hívásának, ezért ne ragadjon el a kezelő túlterhelésétől. Csak akkor szabad használni, ha megkönnyíti a kódírást. De nem annyira, hogy nehezítse az olvasást. Végül is, mint tudod, a kódot sokkal gyakrabban olvassák, mint írják. És ne felejtsd el, hogy soha nem szabad túlterhelni az operátorokat a beépített típusokkal párhuzamosan, csak az egyedi típusokat/osztályokat lehet túlterhelni.

Túlterhelés szintaxis

Az operátor túlterhelésének szintaxisa nagyon hasonló a nevű függvény definiálásához [e-mail védett], ahol a @ az operátor azonosítója (pl. +, -,<<, >>). Fontolgat a legegyszerűbb példa:
class Integer ( privát: int érték; public: Integer(int i): érték(i) () const Integer operátor+(const Integer& rv) const ( return (érték + rv.érték); ) );
Ebben az esetben az operátor az osztály tagjaként kerül keretbe, az argumentum azt az értéket adja meg, amely az operátor jobb oldalán található. Általában két fő módja van az operátorok túlterhelésének: az osztálybarát globális függvények vagy magának az osztálynak a beágyazott függvényei. Hogy melyik módszer, melyik operátor a jobb, azt a téma végén megvizsgáljuk.

A legtöbb esetben az operátorok (a feltételes feltételek kivételével) egy objektumot vagy hivatkozást adnak vissza arra a típusra, amelyre az argumentumai vonatkoznak (ha a típusok eltérőek, akkor Ön dönti el, hogyan értelmezze az operátor kiértékelésének eredményét).

Unary operátorok túlterhelése

Tekintsünk példákat az unáris operátorok túlterhelésére a fent definiált Integer osztályhoz. Ugyanakkor barát függvényként fogjuk meghatározni őket, és figyelembe vesszük a csökkentő és növelő operátorokat:
class Integer ( privát: int érték; publikus: Integer(int i): érték(i) () //unary + barát const Integer& operator+(const Integer& i); //unary - barát const Egész szám operátor-(const Integer& i) ; //előtag növekmény barát const Integer& operátor++(Integer& i); //postfix növekmény barát const Integer operátor++(Integer& i, int); //előtag csökkentés barát const Integer& operator--(Integer& i); //postfix decrement friend const Integer operátor--(Integer& i, int); ); // az unary plus nem csinál semmit. const Integer& operátor+(const Integer& i) ( return i.value; ) const Integer operátor-(const Integer& i) ( return Integer(-i.value); ) //prefix verzió értéket ad vissza a növekmény után const Integer& operátor++(Integer& i) ( i.value++; return i; ) //postfix verzió a const inkrement előtti értéket adja vissza Integer operátor++(Integer& i, int) ( Integer oldValue(i.value); i.value++; return oldValue; ) //prefix version visszatér a decrement utáni érték const Integer& operátor--(Integer& i) ( i.value--; return i; ) //postfix verzió értéket ad vissza a decrement előtt const Integer operátor--(Integer& i, int) ( Integer oldValue(i.value ); i .value--; régi érték visszaadása; )
Most már tudja, hogy a fordító hogyan tesz különbséget a csökkentés és a növelés előtagos és utótagú változatai között. Abban az esetben, ha a ++i kifejezést látja, akkor az operátor++(a) függvényt hívják meg. Ha i++-t lát, akkor az operátor++(a, int) meghívásra kerül. Vagyis a túlterhelt operátor++ függvényt hívják, és erre használják a dummy int paramétert a postfix verzióban.

Bináris operátorok

Tekintsük a bináris operátorok túlterhelésének szintaxisát. Töltsünk túl egy operátort, amely l-értéket ad vissza, egyet feltételes operátorés egy olyan állítás, amely új értéket hoz létre (globálisan definiáljuk őket):
class Integer ( privát: int érték; publikus: Integer(int i): érték(i) () barát const Integer operator+(const Integer& balra, const Integer& jobb); barát Egész& operator+=(Integer& balra, const Integer& jobbra); barát bool operátor==(const Integer& balra, const Integer& jobbra); ); const Integer operátor+(const Integer& left, const Integer& right) ( return Integer(left.value + right.value); ) Integer& operator+=(Integer& left, const Integer& right) ( left.value += right.value; return left; ) bool operator==(const Integer& left, const Integer& right) ( return left.value == right.value; )
Mindezekben a példákban az operátorok ugyanahhoz a típushoz túlterheltek, de ez nem szükséges. Lehetséges például, hogy túlterheljük az Integer típusunk és a hasonlóságában meghatározott Float hozzáadását.

Argumentumok és visszatérési értékek

Mint látható, a példák használnak különböző módokon argumentumok átadása függvényeknek és operátorértékek visszaadása.
  • Ha az argumentumot nem módosítja az operátor, például egy unáris plusz esetén, akkor konstansra való hivatkozásként kell átadni. Általában ez szinte mindenkire igaz. aritmetikai operátorok(összeadás, kivonás, szorzás...)
  • A visszaadott érték típusa az operátor jellegétől függ. Ha az operátornak új értéket kell visszaadnia, akkor új objektumot kell létrehozni (mint a bináris plusz esetében). Ha meg akarja akadályozni, hogy egy objektum l-értékként megváltozzon, akkor azt const-ként kell visszaadnia.
  • A hozzárendelési operátoroknak hivatkozást kell visszaadniuk a megváltozott elemre. Továbbá, ha a hozzárendelési operátort olyan konstrukciókban szeretné használni, mint az (x=y).f(), ahol az f() függvényt hívják meg az x változóhoz, miután y-t rendeltünk hozzá, akkor ne adjon vissza hivatkozást konstansra, csak egy hivatkozást adjon vissza.
  • A logikai operátoroknak legrosszabb esetben int-t, legjobb esetben bool-t kell visszaadniuk.

Megtérülési érték optimalizálása

Új objektumok létrehozásakor és függvényből való visszaküldésekor a fent leírt bináris plusz operátor jelölését kell használni.
return Integer(bal.érték + jobb.érték);
Hogy őszinte legyek, nem tudom, milyen helyzet releváns a C++11 esetében, az alábbi érvek mindegyike érvényes a C++98-ra.
Első pillantásra úgy néz ki, mint egy ideiglenes objektum létrehozásának szintaxisa, ami azt jelenti, hogy nincs különbség a fenti és a jelen kód között:
Integer temp(bal.érték + jobb.érték); visszatérő hőmérséklet;
De valójában ebben az esetben az első sorban a konstruktort hívják meg, majd a másoló konstruktort, amely lemásolja az objektumot, majd a verem feltekercselésekor a destruktor meghívása. Az első bejegyzés használatakor a fordító először létrehoz egy objektumot a memóriában, amelybe be kell másolni, így elmenti a másolás konstruktor és destruktor hívását.

Speciális operátorok

A C++-ban vannak olyan operátorok, amelyek sajátos szintaxissal és túlterhelési módszerrel rendelkeznek. Például az index operátor . Mindig az osztály tagjaként van definiálva, és mivel az indexelt objektum tömbként való viselkedése szándékolt, hivatkozást kell visszaadnia.
vessző operátor
A "speciális" operátorok közé tartozik a vessző is. Olyan objektumok esetén hívják meg, amelyek mellett vessző van (de nem hívják meg a függvényargumentumok listáiban). Nem olyan egyszerű értelmes példát találni ennek az operátornak a használatára. Habrauser az előző cikkhez a túlterhelésről szóló megjegyzésekben.
Mutató-hivatkozási operátor
Ezen operátorok túlterhelése indokolható az intelligens pointer osztályoknál. Ez az operátor szükségszerűen osztályfüggvényként van definiálva, és bizonyos korlátozások is vonatkoznak rá: vagy egy objektumot (vagy hivatkozást), vagy egy mutatót kell visszaadnia, amely lehetővé teszi az objektum elérését.
hozzárendelés operátor
A hozzárendelési operátort szükségszerűen osztályfüggvényként határozzuk meg, mivel elválaszthatatlanul kapcsolódik az "="-től balra lévő objektumhoz. A hozzárendelési operátor globális meghatározása lehetővé tenné az "=" operátor szokásos viselkedésének felülbírálását. Példa:
class Integer ( private: int value; public: Integer(int i): value(i) () Integer& operator=(const Integer& right) ( // ön-hozzárendelés ellenőrzése if (this == &right) ( return *this; ) érték = jobb.érték; *ezt adja vissza; ) );

Amint láthatja, a funkció elején az ön-hozzárendelés ellenőrzése történik. Általában ebben az esetben az önbeosztás ártalmatlan, de a helyzet nem mindig ilyen egyszerű. Például, ha az objektum nagy, sok időt tölthet felesleges másolással, vagy amikor mutatókkal dolgozik.

Nem túlterhelt kezelők
Egyes C++ operátorok egyáltalán nincsenek túlterhelve. Nyilvánvalóan ezt biztonsági okokból teszik.
  • Az osztálytag kiválasztási operátor ".".
  • Mutató a ".*" osztálytag hivatkozási operátorára
  • A C++-ban nincs hatványozási operátor (mint a Fortranban) "**".
  • Tilos operátorait megadni (problémák a prioritások megadásával lehetségesek).
  • A kezelői prioritás nem módosítható
Amint azt már megtudtuk, az operátoroknak két módja van - osztályfüggvény és barát globális függvény formájában.
Rob Murray C++ Strategies and Tactics című könyvében a következő irányelveket határozta meg az operátorforma kiválasztásához:

Miert van az? Először is, egyes szolgáltatók kezdetben korlátozottak. Általában, ha szemantikailag nincs különbség az operátor definiálásában, akkor jobb, ha osztályfüggvényként rendezzük el, hogy hangsúlyozzuk a kapcsolatot, ráadásul a függvény inline (inline) lesz. Ezenkívül néha szükséges lehet a bal oldali operandust egy másik osztály objektumával ábrázolni. A legszembetűnőbb példa talán az újrafogalmazás<< и >> I/O adatfolyamokhoz.

A 15. fejezetben kétféle speciális funkciót fogunk megvizsgálni: a túlterhelt operátorokat és a felhasználó által definiált konverziókat. Lehetővé teszik az osztályobjektumok kifejezésekben történő használatát ugyanolyan intuitív módon, mint a beépített típusú objektumok. Ebben a fejezetben először a túlterhelt kezelők általános tervezési koncepcióit vázoljuk fel. Ezután bemutatjuk a speciális hozzáférési jogokkal rendelkező osztálybarátok fogalmát, és megvitatjuk, hogy miért használják őket, összpontosítva arra, hogyan valósul meg néhány túlterhelt operátor: hozzárendelés, alsó index, hívás, osztálytag hozzáférési nyíl, növelés és csökkentés, valamint az osztály speciális operátorai. operátorok új és törlése. A fejezetben tárgyalt speciális függvények másik kategóriája a tagkonverziós függvények (konverterek), amelyek egy osztálytípus szabványos konverzióinak halmaza. A fordító implicit módon alkalmazza őket, amikor az osztályobjektumokat tényleges függvényargumentumként vagy beépített vagy túlterhelt operátorok operandusaként használja. A fejezet a függvénytúlterhelés feloldására vonatkozó szabályok részletes összefoglalásával zárul, figyelembe véve az objektumok argumentumként való átadását, az osztálytagfüggvényeket és a túlterhelt operátorokat.

15.1. Kezelői túlterhelés

Az előző fejezetekben már bemutattuk, hogy az operátortúlterhelés lehetővé teszi a programozó számára, hogy bevezesse az előre meghatározott operátorok saját verzióit (lásd a 4. fejezetet) az osztály típusú operandusokhoz. Például a 3.15. szakaszban található String osztályban sok túlterhelt operátor van. Az alábbiakban a meghatározása:

#beleértve osztály String; istream& operátor>>(istream &, const String &); stream és operátor<<(ostream &, const String &); class String { public: // набор перегруженных конструкторов // для автоматической инициализации String(const char* = 0); String(const String &); // деструктор: автоматическое уничтожение ~String(); // набор перегруженных операторов присваивания String& operator=(const String &); String& operator=(const char *); // перегруженный оператор взятия индекса char& operator(int); // набор перегруженных операторов равенства // str1 == str2; bool operator==(const char *); bool operator==(const String &); // функции доступа к членам int size() { return _size; }; char * c_str() { return _string; } private: int _size; char *_string; };

A String osztály három túlterhelt operátorkészlettel rendelkezik. Az első a hozzárendelési operátorok halmaza:

Először a másolás-hozzárendelés operátora következik. (Részletesen a 14.7. fejezetben tárgyaltuk.) A következő utasítás támogatja egy C karakterlánc objektumhoz való hozzárendelését String típusú:

karakterláncnév; name="Sherlock"; // az operátor operátor használatával=(char *)

(A másolási hozzárendelésektől eltérő hozzárendelési operátorokról a 15.3. szakaszban lesz szó.)

A második halmaznak csak egy operátora van - az indexet véve:

// túlterhelt index operátor char& operator(int);

Lehetővé teszi a program számára, hogy a String osztályba tartozó objektumokat ugyanúgy indexelje, mint a beépített típusú objektumok tömbjeit:

Ha (név != "S") cout<<"увы, что-то не так\n";

(Ezt az operátort részletesen a 15.4. szakasz ismerteti.)

A harmadik halmaz túlterhelt egyenlőség-operátorokat definiál a String osztály objektumaihoz. Egy program tesztelheti, hogy két ilyen objektum egyenlő-e, vagy egy objektum és egy C karakterlánc egyenlő-e:

// túlterhelt egyenlőség-operátorok halmaza // str1 == str2; bool operátor==(const char *); bool operátor==(const String &);

A túlterhelt operátorok lehetővé teszik egy osztály típusú objektumok használatát a 4. fejezetben definiált operátorokkal, és ugyanolyan intuitív módon kezelhetők, mint a beépített típusú objektumok. Például, ha két String objektum összefűzésének műveletét szeretnénk definiálni, akkor azt concat() tagfüggvényként is megvalósíthatjuk. De miért concat() és nem mondjuk append()? Az általunk választott név logikus és könnyen megjegyezhető, de a felhasználó még mindig elfelejtheti, amit a függvénynek neveztünk. A név gyakran könnyebben megjegyezhető, ha túlterhelt operátort ad meg. Például a concat() helyett az új művelet operátort hívnánk meg+=(). Egy ilyen operátor a következőképpen használatos:

#include "String.h" int main() ( Karakterlánc neve1 "Sherlock"; Karakterlánc neve2 "Holmes"; név1 += " "; név1 += név2; if (! (név1 == "Sherlock Holmes")) cout< < "конкатенация не сработала\n"; }

A túlterhelt operátor az osztály törzsében ugyanúgy deklarálva van, mint egy normál tagfüggvény, csak a neve áll kulcsszó operátort követi a sok C++ előre definiált operátor egyike (lásd 15.1. táblázat). Így deklarálhatja az operátort+=() a String osztályban:

Class String ( public: // túlterhelt operátorok halmaza += String& operator+=(const String &); String& operator+=(const char *); // ... privát: // ... );

és definiálja így:

#beleértve inline String& String::operator+=(const String &rhs) ( // Ha az rhs által hivatkozott karakterlánc nem üres if (rhs._string) ( String tmp(*this); // foglaljon le elegendő memóriát az összefűzött // megtartásához of strings _size += rhs._size; delete _string; _string = new char[ _size + 1 ]; // először másolja be az eredeti karakterláncot a kiválasztott területre // majd adja a végéhez az rhs strcpy(_string, tmp) által hivatkozott karakterláncot ._string) ; strcpy(_string + tmp._size, rhs._string); ) return *this; ) inline String& String::operator+=(const char *s) ( // Ha az s mutató nem nulla if (s) ( String tmp(*this ); // foglaljon le egy elegendő memóriaterületet // az összefűzött karakterláncok tárolására _size += strlen(s); delete _string; _string = new char[ _size + 1 ]; // először másolja be a forrás karakterláncot a lefoglalt terület // majd hozzáfűzi az s által hivatkozott C karakterlánc végéhez. strcpy(_string, tmp._string); strcpy(_string + tmp._size, s); ) return *this; )

15.1.1. Egy osztály tagjai és nem tagjai

Nézzük meg közelebbről a String osztály egyenlőségi operátorait. Az első operátor lehetővé teszi két objektum egyenlőségének beállítását, a második pedig az objektumot és a C karakterláncot:

#include "String.h" int main() ( String flower; // írjon valamit a virág változóba if (virág == "liliom") // helyes // ... else if ("tulipán" == virág ) // hiba // ... )

Amikor először használja az egyenlőség operátort a main()-ban, a String osztály túlterhelt operátora==(const char *) kerül meghívásra. A második if utasításra azonban a fordító hibaüzenetet ad. Mi a helyzet?

A túlterhelt operátor, amely egy osztály tagja, csak akkor érvényes, ha a bal oldali operandus az adott osztály objektuma. Mivel a második esetben a bal oldali operandus nem tartozik a String osztályba, a fordító megpróbál olyan beépített operátort találni, amelynél a bal oldali operandus lehet C-string, a jobb oldali operandus pedig egy String osztályú objektum. Természetesen nem létezik, ezért hibát mond a fordító.

De létrehozhat egy String osztályú objektumot is C-karakterláncból az osztálykonstruktor segítségével. Miért nem végzi el a fordító implicit módon ezt az átalakítást:

If (String("tulipán") == virág) //helyes: tagoperátor meghívásra kerül

Ennek oka az eredménytelenség. A túlterhelt operátorok nem igénylik, hogy mindkét operandus azonos típusú legyen. Például a Szöveg osztály a következő egyenlőségi operátorokat határozza meg:

Osztályszöveg ( public: Text(const char * = 0); Szöveg(const szöveg &); // túlterhelt egyenlőség-operátorok halmaza bool operator==(const char *) const; bool operátor==(const String &) const; bool operátor==(const Szöveg &) const; // ... );

és a main() kifejezés a következőképpen írható át:

If (Szöveg("tulipán") == virág) // Szöveg::operator==() meghívásra kerül

Ezért ahhoz, hogy összehasonlításhoz megfelelő egyenlőségoperátort találjon, a fordítónak át kell néznie az összes osztálydefiníciót, hogy olyan konstruktort keressen, amely képes a bal oldali operandust valamilyen osztálytípusba önteni. Ezután mindegyik típusnál ellenőriznie kell az összes túlterhelt egyenlőségi operátort, amelyek hozzá vannak társítva, hogy megtudja, valamelyikük képes-e elvégezni az összehasonlítást. Ezután a fordítónak el kell döntenie, hogy a konstruktor és az egyenlőség operátor (ha van) talált kombinációi közül melyik felel meg a legjobban a jobb oldali operandusnak! Ha megköveteli, hogy a fordító végrehajtsa ezeket a műveleteket, akkor a C ++ programok fordítási ideje drámaian megnő. Ehelyett a fordító csak a túlterhelt operátorokat nézi, amelyek a bal oldali operandusosztály tagjaiként vannak definiálva (és alaposztályai, amint azt a 19. fejezetben bemutatjuk).

Megengedett azonban olyan túlterhelt operátorok meghatározása, amelyek nem tagjai az osztálynak. A main()-ban fordítási hibát okozó sor elemzésekor az ilyen utasításokat figyelembe vettük. Így egy olyan összehasonlítás, amelyben a C karakterlánc a bal oldalon van, érvényessé tehető, ha a String osztályba tartozó egyenlőség operátorokat a névtér hatókörében deklarált egyenlőség operátorokra cseréljük:

Bool operátor==(const String &, const String &); bool operátor==(const String &, const char *);

Vegye figyelembe, hogy ezek a globális túlterhelt operátorok eggyel több paraméterrel rendelkeznek, mint a tagoperátorok. Ha az operátor egy osztály tagja, akkor a this pointer implicit módon kerül átadásra első paraméterként. Vagyis a tagoperátorok esetében a kifejezés

Virág == "liliom"

a fordító átírta a következőre:

virág.operátor==("liliom")

és a túlterhelt tag operátor definíciójában a virág bal operandusára hivatkozhatunk ezzel. (Ezt a mutatót a 13.4. fejezetben vezettük be.) Globális túlterhelt operátor esetén a bal oldali operandust reprezentáló paramétert kifejezetten meg kell adni.

Aztán a kifejezés

Virág == "liliom"

felhívja a kezelőt

Bool operátor==(const String &, const char *);

Nem világos, hogy melyik operátort hívják meg az egyenlőség operátor második használati esetéhez:

"tulipán" == virág

Nem definiáltunk ilyen túlterhelt operátort:

Bool operátor==(const char *, const String &);

De ez nem kötelező. Ha egy túlterhelt operátor egy függvény egy névtérben, akkor annak első és második paramétere is (bal és jobb oldali operandus) lehetséges konverziónak számít, azaz. a fordító az egyenlőség operátor második használatát úgy értelmezi

Operátor==(String("tulipán"), virág);

és meghívja a következő túlterhelt operátort az összehasonlításhoz: bool operator==(const String &, const String &);

De akkor miért adtunk egy második túlterhelt operátort: ​​bool operator==(const String &, const char *);

A C-karakterlánc típuskonverziója a String osztályba szintén alkalmazható a megfelelő operandusra. A main() függvény hiba nélkül lefordítja, ha egyszerűen definiálunk egy túlterhelt operátort a névtérben, amely két karakterlánc operandust vesz fel:

Bool operátor==(const String &, const String &);

Hogy csak ezt a nyilatkozatot adja meg, vagy még kettőt:

Bool operátor==(const char *, const String &); bool operátor==(const String &, const char *);

attól függ, hogy mekkora költséggel jár a C-karakterláncból String futás közbeni konvertálása, vagyis a String osztályunkat használó programok extra konstruktorhívásainak „költségétől”. Ha az egyenlőség operátort gyakran használják a C karakterláncok és objektumok összehasonlítására, akkor a legjobb, ha mindhárom lehetőséget megadja. (A hatékonyság kérdésére a barátokról szóló részben még visszatérünk.

A konstruktorokkal rendelkező osztálytípusba öntéssel részletesebben a 15.9. szakaszban foglalkozunk; A 15.10 fejezet a funkció túlterhelésének feloldásával foglalkozik a leírt transzformációk segítségével, a 15.12 fejezet pedig a kezelői túlterhelés feloldásával.)

Tehát mi alapján döntik el, hogy egy operátort egy osztály vagy egy névtér tagjává tegyünk? Bizonyos esetekben a programozónak egyszerűen nincs más választása:

  • ha a túlterhelt operátor egy osztály tagja, akkor csak akkor hívódik meg, ha a bal oldali operandus az adott osztály tagja. Ha a bal oldali operandus más típusú, akkor az operátornak a névtér tagjának kell lennie;
  • a nyelv megköveteli, hogy a hozzárendelés ("="), alsó index (""), hívás ("()") és tag hozzáférési ("->") operátorok az osztály tagjaiként legyenek definiálva. Ellenkező esetben fordítási hibaüzenet jelenik meg:
// hiba: a char& osztály tagjának kell lennie operátor(String &, int ix);

(A hozzárendelés operátort részletesebben a 15.3. szakasz tárgyalja, az indexet a 15.4. szakaszban, a meghívást a 15.5. szakaszban, a nyíl tag hozzáférési operátort pedig a 15.6. szakaszban tárgyaljuk.)

Ellenkező esetben a döntést az osztálytervező hozza meg. A szimmetrikus operátorokat, például az egyenlőség operátort, a névtérben lehet legjobban meghatározni, ha bármely operandus az osztály tagja lehet (mint például a Stringben).

Az alszakasz befejezése előtt definiáljuk a String osztály egyenlőségi operátorait a névtérben:

Bool operátor==(const String &str1, const String &str2) ( if (str1.size() != str2.size()) return false; return strcmp(str1.c_str(), str2.c_str()) ? false: true ; ) inline bool operator==(const String &str, const char *s) ( return strcmp(str.c_str(), s) ? false: true ; )

15.1.2. Túlterhelt operátorok nevei

Csak előre meghatározott C++ nyelvű operátorok terhelhetők túl (lásd 15.1. táblázat).

15.1. táblázat. Túlterhelt kezelők

+ - * / % ^ & | ~ ! , = < > <= >= ++ -- << >> == != && || += -= /= %= ^= &= |= *= <= >>= () -> ->* új új törlés törlés

Az osztály tervezője nem nyilváníthat túlterheltnek egy másik nevű operátort. Például, ha megpróbálja deklarálni a ** operátort a hatványozáshoz, a fordító hibaüzenetet generál.

A következő négy C++ nyelvi operátor nem terhelhető túl:

// nem túlterhelhető operátorok:: .* . ?:

Az előre meghatározott operátor hozzárendelés nem módosítható a beépített típusoknál. Például nem megengedett a beépített egész számok összeadási operátorának felülírása az eredmény túlcsordulás-ellenőrzéséhez.

// hiba: nem tudja felülbírálni a beépített addikciós operátort int operátor+(int, int);

Ezenkívül nem definiálhat további operátorokat a beépített adattípusokhoz, például az operátor+ hozzáadásával a beépített műveletek halmazához két tömb hozzáadásához.

A túlterhelt operátor kizárólag osztály- vagy felsorolás típusú operandusokhoz van definiálva, és csak egy osztály vagy névtér tagjaként deklarálható, legalább egy osztály- vagy felsorolástípus-paramétert (értékkel vagy hivatkozással átadva).

Az előre meghatározott operátori precedencia (lásd: 4.13. szakasz) nem módosítható. Függetlenül az osztály típusától és az operátor implementációjától az utasításban

X = = y + z;

Először mindig az operátor+ kerül végrehajtásra, majd az operator==; azonban a zárójelek segítségével módosítható a sorrend.

Az operátorok előre meghatározott aritását is meg kell őrizni. Például unáris logikai operátor A NOT nem definiálható bináris operátorként a String osztály két objektumán. A következő megvalósítás helytelen, és fordítási hibát eredményez:

// hibás: ! egy unáris bool operátor!(const String &s1, const String &s2) ( return (strcmp(s1.c_str(), s2.c_str()) != 0); )

A beépített típusoknál a négy előre meghatározott operátor ("+", "-", "*" és "&") unáris vagy bináris operátorként használatos. Ezen tulajdonságok bármelyikében túlterhelhetők.

Az operator( kivételével) minden túlterhelt operátornak érvénytelen alapértelmezett argumentuma van.

15.1.3. Túlterhelt kezelők fejlesztése

A hozzárendelés és cím operátorok és a vessző operátor előre meghatározott jelentéssel bír, ha az operandusok osztály típusú objektumok. De túlterhelhetők is. Az összes többi operátor szemantikáját ilyen operandusokra alkalmazva a fejlesztőnek kifejezetten meg kell adnia. A biztosítandó operátorok kiválasztása az osztály várható használatától függ.

Kezdje a nyilvános felület meghatározásával. A nyilvános tagfüggvények halmaza azon műveletek alapján jön létre, amelyeket az osztálynak a felhasználóknak ki kell tennie. Ezután megszületik a döntés, hogy mely funkciókat kell túlterhelt operátorként megvalósítani.

Az osztály nyilvános felületének meghatározása után ellenőrizze, hogy van-e logikai megfelelés a műveletek és az operátorok között:

  • Az isEmpty() a LOGICAL NOT operátor, operátor!() lesz.
  • Az isEqual() egyenlőség operátor lesz, operátor==().
  • a copy() hozzárendelési operátorrá válik, operátor=().

Minden operátor rendelkezik bizonyos természetes szemantikával. Így a bináris + mindig az összeadáshoz kapcsolódik, és leképezése egy hasonló műveletre egy osztállyal kényelmes és tömör jelölés lehet. Például egy mátrixtípus esetén két mátrix összeadása a bináris plusz tökéletesen megfelelő kiterjesztése.

Az operátorok túlterhelésével való visszaélésre példa az operátor+() kivonási operátorként való meghatározása, ami értelmetlen: a nem intuitív szemantika veszélyes.

Egy ilyen operátor több különböző értelmezést egyaránt jól támogat. Egy kifogástalanul világos és jól megalapozott magyarázat arra vonatkozóan, hogy mit csinál a+() operátor, valószínűleg nem fog tetszeni a String osztály felhasználóinak, akik azt feltételezik, hogy azt karakterlánc-összefűzésre használják. Ha a túlterhelt operátor szemantikája nem egyértelmű, akkor jobb, ha nem adja meg.

Egy összetett operátor szemantikájának ekvivalenciáját és a beépített típusokhoz tartozó egyszerű operátorok megfelelő sorozatát (például a + után az = és egy += összetett operátor ekvivalenciáját) kifejezetten fenn kell tartani egy osztály esetében is. Tegyük fel, hogy az operátor+() és operátor=() is definiálva van a String számára, hogy támogassa az összefűzési és tagonkénti másolási műveleteket:

String s1("C"); String s2("++"); s1 = s1 + s2; // s1 == "C++"

De ez nem elég az összetett hozzárendelési operátor támogatásához

S1 += s2;

Explicit módon meg kell határozni, hogy megőrizze az elvárt szemantikát.

15.1. gyakorlat

Miért nem hívja meg a következő összehasonlítás a túlterhelt operátort==(const String&, const String&):

"macskaköves" == "kő"

15.2. gyakorlat

Írjon túlterhelt egyenlőtlenségi operátorokat, amelyek használhatók az ilyen összehasonlításokban:

String != String String != C-string C-string != String

Magyarázza el, miért választotta egy vagy több állítás megvalósítását.

15.3. gyakorlat

Határozza meg a 13. fejezetben (13.3., 13.4. és 13.6. szakasz) megvalósított Képernyő osztály azon tagfüggvényeit, amelyek túlterhelhetők.

15.4. gyakorlat

Magyarázza el, hogy a 3.15. szakaszban a String osztályhoz definiált túlterhelt bemeneti és kimeneti operátorok miért vannak globális függvényekként deklarálva, nem pedig tagfüggvényekként.

15.5. gyakorlat

Túlterhelt bemeneti és kimeneti operátorokat valósítson meg a 13. fejezet Screen osztályához.

15.2. Barátok

Tekintsük újra a String osztály túlterhelt egyenlőségi operátorait, amelyek a névtér hatókörében vannak definiálva. Két String objektum egyenlőségi operátora így néz ki:

Bool operátor==(const String &str1, const String &str2) ( if (str1.size() != str2.size()) return false; return strcmp(str1.c_str(), str2.c_str()) ? false: igaz ;)

Hasonlítsa össze ezt a definíciót ugyanazon operátor definíciójával, mint tagfüggvény:

Bool String::operator==(const String &rhs) const ( if (_size != rhs._size) return false; return strcmp(_string, rhs._string) ? false: igaz; )

Módosítanunk kellett a String osztály privát tagjaihoz való hozzáférés módját. Mivel az új egyenlőség operátor az globális funkció, nem tagfüggvény, nem fér hozzá a String osztály privát tagjaihoz. A size() és c_str() tagfüggvények a String objektum méretének és az alapul szolgáló C-karakterláncnak a meghatározására szolgálnak.

Alternatív megoldásként a globális egyenlőség operátorokat a String osztály barátainak nyilvánítjuk. Ha egy funkciót vagy üzemeltetőt ilyen módon deklarálnak, akkor hozzáférést kapnak a nem nyilvános tagok.

A barát deklaráció (amely a barát kulcsszóval kezdődik) csak az osztálydefiníción belül fordul elő. Mivel a barátok nem tagjai annak az osztálynak, amelyik barátságot nyilvánít, nem mindegy, hogy melyik szekcióban – nyilvános, magán vagy védett – nyilvánítják őket. Az alábbi példában úgy döntöttünk, hogy az összes ilyen deklarációt közvetlenül az osztályfejléc után helyezzük el:

Osztálykarakterlánc ( friend bool operator==(const String &, const String &); friend bool operator==(const char *, const String &); barát bool operator==(const String &, const char *); public: // ... a String osztály többi része);

Ebben a három sorban három, a globális hatókörbe tartozó túlterhelt összehasonlító operátort a String osztály barátjának nyilvánítják, így definícióik szerint közvetlenül elérheti ennek az osztálynak a privát tagjait:

// a barát operátorok közvetlenül elérik a String osztály privát tagjait // bool operator==(const String &str1, const String &str2) ( if (str1._size != str2._size) return false; return strcmp(str1._string, str2) . _string) ?false: true; ) inline bool operator==(const String &str, const char *s) ( return strcmp(str._string, s) ?false: true; ) // stb.

Érvelhetnénk, hogy ebben az esetben a _size és _string tagokhoz való közvetlen hozzáférés nem szükséges, mivel a c_str() és size() beépített függvények ugyanolyan hatékonyak, és továbbra is megtartják a beágyazást, ami azt jelenti, hogy nincs különösebb szükség hogy a String osztály egyenlőségi operátorait a barátainak nyilvánítsa.

Honnan tudhatod, hogy egy nem tag operátort az osztály barátjává kell-e tenni, vagy hozzáférő függvényeket kell-e használni? Általában a fejlesztőnek minimálisra kell csökkentenie azon deklarált függvények és operátorok számát, amelyek hozzáférnek az osztály belső reprezentációjához. Ha vannak olyan accessor függvények, amelyek azonos hatékonyságot biztosítanak, akkor azokat előnyben kell részesíteni, ezáltal elszigetelve a névtér operátorokat az osztályábrázolás változásaitól, ahogy más függvényeknél is. Ha az osztály fejlesztője nem biztosít hozzáférési funkciókat egyes tagokhoz, és a névtérben deklarált operátornak hozzá kell férnie ezekhez a tagokhoz, akkor a baráti mechanizmus használata elkerülhetetlenné válik.

Ennek a mechanizmusnak a legáltalánosabb módja az, hogy lehetővé tegye a túlterhelt operátorok számára, amelyek nem tagjai egy osztálynak, hogy hozzáférjenek az osztály privát tagjaihoz. Ha nem kellene szimmetriát tartani a bal és jobb operandusok között, akkor a túlterhelt operátor teljes hozzáférési joggal rendelkező tagfüggvény lenne.

Bár a barát deklarációkat általában operátorokkal kapcsolatban használják, előfordul, hogy egy függvény egy névtérben, egy másik osztály tagfüggvénye, vagy akár egész osztályígy kell nyilatkozni. Ha az egyik osztályt a második barátjának nyilvánítják, akkor az első osztály minden tagfüggvénye hozzáfér a másik nem nyilvános tagjaihoz. Tekintsük ezt olyan függvények példáján, amelyek nem operátorok.

Egy osztálynak barátként kell deklarálnia a sok túlterhelt függvény mindegyikét, amelyekhez korlátlan hozzáférési jogokat szeretne adni:

extern ostream& storeOn(ostream &, Screen &); extern BitMap& storeOn(BitMap &, Screen &); // ... osztály Képernyő ( barát ostream& storeOn(ostream &, Screen &); barát BitMap& storeOn(BitMap &, Screen &); // ... );

Ha egy függvény két különböző osztályba tartozó objektumokat manipulál, és hozzáférést kell kapnia azok nem nyilvános tagjaihoz, akkor az ilyen függvényt vagy mindkét osztály barátjának nyilváníthatja, vagy az egyik tagjává, a második barátjává tehető.

Ha egy függvényt két osztály barátjaként deklarálunk, akkor így kell kinéznie:

Osztály ablak; // ez csak a Képernyő osztály deklarációja ( friend bool is_equal(Screen &, Window &); // ... ); class Window ( barát bool is_equal(Screen &, Window &); // ... );

Ha úgy döntünk, hogy a függvényt az egyik osztály tagjává, a második barátjává tesszük, akkor a deklarációk a következőképpen épülnek fel:

Osztály ablak; class Képernyő ( // copy() a Képernyő osztály tagja Screen& copy(Window &); // ... ); class Window ( // A Screen::copy() egy Window osztály barátja Képernyő& Képernyő::copy(Window &; // ... ); Képernyő&képernyő::másolás(Ablak &) ( /* ... */ )

Egy osztály tagfüggvénye nem nyilvánítható egy másik barátjának, amíg a fordító nem látta saját osztályának definícióját. Ez nem mindig lehetséges. Tegyük fel, hogy a Screen-nek a Window néhány tagfüggvényét barátként kell deklarálnia, a Windownak pedig ugyanígy kell deklarálnia a Screen egyes tagfüggvényeit. Ebben az esetben a teljes Window osztály a Screen barátjának lesz nyilvánítva:

Osztály ablak; osztály Képernyő ( barát osztály ablaka; // ... );

A Screen osztály privát tagjai mostantól elérhetők bármelyik Window tag függvényből.

15.6. gyakorlat

A 15.5 gyakorlatban a Képernyő osztályhoz definiált bemeneti és kimeneti operátorokat barátként valósítsa meg, és módosítsa a definícióikat a privát tagok közvetlen eléréséhez. Melyik megvalósítás jobb? Mondd el miért.

15.3. Kezelő =

Egy objektum hozzárendelése ugyanazon osztály másik objektumához a másolás-hozzárendelés operátor használatával történik. (Erről a speciális esetről a 14.7. szakaszban volt szó.)

Más hozzárendelési operátorok is meghatározhatók egy osztályhoz. Ha egy osztály objektumaihoz ettől az osztálytól eltérő típusú értékeket kell hozzárendelni, akkor megengedett olyan operátorok meghatározása, amelyek hasonló paramétereket vesznek fel. Például egy C karakterlánc String objektumhoz való hozzárendelésének támogatásához:

Stringcar("Volks"); autó = "Studebaker";

biztosítunk egy operátort, amely egy const char* típusú paramétert vesz fel. Ezt a műveletet már deklaráltuk az osztályunkban:

Class String ( public: // hozzárendelési operátor a char* számára String& operator=(const char *); // ... private: int _size; char *string; );

Egy ilyen operátor a következőképpen valósul meg. Ha nullmutatót rendelünk egy String objektumhoz, akkor az „üres” lesz. Ellenkező esetben a C karakterlánc másolata lesz hozzárendelve:

String& String::operator=(const char *sobj) ( // sobj egy null mutató if (! sobj) ( _size = 0; delete _string; _string = 0; ) else ( _size = strlen(sobj); delete _string; _string = new char[ _size + 1 ]; strcpy(_string, sobj); ) return *this; )

A karakterlánc a C karakterlánc egy példányára utal, amelyre a sobj mutat. Miért egy másolat? Mivel a sobj-t nem lehet közvetlenül hozzárendelni a _string taghoz:

String = sobj; // hiba: típushiba

A sobj a const mutatója, ezért nem rendelhető hozzá a "nem const" mutatóhoz (lásd a 3.5. szakaszt). Változtassuk meg a hozzárendelési operátor definícióját:

String&String::operator=(const *sobj) ( // ... )

Most a _string közvetlenül a sobj-nek címzett C karakterláncra utal. Ez azonban más problémákat is felvet. Emlékezzünk vissza, hogy a C karakterlánc const char* típusú. Ha egy paramétert nem-const mutatóként definiálunk, akkor a hozzárendelés lehetetlenné válik:

Autó = "Studebaker"; // érvénytelen operátor=(char *) !

Tehát nincs más választás. Ahhoz, hogy egy C-karakterláncot String típusú objektumhoz rendeljen, a paraméternek const char* típusúnak kell lennie.

A sobj által címzett C karakterláncra való közvetlen hivatkozás tárolása a _string-ben további bonyodalmakat okoz. Nem tudjuk, hogy a sobj mire mutat pontosan. Ez lehet egy karaktertömb, amelyet a String objektum által nem ismert módon módosítottak. Például:

Char ia = ( "d", "a", "n", "c", "e", "r" ); String trap = ia; // trap._string a következőre hivatkozik: ia ia = "g"; // de erre nincs szükségünk: // az ia és a trap._string is módosul

Ha a trap._string közvetlenül az ia-ra hivatkozna, akkor a trap objektum sajátos viselkedést mutatna: értéke változhat anélkül, hogy meghívná a String osztály tagfüggvényeit. Ezért úgy gondoljuk, hogy egy memóriaterület lefoglalása a C-karakterlánc érték másolatának tárolására kevésbé veszélyes.

Vegye figyelembe, hogy a hozzárendelés operátora a delete parancsot használja. A _string tag hivatkozást tartalmaz a kupacban található karaktertömbre. A szivárgás elkerülése érdekében a régi karakterlánchoz lefoglalt memória törléssel felszabadul, mielőtt lefoglalná az újat. Mivel a _string karaktertömböt címez meg, a delete tömbváltozatát kell használnia (lásd: 8.4. szakasz).

És egy utolsó megjegyzés a hozzárendelés operátorával kapcsolatban. A visszatérési típusa a String osztályra való hivatkozás. Miért link? A tény az, hogy a beépített típusok esetében a hozzárendelési operátorok láncolhatók:

// hozzárendelési operátorok összefűzése int iobj, jobj; iobj = jobj = 63;

Jobbról balra kapcsolódnak, azaz. az előző példában a feladatokat a következőképpen hajtjuk végre:

iobj = (jobj = 63);

Ez akkor is kényelmes, ha a String osztályba tartozó objektumokkal dolgozik: például a következő konstrukció támogatott:

String ver, főnév; ige = főnév = "szám";

A lánc első hozzárendelése meghívja a const char* korábban meghatározott operátorát. Az eredmény típusának olyannak kell lennie, hogy argumentumként használható legyen a String osztály másolás-hozzárendelési operátora számára. Ezért bár a paraméter adott operátor a const char * típusú, akkor is a Stringre való hivatkozás kerül visszaadásra.

A hozzárendelési operátorok túlterheltek. Például a String osztályunk a következővel rendelkezik:

// túlterhelt hozzárendelési operátorok halmaza String& operator=(const String &); String& operator=(const char *);

Külön hozzárendelési operátor létezhet minden olyan típushoz, amely hozzárendelhető egy String objektumhoz. Azonban minden ilyen operátort az osztály tagfüggvényeiként kell definiálni.

15.4. Indexvétel operátor

Az indexvevő operátor() olyan osztályokon definiálható, amelyek a tároló absztrakcióját képviselik, amelyből az egyes elemeket lekérjük. Ilyen konténerek például a String osztályunk, a 2. fejezetben bemutatott IntArray osztály, vagy a C++ Standard Library-ben definiált vektorosztály sablon. Az index take operátornak az osztály tagfüggvényének kell lennie.

A String felhasználóinak képesnek kell lenniük a _string tag egyes karaktereinek olvasására és írására. Támogatni kívánjuk az osztály objektumai használatának következő módját:

String bejegyzés("extravagáns"); String mikópia; for (int ix = 0; ix< entry.size(); ++ix) mycopy[ ix ] = entry[ ix ];

Az alsó index operátor megjelenhet a hozzárendelési operátor bal vagy jobb oldalán. Ahhoz, hogy a bal oldalon legyen, vissza kell adnia az indexelt elem l-értékét. Ehhez egy hivatkozást adunk vissza:

#beleértve inine char& String::operator(int elem) const ( assert(elem >= 0 && elem< _size); return _string[ elem ]; }

A következő töredékben a színtömb null eleméhez a "V" karakter van hozzárendelve:

Stringcolor("ibolya"); szín[0] = "V";

Vegye figyelembe, hogy az operátordefiníció ellenőrzi, hogy az index kívül esik-e a tömb határain. Erre a C könyvtár assert() függvény szolgál. Kivételt is lehet dobni, amely azt jelzi, hogy az elem értéke kisebb, mint 0, vagy nagyobb, mint a _string által hivatkozott C-sztring hossza. (A kivételek felvetéséről és kezeléséről a 11. fejezetben volt szó.)

15.5. Funkcióhívás operátor

A függvényhívás operátor túlterhelhető osztály típusú objektumok esetén. (A 12.3. szakaszban már láthattuk, hogyan használjuk a függvényobjektumok tárgyalásakor.) Ha egy osztályt definiálunk, amely egy műveletet reprezentál, akkor a megfelelő operátor túlterhelve lesz, hogy meghívja. Például egy int típusú szám abszolút értékének meghatározásához megadhatja az absInt osztályt:

Osztály absInt ( public: int operator()(int val) ( int eredmény = érték< 0 ? -val: val; return result; } };

A túlterhelt operátort () tagfüggvényként kell deklarálni tetszőleges számú paraméterrel. A paraméterek és a visszatérési értékek bármilyen típusúak lehetnek a függvényekhez (lásd a 7.2, 7.3 és 7.4 szakaszokat). Az operátor() meghívása egy argumentumlista alkalmazásával történik annak az osztálynak az objektumára, amelyben definiálva van. Megnézzük, hogyan használható a fejezetben ismertetett általánosított algoritmusok egyikében. A következő példában az általános transform() algoritmust hívjuk meg, hogy az absInt-en definiált műveletet alkalmazza az ivec vektor minden elemére, azaz. hogy egy elemet annak abszolút értékére cseréljünk.

#beleértve #beleértve int main() ( int ia = ( -0, 1, -1, -2, 3, 5, -5, 8 ); vektor ivec(ia, ia+8); // minden elem cseréje abszolút értékével transform(ivec.begin(), ivec.end(), ivec.begin(), absInt()); //... )

A transform() első és második argumentuma korlátozza azon elemek tartományát, amelyekre az absInt művelet vonatkozik. A harmadik a vektor elejére mutat, ahol a művelet alkalmazásának eredménye tárolódik.

A negyedik argumentum az absInt osztály ideiglenes objektuma, amely az alapértelmezett konstruktorral jön létre. A main()-ból meghívott általánosított transform() algoritmus példányosítása így nézhet ki:

typedef vektor ::iterátor iter_type; // a transform() példányosítása // az absInt műveletet alkalmazzuk a vektorelemre int iter_type transform(iter_type iter, iter_type last, iter_type result, absInt func) ( while (iter != last) *result++ = func(*iter++) ; // az absInt::operator() visszatérési itert hívják; )

A func egy osztályobjektum, amely biztosítja az absInt műveletet, amely lecseréli az int abszolút értékére. Az absInt osztály túlterhelt operátorának () meghívására szolgál. Az *iter argumentum átadásra kerül ennek az operátornak, amely a vektor azon elemére mutat, amelynek abszolút értékét szeretnénk megkapni.

15.6. nyíl operátor

A tagok elérését lehetővé tévő nyíl operátor túlterhelhető osztályobjektumoknál. Tagfüggvényként kell meghatározni, és mutató szemantikát kell biztosítania. Ezt az operátort leggyakrabban olyan osztályokban használják, amelyek egy "okos mutatót" biztosítanak, amely a beépítettekhez hasonlóan viselkedik, de további funkciókat biztosít.

Tegyük fel, hogy meg akarunk határozni egy osztálytípust, amely egy Képernyő objektumra mutató mutatót reprezentál (lásd a 13. fejezetet):

Osztály ScreenPtr ( // ... privát: Képernyő *ptr; );

A ScreenPtr definíciójának olyannak kell lennie, hogy egy ebbe az osztályba tartozó objektum garantáltan egy Screen objektumra mutasson: a beépített mutatótól eltérően ez nem lehet null. Az alkalmazás ezután használhat ScreenPtr típusú objektumokat anélkül, hogy ellenőrizné, hogy azok bármelyik Screen objektumra mutatnak-e. Ehhez meg kell adni a ScreenPtr osztályt egy konstruktorral, de alapértelmezett konstruktor nélkül (a konstruktorokat részletesen a 14.2 fejezetben tárgyaltuk):

Osztály ScreenPtr ( public: ScreenPtr(const Screen &s) : ptr(&s) ( ) // ... );

A ScreenPtr osztály objektumának bármely definíciójában tartalmaznia kell egy inicializálót – a Screen osztály egyik objektumát, amelyre a ScreenPtr objektum hivatkozni fog:

ScreenPtr p1; // hiba: A ScreenPtr osztálynak nincs alapértelmezett konstruktora Screen myScreen(4, 4); ScreenPtr ps(myScreen); // Jobb

Annak érdekében, hogy a ScreenPtr osztály beépített mutatóként viselkedjen, meg kell határozni néhány túlterhelt operátort - dereferenciát (*) és egy „nyilat” a tagokhoz való hozzáféréshez:

// túlterhelt operátorok a mutató viselkedési osztályának támogatására ScreenPtr ( public: Screen& operator*() ( return *ptr; ) Screen* operator->() ( return ptr; ) // ... ); A tag hozzáférési operátor unáris, ezért nem adnak át neki paramétereket. Ha egy kifejezés részeként használjuk, az eredmény csak a bal oldali operandus típusától függ. Például a point->action(); ponttípust vizsgálunk. Ha ez egy mutató valamilyen osztálytípusra, akkor a beépített tag hozzáférési operátor szemantikája érvényes. Ha ez egy objektum vagy egy objektumra való hivatkozás, akkor a rendszer ellenőrzi, hogy nincs-e túlterhelt hozzáférési operátor ebben az osztályban. Ha túlterhelt nyíl operátort definiálunk, akkor az egy pontobjektumra kerül meghívásra, ellenkező esetben az utasítás érvénytelen, mert a pont operátort kell használni az objektum tagjaira hivatkozni (beleértve a hivatkozást is). A túlterhelt nyíl operátornak vagy egy mutatót kell visszaadnia egy osztálytípusra, vagy annak az osztálynak egy objektumát, amelyben definiálva van. Ha egy mutatót ad vissza, akkor a beépített nyíl operátor szemantikája vonatkozik rá. Ellenkező esetben a folyamat rekurzív módon folytatódik mindaddig, amíg mutatót nem kap, vagy hibát észlel. Például így használhatjuk a ScreenPtr osztály ps objektumát a Screen tagok eléréséhez: ps->move(2, 3); Mivel a "nyíl" operátortól balra van egy ScreenPtr típusú objektum, ennek az osztálynak a túlterhelt operátora kerül felhasználásra, amely egy mutatót ad vissza a Screen objektumra. Ezután a beépített nyíl operátort alkalmazzuk a visszakeresett értékre a move() tagfüggvény meghívásához. A következő az kis program a ScreenPtr osztály tesztelésére. A ScreenPtr típusú objektum ugyanúgy használatos, mint bármely Screen* típusú objektum: #include #beleértve #include "Screen.h" void printScreen(const ScreenPtr &ps) ( cout<< "Screen Object (" << ps->magasság()<< ", " << ps->szélesség()<< ")\n\n"; for (int ix = 1; ix <= ps->magasság(); ++ix) ( for (int iy = 1; iy<= ps->szélesség(); ++iy) cout<get(ix, iy); cout<< "\n"; } } int main() { Screen sobj(2, 5); string init("HelloWorld"); ScreenPtr ps(sobj); // Установить содержимое экрана string::size_type initpos = 0; for (int ix = 1; ix <= ps->magasság(); ++ix) for (int iy = 1; iy<= ps->szélesség(); ++iy) ( ps->move(ix, iy); ps->set(init[ initpos++ ]); ) // Képernyőtartalom nyomtatása printScreen(ps); visszatérés 0; )

Természetesen az osztályobjektumokra mutató mutatókkal végzett ilyen manipulációk nem olyan hatékonyak, mint a beépített mutatókkal való munka. Ezért az intelligens mutatónak további olyan funkciókat kell biztosítania, amelyek fontosak az alkalmazás számára, hogy igazolják a használat összetettségét.

15.7. Növelési és csökkentési operátorok

Folytatva az előző részben bemutatott ScreenPtr osztály megvalósításának fejlesztését, nézzünk meg még két olyan operátort, amelyek támogatják a beépített pointereket, és amelyeket az intelligens mutatónk szeretne: inkrement (++) és decrement (--) . Ahhoz, hogy a ScreenPtr osztályt a Screen objektumok tömbjének elemeire hivatkozzon, hozzá kell adni néhány extra tagot.

Először meghatározunk egy új tagot, a méretet, amely vagy nulla (jelezve, hogy a ScreenPtr objektum egyetlen objektumra mutat), vagy a ScreenPtr objektum által mutatott tömb mérete. Szükségünk van egy offset tagra is, amely megjegyzi az eltolást az adott tömb elejétől:

Class ScreenPtr ( public: // ... private: int size; // tömb mérete: 0, ha az egyetlen objektum int offset; // a ptr eltolása a tömb elejétől Screen *ptr; );

Módosítsa a ScreenPtr osztály konstruktort, hogy tükrözze annak új funkcióit és további tagjait. Az osztályunk felhasználójának egy további argumentumot kell átadnia a konstruktornak, ha a létrehozandó objektum egy tömbre mutat:

Osztály ScreenPtr ( public: ScreenPtr(Screen &s , int arraySize = 0) : ptr(&s), méret (arraySize), offset(0) ( ) private: int size; int offset; Képernyő *ptr; );

Ez az argumentum a tömb méretét adja meg. A funkcionalitás megtartása érdekében adjunk meg egy alapértelmezett nulla értéket. Így, ha a konstruktor második argumentumát kihagyjuk, akkor a mérettag 0 lesz, és ezért egy ilyen objektum egyetlen Képernyő objektumra fog mutatni. Az új ScreenPtr osztály objektumai a következők szerint definiálhatók:

Képernyő myScreen(4, 4); ScreenPtr pobj(myScreen); // helyes: egy objektumra mutat const int arrSize = 10; Képernyő *parray = új Képernyő[arrSize]; ScreenPtr parr(*parray, arrSize); // helyes: egy tömbre mutat

Most már készen állunk a túlterhelt növelési és csökkentési operátorok meghatározására a ScreenPtr-ben. Ennek azonban két típusa van: előtag és utótag. Szerencsére mindkét lehetőség meghatározható. Előtag operátor esetén a deklaráció semmi váratlant nem tartalmaz:

Osztály ScreenPtr ( publikus: Képernyő& operátor++(); Képernyő& operátor--(); // ... );

Az ilyen operátorok unáris operátorfüggvények. Használhatja az előtag increment operátort például a következőképpen: const int arrSize = 10; Képernyő *parray = új Képernyő[arrSize]; ScreenPtr parr(*parray, arrSize); for (int ix = 0; ix

Ezeknek a túlterhelt operátoroknak a definíciói az alábbiak:

Képernyő& ScreenPtr::operator++() ( if (méret == 0) ( cerr<<"не могу инкрементировать указатель для одного объекта\n"; return *ptr; } if (offset >= méret - 1) ( cerr<< "уже в конце массива\n"; return *ptr; } ++offset; return *++ptr; } Screen& ScreenPtr::operator--() { if (size == 0) { cerr << "не могу декрементировать указатель для одного объекта\n"; return *ptr; } if (offset <= 0) { cerr << "уже в начале массива\n"; return *ptr; } --offset; return *--ptr; }

Az előtag operátorok és a postfix operátorok megkülönböztetésére az utóbbiak deklarációi tartalmaznak egy további int típusú paramétert. A következő töredék deklarálja a ScreenPtr osztály növekedési és csökkentési operátorainak elő- és utótag-verzióit:

Osztály ScreenPtr ( public: Screen& operator++(); // előtag operátorok Screen& operator--(); Screen& operator++(int); // postfix operátorok Screen& operator--(int); // ... );

A postfix operátorok lehetséges megvalósítása a következő:

Képernyő& ScreenPtr::operator++(int) ( if (méret == 0) ( cerr<< "не могу инкрементировать указатель для одного объекта\n"; return *ptr; } if (offset == size) { cerr << "уже на один элемент дальше конца массива\n"; return *ptr; } ++offset; return *ptr++; } Screen& ScreenPtr::operator--(int) { if (size == 0) { cerr << "не могу декрементировать указатель для одного объекта\n"; return *ptr; } if (offset == -1) { cerr <<"уже на один элемент раньше начала массива\n"; return *ptr; } --offset; return *ptr--; }

Megjegyzendő, hogy a második paramétert nem szükséges megnevezni, mivel az operátordefiníción belül nem kerül felhasználásra. A fordító maga helyettesít egy alapértelmezett értéket, amely figyelmen kívül hagyható. Íme egy példa a postfix operátor használatára:

Const int arrSize = 10; Képernyő *parray = új Képernyő[arrSize]; ScreenPtr parr(*parray, arrSize); for (int ix = 0; ix

Ha kifejezetten meghívja, akkor is át kell adnia a második egész argumentum értékét. A ScreenPtr osztályunk esetében ez az érték figyelmen kívül marad, így bármi lehet:

parr.operator++(1024); // a postfix operátor++ hívása

A túlterhelt növelő és csökkentő operátorok barát függvényként deklarálhatók. Módosítsa a ScreenPtr osztály definícióját ennek megfelelően:

Osztály ScreenPtr ( // nem tag nyilatkozatok barát Képernyő& operator++(Screen &); // előtag operátorok barát Képernyő& operátor--(Képernyő &); barát Képernyő& operator++(Screen &, int); // postfix operátorok barát Képernyő& operátor-- ( Screen &, int); public: // tagdefiníciók );

15.7. gyakorlat

Írjon definíciókat a ScreenPtr osztály túlterhelt növelési és csökkentési operátoraihoz, feltételezve, hogy az osztály barátainak nyilvánították őket.

15.8. gyakorlat

A ScreenPtr használható egy mutató megjelenítésére a Screen osztályba tartozó objektumok tömbjére. Módosítsa a túlterhelt operátort*() és a >() operátort (lásd: 15.6. szakasz), hogy a mutató semmilyen körülmények között ne címezzen egy elemet a tömb vége előtt vagy után. Tipp: Ezeknek az operátoroknak az új méretű és eltolási tagokat kell használniuk.

15.8. Operátorok újak és törölhetők

Alapértelmezés szerint egy osztályobjektum halomból történő lefoglalása és az általa elfoglalt memória felszabadítása a C++ Standard Library-ban meghatározott globális new() és delete() operátorok használatával történik. (Ezeket az operátorokat a 8.4. részben tárgyaltuk.) De egy osztály saját memóriakezelési stratégiáját is megvalósíthatja, ha azonos nevű tagoperátorokat biztosít. Ha egy osztályban vannak definiálva, akkor globális operátorok helyett hívják őket, hogy lefoglalják és felszabadítsák a memóriát az adott osztály objektumai számára.

Adjunk meg new() és delete() operátorokat a Képernyő osztályunkban.

A new() tag operátornak void* típusú értéket kell visszaadnia, és első paramétereként egy size_t típusú értéket kell vennie, ahol a size_t a rendszer fejlécfájljában definiált typedef. Íme a bejelentése:

Amikor a new()-t egy osztály típusú objektum létrehozására használjuk, a fordító ellenőrzi, hogy az osztályban van-e ilyen operátor definiálva. Ha igen, akkor az objektumot hívják meg, hogy lefoglaljon memóriát az objektum számára, ellenkező esetben a new() globális operátor kerül meghívásra. Például a következő állítás

Képernyő *ps = új képernyő;

létrehoz egy Képernyő objektumot a kupacban, és mivel ennek az osztálynak van egy new() operátora, ezért meghívásra kerül. Az operátor size_t paramétere automatikusan inicializálódik a Képernyő méretére bájtokban.

A new() hozzáadása vagy eltávolítása egy osztályhoz nincs hatással a felhasználói kódra. Az új hívás ugyanúgy néz ki mind a globális operátornál, mind a tag operátornál. Ha a Screen osztálynak nem lenne saját new()-ja, akkor a hívás helyes marad, csak a globális operátort hívják meg a tag operátor helyett.

A globális hatókör felbontási operátorával a global new() akkor is meghívható, ha a Képernyő osztálynak saját verziója van megadva:

Képernyő *ps = ::új Képernyő;

Ha a delete operandus egy osztály típusú objektumra mutató mutató, a fordító ellenőrzi, hogy a delete() operátor definiálva van-e az adott osztályban. Ha igen, akkor őt hívják fel a memória felszabadítására, ellenkező esetben az operátor globális verzióját. Következő utasítás

ps törlése;

felszabadítja a ps által mutatott Képernyő objektum által elfoglalt memóriát. Mivel a Screen-nek van delete() tag operátora, ez az érvényes. A void* típusú operátori paraméter automatikusan ps-re inicializálódik. A delete() hozzáadása egy osztályhoz vagy onnan való eltávolítása nincs hatással a felhasználói kódra. A törlési hívás ugyanúgy néz ki mind a globális operátor, mind a tag operátor esetében. Ha a Screen osztálynak nem lenne saját delete() operátora, akkor a hívás helyes marad, csak a globális operátort hívják meg a tag operátor helyett.

A globális hatókör felbontása operátorral a global delete() akkor is meghívható, ha a képernyőnek saját verziója van megadva:

::töröl ps;

Általában a használt delete() operátornak meg kell egyeznie a memóriát lefoglaló new() operátorral. Például, ha a ps a global new() által lefoglalt memóriaterületre mutat, akkor a globális delete() parancsot kell használni a felszabadításához.

Az osztálytípushoz definiált delete() operátornak két paramétere lehet egy helyett. Az első paraméternek továbbra is void* típusúnak kell lennie, a másodiknak pedig az előre meghatározott size_t típusúnak kell lennie (ne felejtse el megadni a fejlécfájlt):

Class Screen ( public: // lecseréli // void operator delete(void *); void operator delete(void *, size_t); );

Ha van második paraméter, a fordító automatikusan inicializálja azt az első paraméter által megcímzett objektum bájtban megadott méretével megegyező értékkel. (Ez az opció fontos az osztályhierarchiában, amikor a delete() operátort egy származtatott osztály örökölheti. Az öröklődésről bővebben a fejezetben lesz szó.)

Nézzük meg részletesebben a New() és delete() operátorok megvalósítását a Screen osztályban. Memóriakiosztási stratégiánk a Screen objektumok linkelt listáján fog alapulni, amelyre a freeStore tag mutat rá. A new() tag operátor minden egyes meghívásakor a lista következő objektuma kerül visszaadásra. A delete() meghívásakor az objektum visszakerül a listába. Ha egy új objektum létrehozásakor a freeStore-nak címzett lista üres, akkor a new() globális operátor meghívásra kerül, hogy egy elég nagy memóriablokkot kapjon a Screen osztályba tartozó screenChunk objektumok tárolására.

Mind a screenChunk, mind a freeStore csak a Screen számára érdekes, ezért privát tagokká tesszük őket. Ezenkívül az osztályunk összes létrehozott objektumánál ezen tagok értékének azonosnak kell lennie, ezért statikusnak kell őket nyilvánítani. A képernyőobjektumok linkelt listastruktúrájának támogatásához szükségünk van egy harmadik következő tagra:

Class Screen ( public: void *operator new(size_t); void operator delete(void *, size_t); // ... private: Screen *next; static Screen *freeStore; static const int screenChunk; );

Íme a new() operátor egy lehetséges megvalósítása a Screen osztályhoz:

#include "Screen.h" #include // a statikus tagok inicializálása // a program forrásfájljaiban történik, nem a fejlécfájlokban Képernyő *Képernyő::freeStore = 0; const int Képernyő::screenChunk = 24; void *Screen::operator new(size_t size) ( Képernyő *p; if (!freeStore) ( // a linkelt lista üres: get new block // a globális operátort new size_t chunk = screenChunk * size; freeStore = p = reinterpret_cast< Screen* >(új char[csonk]); // az eredményül kapott blokkot felveszi a listába: (; p != &freeStore[ screenChunk - 1 ]; ++p) p->next = p+1; p->következő = 0; ) p = FreeStore; freeStore = freeStore->next; return p; ) És itt van a delete() operátor megvalósítása: void Screen::operator delete(void *p, size_t) ( // a "törölt" objektumot visszahelyezzük, // a szabad listába (static_cast< Screen* >(p))->next = freeStore; freeStore = static_cast< Screen* >(p); )

A new() operátor deklarálható egy osztályban a megfelelő delete() nélkül. Ebben az esetben az objektumok az azonos nevű globális operátor használatával kerülnek kiadásra. A delete() operátor deklarálása new() nélkül is megengedett: az objektumok az azonos nevű globális operátor használatával jönnek létre. Ezek az operátorok azonban általában egy időben kerülnek implementálásra, mint a fenti példában, mivel egy osztály tervezőjének jellemzően mindkettőre szüksége van.

Statikus tagjai az osztálynak, még akkor is, ha a programozó kifejezetten nem deklarálja őket, és az ilyen tagfüggvényeknél szokásos korlátozások vonatkoznak rájuk: nem kapják meg ezt a mutatót, ezért csak közvetlenül férhetnek hozzá statikus tagok. (Lásd a statikus tagfüggvények tárgyalását a 13.5. szakaszban.) Azért van ezek az operátorok statikusak, mert vagy az osztályobjektum felépítése előtt (new()), vagy megsemmisítése után hívódnak meg (delete()).

Memória lefoglalása a new() operátor használatával, például:

Képernyő *ptr = új Képernyő(10, 20);

// C++ pszeudo kód ptr = Képernyő::operator new(sizeof(Screen)); Képernyő::Képernyő(ptr, 10, 20);

Más szavakkal, az osztályban definiált new() operátor először meghívásra kerül, hogy lefoglalja a memóriát az objektum számára, majd az objektumot inicializálja egy konstruktor. Ha a new() sikertelen, akkor a rendszer egy bad_alloc típusú kivételt dob, és nem hívja meg a konstruktort.

Memória felszabadítása a delete() operátor használatával, például:

ptr törlése;

egyenértékű a következő utasítások egymás utáni végrehajtásával:

// C++ pszeudokód Képernyő::~Screen(ptr); Képernyő::operátor delete(ptr, sizeof(*ptr));

Így ha egy objektum megsemmisül, először az osztálydestruktort hívják meg, majd az osztályban definiált delete() operátort hívják meg a memória felszabadítására. Ha ptr 0, akkor sem a destruktor, sem a delete() nem kerül meghívásra.

15.8.1. Operátorok új és törlése

Az előző alfejezetben definiált new() operátor csak akkor kerül meghívásra, ha egyetlen objektumhoz memória van lefoglalva. Tehát ebben az utasításban a Screen osztály new()-ját hívják:

// A Screen::operator new() neve Screen *ps = new Screen(24, 80);

míg a new() globális operátort az alábbiakban hívjuk meg, hogy memóriát foglaljon le a kupacból egy Screen típusú objektumtömb számára:

// A Screen::operator new() neve Képernyő *psa = new Képernyő;

Az osztály a new() és delete() operátorokat is deklarálhatja a tömbökkel való együttműködéshez.

A new() tag operátornak void* típusú értéket kell visszaadnia, és első paraméterként a size_t típusú értéket kell vennie. Íme a nyilatkozata a Screen számára:

Osztály képernyő ( public: void *operator new(size_t); // ... );

Amikor a new parancsot használja egy osztálytípusú objektumtömb létrehozásához, a fordító ellenőrzi, hogy a new() operátor definiálva van-e az osztályban. Ha igen, akkor a tömb hívja meg, hogy lefoglalja a memóriát a tömb számára, ellenkező esetben a globális new() függvény kerül meghívásra. A következő utasítás tíz képernyőobjektumból álló tömböt hoz létre a csípőben:

Képernyő *ps = új képernyő;

Ennek az osztálynak van egy new() operátora, ezért hívják a memória lefoglalására. A size_t paramétere automatikusan inicializálódik a tíz képernyőobjektum tárolásához szükséges bájtban megadott memóriamennyiségre.

Még ha az osztálynak van new() tagoperátora is, a programozó meghívhatja a global new()-t, hogy létrehozzon egy tömböt a globális hatókör felbontási operátorával:

Képernyő *ps = ::új Képernyő;

A delete() operátornak, amely az osztály tagja, void típusúnak kell lennie, és első paraméterként void*-ot kell venni. Így néz ki a képernyőre vonatkozó nyilatkozata:

Osztály képernyő ( public: void operator delete(void *); );

Osztályobjektumok tömbjének törléséhez a deletet a következőképpen kell meghívni:

ps törlése;

Ha a delete operandus egy osztály típusú objektumra mutató mutató, a fordító ellenőrzi, hogy a delete() operátor definiálva van-e az adott osztályban. Ha igen, akkor ő az, akit a memória felszabadítására hívnak, ellenkező esetben az ő globális verzióját. A void* típusú paraméter automatikusan inicializálódik a memóriaterület kezdetének címére, ahol a tömb található.

Még ha az osztálynak van delete() tagoperátora is, a programozó meghívhatja a globális delete()-t a globális hatókör felbontási operátorával:

::töröl ps;

A new() vagy delete() operátorok osztályhoz való hozzáadása vagy törlése nincs hatással a felhasználói kódra: a globális operátorok és a tag operátorok hívásai ugyanúgy néznek ki.

Egy tömb létrehozásakor először a new() meghívásra kerül, hogy lefoglalja a szükséges memóriát, majd minden elemet inicializálunk egy alapértelmezett konstruktorral. Ha egy osztálynak legalább egy konstruktora van, de nincs alapértelmezett konstruktora, akkor a new() operátor meghívása hibának minősül. A tömb ilyen módon történő létrehozásakor nincs szintaxis a tömbelem inicializálóinak vagy az osztálykonstruktor argumentumainak megadására.

Ha egy tömb megsemmisül, először az osztálydestruktort hívják meg az elemek megsemmisítésére, majd a delete() operátort hívják meg az összes memória felszabadítására. Fontos, hogy ehhez a megfelelő szintaxist használjuk. Ha az utasításokat

ps törlése;

ps osztályobjektumok tömbjére mutat, akkor a szögletes zárójelek hiánya miatt a destruktor csak az első elemhez kerül meghívásra, bár a memória teljesen felszabadul.

A delete() tag operátornak nem egy, hanem két paramétere lehet, a második size_t típusú:

Class Screen ( public: // lecseréli a // void operator delete(void*); void operator delete(void*, size_t); );

Ha a második paraméter jelen van, a fordító automatikusan inicializálja azt egy olyan értékkel, amely megegyezik a tömb számára lefoglalt memória bájtokban kifejezett mennyiségével.

15.8.2. Elhelyezés operátor new() és operátor delete()

A new() tag operátor túlterhelhető, feltéve, hogy minden deklarációnak különböző paraméterlistája van. Az első paraméternek size_t típusúnak kell lennie:

Class Screen ( public: void *operator new(size_t); void *operator new(size_t, Screen *); // ... );

A többi paraméter inicializálása az új hívásakor megadott elhelyezési argumentumokkal történik:

Void func (Képernyő *start) ( // ... )

A kifejezésnek az új kulcsszó után következő és zárójelben lévő része az elhelyezési argumentumokat jelenti. A fenti példa meghívja a new() operátort, amely két paramétert vesz fel. Az első automatikusan inicializálódik a Képernyő osztály méretére bájtokban, a második pedig a start elhelyezés argumentum értékére.

A delete() tag operátort is túlterhelheti. Egy ilyen operátort azonban soha nem hívunk meg törlési kifejezésből. A túlterhelt delete() parancsot implicit módon akkor hívja meg a fordító, ha az új operátor végrehajtásakor meghívott konstruktor (ez nem elírás, igazából a new) kivételt dob. Nézzük meg közelebbről a delete() használatát.

A műveletek sorrendje egy kifejezés kiértékelésekor

Képernyő *ps = új (kezdő) Képernyő;

  1. Az osztályban definiált new(size_t, Screen*) operátor meghívásra kerül.
  2. A Screen osztály alapértelmezett konstruktora meghívásra kerül a létrehozott objektum inicializálására.

A ps változó az új Screen objektum címével inicializálódik.

Tegyük fel, hogy a new(size_t, Screen*) osztályoperátor a globális new() függvény segítségével foglal le memóriát. Hogyan biztosíthatja a fejlesztő a memória felszabadítását, ha a 2. lépésben meghívott konstruktor kivételt dob? A felhasználói kód memóriaszivárgások elleni védelme érdekében biztosítson egy túlterhelt delete() operátort, amely csak ebben a helyzetben kerül meghívásra.

Ha egy osztálynak van egy túlterhelt operátora olyan paraméterekkel, amelyek típusai megegyeznek a new() paraméterek típusával, akkor a fordító automatikusan meghívja azt, hogy felszabadítsa a memóriát. Tegyük fel, hogy a következő kifejezéssel rendelkezünk az új elhelyezési operátorral:

Képernyő *ps = új (kezdő) Képernyő;

Ha a Screen osztály alapértelmezett konstruktora kivételt ad, akkor a fordító a delete() függvényt keresi a Képernyő hatókörében. Egy ilyen operátor megtalálásához a paraméterei típusának meg kell egyeznie a hívott new() paramétereinek típusával. Mivel a new() első paramétere mindig size_t típusú, a delete() operátor pedig mindig void*, az első paramétereket nem vesszük figyelembe az összehasonlítás során. A fordító a Képernyő osztályban a következő formátumú delete() operátort keresi:

Void operátor delete(void*, Screen*);

Ha ilyen operátort találunk, a program meghívja a memória felszabadítására, ha a new() kivételt ad. (Egyébként nem hívják.)

Az osztálytervező dönti el, hogy adjon-e valamilyen new()-nak megfelelő delete()-t, attól függően, hogy ez a new() operátor maga foglalja le a memóriát, vagy a már lefoglalt memóriát használja. Az első esetben a delete() függvényt bele kell foglalni a memória felszabadításához, ha a konstruktor kivételt dob; különben nincs rá szükség.

A new() allokációs operátort és a delete() operátort is túlterhelheti a tömböknél:

Osztály képernyő ( public: void *operator new(size_t); void *operator new(size_t, Screen*); void operator delete(void*, size_t); void operator delete(void*, Screen*); // ... );

A new() operátor akkor használatos, ha a new tartalmú kifejezés egy tömb kiosztásához rendelkezik a megfelelő kiosztási argumentumokkal:

Void func(Képernyő *start) ( // Screen::operator new(size_t, Screen*) Screen *ps = new (start) Screen; // ... )

Ha a konstruktor kivételt dob ​​az új operátor működése közben, akkor a megfelelő delete() automatikusan meghívásra kerül.

15.9. gyakorlat

Magyarázza el, hogy az alábbi inicializálások közül melyik hibás:

iStack osztály ( publikus: iStack(int kapacitás) : _stack(kapacitás), _top(0) () // ... privát: int _top; vatcor< int>_Kazal; ); (a) iStack *ps = new iStack(20); (b) iStack *ps2 = new const iStack(15); (c) iStack *ps3 = új iStack[100];

15.10. gyakorlat

Mi történik a következő, new és delete kifejezéseket tartalmazó kifejezésekben?

Osztálygyakorlat ( nyilvános: Gyakorlat(); ~Gyakorlat(); ); Gyakorlat *pe = új Gyakorlat; ps törlése;

Módosítsa ezeket a kifejezéseket úgy, hogy a new() és delete() globális operátorok legyenek meghívva.

15.11. gyakorlat

Magyarázza el, miért kell az osztálytervezőnek megadnia a delete() operátort.

15.9. Felhasználó által meghatározott konverziók

Azt már láttuk, hogyan alkalmazzák a típuskonverziókat a beépített típusok operandusaira: a 4.14. szakaszban ezt a kérdést a beépített operátorok operandusainak példáján, a 9.3. szakaszban pedig a beépített operátorok tényleges argumentumainak példáján vettük figyelembe. függvénynek hívjuk, hogy a formális paraméterek típusaiba öntsük őket. Tekintsük a következő hat összeadási műveletet ebből a szempontból:

Charch; rövid sh;, int ival; /* műveletenként egy operandus * típuskonverziót igényel */ ch + ival; ival + ch; ch+sh; ch+ch; ival + sh; sh + ival;

A ch és sh operandusok kibővülnek az int típusra. Egy művelet végrehajtásakor két int típusú érték kerül hozzáadásra. A típusbővítést implicit módon a fordító hajtja végre, és az átlátható a felhasználó számára.

Ebben a részben azt nézzük meg, hogy a fejlesztő hogyan határozhat meg egyéni konverziókat egy osztálytípusú objektumokhoz. Az ilyen, felhasználó által definiált konverziókat a fordító szükség szerint automatikusan meghívja. Hogy megmutassuk, miért van szükség rájuk, nézzük meg újra a 10.9. szakaszban bemutatott SmallInt osztályt.

Emlékezzünk vissza, hogy a SmallInt lehetővé teszi olyan objektumok meghatározását, amelyek az előjel nélküli karakterekkel azonos tartományból képesek értékeket tárolni, pl. 0-tól 255-ig, és elkapja a határon kívüli hibákat. Minden más tekintetben ez az osztály pontosan úgy viselkedik, mint az előjel nélküli karakter.

Ahhoz, hogy SmallInt objektumokat tudjunk hozzáadni és kivonni az azonos osztályba tartozó más objektumokhoz vagy beépített típusú értékekhez, hat operátori funkciót valósítunk meg:

Class SmallInt ( barát operátor+(const SmallInt &, int); barát operátor-(const SmallInt &, int); barát operátor-(int, const SmallInt &); barát operátor+(int, const SmallInt &); nyilvános: SmallInt(int ival) : value(ival) ( ) operator+(const SmallInt &); operátor-(const SmallInt &); // ... private: int value; );

A tagoperátorok lehetővé teszik két SmallInt objektum hozzáadását és kivonását. A globális barát operátorok lehetővé teszik, hogy ezeket a műveleteket egy adott osztályba tartozó objektumokon és beépített aritmetikai típusú objektumokon hajtsa végre. Mindössze hat operátorra van szükség, mert bármilyen beépített aritmetikai típust ki lehet önteni az int-be. Például a kifejezés

két lépésben oldható meg:

  1. A 3,14159 dupla állandót 3 egész számmá alakítjuk.
  2. Az operátor+(const SmallInt &,int) meghívásra kerül, ami a 6-os értéket adja vissza.

Ha támogatni akarjuk a bitenkénti és logikai műveleteket, valamint az összehasonlító és összetett hozzárendelési operátorokat, akkor hány operátort kell túlterhelni? Nem számolsz azonnal. Sokkal kényelmesebb a SmallInt osztály objektumát automatikusan int típusú objektummá konvertálni.

A C++ nyelv rendelkezik egy olyan mechanizmussal, amely lehetővé teszi bármely osztály számára, hogy meghatározza az objektumaira alkalmazható transzformációk halmazát. SmallInt esetén meghatározunk egy objektumot, amely az int-be öntött. Íme a megvalósítása:

Class SmallInt ( public: SmallInt(int ival) : value(ival) ( ) // konverter // SmallInt ==> int operátor int() ( return value; ) // nincs szükség túlterhelt operátorokra private: int value; );

Az int() operátor egy olyan konverter, amely felhasználó által definiált konverziót valósít meg, ebben az esetben egy osztálytípust egy adott int típusba önt. A konverter definíciója leírja, hogy mit jelent egy konverzió, és milyen műveleteket kell végrehajtania a fordítónak az alkalmazásához. Egy SmallInt objektum esetében az int-re való átalakítás lényege, hogy az értéktagban tárolt int típusú int számát adja vissza.

Mostantól a SmallInt osztály objektumai mindenhol használhatók, ahol az int megengedett. Feltéve, hogy nincs több túlterhelt operátor, és a SmallIntben van egy int konvertáló, az összeadási művelet

SmallInt si(3); si+3,14159

két lépésben oldható meg:

  1. A SmallInt osztályátalakító meghívásra kerül, és a 3 egész számot adja vissza.
  2. A 3-as egész szám 3,0-ra bővül, és hozzáadódik a 3,14159 dupla pontosságú állandóhoz, amely 6,14159-et ad.

Ez a viselkedés jobban megfelel a beépített típusú operandusok viselkedésének, összehasonlítva a korábban meghatározott túlterhelt operátorokkal. Ha egy int-et adunk egy duplához, akkor két duplát adunk hozzá (mivel az int duplájára bővül), és az eredmény egy azonos típusú szám.

Ez a program a SmallInt osztály használatát szemlélteti:

#beleértve #include "SmallInt.h" int main() ( cout<< "Введите SmallInt, пожалуйста: "; while (cin >> si1) ( cout<< "Прочитано значение " << si1 << "\nОно "; // SmallInt::operator int() вызывается дважды cout << ((si1 >127)? "nagyobb, mint" : ((si1< 127) ? "меньше, чем " : "равно ")) <<"127\n"; cout << "\Введите SmallInt, пожалуйста \ (ctrl-d для выхода): "; } cout <<"До встречи\n"; }

Az összeállított program a következő eredményeket produkálja:

Kérjük, írja be a SmallInt-et: 127

Olvasott érték 127

Ez egyenlő 127-tel

Kérjük, írja be a SmallInt parancsot (ctrl-d a kilépéshez): 126

Kevesebb, mint 127

Kérjük, írja be a SmallInt parancsot (ctrl-d a kilépéshez): 128

Ez több mint 127

Kérjük, írja be a SmallInt parancsot (ctrl-d a kilépéshez): 256

*** SmallInt tartomány hiba: 256 ***

#beleértve osztály SmallInt ( barát istream& operátor>(istream &is, SmallInt &s); barát ostream& operátor<<(ostream &is, const SmallInt &s) { return os << s.value; } public: SmallInt(int i=0) : value(rangeCheck(i)){} int operator=(int i) { return(value = rangeCheck(i)); } operator int() { return value; } private: int rangeCheck(int); int value; };

A következők az osztálytörzsön kívüli tagfüggvény-definíciók:

Istream& operátor>>(istream &is, SmallInt &si) ( int ix; is >> ix; si = ix; // SmallInt::operator=(int) return is; ) int SmallInt::rangeCheck(int i) ( /* ha legalább egy bit az első nyolctól eltérő, * akkor az érték túl nagy; jelentse és azonnal lépjen ki */ if (i & ~0377) ( cerr< <"\n*** Ошибка диапазона SmallInt: " << i << " ***" << endl; exit(-1); } return i; }

15.9.1. Átalakítók

A konverter egy olyan osztálytag függvény speciális esete, amely egy objektum felhasználó által definiált konvertálását valósítja meg valamilyen más típusra. A konvertert az osztály törzsében deklarálják a kulcsszó operátor, majd a konverzió céltípusának megadásával.

A kulcsszót követő névnek nem kell az egyik beépített típus nevének lennie. Az alább látható Token osztály több konvertert határoz meg. Az egyik a typedef tName segítségével adja meg a típus nevét, a másik pedig a SmallInt osztálytípust.

#include "SmallInt.h" typedef char *tName; class Token ( public: Token(char *, int); operátor SmallInt() ( return val; ) operator tName() ( return name; ) operator int() ( return val; ) // egyéb nyilvános tagok privát: SmallInt val; char*név;);

Vegye figyelembe, hogy a SmallInt és int konverterek definíciói megegyeznek. A Token::operator int() konverter visszaadja a val tag értékét. Mivel a val SmallInt típusú, a SmallInt::operator int() implicit módon a val int-vé konvertálására szolgál. Magát a Token::operator int()-et a fordító implicit módon arra használja, hogy egy Token típusú objektumot int típusú értékké alakítson át. Például ez a konverter arra szolgál, hogy a tényleges t1 és t2 Token típusú argumentumokat implicit módon a print() függvény formális paraméterének int típusába adja:

#include "Token.h" void print(int i) ( cout< < "print(int) : " < < i < < endl; } Token t1("integer constant", 127); Token t2("friend", 255); int main() { print(t1); // t1.operator int() print(t2); // t2.operator int() return 0; }

A program fordítása és futtatása után a következő sorokat jeleníti meg:

Nyomtatás(int): 127 print(int): 255

A konverter általános képe a következő:

operátortípus();

ahol a típus lehet beépített típus, osztálytípus vagy typedef név. Az olyan konverterek, ahol a típus tömb vagy függvénytípus, nem engedélyezettek. A konverternek tagfüggvénynek kell lennie. Deklarációjában nem szabad megadni sem a visszatérési típust, sem a paraméterlistát:

Operator int(SmallInt &); // hiba: nem tagja a SmallInt osztálynak ( publikus: int operátor int(); // hiba: megadott visszatérési típus operátor int(int = 0); // hiba: paraméterlista megadva // ... );

A konvertert explicit típuskonverzió eredményeként hívják meg. Ha a konvertálandó érték egy konverterrel rendelkező osztályhoz tartozik, és ennek az átalakítónak a típusa a cast műveletben van megadva, akkor ezt nevezzük:

#include "Token.h" Token tok("függvény", 78); // funkcionális jelölés: Token::operator SmallInt() neve SmallInt tokVal = SmallInt(tok); // static_cast: Token::operator tName() char *tokName = static_cast< char * >(tok);

A Token::operator tName() konverternek nem kívánt mellékhatása lehet. A Token::name privát tag közvetlen elérésére irányuló kísérletet hibaként jelölte meg a fordító:

Char *tokName = tok.name; // hiba: A Token::name egy privát tag

Átalakítónk azonban azáltal, hogy lehetővé teszi a felhasználók számára a Token::name közvetlen megváltoztatását, pontosan azt teszi, ami ellen védekezni akartunk. Valószínűleg nem fog működni. Íme egy példa arra, hogyan történhet ilyen módosítás:

#include "Token.h" Token tok("függvény", 78); char *tokName = tok; // helyes: implicit konverzió *tokname = "P"; // de most a névtagnak Punction van!

A Token osztály konvertált objektumához csak olvasási hozzáférést kívánunk engedélyezni. Ezért a konverternek egy const char* típust kell visszaadnia:

Typedef const char *cchar; class Token ( public: operátor cchar() ( return name; ) // ... ); // hiba: konverzió char*-ról const-ra char* nem engedélyezett char *pn = tok; const char *pn2 = tok; // Jobb

Egy másik megoldás, hogy a Token definíciójában a char* típust lecseréljük a C++ szabványos könyvtárból származó karakterlánc típusra:

Class Token ( public: Token(string, int); operátor SmallInt() ( return val; ) operator string() ( return name; ) operator int() ( return val; ) // egyéb nyilvános tagok privát: SmallInt val; string név; );

A Token::operator string() konverter szemantikája az, hogy visszaadja a tokennevet képviselő karakterlánc értékének másolatát (nem az értékre mutató mutatót). Ez megakadályozza a Token osztály privát név tagjának véletlen módosítását.

A céltípusnak pontosan meg kell egyeznie a konverter típusával? Például a következő kód meghívja a Token osztályban definiált int() konvertert?

extern void calc(double); Token tok("állandó", 44); // Meg van hívva az int() operátor? Igen // standard konverziót alkalmazunk int --> double calc(tok);

Ha a céltípus (jelen esetben dupla) nem egyezik pontosan a konverter típusával (esetünkben int), akkor a konverter továbbra is meghívásra kerül, feltéve, hogy van egy szabványos konverziós sorozat, amely a célhoz vezet. típust a konverter típusából. (Ezeket a sorozatokat a 9.3. fejezet ismerteti.) A calc() függvény meghívása a Token::operator int() függvényt a tok típusból int típussá konvertálásához. Ezután a szabványos átalakítást alkalmazzák az eredmény intről duplára öntéséhez.

A felhasználó által definiált átalakítást követően csak a szabványos átalakítások engedélyezettek. Ha egy másik felhasználó által meghatározott konverzióra van szükség a céltípus eléréséhez, akkor a fordító nem alkalmaz semmilyen konverziót. Tegyük fel, hogy nincs int() operátor definiálva a Token osztályban, akkor a következő hívás hibás lesz:

extern void calc(int); token tok("mutató", 37); // ha a Token::operator int() nincs definiálva, // akkor ez a hívás fordítási hibát eredményez calc(tok);

Ha a Token::operator int() konverter nincs megadva, akkor a tok int-be öntéséhez két felhasználó által definiált konvertert kell hívni. Először a tényleges argumentumot kell konvertálni Token típusról SmallInt típusra egy konverter segítségével

Token::operator SmallInt()

majd az eredményt int típusba öntjük – szintén egyedi konverter segítségével

Token::operator int()

A calc(tok) hívását a fordító hibaként jelzi, mert nincs implicit konverzió Token típusról int típusra.

Ha nincs logikai egyezés az átalakító típusa és az osztály típusa között, előfordulhat, hogy a konverter célja nem egyértelmű a program olvasója számára:

Osztály dátuma ( public: // próbáld kitalálni, melyik tag adható vissza! operátor int(); private: int hónap, nap, év; );

Milyen értéket kell visszaadnia a Date osztály int() konverterének? Bármennyire is indokolják ezt vagy azt a döntést, az olvasó tanácstalan lesz a Date osztály objektumainak használatában, mivel nincs nyilvánvaló logikai megfelelés köztük és az egész számok között. Ilyen esetekben jobb, ha egyáltalán nem definiáljuk az átalakítót.

15.9.2. Konstruktor mint átalakító

Osztálykonstruktorok halmaza, amelyek egyetlen paramétert vesznek fel, például a SmallInt osztály SmallInt(int)-je, meghatározza a SmallInt értékekké történő implicit konverziók halmazát. Például a SmallInt(int) konstruktor az int értékeket SmallInt értékekké alakítja.

Extern void calc(SmallInt); int i; // az i-t át kell alakítani SmallInt értékre // ez a SmallInt(int) calc(i) használatával érhető el; A calc(i) meghívásakor az i-t SmallInt értékké alakítja a fordító által meghívott SmallInt(int) konstruktor segítségével a kívánt típusú ideiglenes objektum létrehozásához. Ennek az objektumnak a másolata ezután átadásra kerül a calc()-nak, mintha a függvényhívás a következő formájú lenne: // Pszeudokód C++-ban // hozzon létre egy ideiglenes objektumot SmallInt típusú ( SmallInt temp = SmallInt(i); calc(temp); )

Ebben a példában a kapcsos zárójelek az objektum élettartamát jelölik: a függvény kilépésekor megsemmisül.

A konstruktor paraméter típusa lehet valamelyik osztály típusa:

Class Number ( public: // Number típusú érték létrehozása SmallInt típusú értékből Number(const SmallInt &); // ... );

Ebben az esetben a SmallInt típusú érték mindenhol használható, ahol megengedett a Number típusú érték:

extern void func(Szám); SmallInt si(87); int main() ( // Calling Number(const SmallInt &) func(si); // ... )

Ha egy konstruktort használunk implicit konverzió végrehajtására, akkor a paraméterének típusának pontosan meg kell egyeznie a konvertálandó érték típusával? Például a következő kód meghívná a SmallInt osztályban definiált SmallInt(int) dobj-t egy SmallInt típusba?

Extern void calc(SmallInt); dupla dobj; // a SmallInt(int) hívva van? Igen // a dobj-t double-ról int-re alakítjuk // szabványos konverzióval calc(dobj);

Ha szükséges, a rendszer szabványos konverziók sorozatát alkalmazza az aktuális argumentumra, mielőtt a konstruktort meghívnák a felhasználó által definiált konverzió végrehajtására. A calc() függvény meghívásakor a standard dobj konverziót használjuk double-ről int-re. Ezután a SmallInt(int) meghívásra kerül, hogy az eredményt a SmallInt-be küldje.

A fordító implicit módon egyetlen paraméterrel rendelkező konstruktort használ, hogy a típusát annak az osztálynak a típusára konvertálja, amelyhez a konstruktor tartozik. Néha azonban kényelmesebb, ha a Number(const SmallInt&) konstruktor csak egy Number típusú objektum SmallInt típusú értékkel történő inicializálására hívható meg, és soha nem hajt végre implicit konverziót. A konstruktor ilyen használatának elkerülése érdekében nyilvánítsuk explicitnek:

Osztályszám ( public: // soha ne használjon explicit Number(const SmallInt &); // ... );

A fordító soha nem használ explicit konstruktorokat implicit típuskonverziók végrehajtására:

extern void func(Szám); SmallInt si(87); int main() ( // hiba: nincs implicit konverzió SmallIntről Number func(si); // ... )

Egy ilyen konstruktor azonban továbbra is használható típuskonverzióhoz, ha ezt kifejezetten kérik cast operátor formájában:

SmallInt si(87); int main() ( // hiba: nincs implicit konverzió a SmallIntről a Number funkcióra func(si); func(Number(si)); // helyes: cast func(static_cast< Number >(si)); // helyesen: cast )

15.10. Az átalakítás kiválasztása A

A felhasználó által meghatározott konverzió konverterként vagy konstruktorként valósul meg. Mint már említettük, a konverter által végrehajtott konverzió után megengedett a normál konverzió használata a visszaadott érték céltípusba öntésére. A konstruktor által végrehajtott transzformációt megelőzheti egy szabványos konverzió is, amely az argumentum típusát a konstruktor formális paraméterének típusára adja.

A felhasználó által meghatározott transzformációk sorozata egy kombináció felhasználó által meghatározottés a szabványos konverzió, amely az érték céltípusra való átadásához szükséges. Egy ilyen sorrend így néz ki:

Standard transzformációk sorrendje ->

Felhasználó által meghatározott átalakítás ->

A standard transzformációk sorrendje

ahol a felhasználó által meghatározott konverziót egy konverter vagy egy konstruktor valósítja meg.

Lehetséges, hogy a felhasználó által definiált konverzióknak két különböző sorozata van a forrásérték céltípussá alakításához, majd a fordítónak ezek közül kell kiválasztania a legjobbat. Lássuk, hogyan készült.

Egy osztályban sok konverter definiálható. Például a Number osztályunkban kettő van: az int() operátor és a float() operátor, mindkettő képes egy Number objektumot lebegő értékké alakítani. Természetesen használhatja a Token::operator float() konvertert a közvetlen transzformációhoz. De a Token::operator int() is működik, mert az eredménye int típusú, és ezért a szabványos konverzióval floatlá alakítható. Kétértelmű egy transzformáció, ha több ilyen sorozat van? Vagy az egyik jobb, mint a többi?

Osztályszám ( public: operator float(); operátor int(); // ... ); szám szám; floatff = szám; // melyik konverter? úszó()

Ilyen esetekben a felhasználó által definiált transzformációk legjobb sorozatának kiválasztása az átalakító után alkalmazott átalakítási sorozat elemzésén alapul. Az előző példában a következő két szekvencia alkalmazható:

  1. operátor float() -> pontos egyezés
  2. operátor int() -> standard konverzió

Ahogy a 9.3. szakaszban tárgyaltuk, a pontos egyezés jobb, mint a szabványos transzformáció. Ezért az első sorozat jobb, mint a második, ami azt jelenti, hogy a Token::operator float() konvertert választottuk.

Előfordulhat, hogy két különböző konstruktor használható egy érték konvertálására a céltípusra. Ebben az esetben a konstruktorhívást megelőző szabványos transzformációk sorozatát elemzik:

Class SmallInt ( publikus: SmallInt(int ival) : value(ival) ( ) SmallInt(double dval) : value(static_cast< int >(dwal)); ( ) ); extern void manip(const SmallInt &); int main() ( double dobj; manip(dobj); // helyes: SmallInt(double) )

Itt a SmallInt osztály két konstruktort definiál, a SmallInt(int) és a SmallInt(double), amelyek segítségével a dupla értéket SmallInt objektummá változtathatjuk: a SmallInt(double) a duplát közvetlenül SmallInt objektummá alakítja, míg a SmallInt(int) egy standard double int konvertálás eredményén működik. Így a felhasználó által meghatározott transzformációknak két sorozata van:

  1. pontos egyezés -> SmallInt(dupla)
  2. szabványos konverzió -> SmallInt(int)

Mivel a pontos egyezés jobb, mint a normál konverzió, a SmallInt(double) konstruktort választjuk.

Nem mindig lehet eldönteni, melyik sorozat a jobb. Előfordulhat, hogy mindegyik egyformán jó, ilyenkor azt mondjuk, hogy az átalakulás nem egyértelmű. Ebben az esetben a fordító nem alkalmaz semmilyen implicit transzformációt. Például, ha a Number osztálynak két konvertere van:

Osztályszám ( public: operator float(); operátor int(); // ... );

akkor nem lehet implicit módon egy Number típusú objektumot long típusúvá alakítani. A következő utasítás fordítási hibát okoz, mivel a felhasználó által meghatározott konverziók sorrendje nem egyértelmű:

// hiba: a float() és az int() is használható long lval = num;

A szám hosszú típusú értékké alakításához két ilyen sorozat alkalmazható:

  1. operátor float() -> standard konverzió
  2. operátor int() -> standard konverzió

Mivel mindkét esetben a konverter használatát követi a standard konverzió alkalmazása, mindkét sorozat egyformán jó, és a fordító nem választhatja ki egyiket sem.

Explicit típusú öntéssel a programozó megadhatja a kívánt változtatást:

// helyes: explicit cast long lval = static_cast (szám);

Ennek a specifikációnak az eredményeként a Token::operator int() konverter kerül kiválasztásra, majd a szabványos átalakítás hosszúra.

A transzformációk sorrendjének megválasztásában kétértelműség is felmerülhet, ha két osztály definiál egymásba transzformációt. Például:

Class SmallInt ( public: SmallInt(const Number &); // ... ); osztály Szám ( publikus: operátor SmallInt(); // ... ); extern void compute(SmallInt); külső Szám num; számítás(szám); // hiba: két átalakítás lehetséges

A szám argumentumot a rendszer SmallIntre konvertálja kettővel különböző utak: a SmallInt::SmallInt(const Number&) konstruktor vagy a Number::operator SmallInt() konverter használatával. Mivel mindkét változtatás egyformán jó, a hívás hibának minősül.

A kétértelműség feloldása érdekében a programozó kifejezetten meghívhatja a Számosztály konverterét:

// helyes: az explicit hívás egyértelművé teszi a compute(num.operator SmallInt());

Az explicit öntések azonban nem használhatók a kétértelműségek feloldására, mivel mind az átalakítót, mind a konstruktort figyelembe veszik a típusöntéshez alkalmas konverziók kiválasztásakor:

Számítás(SmallInt(szám)); // hiba: továbbra is kétértelmű

Mint látható, a jelenlét egy nagy szám az ilyen konverterek és konstruktorok nem biztonságosak, így azok. óvatosan kell használni. Korlátozhatja a konstruktorok használatát implicit konverziók végrehajtásakor (és ezáltal csökkentheti a váratlan hatások esélyét), ha explicitté teszi őket.

15.10.1. A funkció túlterhelési felbontásának ismételt megtekintése

A 9. fejezet részletesen leírja a túlterhelt függvényhívás feloldását. Ha a tényleges argumentumok meghívásakor osztály típusúak, osztálytípusra mutató mutató vagy osztálytagokra mutató mutató, akkor több függvény verseng a lehetséges jelöltekért. Ezért az ilyen érvek jelenléte befolyásolja a túlterhelés-feloldási eljárás első lépését - a jelölt függvénykészlet kiválasztását.

Ennek az eljárásnak a harmadik lépésében kiválasztják a legjobb egyezést. Ebben az esetben a tényleges argumentumok típusainak a függvény formális paramétereinek típusaira való konvertálása rangsorolódik. Ha az argumentumok és paraméterek osztály típusúak, akkor a lehetséges konverziók halmazának tartalmaznia kell a felhasználó által definiált konverziók sorozatait is, amelyek szintén rangsorolandóak.

Ebben a részben közelebbről megvizsgáljuk, hogy a tényleges argumentumok és a formális osztálytípus-paraméterek hogyan befolyásolják a jelölt függvények kiválasztását, és hogyan befolyásolják a felhasználó által definiált konverziók sorozata a legjobban létrehozott függvény kiválasztását.

15.10.2. Jelölt funkciók

A jelölt függvény olyan függvény, amelynek a neve megegyezik a meghívottakkal. Tegyük fel, hogy van egy ilyen hívásunk:

SmallInt si(15); add(si, 566);

A jelölt függvénynek add nevet kell adni. Az add() deklarációk közül melyiket veszik figyelembe? Azok, amelyek a jelzési ponton láthatók.

Például a globális hatókörben deklarált mindkét add() függvény alkalmas lehet a következő hívásra:

const mátrix& add(const matrix &, int); double add(double, double); int main() ( SmallInt si(15); add(si, 566); // ... )

Azon függvények figyelembevétele, amelyek deklarációi a hívási ponton láthatók, nem korlátozódnak az osztály típusú argumentumokkal rendelkező hívásokra. Számukra azonban a nyilatkozatok keresése további két körben történik:

  • ha a tényleges argumentum egy osztálytípus objektuma, egy mutató vagy hivatkozás egy osztálytípusra, vagy egy mutató egy osztálytagra, és ez a típus egy felhasználó által definiált névtérben van deklarálva, akkor az adott térben deklarált függvények ugyanazt a nevet adják hozzá a jelölt függvénykészlethez, és hívják:
névtér NS ( class SmallInt ( /* ... */ ); class String ( /* ... */ ); String add(const String &, const String &); ) int main() ( // si az típus osztály SmallInt: // az osztály az NS névtérben van deklarálva NS::SmallInt si(15); add(si, 566); // Az NS::add() egy jelölt függvény, return 0; )

Az si argumentum SmallInt típusú, azaz. az NS névtérben deklarált osztály típusa. Ezért az ebben a névtérben deklarált add(const String &, const String &) hozzáadódik a jelölt függvények halmazához;

  • ha a tényleges argumentum egy osztály típusú objektum, egy mutató vagy hivatkozás egy osztályra, vagy egy mutató egy osztálytagra, és az osztálynak vannak barátai, akiknek ugyanaz a neve, mint a hívott függvénynek, akkor hozzáadódnak a halmazhoz jelölt funkciók közül:
  • névtér NS ( osztály SmallInt ( barát SmallInt add(SmallInt, int) ( /* ... */ ) ); ) int main() ( NS::SmallInt si(15); add(si, 566); // függvény - barát hozzáadása() - jelölt visszaadása 0; )

    Az si függvény argumentuma SmallInt típusú. A SmallInt osztály barát függvénye add(SmallInt, int) az NS névtér tagja, bár nincs közvetlenül deklarálva ezen a téren. Az NS normál keresése nem talál barát funkciót. Ha azonban az add() függvényt SmallInt osztály típusú argumentummal hívjuk meg, akkor az osztály taglistájában deklarált barátai is figyelembe lesznek véve, és hozzáadódnak a jelölthalmazhoz.

    Így, ha a tényleges függvényargumentumok listája tartalmaz egy objektumot, egy mutatót vagy hivatkozást egy osztályra, és mutat az osztálytagokra, akkor a jelölt függvények halmaza a hívás helyén látható, vagy a programban deklarált függvények halmazából áll. ugyanaz a névtér, ahol definiálva van. osztálytípus, vagy ennek az osztálynak deklarált barátai.

    Tekintsük a következő példát:

    Névtér NS ( Class SmallInt ( barát SmallInt add(SmallInt, int) ( /* ... */ ) ); class String ( /* ... */ ); String add(const String &, const String &); ) const mátrix& add(const matrix &, int); double add(double, double); int main() ( // si típusú Class SmallInt: // az osztály az NS névtérben van deklarálva NS::SmallInt si(15); add(si, 566); // a barát függvényt return 0-nak hívják; )

    Íme a jelöltek:

    • globális funkciók:
    const mátrix& add(const mátrix &, int) double add(double, double)
  • függvény névtérből:
  • NS::add(const String &, const String &)
  • barát funkció:
  • NS::add(SmallInt, int)

    A túlterhelési megoldás az NS::add(SmallInt, int) SmallInt osztály barátfüggvényét választja a legjobb illeszkedésnek: mindkét tényleges argumentum pontosan megegyezik a megadott formális paraméterekkel.

    Természetesen a meghívott függvénynek több osztálytípus argumentuma is lehet, egy osztályra mutató mutató vagy hivatkozás, vagy egy osztálytag mutatója. Mindegyik argumentumhoz különböző osztálytípusok engedélyezettek. A jelölt függvények keresése abban a névtérben történik, ahol az osztály definiálva van, valamint az osztály barát függvényei között. Ezért az eredményül kapott jelölthalmaz egy függvény ilyen argumentumokkal történő meghívására különböző névterekből származó függvényeket és különböző osztályokban deklarált barátfüggvényeket tartalmaz.

    15.10.3. Jelölt függvények osztály hatókörében lévő függvények meghívásához

    Az űrlap függvényének meghívásakor

    osztály hatókörében fordul elő (például egy tagfüggvényen belül), akkor az előző alfejezetben leírt jelölthalmaz első része (azaz a hívásponton látható függvénydeklarációkat tartalmazó halmaz) többet is tartalmazhat, mint csak az osztály tagfüggvényei. Egy ilyen halmaz felépítéséhez névfelbontást használnak. (Ezt a témát részletesen tárgyaltuk a 13.9-13.12 fejezetekben.)

    Vegyünk egy példát:

    Névtér NS ( struct myClass ( void k(int); static void k(char*); void mf(); ); int k(double); ); void h(char); void NS::myClass::mf() ( h("a"); // globális h(char) k(4) hívása; // myClass::k(int) hívása)

    A 13.11. szakaszban leírtak szerint az NS::myClass:: minősítőket fordított sorrendben keresi meg: először az mf() tagfüggvény definíciójában használt név látható deklarációját keresi meg a myClass osztályban, majd az NS-ben. névtér. Fontolja meg az első hívást:

    A h() név feloldásakor az mf() tagfüggvény definíciójában először a myClass tagfüggvényeket keresi meg. Mivel ennek az osztálynak a hatókörében nincs ilyen nevű tagfüggvény, így a keresés az NS névtérben megy tovább. A h() függvény sincs benne, ezért áttérünk a globális hatókörre. Az eredmény a h(char) globális függvény, az egyetlen jelölt függvény, amely a hívás helyén látható.

    Amint talál egy megfelelő hirdetést, a keresés leáll. Ezért a készlet csak azokat a függvényeket tartalmazza, amelyek deklarációi olyan hatókörben vannak, ahol a névfeloldás sikerült. Ez látható a hívásra jelöltek halmazának felépítésének példáján

    Először is, a keresés a myClass osztály hatókörében történik. Ez talált két tagfüggvényt, k(int) és k(char*). Mivel a jelölthalmaz csak azon hatókörben deklarált függvényeket tartalmazza, ahol a feloldás sikeres volt, az NS névteret nem keresi a rendszer, és a k(double) függvény nem szerepel ebben a halmazban.

    Ha a hívást nem egyértelműnek találjuk, mert nincs a legjobban illeszkedő függvény a halmazban, akkor a fordító hibaüzenetet ad ki. Azok a jelöltek, amelyek jobban megfelelnek a tényleges érveknek, nem a befoglaló hatókörben keresendők.

    15.10.4. A felhasználó által definiált transzformációk rangsorolási sorozatai

    Egy tényleges függvényargumentum implicit módon átadható egy formális paraméter típusára a felhasználó által definiált konverziók sorozatával. Hogyan befolyásolja ez a túlterhelés felbontást? Például, ha van egy következő calc() hívás, akkor milyen függvény lesz meghívva?

    Class SmallInt ( nyilvános: SmallInt(int); ); extern void calc(double); extern void calc(SmallInt); int ival; int main() ( calc(ival); // melyik calc()-t hívjuk? )

    Az a függvény kerül kiválasztásra, amelynek formális paraméterei a legjobban egyeznek a tényleges argumentumok típusával. Ezt nevezik a legjobb illeszkedésnek vagy a legjobb állóképességnek. Egy ilyen függvény kiválasztásához a tényleges argumentumokra alkalmazott implicit konverziókat rangsorolja. A legjobban az marad fenn, amelynél az argumentumokra alkalmazott változtatások nem rosszabbak, mint bármely más túlélő függvénynél, és legalább egy argumentumnál jobbak, mint az összes többi függvénynél.

    A szabványos konverziók sorozata mindig jobb, mint a felhasználó által meghatározott konverziók sorozata. Így a fenti példából a calc() függvény meghívásakor mindkét calc() függvény jól bevált. A calc(double) azért maradt fenn, mert létezik egy szabványos konverzió az int tényleges argumentumból a double formális paramétertípusba, a calc(SmallInt) pedig azért, mert van egy felhasználó által definiált konverzió intről SmallIntre, amely a SmallInt(int) konstruktort használja. Ezért a legjobb állófüggvény a calc(double).

    Hogyan hasonlítható össze a felhasználó által definiált transzformációk két sorozata? Ha különböző konvertereket vagy különböző konstruktorokat használnak, akkor mindkét ilyen sorozat egyformán jónak tekinthető:

    Osztályszám ( nyilvános: operátor SmallInt(); operátor int(); // ... ); extern void calc(int); extern void calc(SmallInt); külső Szám num; calc(szám); // hiba: kétértelműség

    A calc(int) és a calc(SmallInt) is fennmarad; az első azért van, mert a Number::operator int() konverter a tényleges szám típusú argumentumot formális int típusú paraméterré alakítja, a második pedig azért, mert a Number::operator SmallInt() konverter egy tényleges szám típusú argumentumot formális SmallInt típussá alakít. paraméter. Mivel a felhasználó által definiált konverziók sorozatának mindig ugyanaz a rangja, a fordító nem tudja eldönteni, melyik a jobb. Így ez a függvényhívás nem egyértelmű, és fordítási hibát eredményez.

    Van mód a kétértelműség feloldására a konverzió kifejezett megadásával:

    // Az explicit casting egyértelművé teszi a calc(static_cast< int >(szám));

    Az explicit cast hatására a fordító a Number::operator int() konverter segítségével int-vé alakítja a num argumentumot. A tényleges argumentum ekkor int típusú lesz, ami pontosan megegyezik a legjobbnak választott calc(int) függvényrel.

    Tegyük fel, hogy a Number::operator int() konverter nincs megadva a Number osztályban. Akkor lesz kihívás

    // csak Number::operator SmallInt() definiálva calc(num); // még mindig kétértelmű?

    még mindig kétértelmű? Emlékezzünk vissza, hogy a SmallIntnek van egy átalakítója is, amely képes a SmallInt értéket int értékké alakítani.

    Class SmallInt ( public: operator int(); // ... );

    Feltételezhetjük, hogy a calc() függvény meghívása úgy történik, hogy a Number::operator SmallInt() konverter segítségével először a tényleges num argumentumot Number típusról SmallInt típusra konvertáljuk, majd az eredményt a SmallInt::operator SmallInt() segítségével int-be öntjük. . Azonban nem. Emlékezzünk vissza, hogy a felhasználó által definiált átalakítások sorozata több szabványos átalakítást is tartalmazhat, de csak egyet. Ha a Number::operator int() konverter nincs megadva, akkor a calc(int) függvény nem tekinthető stabilnak, mert nincs implicit konverzió a tényleges num argumentum típusáról az int formális paraméter típusára.

    Ezért a Number::operator int() konverter hiányában az egyetlen megmaradt függvény a calc(SmallInt), amely javára engedélyezett a hívás.

    Ha a felhasználó által meghatározott konverziók két sorozata ugyanazt a konvertert használja, akkor a legjobb választása a hívás után végrehajtott szabványos konverziók sorrendjétől függ:

    Class SmallInt ( public: operator int(); // ... ); void manip(int); void manip(char); SmallInt si(68); main() ( manip(si); // a manip(int) hívása)

    A manip(int) és a manip(char) is beállított függvények; az első azért van, mert a SmallInt::operator int() konverter a SmallInt típusú tényleges argumentumot az int formális paraméter típusává alakítja, a második pedig azért, mert ugyanaz a konverter konvertálja a SmallInt int-re, ami után az eredményt a rendszer char-ba önti. szabványos átalakítást használva. A felhasználó által definiált transzformációk sorozata így néz ki:

    Manip(int) : operátor int()->pontos egyezés manip(int) : operátor int()->standard konverzió

    Mivel mindkét szekvenciában ugyanazt az átalakítót használjuk, a standard transzformációk sorozatának rangját elemezzük, hogy meghatározzuk a legjobbat. Mivel a pontos egyezés jobb, mint a konverzió, a manip(int) a legjobban megalapozott függvény.

    Hangsúlyozzuk, hogy egy ilyen kiválasztási kritérium csak akkor fogadható el, ha ugyanazt az átalakítót használjuk a felhasználó által definiált transzformációk mindkét sorozatában. Itt tér el a példánk a 15.9. szakasz végén találhatóaktól, ahol bemutattuk, hogy a fordító hogyan választja ki a felhasználó által meghatározott konverziót valamilyen értékről egy adott céltípusra: a forrás és a cél típusa rögzített volt, és a fordítónak választania kellett a felhasználó által meghatározott konverziók között az egyik típusról a másikra. Itt két különböző függvényt veszünk figyelembe, különböző típusú formális paraméterekkel, és a céltípusok eltérőek. Ha kettőre különböző típusok A paraméterek különböző, felhasználó által definiált konverziókat igényelnek, csak akkor lehet az egyik típust előnyben részesíteni a másikkal szemben, ha mindkét sorozatban ugyanazt az átalakítót használjuk. Ha nem, akkor a konverter alkalmazását követő szabványos konverziókat értékelik a legjobb céltípus kiválasztásához. Például:

    Class SmallInt ( public: operator int(); operátor float(); // ... ); void compute(float); void compute(char); SmallInt si(68); main() ( compute(si); // kétértelműség )

    A compute(float) és a compute(int) egyaránt beállított függvények. A compute(float) azért van, mert a SmallInt::operator float() konverter a SmallInt típusú argumentumot float paramétertípussá alakítja, a compute(char) pedig azért, mert a SmallInt::operator int() egy SmallInt típusú argumentumot int típussá alakít, miután így az eredményt szabványosan a char típusra öntjük. Tehát vannak sorozatok:

    Compute(float) : operátor float()->pontos egyezés compute(char) : operátor char()->standard konverzió

    Mivel különböző konvertereket használnak, lehetetlen meghatározni, hogy melyik függvénynek vannak olyan formális paraméterei, amelyek jobban illeszkednek a híváshoz. A kettő közül a legjobb kiválasztásához a standard transzformációk sorozatának rangját nem használjuk. A hívást a fordító félreérthetőnek jelöli.

    15.12. gyakorlat

    A C++ Standard Library osztályokban nincsenek konverterdefiníciók, és a legtöbb, egy paramétert felvevő konstruktor explicitnek van deklarálva. Azonban sok túlterhelt operátor van meghatározva. Ön szerint miért született ez a döntés a tervezés során?

    15.13. gyakorlat

    A szakasz elején definiált SmallInt osztály túlterhelt bemeneti operátora miért nincs így implementálva:

    Istream& operátor>>(istream &is, SmallInt &si) ( return (is >> is.value); )

    15.14. gyakorlat

    Adja meg a felhasználó által definiált konverziók lehetséges sorozatait a következő inicializálásokhoz. Mi lesz az egyes inicializálás eredménye?

    Class LongDouble ( operátor double(); operátor float(); ); külső LongDouble ldObj; (a) int ex1 = ldObj; (b) úszó ex2 = ldObj;

    15.15. gyakorlat

    Nevezzen meg három olyan jelölt függvénykészletet, amelyeket a rendszer figyelembe vesz a függvénytúlterhelés feloldásakor, ha legalább egy argumentuma osztálytípusú.

    15.16. gyakorlat

    Ebben az esetben a calc() függvények közül melyik a legjobb? Mutassa be az egyes függvények meghívásához szükséges transzformációk sorozatát, és magyarázza el, miért jobb az egyik, mint a másik.

    Class LongDouble( public: LongDouble(double); // ... ); extern void calc(int); extern void calc(LongDouble); dupla dval; int main() ( calc(dval); // milyen függvény? )

    15.11. Túlterhelési megoldás és tagfunkciók A

    A tagfüggvények is túlterhelhetők, ebben az esetben is a túlterhelés-feloldási eljárást alkalmazzuk, hogy kiválasszuk a legmegfelelőbbet. Ez a felbontás nagyon hasonlít a normál funkciókhoz, és ugyanabból a három lépésből áll:

    1. A jelölt funkciók kiválasztása.
    2. A kialakított funkciók kiválasztása.

    A jelöltek halmazának generálására és a stabil tagfüggvények kiválasztására szolgáló algoritmusok között azonban vannak kisebb eltérések. Ebben a részben ezeket a különbségeket fogjuk megvizsgálni.

    15.11.1. Túlterhelt tagfüggvény-deklarációk

    Az osztálytagok funkciói túlterhelhetők:

    Class myClass ( public: void f(double); char f(char, char); // túlterheli a myClass::f(double) // ... );

    A névtérben deklarált függvényekhez hasonlóan a tagfüggvényeknek is lehet ugyanaz a neve, feltéve, hogy a paraméterlistáik eltérőek akár a paraméterek számában, akár típusaiban. Ha két tagfüggvény deklarációja csak a visszatérési típusban tér el, akkor a második deklaráció fordítási hibának minősül:

    Class myClass ( public: void mf(); double mf(); // hiba: ezt nem lehet túlterhelni // ... );

    A névterekben lévő függvényekkel ellentétben a tagfüggvényeket csak egyszer kell deklarálni. Még ha két tagfüggvény visszatérési típusa és paraméterlistája megegyezik, a fordító a második deklarációt érvénytelen újradeklarációként értelmezi:

    Osztály myClass ( public: void mf(); void mf(); // hiba: újradeklaráció // ... );

    Minden túlterhelt funkciót ugyanabban a hatókörben kell deklarálni. Ezért a tagfüggvények soha nem terhelik túl a névtérben deklarált függvényeket. Továbbá, mivel minden osztálynak saját hatóköre van, a különböző osztályokhoz tartozó függvények nem terhelik túl egymást.

    A túlterhelt tagfüggvények készlete tartalmazhat statikus és nem statikus függvényeket is:

    Class myClass ( public: void mcf(double); static void mcf(int*); // túlterheli a myClass::mcf(double) // ... );

    A túlterhelés-feloldás eredményétől függ, hogy a tagfüggvények közül melyiket hívjuk meg – statikus vagy nem statikus –. A feloldási folyamatot egy olyan helyzetben, ahol statikus és nem statikus elemek is fennmaradtak, a következő részben tárgyaljuk részletesen.

    15.11.2. Jelölt funkciók

    Tekintsünk kétféle tagfüggvényhívást:

    Mc.mf(arg); pmc->mf(arg);

    ahol mc a myClass típusú kifejezés, a pmc pedig a "pointer to type myClass" típusú kifejezés. Mindkét hívás jelöltkészlete a myClass hatókörében található függvényekből áll, amikor mf() deklarációt keresünk.

    Hasonlóképpen az űrlap függvényének meghívásához

    MyClass::mf(arg);

    a jelöltek halmaza az mf() deklaráció keresésekor a myClass osztály hatókörében található függvényekből is áll. Például:

    Class myClass ( public: void mf(double); void mf(char, char = "\n"); static void mf(int*); // ... ); int main() ( myClass mc; int iobj; mc.mf(iobj); )

    A main() függvényhívási jelöltek mindhárom mf() tagfüggvény, amelyek a myClass-ban deklarálva vannak:

    Void mf(double); void mf(char, char = "\n"); static void mf(int*);

    Ha nincs mf() nevű tagfüggvény deklarálva a myClass-ban, akkor a jelöltek halmaza üres lenne. (Tulajdonképpen az alaposztályokból származó függvények is számításba jöhetnek. A 19.3. szakaszban megvitatjuk, hogyan esnek ebbe a halmazba.) Ha a függvényhívásra nincs jelölt, a fordító hibaüzenetet ad ki.

    15.11.3. Kialakult jellemzők

    A jól bevált függvény a jelöltek halmazából származó függvény, amely az adott tényleges argumentumokkal hívható meg. Ahhoz, hogy fennmaradjon, implicit konverziókra van szükség a tényleges argumentumok típusai és a formális paraméterek között. Például: class myClass ( public: void mf(double); void mf(char, char = "\n"); static void mf(int*); // ... ); int main() ( myClass mc; int iobj; mc.mf(iobj); // melyik mf() tagfüggvény? Kétértelmű )

    Ebben a kódrészletben két jól bevált függvény található az mf() meghívására a main()-ból:

    Void mf(double); void mf(char, char = "\n");

    • Az mf(double) megmaradt, mert csak egy paramétere van, és az iobj int argumentum szabványos átalakítása kettős paraméterré történik;
    • Az mf(char,char) azért maradt fenn, mert van egy alapértelmezett érték a második paraméterhez, és az iobj int argumentum szabványos átalakítása az első formális paraméter char típusává.

    Az egyes tényleges argumentumokra alkalmazott jól bevált típuskonverziós függvények közül a legjobb kiválasztásakor ezek rangsorolódnak. A legjobb az, amelyre az összes használt transzformáció nem rosszabb, mint bármely más jól bevált függvénynél, és legalább egy argumentum esetében egy ilyen transzformáció jobb, mint az összes többi függvény esetében.

    Az előző példában a két beállított függvény mindegyike szabványos konverziót használ az aktuális argumentum típusának a formális paraméter típusára történő átadásához. A hívás nem egyértelmű, mert mindkét tagfüggvény egyformán jól oldja meg.

    Függetlenül a függvényhívás típusától, statikus és nem statikus tagok is szerepelhetnek a fennmaradó tagok halmazában:

    Class myClass ( publikus: static void mf(int); char mf(char); ); int main() ( char cobj; myClass::mf(cobj); // melyik tagfüggvény? )

    Itt az mf() tagfüggvényt az osztálynévvel és a myClass::mf() hatókör-feloldó operátorral hívjuk meg. Azonban sem objektum ("pont" operátorral), sem mutató ("nyíl" operátorral) nincs megadva. Ennek ellenére az mf(char) nem statikus tagfüggvény továbbra is benne van a túlélő halmazban az mf(int) statikus taggal együtt.

    A túlterhelés-feloldási folyamat ezután folytatódik: a tényleges argumentumokra alkalmazott típuskonverziók rangsorolása alapján a legjobb állapotú függvény kiválasztása. A char típusú cobj argumentum pontosan megfelel az mf(char) formális paraméternek, és kiterjeszthető az mf(int) formális paraméter típusára. Mivel a pontos egyezés rangja magasabb, az mf(char) függvényt választjuk.

    Ez a tagfüggvény azonban nem statikus, ezért csak egy objektumon vagy egy myClass osztályú objektumra mutató mutatón keresztül hívható meg az egyik hozzáférési eszköz használatával. Ilyen helyzetben, ha az objektum nincs megadva, és ezért a függvényhívás lehetetlen (csak a mi esetünkben), a fordító hibának tekinti.

    A tagfüggvények másik jellemzője, amelyet figyelembe kell venni a létrehozott függvények halmazának kialakításakor, a const vagy volatile specifikációk jelenléte a nem statikus tagokon. (A 13.3. fejezetben tárgyaltuk őket.) Hogyan befolyásolják a túlterhelés megszüntetésének folyamatát? Legyen a myClass osztály a következő tagfüggvényekkel:

    Class myClass ( publikus: static void mf(int*); void mf(double); void mf(int) const; // ... );

    Ekkor az mf(int*) statikus tagfüggvény, az mf(int) konstans függvény és a nem állandó mf(double) függvény egyaránt szerepel az alább látható hívás jelölthalmazában. De melyikük kerül be a túlélők sorába?

    int main() ( const myClass mc; double dobj; mc.mf(dobj); // melyik mf() tagfüggvény? )

    A tényleges argumentumokra alkalmazandó transzformációk vizsgálata során azt találjuk, hogy az mf(double) és mf(int) függvények fennmaradtak. A dobj tényleges argumentum double típusa pontosan megegyezik az mf(double) formális paraméter típusával, és szabványos konverzióval átadható az mf(int) paraméter típusához.

    Ha egy tagfüggvényhívás pont vagy nyíl hozzáférési operátorokat használ, akkor az objektum vagy mutató típusát, amelyre a függvényt hívják, a program figyelembe veszi a függvények kiválasztásakor a stagnálók halmazába.

    Az mc egy const objektum, amelyen csak nem statikus const tagfüggvények hívhatók meg. Ezért az mf(double) nem állandó tagú függvényt kizárjuk a túlélők halmazából, és benne marad az egyetlen mf(int) függvény, amelyet hívunk.

    Mi van, ha egy const objektumot használunk egy statikus tagfüggvény meghívására? Hiszen egy ilyen függvénynél lehetetlen beállítani a const vagy a volatile specifikátort, tehát meghívható egy const objektumon keresztül?

    Class myClass ( publikus: static void mf(int); char mf(char); ); int main() ( const myClass mc; int iobj; mc.mf(iobj); // hívható-e statikus tagfüggvény? )

    A statikus tagfüggvények ugyanazon osztály minden objektumánál közösek. Csak az osztály statikus tagjaihoz férhetnek hozzá közvetlenül. Így az mc konstans objektum nem statikus tagjai nem elérhetők a statikus mf(int) számára. Emiatt megengedett egy statikus tagfüggvény meghívása egy const objektumon a pont vagy nyíl operátorok használatával.

    Így a statikus tagfüggvények nincsenek kizárva a túlélők halmazából, még akkor sem, ha vannak const vagy volatile specifikációk azon az objektumon, amelyen meghívásra kerültek. A statikus tagfüggvényeket úgy kezeljük, mint amelyek megfelelnek az osztályuk bármely objektumára vagy mutatójára.

    A fenti példában az mc egy const objektum, így az mf(char) tagfüggvény ki van zárva a túlélők halmazából. De az mf(int) tagfüggvény benne marad, mert statikus. Mivel ez az egyetlen stabil funkció, ez bizonyul a legjobbnak.

    15.12. Túlterhelési felbontás és A kezelők

    Az osztályok deklarálhatják a túlterhelt operátorokat és konvertereket. Tegyük fel, hogy az inicializálás során egy addíciós operátor találkozott:

    SomeClass sc; int iobj = sc + 3;

    Hogyan dönti el a fordító, hogy meghívja-e a túlterhelt operátort a SomeClass-on, vagy átalakítja az sc operandust beépített típusra, majd a beépített operátort használja?

    A válasz a SomeClass-ban meghatározott sok túlterhelt operátortól és konvertertől függ. Amikor kiválaszt egy operátort a hozzáadáshoz, a funkció túlterhelés-feloldási folyamata kerül alkalmazásra. BAN BEN ez a szekció leírjuk, hogy ez a folyamat hogyan teszi lehetővé a kívánt operátor kiválasztását, ha az operandusok osztály típusú objektumok.

    A túlterhelés-feloldás ugyanazt a háromlépéses eljárást használja, amelyet a 9.2. szakasz ismertet:

    • A jelölt funkciók kiválasztása.
    • A kialakított funkciók kiválasztása.
    • A beállított funkciók közül a legjobbak kiválasztása.
    • Nézzük meg ezeket a lépéseket részletesebben.

      A funkció túlterhelési felbontása nem érvényes, ha minden operandus beépített típusú. Ebben az esetben a beépített kezelő garantáltan használható lesz. (A beépített típusú operandusokkal rendelkező operátorok használatával a 4. fejezet foglalkozik.) Például:

    osztály SmallInt ( publikus: SmallInt(int); ); SmallInt operátor+ (const SmallInt &, const SmallInt &); void func() ( int i1, i2; int i3 = i1 + i2; )

    Mivel az i1 és i2 operandusok int típusúak, nem pedig osztály típusúak, a beépített + operátor is használatos. A túlterhelt operátor+(const SmallInt &, const SmallInt &) figyelmen kívül marad, bár az operandusok a SmallInt(int) konstruktor formájában, a felhasználó által definiált konverzióval átküldhetők a SmallIntbe. Az alábbiakban ismertetett túlterhelés-feloldási eljárás ezekben a helyzetekben nem alkalmazható.

    Ezenkívül az operátorok túlterhelési felbontása csak az operátori szintaxis használatakor használható:

    Void func() ( SmallInt si(98); int iobj = 65; int res = si + iobj; // operátor szintaxisa használt)

    Ha ehelyett a függvényhívás szintaxisát használjuk: int res = operator+(si, iobj); // függvényhívás szintaxisa

    akkor a névtér függvényeinek túlterhelés-feloldási eljárása érvényes (lásd: 15.10. szakasz). Ha a tagfüggvény meghívásának szintaxisát használjuk:

    // tagfüggvényhívás szintaxisa int res = si.operator+(iobj);

    akkor a tagfüggvényekre vonatkozó megfelelő eljárás működik (lásd 15.11. szakasz).

    15.12.1. Operátorjelölt funkciók

    Az operátorfüggvény akkor jelölt, ha megegyezik a meghívott névvel. A következő addíciós operátor használatakor

    SmallInt si(98); intiobj = 65; int res = si + iobj;

    az operátorjelölt függvény operátor+. Mely operátor+ nyilatkozatokat veszik figyelembe?

    Az operátor szintaxis osztály típusú operandusokkal való használata esetén potenciálisan öt jelölt halmaz készül. Az első három ugyanaz, mint a szabályos függvények osztály típusú argumentumokkal történő meghívásakor:

    • a hívóponton látható operátorok halmaza. Az operátor használatának helyén látható operátor+() függvénydeklarációk jelöltek. Például a globális hatókörben deklarált operátor+() jelölt, ha az operátor+() szerepel a main()-on belül:
    SmallInt operátor+ (const SmallInt &, const SmallInt &); int main() ( SmallInt si(98); int iobj = 65; int res = si + iobj; // ::operator+() egy függvényjelölt)
  • a névtérben deklarált operátorok halmaza, amelyben az operandus típusa definiálva van. Ha az operandus osztálytípusú, és ez a típus egy felhasználó által definiált névtérben van deklarálva, akkor az ugyanabban a térben deklarált és a használt operátorral azonos nevű operátorfüggvények jelöltnek számítanak:
  • névtér NS ( osztály SmallInt ( /* ... */ ); SmallInt operátor+ (const SmallInt&, double); ) int main() ( // si SmallInt típusú: // ez az osztály az NS névtérben van deklarálva NS: :SmallInt si(15); // NS::operator+() - jelölt függvény int res = si + 566; return 0; )

    Az si operandus SmallInt osztálytípusú, az NS névtérben deklarálva. Ezért az ugyanabban a térben deklarált túlterhelt operátor+(const SmallInt, double) hozzáadódik a jelölthalmazhoz;

  • operátorok halmaza, akik barátokká nyilvánították azokat az osztályokat, amelyekhez az operandusok tartoznak. Ha az operandus egy osztálytípushoz tartozik, és ennek az osztálynak a definíciójában vannak az alkalmazott operátorral azonos nevű barátfüggvények, akkor ezek hozzáadódnak a jelöltek halmazához:
  • névtér NS ( osztály SmallInt ( barát SmallInt operátor+(const SmallInt&, int) ( /* ... */ ) ); ) int main() ( NS::SmallInt si(15); // barát függvény operátor+() - jelölt int res = si + 566; return 0; )

    Az si operandus SmallInt típusú. Az operator+(const SmallInt&, int) operátorfüggvény, amely ennek az osztálynak a barátja, az NS névtér tagja, bár nincs közvetlenül deklarálva ebben a térben. Az NS normál keresése nem találja meg ezt az operátori funkciót. Ha azonban az operátor+() paramétert SmallInt típusú argumentummal együtt használja, akkor az adott osztály hatókörében deklarált barát függvények beleszámítanak a mérlegelésbe, és hozzáadódnak a jelöltek halmazához. A jelölt operátorfüggvények három halmaza ugyanúgy jön létre, mint az osztály típusú argumentumokkal rendelkező normál függvényhívásoknál. Operátori szintaxis használatakor azonban két további halmaz épül fel:

    • a bal oldali operandus osztályában deklarált tagoperátorok halmaza. Ha az operátor+() ilyen operandusa osztálytípussal rendelkezik, akkor az adott osztályhoz tartozó operátor+() deklarációk szerepelnek a jelölt függvények halmazában:
    osztály myFloat ( myFloat(double); ); osztály SmallInt ( nyilvános: SmallInt(int); SmallInt operátor+ (const myFloat &;); int main() ( SmallInt si(15); int res = si + 5,66; // tagjelölt operátor+() )

    A SmallInt-ben definiált SmallInt::operator+(const myFloat &) tagoperátor szerepel egy sor olyan jelölt függvényben, amely az operator+() hívást a main()-ban megoldja;

  • sok beépített operátor. Tekintettel a beépített+() operátorral használható típusokra, a jelöltek a következők is:
  • int operátor+(int, int); kettős operátor+(dupla, kettős); T* operátor+(T*, I); T* operátor+(I, T*);

    Az első deklaráció a beépített operátorra vonatkozik két egész típusú érték hozzáadására, a második a lebegőpontos típusú értékek hozzáadására szolgáló operátorra. A harmadik és a negyedik a beépített mutató típusú összeadás operátornak felel meg, amely egész szám mutatóhoz való hozzáadására szolgál. Az utolsó két deklaráció szimbolikus, és a beépített operátorok egész családját írja le, amelyeket a fordító kiválaszthat jelöltként az összeadási műveletek feldolgozása során.

    Az első négy készlet bármelyike ​​üres lehet. Például, ha a SmallInt osztály tagjai között nincs operator+() nevű függvény, akkor a negyedik halmaz üres lesz.

    A jelölt operátorfüggvények teljes halmaza a fent leírt öt részhalmaz egyesítése:

    Névtér NS ( class myFloat ( myFloat(double); ); osztály SmallInt ( barát SmallInt operátor+(const SmallInt &, int) ( /* ... */ ) public: SmallInt(int); operátor int(); SmallInt operátor+ ( const myFloat &); // ... ); SmallInt operátor+ (const SmallInt &, double); ) int main() ( // si típus - osztály SmallInt: // Ez az osztály az NS névtérben van deklarálva NS::SmallInt si (15); int res = si + 5,66; // melyik operátor+()? return 0; )

    Ez az öt halmaz hét operátor-jelölt függvényt tartalmaz a main() operátor+() szerepére:

      az első készlet üres. A globális hatókörben, nevezetesen abban, ahol az operátor+() szerepel a main() függvényben, nincsenek deklarációk a túlterhelt operátor+() operátorról;
    • a második halmaz az NS névtérben deklarált operátorokat tartalmazza, ahol a SmallInt osztály definiálva van. Ezen a területen egy operátor található: NS::SmallInt NS::operator+(const SmallInt &, double);
    • a harmadik halmaz a SmallInt osztály barátainak nyilvánított operátorokat tartalmazza. Ide tartozik az NS::SmallInt NS::operator+(const SmallInt &, int);
    • a negyedik halmaz a SmallInt tagjaként deklarált operátorokat tartalmazza. Van még egy: NS::SmallInt NS::SmallInt::operator+(const myFloat &);
    • az ötödik készlet beépített bináris operátorokat tartalmaz:
    int operátor+(int, int); kettős operátor+(dupla, kettős); T* operátor+(T*, I); T* operátor+(I, T*);

    Igen, egy operátor szintaxissal használt operátor feloldásához jelölt készlet létrehozása unalmas. De a megalkotása után a kiválasztott jelöltek operandusaira alkalmazott transzformációk elemzésével, mint korábban, megtalálják a tartós függvényeket és azok legjobbjait.

    15.12.2. Kialakult jellemzők

    A beállított operátorfüggvények halmaza a jelöltek halmazából úgy alakul ki, hogy csak azokat az operátorokat választjuk ki, amelyek az adott operandusokkal hívhatók. Például a fent talált hét jelölt közül melyik áll fel? Az operátor a következő összefüggésben használatos:

    NS::SmallInt si(15); si + 5,66;

    A bal oldali operandus SmallInt típusú, a jobb oldali pedig dupla.

    Az első jelölt egy jól bevált funkció ezt a használatot operátor+():

    A SmallInt típusú bal oldali operandus inicializálóként pontosan megegyezik a túlterhelt operátor formális referenciaparaméterével. A jobb oldali, amely double típusú, szintén pontosan megegyezik a második formális paraméterrel.

    A következő jelölt tisztség is megállja a helyét:

    NS::SmallInt NS::operator+(const SmallInt &, int);

    A SmallInt típusú bal oldali operandus si inicializálóként pontosan megegyezik a túlterhelt operátor formális referenciaparaméterével. A jobb oldali int típusú, és a szabványos konverzióval a második formális paraméter típusába önthető.

    Egy harmadik jelölt funkció is megállja a helyét:

    NS::SmallInt NS::SmallInt::operator+(const myFloat &);

    A bal oldali operandus si SmallInt típusú, azaz. annak az osztálynak a típusa, amelynek a túlterhelt operátor tagja. A jobb oldali int típusú, és a myFloat osztálytípusba kerül a myFloat(double) konstruktor formájú, felhasználó által definiált konverzió segítségével.

    A negyedik és ötödik beállított függvény a beépített operátorok:

    int operátor+(int, int); kettős operátor+(dupla, kettős);

    A SmallInt osztály tartalmaz egy átalakítót, amely képes SmallInt értéket adni egy int-re. Ezt az átalakítót az első beépített operátorral együtt használják a bal oldali operandus intgé konvertálására. A double típusú második operandust a szabványos konverzióval int típusúvá alakítjuk. Ami a második beépített operátort illeti, a konverter a bal oldali operandust SmallInt típusról int típusra konvertálja, ami után az eredményt szabványosan duplává alakítja. A double típusú második operandus pontosan megegyezik a második paraméterrel.

    Az öt tartós függvény közül a legjobb az első, az operator+(), amely az NS névtérben deklarált:

    NS::SmallInt NS::operator+(const SmallInt &, double);

    Mindkét operandusa pontosan megegyezik a paraméterekkel.

    15.12.3. Kétértelműség

    Az azonos osztályú konverterek jelenléte, amelyek implicit konverziót hajtanak végre beépített típusokká és túlterhelt operátorokká, kétértelműséget okozhat a köztük való választás során. Például létezik a következő String osztály definíció összehasonlító függvénnyel:

    Class String ( // ... public: String(const char * = 0); bool operator== (const String &) const; // nincs operátor operator== (const char *) );

    és ez az operátor használata==:

    Fúróvirág("tulipán"); void foo(const char *pf) ( // túlterhelt operátor hívása String::operator==() if (virág == pf) cout<< pf <<" is a flower!\en"; // ... }

    Aztán összehasonlításkor

    Virág == pf

    a String osztály egyenlőség operátora a következő:

    A pf jobb operandusának a const char* típusról az operator==() paraméter String típusára történő átalakításához egy felhasználó által definiált konverziót alkalmazunk, amely meghívja a konstruktort:

    Karakterlánc (állandó karakter *)

    Ha hozzáadunk egy konvertert a string osztálydefinícióban szereplő const char* típushoz:

    Class String ( // ... public: String(const char * = 0); bool operator== (const String &) const; operátor const char*(); // új konverter );

    akkor az operator==() bemutatott használata kétértelművé válik:

    // az egyenlőségellenőrzés már nem fordít! ha (virág == pf)

    A const char*() konverter operátor hozzáadása miatt beépített összehasonlító operátor

    is stabil függvénynek számít. Ezzel a String típusú bal oldali operandusvirág konvertálható const char * típusúvá.

    Most már két beállított operátorfüggvény létezik az operator==() használatához az foo()-ban. Az első

    String::operator==(const String &) const;

    megköveteli a pf jobb operandusának felhasználó által definiált konvertálását const char*-ról String-re. Második

    bool operátor==(const char *, const char *)

    megköveteli a virág bal oldali operandusának egyéni konvertálását Stringről const char*-ra.

    Így az első jól bevált függvény jobb a bal oldali, a második pedig a jobb operandus számára. Mivel nincs legjobb függvény, a hívást kétértelműnek jelöli meg a fordító.

    A túlterhelt operátorok, konstruktorok és konverterek deklarációját tartalmazó osztályinterfész tervezésekor nagyon óvatosnak kell lennie. A felhasználó által meghatározott konverziókat a fordító implicit módon alkalmazza. Ez azt eredményezheti, hogy a beépített operátorok ellenállóak a túlterhelési felbontásban az osztály típusú operandusokkal rendelkező operátorok esetében.

    15.17. gyakorlat

    Nevezzen meg öt olyan jelölt függvényhalmazt, amelyeket figyelembe vettek az operátorok osztálytípusú operandusokkal való túlterhelésének feloldásakor.

    15.18. gyakorlat

    Melyik operátor+() lesz a legjobban teljesítő összeadás operátor a main()-ban? Sorolja fel az összes jelölt függvényt, minden létrehozott függvényt és típuskonverziót, amelyet az egyes létrehozott függvények argumentumaira alkalmazni kell.

    Névtér NS ( osztály komplex ( complex(double); // ... ); osztály LongDouble ( barát LongDouble operátor+(LongDouble &, int) ( /* ... */ ) public: LongDouble(int); operátor double() ; LongDouble operátor+(const komplex &); // ... ); LongDouble operátor

    Kezelői túlterhelés alapjai

    A C#-nak, mint minden programozási nyelvnek, van egy sor tokenek, amelyet a beépített típusokon végzett alapvető műveletek elvégzésére használnak. Például tudjuk, hogy a + művelet alkalmazható két egész számra, hogy megadjuk az összegüket:

    // + művelet egész számokkal. int a = 100; int b = 240; int c = a + b; //s most 340

    Nincs itt semmi új, de gondoltál már arra, hogy ugyanaz a + művelet alkalmazható a legtöbb beépített C# adattípusra? Vegyük például ezt a kódot:

    // Művelet + karakterláncokkal. string si = "Hello"; string s2 = "világ!"; string s3 = si + s2; // Az s3 mostantól tartalmazza a "Hello world!"

    Lényegében a + művelet funkcionalitása egyértelműen a megjelenített adattípusokon (jelen esetben karakterláncokon vagy egész számokon) alapul. Ha a + operátort numerikus típusokra alkalmazzuk, akkor megkapjuk az operandusok számtani összegét. Ha azonban ugyanazt a műveletet alkalmazza a karakterlánctípusokra, akkor karakterlánc-összefűzést eredményez.

    A C# nyelv lehetővé teszi olyan speciális osztályok és struktúrák létrehozását, amelyek egyedileg reagálnak ugyanarra az alapjogkivonat-készletre (mint például a + operátor). Ne feledje, hogy abszolút minden beépített C# operátor nem terhelhető túl. Az alábbi táblázat a fő műveletek túlterhelési lehetőségeit írja le:

    Művelet C# Túlterhelési képesség
    +, -, !, ++, --, igaz, hamis Az unáris műveletek ezen halmaza túlterhelhető
    +, -, *, /, %, &, |, ^, > Ezek a bináris műveletek túlterhelhetők
    ==, !=, <, >, <=, >= Ezek az összehasonlító operátorok túlterhelhetők. A C# megköveteli a "like" operátorok kollaboratív túlterhelését (pl.< и >, <= и >=, == és !=)
    A műveletet nem lehet túlterhelni. Az indexelők azonban hasonló funkciókat kínálnak.
    () A () operátort nem lehet túlterhelni. Ugyanezt a funkcionalitást azonban speciális átalakítási módszerek biztosítják
    +=, -=, *=, /=, %=, &=, |=, ^=, >= A gyorsírási hozzárendelési operátorokat nem lehet túlterhelni; azonban a megfelelő bináris művelet túlterhelésével automatikusan megkapja őket

    A kezelői túlterhelés szorosan összefügg a módszer túlterhelésével. Az operátor túl van terhelve a kulcsszóval operátor An, amely egy operátori metódust definiál, amely viszont meghatározza az operátor műveletét az osztályához képest. Az operátori metódusoknak (operátornak) két formája van: az egyik az unáris, a másik a bináris operátorokhoz. Az alábbiakban a módszerek minden egyes változatának általános formája látható:

    // Az unáris operátor túlterhelés általános formája. public static return_type operátor op(operand parameter_type) ( // műveletek ) // A bináris operátor túlterhelésének általános formája. public static return_type operátor op(param_type1 operandus1, paraméter_típus2 operandus2) ( // műveletek )

    Itt az op helyett egy túlterhelt operátor, például + vagy /, és return_type a megadott művelet által visszaadott érték adott típusát jelöli. Ez az érték bármilyen típusú lehet, de gyakran úgy adják meg, hogy ugyanolyan típusú legyen, mint az az osztály, amelyhez az operátor túlterhelt. Ez a korreláció megkönnyíti a túlterhelt operátorok használatát a kifejezésekben. Unáris operátorok számára operandus jelöli az átvitt operandust, a bináris operátorok esetében pedig ugyanezt jelöli operandus1És operandus2. Vegye figyelembe, hogy az operátori metódusoknak nyilvános és statikus típusmeghatározókkal is rendelkezniük kell.

    Bináris operátorok túlterhelése

    Nézzük meg a bináris operátor túlterhelés használatát a legegyszerűbb példán keresztül:

    Rendszer használata; a System.Collections.Generic használatával; a System.Linq; a System.Text használatával; namespace ConsoleApplication1 ( class MyArr ( // Egy pont koordinátái a 3D térben public int x, y, z; public MyArr(int x = 0, int y = 0, int z = 0) ( this.x = x; this. y = y; this.z = z; ) // Túlterhelés bináris operátor + nyilvános statikus MyArr operátor +(MyArr obj1, MyArr obj2) ( MyArr arr = new MyArr(); arr.x = obj1.x + obj2.x; arr. y = obj1.y + obj2.y; arr.z = obj1.z + obj2.z; return arr; ) // Túlterhelés bináris operátor - nyilvános statikus MyArr operátor -(MyArr obj1, MyArr obj2) ( MyArr arr = new MyArr (); arr.x = obj1.x - obj2.x; arr.y = obj1.y - obj2.y; arr.z = obj1.z - obj2.z; return arr; ) ) osztály Program ( statikus void Main (string args) ( MyArr Point1 = new MyArr(1, 12, -4); MyArr Point2 = new MyArr(0, -3, 18); Console.WriteLine("Első pont koordinátái: " + Pont1.x + " " + Pont1.y + " " + Pont1.z); Console.WriteLine("Második pont koordinátái: " + Pont2.x + " " + Pont2.y + " " + Pont2.z + "\n"); MyArr Pont3 = Pont1 + Pont2;Console.WriteLine("\nPont1 + Pont2 = " + Pont3.x + " " + Pont3.y + " " + Pont3.z); 3. pont = 1. pont – 2. pont; Console.WriteLine("\nPont1 - Pont2 = " + Pont3.x + " " + Pont3.y + " " + Pont3.z); Console.ReadLine(); ) ) )

    Unary operátorok túlterhelése

    Az unáris operátorok ugyanúgy túlterheltek, mint a binárisok. A fő különbség természetesen az, hogy csak egy operandusuk van. Modernizáljuk az előző példát a ++, --, - operátorok túlterhelésének hozzáadásával:

    Rendszer használata; a System.Collections.Generic használatával; a System.Linq; a System.Text használatával; namespace ConsoleApplication1 ( class MyArr ( // Egy pont koordinátái a 3D térben public int x, y, z; public MyArr(int x = 0, int y = 0, int z = 0) ( this.x = x; this. y = y; this.z = z; ) // Túlterhelés bináris operátor + nyilvános statikus MyArr operátor +(MyArr obj1, MyArr obj2) ( MyArr arr = new MyArr(); arr.x = obj1.x + obj2.x; arr. y = obj1.y + obj2.y; arr.z = obj1.z + obj2.z; return arr; ) // Túlterhelés bináris operátor - nyilvános statikus MyArr operátor -(MyArr obj1, MyArr obj2) ( MyArr arr = new MyArr (); arr.x = obj1.x - obj2.x; arr.y = obj1.y - obj2.y; arr.z = obj1.z - obj2.z; return arr; ) // Túlterhelés az unáris operátor - nyilvános statikus MyArr operátor -(MyArr obj1) ( MyArr arr = new MyArr(); arr.x = -obj1.x; arr.y = -obj1.y; arr.z = -obj1.z; return arr; ) // Az unáris operátor túlterhelése ++ nyilvános statikus MyArr operátor ++(MyArr obj1) ( obj1.x += 1; obj1.y += 1; obj1.z +=1; return obj1; ) // Az unáris operátor túlterhelése operátor -- nyilvános statikus MyArr operátor ---(MyArr obj1) ( obj1.x -= 1; obj1.y -= 1; obj1.z -= 1; return obj1; ) ) class Program ( static void Main(string args) ( MyArr Point1 = new MyArr(1, 12, -4); MyArr Point2 = new MyArr(0, -3, 18); Console.WriteLine("Első pont koordinátái: " + Pont1.x + " " + Pont1.y + " " + Pont1.z); Console.WriteLine("A második pont koordinátái: " + Pont2.x + " " + Pont2.y + " " + Pont2. z + "\n"); MyArr Pont3 = Pont1 + Pont2; Console.WriteLine("\nPont1 + Pont2 = " + Pont3.x + " " + Pont3.y + " " + Pont3.z); Pont3 = Pont1 - pont + " " + Pont3.y + " " + Pont3.z); Pont2++; Console.WriteLine("Pont2++ = " + Pont2.x + " " + Pont2.y + " " + Pont2.z); Pont2--; Konzol. WriteLine("Point2-- = " + Pont2.x + " " + Pont2.y + " " + Pont2.z); Console.ReadLine(; ) ) )

    Sok programozási nyelv operátorokat használ: legalább hozzárendeléseket (= , := vagy hasonló) és aritmetikai operátorokat (+ , - , * és /). A legtöbb statikusan beírt nyelvben ezek az operátorok típusokhoz vannak kötve. Például Java nyelven a + operátorral történő összeadás csak egész számok, lebegőpontos számok és karakterláncok esetén lehetséges. Ha saját osztályokat definiálunk a matematikai objektumokhoz, például mátrixokhoz, akkor megvalósíthatunk egy metódust az összeadásukhoz, de ez csak így hívható meg: a = b.add(c) .

    A C++ nyelvben nincs ilyen megkötés – szinte minden ismert operátort túlterhelhetünk. A lehetőségek végtelenek: az operandustípusok tetszőleges kombinációja közül választhat, az egyetlen korlátozás, hogy legalább egy felhasználó által definiált típusú operandusnak jelen kell lennie. Vagyis definiáljon egy új operátort a beépített típusokon, vagy írjon át egy meglévőt ez tiltott.

    Mikor kell túlterhelni a kezelőket?

    Ne feledje a legfontosabb dolgot: akkor és csak akkor terhelje túl az operátorokat, ha annak van értelme. Vagyis ha a túlterhelés jelentése nyilvánvaló, és nem hordoz rejtett meglepetéseket. A túlterhelt kezelőknek ugyanúgy kell működniük, mint az alapverziójuknak. Természetesen kivételek megengedettek, de csak olyan esetekben, ahol érthető magyarázatok kísérik. Jó példa erre az operátorok<< и >> szabványos iostream könyvtár, amelyek egyértelműen a normáltól eltérően viselkednek.

    Íme, jó és rossz példák az operátor túlterhelésére. A fenti mátrixösszeadás egy szemléltető eset. Itt az addíciós operátor túlterhelése intuitív, és ha helyesen van implementálva, akkor magától értetődő:

    Mátrix a, b; c mátrix = a + b;

    A rossz addíciós operátor túlterhelésre példa lehet két játékos objektum hozzáadása egy játékban. Mire gondolt az osztály létrehozója? mi lesz az eredmény? Nem tudjuk, mit csinál a művelet, ezért veszélyes ennek az operátornak a használata.

    Hogyan lehet túlterhelni a kezelőket?

    A kezelői túlterhelés hasonló a funkció túlterheléséhez speciális nevekkel. Valójában, amikor a fordító olyan kifejezést lát, amely egy operátort és egy felhasználó által definiált típust tartalmaz, lecseréli ezt a kifejezést a túlterhelt operátor megfelelő függvényének hívására. A legtöbb nevük az operátor kulcsszóval kezdődik, amelyet az operátor neve követ. Ha a megnevezés nem tartalmaz speciális karaktereket, például cast vagy memóriakezelő operátor esetén (új , delete , stb.), akkor az operátor szót és az operátormegnevezést szóközzel kell elválasztani (operator new), ellenkező esetben a szóköz figyelmen kívül hagyható (operátor+ ).

    A legtöbb operátor túlterhelhető mind osztálymetódusokkal, mind egyszerű funkciók, de van néhány kivétel. Ha a túlterhelt operátor egy osztálymetódus, akkor az első operandus típusának az adott osztálynak kell lennie (mindig *this), a másodikat pedig deklarálni kell a paraméterlistában. Ezenkívül a metódus utasításai nem statikusak, kivéve a memóriakezelési utasításokat.

    Ha egy operátort túlterhelünk egy osztálymetódusban, az hozzáfér az osztály privát mezőihez, de az első argumentum rejtett konverziója nem érhető el. Ezért a bináris függvények általában szabad függvényként túlterheltek. Példa:

    Osztály Rational ( publikus: //A konstruktor int-ből implicit konverzióra használható: Rational(int számláló, int nevező = 1); Rational operátor+(Rational const& rhs) const; ); int main() ( Racionális a, b, c; int i; a = b + c; //ok, nincs szükség konverzióra a = b + i; //ok, a második argumentum implicit átalakítása a = i + c; //HIBA: az első argumentum nem konvertálható implicit módon )

    Ha az unáris operátorokat szabad függvényként túlterheljük, elérhető egy rejtett argumentumkonverzió, de ezt általában nem használják. Másrészt ez a tulajdonság szükséges a bináris operátorok számára. Tehát a fő tanács a következő lenne:

    Egyes és bináris operátorok implementálása, mint pl. x=” osztálymetódusként, más bináris operátorok pedig szabad függvényekként.

    Milyen operátorokat lehet túlterhelni?

    Szinte minden C++ operátort túlterhelhetünk, az alábbi kivételekkel és korlátozásokkal:

    • Nem definiálhat új operátort, például operátort**.
    • A következő operátorokat nem lehet túlterhelni:
      1. ?: (hármas operátor);
      2. :: (beágyazott nevek elérése);
      3. . (mezők hozzáférése);
      4. .* (mező hozzáférés mutatóval);
      5. sizeof , typeid és cast operátorok.
    • A következő operátorok csak metódusként terhelhetők túl:
      1. = (hozzárendelés);
      2. -> (a mezők elérése mutató segítségével);
      3. () (függvényhívás);
      4. (elérés indexen keresztül);
      5. ->* (mutatóról mezőre való hozzáférés mutatónként);
      6. konverziós és memóriakezelési operátorok.
    • Az operandusok számát, a végrehajtás sorrendjét és az operátorok asszociativitását a szabványos verzió határozza meg.
    • Legalább egy operandusnak felhasználó által meghatározott típusúnak kell lennie. A Typedef nem számít.

    A következő részben a C++ túlterhelt operátorokkal ismerkedhetsz meg csoportosan és egyénileg is. Mindegyik szakaszt szemantika jellemez, pl. elvárt viselkedés. Ezenkívül bemutatjuk az operátorok deklarálásának és végrehajtásának tipikus módjait.