Ülekoormatud operaatorid. Operaatorid uued ja kustutada. Kursori viite tühistamise operaator

Head päeva!

Soov seda artiklit kirjutada tekkis pärast postituse Overloading C++ Operators lugemist, sest paljud olulised teemad jäid selles käsitlemata.

Kõige tähtsam on meeles pidada, et operaatori ülekoormus on lihtsalt suurem mugav viis funktsioonikutsed, nii et ärge laske operaatori ülekoormamisest meelitada. Seda tuleks kasutada ainult siis, kui see muudab koodi kirjutamise lihtsamaks. Aga mitte nii palju, et see lugemist keeruliseks muudaks. Lõppude lõpuks, nagu teate, loetakse koodi palju sagedamini kui seda kirjutatakse. Ja ärge unustage, et teil ei lubata kunagi operaatoreid üle koormata koos sisseehitatud tüüpidega; ülekoormuse võimalus on saadaval ainult kasutaja määratud tüüpide/klasside puhul.

Ülekoormuse süntaks

Operaatori ülekoormamise süntaks on väga sarnane funktsiooni operaator@ määratlemisele, kus @ on operaatori identifikaator (näiteks +, -,<<, >>). Mõelgem lihtsaim näide:
klass Integer ( privaatne: int väärtus; public: Integer(int i): väärtus(i) () const Täisarvu operaator+(const Integer& rv) const ( tagastamine (väärtus + rv.väärtus); ) );
IN sel juhul, on operaator raamitud klassi liikmena, määrab argument väärtuse, mis asub operaatori paremal küljel. Üldiselt on operaatorite ülekoormamiseks kaks peamist viisi: globaalsed funktsioonid, klassi sõbralik või klassi enda funktsioonid. Milline meetod on millise operaatori jaoks parem, kaalume teema lõpus.

Enamasti tagastavad operaatorid (v.a tingimuslikud) objekti või viite tüübile, kuhu selle argumendid kuuluvad (kui tüübid on erinevad, siis otsustate, kuidas operaatori hindamise tulemust tõlgendada).

Unaarsete operaatorite ülekoormamine

Vaatame näiteid unaaroperaatori ülekoormusest ülaltoodud täisarvude klassis. Samal ajal defineerime need sõbralike funktsioonide kujul ja kaalume kahandamis- ja suurendamisteateid:
klass Täisarv ( privaatne: int väärtus; avalik: täisarv(int i): väärtus(i) () //unary + sõber const Integer& operaator+(const Integer& i); //unary - sõber const Täisarvu operaator-(const Integer& i) ; // eesliite juurdekasv sõber const Täisarv& operaator++(Täisarv& i); //postfix inkrement sõber const Täisarvu operaator++(Täisarv& i, int); //eesliide vähendamine sõber const Täisarv& operaator--(Täisarv& i); // postfix dekrement sõber const Täisarvu operaator--(Täisarv& i, int); ); //unary pluss ei tee midagi. const Integer& operaator+(const Integer& i) ( return i.value; ) const Täisarvu operaator-(const Integer& i) ( tagastab Integer(-i.value); ) //prefix versioon tagastab väärtuse pärast juurdekasvu const Integer& operaator++(Täisarv& i) ( i.value++; return i; ) //postfix versioon tagastab väärtuse enne inkrementi const Integer operaator++(Täisarv& i, int) ( Integer oldValue(i.value); i.value++; return oldValue; ) //eesliide versioon tagastab väärtus pärast dekrementi const Integer& operaator--(Täisarv& i) ( i.value--; return i; ) //postfix versioon tagastab väärtuse enne dekrementi const Integer operaator--(Täisarv& i, int) ( Integer oldValue(i. väärtus); i .value - tagastab
Nüüd teate, kuidas kompilaator eristab kahanemise ja suurendamise eesliite ja järelliitega versioone. Kui ta näeb avaldist ++i, kutsutakse välja funktsiooni operaator++(a). Kui see näeb i++, kutsutakse operaator++(a, int). See tähendab, et kutsutakse funktsiooni overloaded operator++ ja selleks kasutatakse postfixi versiooni dummy int parameetrit.

Binaarsed operaatorid

Vaatame kahendoperaatorite ülekoormamise süntaksit. Ülekoormame ühte operaatorit, mis tagastab l-väärtuse, ühe tingimuslik operaator ja üks operaator, kes loob uue väärtuse (määratleme need globaalselt):
klass Täisarv ( privaatne: int väärtus; avalik: täisarv(int i): väärtus(i) () sõber const Integer operator+(const Integer& vasak, const Integer& parem); sõber Täisarv& operaator+=(Täisarv& vasak, konst Integer& parem); sõber bool operaator==(const Integer& vasak, const Täisarv& parem); const Integer operaator+(const Integer& vasak, const Integer& right) ( return Integer(left.value + right.value); ) Täisarv& operaator+=(Täisarv& vasak, const Täisarv& parem) ( left.value += right.value; tagastab vasakule; ) bool operaator==(const Integer& left, const Integer& right) ( return left.value == right.value; )
Kõigis neis näidetes on sama tüübi operaatorid ülekoormatud, kuid see pole vajalik. Võite näiteks meie lisamisega üle koormata tippige täisarv ja määratletud selle sarnasusega Float.

Argumendid ja tagastusväärtused

Nagu näete, kasutavad näited erinevaid viise argumentide edastamine funktsioonidele ja operaatori väärtuste tagastamine.
  • Kui argumenti operaatoriga ei muudeta, siis näiteks unaarse plussi puhul tuleb see edastada viitena konstandile. Üldiselt kehtib see peaaegu kõigi kohta aritmeetilised operaatorid(liitmine, lahutamine, korrutamine...)
  • Tagastatava väärtuse tüüp sõltub operaatori olemusest. Kui operaator peab tagastama uue väärtuse, siis tuleb see luua uus objekt(nagu binaarse plussi puhul). Kui soovite takistada objekti muutmist l-väärtusena, peate selle tagastama konstandina.
  • Määramise operaatorid peavad tagastama viite muudetud elemendile. Samuti, kui soovite kasutada omistamisoperaatorit sellistes konstruktsioonides nagu (x=y).f(), kus muutuja x jaoks kutsutakse funktsioon f() pärast selle määramist y-le, siis ärge tagastage viidet konstantne, tagastage lihtsalt viide.
  • Loogilised operaatorid peaksid halvimal juhul tagastama int ja parimal juhul booli.

Tagastusväärtuse optimeerimine

Uute objektide loomisel ja nende funktsioonist tagastamisel tuleks kasutada tähistust, mis sarnaneb ülalkirjeldatud binaarse plussoperaatori näitega.
return Integer(left.value + right.value);
Ausalt öeldes ei tea ma, milline olukord on C++11 jaoks asjakohane, kõik täiendavad argumendid kehtivad C++98 puhul.
Esmapilgul tundub see sarnane ajutise objekti loomise süntaksiga, see tähendab, et ülaltoodud koodi ja selle vahel pole vahet:
Integer temp(left.value + right.value); tagasivoolu temp;
Kuid tegelikult kutsutakse sel juhul esimesel real konstruktor, seejärel kopeerimiskonstruktor, mis kopeerib objekti ja seejärel virna lahti kerimisel kutsutakse välja hävitaja. Esimese kirje kasutamisel loob kompilaator algselt mällu objekti, kuhu see tuleb kopeerida, salvestades nii kõned koopiakonstruktorile ja -destruktorile.

Spetsiaalsed operaatorid

C++-l on operaatorid, millel on spetsiifilised süntaks ja ülekoormusmeetodid. Näiteks indekseerimisoperaator. See on alati määratletud kui klassi liige ja kuna indekseeritud objekt on mõeldud massiivina käituma, peaks see tagastama viite.
Komaoperaator
"Eriliste" operaatorite hulka kuulub ka koma. Seda kutsutakse välja objektidel, mille kõrval on koma (kuid seda ei kutsuta funktsiooni argumentide loendites). Selle operaatori jaoks sisuka kasutusjuhtumi leidmine pole lihtne. Habrowser AxisPod rääkis eelmise ülekoormust käsitleva artikli kommentaarides ühest asjast.
Kursori viite tühistamise operaator
Nende operaatorite ülekoormamine võib olla põhjendatud nutikate kursorite klasside puhul. See operaator on tingimata määratletud klassifunktsioonina ja sellele on kehtestatud mõned piirangud: see peab tagastama kas objekti (või viite) või osuti, mis võimaldab objektile juurdepääsu.
Määramise operaator
Omistamisoperaator on tingimata määratletud klassifunktsioonina, kuna see on olemuslikult seotud "="-st vasakul oleva objektiga. Määramise operaatori määratlemine globaalselt võimaldaks alistada operaatori "=" vaikekäitumise. Näide:
klass Täisarv ( privaatne: int väärtus; public: Integer(int i): väärtus(i) () Integer& operator=(const Integer& right) ( //kontrollige enesemääramist if (this == &right) ( return *this; ) väärtus = õigus.väärtus tagastab *see;

Nagu näete, kontrollitakse funktsiooni alguses enesemääramist. Üldiselt on sellisel juhul enese omastamine kahjutu, kuid olukord pole alati nii lihtne. Näiteks kui objekt on suur, võite sellele palju aega kulutada tarbetu kopeerimine või osutitega töötades.

Mitteülekoormatavad operaatorid
Mõned C++ operaatorid pole üldse ülekoormatud. Ilmselt tehti seda turvakaalutlustel.
  • Klassiliikme valiku operaator ".".
  • Operaator kursori viitamise eemaldamiseks klassi liikmele ".*"
  • C++-l puudub astendamise operaator (nagu Fortranis) "**".
  • Keelatud on defineerida oma operaatoreid (võib esineda probleeme prioriteetide määramisega).
  • Operaatori prioriteete ei saa muuta
Nagu me juba teada saime, on operaatoritel kaks võimalust - klassifunktsioonina ja sõbraliku globaalfunktsioonina.
Rob Murray määratles oma raamatus C++ Strategies and Tactics järgmisi soovitusi operaatori vormi valikul:

Miks nii? Esiteks on mõned operaatorid esialgu piiratud. Üldiselt, kui operaatori defineerimisel pole semantilist erinevust, siis on parem kujundada see klassifunktsioonina, et rõhutada seost, lisaks on funktsioon inline. Lisaks võib mõnikord tekkida vajadus esitada vasakpoolset operandi mõne teise klassi objektina. Ilmselt kõige rohkem särav eeskuju- tühistada<< и >> I/O voogude jaoks.

Kirjandus

Bruce Eckel – C++ filosoofia. Sissejuhatus standardisse C++.

Sildid: lisa sildid

Oleme käsitlenud operaatori ülekoormuse kasutamise põhitõdesid. Selles materjalis tutvustatakse teie tähelepanu ülekoormatud C++ operaatoritele. Iga lõiku iseloomustab semantika, s.t. eeldatav käitumine. Lisaks näidatakse tüüpilisi operaatorite deklareerimise ja rakendamise viise.

Koodinäidetes tähistab X kasutaja määratud tüüpi, mille jaoks operaator on rakendatud. T on valikuline tüüp, kas kasutaja määratud või sisseehitatud. Kahendoperaatori parameetrid saavad nimeks lhs ja rhs . Kui operaator on deklareeritud klassimeetodina, lisatakse selle deklaratsiooni eesliide X:: .

operaator=

  • Definitsioon paremalt vasakule: Erinevalt enamikust operaatoritest on operaator= õige assotsiatiivne, st. a = b = c tähendab a = (b = c) .

Kopeeri

  • Semantika: ülesanne a = b . B väärtus või olek edastatakse a-le. Lisaks tagastatakse viide a-le. See võimaldab teil luua ahelaid nagu c = a = b.
  • Tüüpiline reklaam: X& X::operator= (X const& rhs) . Võimalikud on ka muud tüüpi argumendid, kuid neid ei kasutata sageli.
  • Tüüpiline teostus: X& X::operator= (X const& rhs) ( if (this != &rhs) ( //perform element wise copy, or: X tmp(rhs); //copy constructor swap(tmp); ) return *this; )

Teisalda (alates C++11)

  • Semantika: määramine a = ajutine() . Õige väärtuse väärtus või olek määratakse a-le sisu teisaldamisega. Tagatakse viide a-le.
  • : X& X::operator= (X&& rhs) ( //võtke rhs-ilt sisikond tagasi *see; )
  • Kompilaator loodud operator= : kompilaator saab luua ainult kahte tüüpi seda operaatorit. Kui klassis ei ole operaatorit deklareeritud, proovib kompilaator luua avalikke koopiaid ja teisaldajaid. Alates C++11-st saab kompilaator luua vaikeoperaatori: X& X::operator= (X const& rhs) = default;

    Loodud avaldus on lihtsalt kopeeri/teisalda määratud element, kui selline toiming on lubatud.

operaator+, -, *, /, %

  • Semantika: liitmise, lahutamise, korrutamise, jagamise, jäägiga jagamise tehted. Tagatakse uus objekt saadud väärtusega.
  • Tüüpiline deklaratsioon ja rakendamine: X operaator+ (X const lhs, X const rhs) ( X tmp(lhs); tmp += rhs; return tmp; )

    Tavaliselt, kui operaator+ on olemas, on mõistlik ka operaator+= üle koormata, et kasutada a = a + b asemel tähistust a += b. Kui operaator+= pole ülekoormatud, näeb rakendus välja umbes selline:

    X operaator+ (X const& lhs, X const& rhs) ( // loob uue objekti, mis esindab lhs ja rhs summat: return lhs.plus(rhs); )

Unaaroperaator+, —

  • Semantika: positiivne või negatiivne märk. operaator+ ei tee tavaliselt midagi ja seetõttu ei kasutata seda peaaegu. operaator- tagastab argumendi vastupidise märgiga.
  • Tüüpiline deklaratsioon ja rakendamine: X X::operator- () const ( tagasta /* *selle */ negatiivne koopia; ) X X::operator+ () const ( tagasta *see; )

operaator<<, >>

  • Semantika: Sisseehitatud tüüpides kasutatakse operaatoreid vasakpoolse argumendi biti nihutamiseks. Nende operaatorite ülekoormamine täpselt sellise semantikaga on haruldane, mis meenub on std::bitset . Voogudega töötamiseks on aga kasutusele võetud uus semantika ja I/O-lausete ülekoormamine on üsna tavaline.
  • Tüüpiline deklaratsioon ja rakendamine: kuna standardsetele iostream klassidele meetodeid lisada ei saa, tuleb defineeritud klasside vahetustehtereid vaba funktsioonidena üle koormata: operaator ostream&<< (ostream& os, X const& x) { os << /* the formatted data of rhs you want to print */; return os; } istream& operator>> (istream& is, X& x) ( SomeData sd; SomeMoreData smd; if (on >> sd >> smd) ( rhs.setSomeData(sd); rhs.setSomeMoreData(smd; ) tagastab lhs; )

    Lisaks võib vasakpoolse operandi tüüp olla mis tahes klass, mis peaks käituma nagu I/O-objekt, st parem operandi tüüp võib olla sisseehitatud tüüp.

    MyIO& MyIO::operaator<< (int rhs) { doYourThingWith(rhs); return *this; }

Kahendoperaator&, |, ^

  • Semantika: bitioperatsioonid "ja", "või", "ainuõiguslik või". Need operaatorid on väga harva ülekoormatud. Jällegi on ainus näide std::bitset .

operaator+=, -=, *=, /=, %=

  • Semantika: a += b tähendab tavaliselt sama, mis a = a + b . Sarnane on ka teiste operaatorite käitumine.
  • Tüüpiline määratlus ja rakendamine: kuna operatsioon muudab vasakut operandi, ei ole peidetud tüüpi ülekandmine soovitav. Seetõttu tuleb need operaatorid klassimeetoditena üle koormata. X& X::operator+= (X const& rhs) ( //rakenda muudatused *sellele tagastamisele *see; )

operaator&=, |=, ^=,<<=, >>=

  • Semantika: sarnane operaatorile+= , kuid loogiliste operatsioonide jaoks. Need operaatorid on ülekoormatud sama harva kui operaator| jne. operaator<<= и operator>>= ei kasutata I/O operatsioonide jaoks, kuna operaator<< и operator>> juba vasakpoolset argumenti muutes.

operaator==, !=

  • Semantika: Võrdsuse/ebavõrdsuse test. Võrdsuse tähendus on klassiti väga erinev. Igal juhul võtke arvesse järgmisi võrdsuse omadusi:
    1. Refleksiivsus, st. a == a .
    2. Sümmeetria, s.t. kui a == b , siis b == a .
    3. Transitiivsus, st. kui a == b ja b == c , siis a == c .
  • Tüüpiline deklaratsioon ja rakendamine: bool operaator== (X const& lhs, X cosnt& rhs) ( tagastab /* kontrollige mis tahes võrdsust */ ) bool operaator!= (X const& lhs, X const& rhs) ( tagastab !(lhs == rhs); )

    Operaatori!= teine ​​teostus väldib koodi kordamist ja välistab võimalikud ebaselgused mis tahes kahe objekti puhul.

operaator<, <=, >, >=

  • Semantika: kontrollige suhet (rohkem, vähem jne). Tavaliselt kasutatakse siis, kui elementide järjekord on üheselt määratletud, st keerulised objektid Pole mõtet võrrelda mitme tunnusega.
  • Tüüpiline deklaratsioon ja rakendamine: bool operaator< (X const& lhs, X const& rhs) { return /* compare whatever defines the order */ } bool operator>(X const& lhs, X const& rhs) ( tagasi rhs< lhs; }

    Rakendusoperaator> kasutades operaatorit< или наоборот обеспечивает однозначное определение. operator<= может быть реализован по-разному, в зависимости от ситуации . В частности, при отношении строго порядка operator== можно реализовать лишь через operator< :

    Booli operaator== (X const& lhs, X const& rhs) ( return !(lhs)< rhs) && !(rhs < lhs); }

operaator++, –

  • Semantika: a++ (post-inkrement) suurendab väärtust 1 võrra ja tagastab vana tähenduses. ++a (eelkasv) tagastab uus tähenduses. Vähendamisoperaatoriga – kõik on sarnane.
  • Tüüpiline deklaratsioon ja rakendamine: X& X::operator++() ( //eelkasvatamine /* kuidagi juurdekasv, nt *this += 1*/; return *this; ) X X::operator++(int) ( //postincrement X oldValue(*this); + +(*this);

operaator()

  • Semantika: funktsiooniobjekti (funktor) täitmine. Tavaliselt kasutatakse seda mitte objekti muutmiseks, vaid funktsioonina kasutamiseks.
  • Parameetritele pole piiranguid: Erinevalt eelmistest operaatoritest ei ole antud juhul parameetrite arvule ja tüübile piiranguid. Operaatorit saab üle koormata ainult klassimeetodina.
  • Näidisreklaam: Foo X::operator() (Bar br, Baz const& bz);

operaator

  • Semantika: juurdepääsu massiivi või konteineri elementidele, näiteks std::vector , std::map , std::massiivi .
  • Teadaanne: parameetri tüüp võib olla ükskõik milline. Tagastamise tüüp on tavaliselt viide konteineris talletatule. Sageli on operaator ülekoormatud kahes versioonis, const ja non-const: Element_t& X::operator(Index_t const& index); const Element_t& X::operator(Indeks_t const& index) const;

operaator!

  • Semantika: eitus loogilises mõttes.
  • Tüüpiline deklaratsioon ja rakendamine: bool X::operator!() const ( return !/*mingi hinnang *sellele*/; )

selge operaatori bool

  • Semantika: Kasutage loogilises kontekstis. Kõige sagedamini kasutatakse nutikate osutitega.
  • Rakendamine: eksplitsiitne X::operator bool() const ( tagastab /*, kui see on tõene või väär */; )

operaator&&, ||

  • Semantika: loogiline "ja", "või". Need operaatorid on määratletud ainult sisseehitatud tõeväärtuse tüübi jaoks ja töötavad laiskadel alustel, see tähendab, et teist argumenti arvestatakse ainult siis, kui esimene ei määra tulemust. Ülekoormamisel see omadus kaob, nii et need operaatorid on harva ülekoormatud.

Unaarne operaator*

  • Semantika: Osuti viide. Tavaliselt ülekoormatud nutikate osutite ja iteraatoritega klasside jaoks. Tagastab viite sellele, kuhu objekt osutab.
  • Tüüpiline deklaratsioon ja rakendamine: T& X::operator*() const (tagasi *_ptr; )

operaator->

  • Semantika: väljale juurdepääs kursoriga. Nagu eelminegi, on see operaator nutikate osutite ja iteraatoritega kasutamiseks ülekoormatud. Kui teie koodis esineb operaator ->, suunab kompilaator kohandatud tüübi tulemuse tagastamisel kõned ümber operaatori->.
  • Tavaline rakendamine: T* X::operaator->() const ( return _ptr; )

operaator->*

  • Semantika: juurdepääs kursorilt väljale kursori kaupa. Operaator viib kursori väljale ja rakendab selle kõigele, millele *see viitab, seega on objPtr->*memPtr sama mis (*objPtr).*memPtr . Väga harva kasutatud.
  • Võimalik rakendamine: mall T& X::operaator->*(T V::* memptr) ( return (operator*()).*memptr; )

    Siin on X nutikas osuti, V on tüüp, millele osutab X , ja T on tüüp, millele osutab välja osuti. Pole üllatav, et see operaator on harva ülekoormatud.

Unaarne operaator&

  • Semantika: aadressi operaator. See operaator on väga harva ülekoormatud.

operaator

  • Semantika: kahele avaldisele rakendatud sisseehitatud komaoperaator hindab need mõlemad kirjalikus järjekorras ja tagastab teise väärtuse. Seda ei soovitata üle koormata.

operaator~

  • Semantika: bitipõhise inversiooni operaator. Üks harvemini kasutatavaid operaatoreid.

Valamisoperaatorid

  • Semantika: Võimaldab klassiobjektide kaudset või otsest ülekandmist muudesse tüüpidesse.
  • Teadaanne: //teisendamine T-ks, eksplitsiitne või kaudne X::operaator T() const; //eksplitsiitne teisendamine U-ks const& selgesõnaline X::operaator U const&() const; //teisendamine V& V& X::operaator V&();

    Need deklaratsioonid näevad imelikud välja, kuna neil puudub tagastustüüp. See on osa operaatori nimest ja seda ei määrata kaks korda. Seda tasub meeles pidada suur hulk varjatud kummitused võivad kaasa tuua ootamatud vead programmi töös.

operaator uus, uus, kustuta, kustuta

Need operaatorid erinevad täielikult kõigist ülalmainitutest, kuna nendega ei tööta kohandatud tüübid. Nende ülekoormus on väga keeruline ja seetõttu ei võeta seda siin arvesse.

Järeldus

Põhiidee on järgmine: ärge koormake operaatoreid üle lihtsalt sellepärast, et teate, kuidas seda teha. Ülekoormake neid ainult juhtudel, kui see tundub loomulik ja vajalik. Kuid pidage meeles, et kui koormate ühte operaatorit üle, peate üle koormama ka teisi.

Head päeva!

Soov seda artiklit kirjutada tekkis pärast postituse lugemist, sest paljud olulised teemad jäid selles käsitlemata.

Kõige tähtsam on meeles pidada, et operaatori ülekoormamine on lihtsalt mugavam viis funktsioonide väljakutsumiseks, nii et ärge laske operaatori ülekoormamisest end häirida. Seda tuleks kasutada ainult siis, kui see muudab koodi kirjutamise lihtsamaks. Aga mitte nii palju, et see lugemist keeruliseks muudaks. Lõppude lõpuks, nagu teate, loetakse koodi palju sagedamini kui seda kirjutatakse. Ja ärge unustage, et teil ei lubata kunagi operaatoreid koos sisseehitatud tüüpidega üle koormata.

Ülekoormuse süntaks

Operaatori ülekoormamise süntaks on väga sarnane funktsiooni operaator@ määratlemisele, kus @ on operaatori identifikaator (näiteks +, -,<<, >>). Vaatame lihtsat näidet:
klass Integer ( privaatne: int väärtus; public: Integer(int i): väärtus(i) () const Täisarvu operaator+(const Integer& rv) const ( tagastamine (väärtus + rv.väärtus); ) );
Sel juhul on operaator raamitud klassi liikmena, argument määrab väärtuse, mis asub operaatori paremal küljel. Üldiselt on operaatorite ülekoormamiseks kaks peamist võimalust: klassisõbralikud globaalsed funktsioonid või klassi enda funktsioonid. Milline meetod on millise operaatori jaoks parem, kaalume teema lõpus.

Enamasti tagastavad operaatorid (v.a tingimuslikud) objekti või viite tüübile, kuhu selle argumendid kuuluvad (kui tüübid on erinevad, siis otsustate, kuidas operaatori hindamise tulemust tõlgendada).

Unaarsete operaatorite ülekoormamine

Vaatame näiteid unaaroperaatori ülekoormusest ülaltoodud täisarvude klassis. Samal ajal defineerime need sõbralike funktsioonide kujul ja kaalume kahandamis- ja suurendamisteateid:
klass Täisarv ( privaatne: int väärtus; avalik: täisarv(int i): väärtus(i) () //unary + sõber const Integer& operaator+(const Integer& i); //unary - sõber const Täisarvu operaator-(const Integer& i) //eesliide inkrement const Integer& operaator++(Täisarv&i); i, int); //unary pluss ei tee midagi. const Integer& operaator+(const Integer& i) ( return i.value; ) const Täisarvu operaator-(const Integer& i) ( tagastab Integer(-i.value); ) //prefix versioon tagastab väärtuse pärast juurdekasvu const Integer& operaator++(Täisarv& i) ( i.value++; return i; ) //postfix versioon tagastab väärtuse enne inkrementi const Integer operaator++(Täisarv& i, int) ( Integer oldValue(i.value); i.value++; return oldValue; ) //eesliide versioon tagastab väärtus pärast dekrementi const Integer& operaator--(Täisarv& i) ( i.value--; return i; ) //postfix versioon tagastab väärtuse enne dekrementi const Integer operaator--(Täisarv& i, int) ( Integer oldValue(i. väärtus); i .value - tagastab
Nüüd teate, kuidas kompilaator eristab kahanemise ja suurendamise eesliite ja järelliitega versioone. Kui ta näeb avaldist ++i, kutsutakse välja funktsiooni operaator++(a). Kui see näeb i++, kutsutakse operaator++(a, int). See tähendab, et kutsutakse funktsiooni overloaded operator++ ja selleks kasutatakse postfixi versiooni dummy int parameetrit.

Binaarsed operaatorid

Vaatame kahendoperaatorite ülekoormamise süntaksit. Laadime üle ühe operaatori, mis tagastab l-väärtuse, ühe tingimusliku operaatori ja ühe uue väärtuse loova operaatori (defineerime need globaalselt):
klass Täisarv ( privaatne: int väärtus; avalik: täisarv(int i): väärtus(i) () sõber const Integer operator+(const Integer& vasak, const Integer& parem); sõber Täisarv& operaator+=(Täisarv& vasak, konst Integer& parem); sõber bool operaator==(const Integer& vasak, const Täisarv& parem); const Integer operaator+(const Integer& vasak, const Integer& right) ( return Integer(left.value + right.value); ) Täisarv& operaator+=(Täisarv& vasak, const Täisarv& parem) ( left.value += right.value; tagastab vasakule; ) bool operaator==(const Integer& left, const Integer& right) ( return left.value == right.value; )
Kõigis neis näidetes on sama tüübi operaatorid ülekoormatud, kuid see pole vajalik. Võite näiteks üle koormata meie täisarvu tüübi ja selle sarnasuse järgi defineeritud Float lisamist.

Argumendid ja tagastusväärtused

Nagu näete, kasutatakse näidetes erinevaid võimalusi argumentide edastamiseks funktsioonidele ja operaatori väärtuste tagastamiseks.
  • Kui argumenti operaatoriga ei muudeta, siis näiteks unaarse plussi puhul tuleb see edastada viitena konstandile. Üldiselt kehtib see peaaegu kõigi aritmeetiliste operaatorite kohta (liitmine, lahutamine, korrutamine ...)
  • Tagastatava väärtuse tüüp sõltub operaatori olemusest. Kui operaator peab tagastama uue väärtuse, siis tuleb luua uus objekt (nagu binaarse plussi puhul). Kui soovite takistada objekti muutmist l-väärtusena, peate selle tagastama konstandina.
  • Määramise operaatorid peavad tagastama viite muudetud elemendile. Samuti, kui soovite kasutada omistamisoperaatorit sellistes konstruktsioonides nagu (x=y).f(), kus muutuja x jaoks kutsutakse funktsioon f() pärast selle määramist y-le, siis ärge tagastage viidet konstantne, tagastage lihtsalt viide.
  • Loogilised operaatorid peaksid halvimal juhul tagastama int ja parimal juhul booli.

Tagastusväärtuse optimeerimine

Uute objektide loomisel ja nende funktsioonist tagastamisel tuleks kasutada tähistust, mis sarnaneb ülalkirjeldatud binaarse plussoperaatori näitega.
return Integer(left.value + right.value);
Ausalt öeldes ei tea ma, milline olukord on C++11 jaoks asjakohane, kõik täiendavad argumendid kehtivad C++98 puhul.
Esmapilgul tundub see sarnane ajutise objekti loomise süntaksiga, see tähendab, et ülaltoodud koodi ja selle vahel pole vahet:
Integer temp(left.value + right.value); tagasivoolu temp;
Kuid tegelikult kutsutakse sel juhul esimesel real konstruktor, seejärel kopeerimiskonstruktor, mis kopeerib objekti ja seejärel virna lahti kerimisel kutsutakse välja hävitaja. Esimese kirje kasutamisel loob kompilaator algselt mällu objekti, kuhu see tuleb kopeerida, salvestades nii kõned koopiakonstruktorile ja -destruktorile.

Spetsiaalsed operaatorid

C++-l on operaatorid, millel on spetsiifilised süntaks ja ülekoormusmeetodid. Näiteks indekseerimisoperaator. See on alati määratletud kui klassi liige ja kuna indekseeritud objekt on mõeldud massiivina käituma, peaks see tagastama viite.
Komaoperaator
"Eriliste" operaatorite hulka kuulub ka koma. Seda kutsutakse välja objektidel, mille kõrval on koma (kuid seda ei kutsuta funktsiooni argumentide loendites). Selle operaatori jaoks sisuka kasutusjuhtumi leidmine pole lihtne. Habrauser eelmise ülekoormust käsitleva artikli kommentaarides.
Kursori viite tühistamise operaator
Nende operaatorite ülekoormamine võib olla põhjendatud nutikate kursorite klasside puhul. See operaator on tingimata määratletud klassifunktsioonina ja sellele on kehtestatud mõned piirangud: see peab tagastama kas objekti (või viite) või osuti, mis võimaldab objektile juurdepääsu.
Määramise operaator
Omistamisoperaator on tingimata defineeritud klassifunktsioonina, kuna see on olemuslikult seotud "="-st vasakul oleva objektiga. Määramise operaatori globaalne defineerimine võimaldaks alistada operaatori "=" vaikekäitumise. Näide:
klass Täisarv ( privaatne: int väärtus; public: Integer(int i): väärtus(i) () Integer& operator=(const Integer& right) ( //kontrollige enesemääramist if (this == &right) ( return *this; ) väärtus = õigus.väärtus tagastab *see;

Nagu näete, kontrollitakse funktsiooni alguses enesemääramist. Üldiselt on sellisel juhul enese omastamine kahjutu, kuid olukord pole alati nii lihtne. Näiteks kui objekt on suur, võite raisata palju aega tarbetule kopeerimisele või osutitega töötamisele.

Mitteülekoormatavad operaatorid
Mõned C++ operaatorid pole üldse ülekoormatud. Ilmselt tehti seda turvakaalutlustel.
  • Klassiliikme valiku operaator ".".
  • Operaator kursori viitamise eemaldamiseks klassi liikmele ".*"
  • C++-l puudub astendamise operaator (nagu Fortranis) "**".
  • Keelatud on defineerida oma operaatoreid (võib esineda probleeme prioriteetide määramisega).
  • Operaatori prioriteete ei saa muuta
Nagu me juba teada saime, on operaatoritel kaks võimalust - klassifunktsioonina ja sõbraliku globaalfunktsioonina.
Rob Murray määratleb oma raamatus C++ Strategies and Tactics operaatorivormi valimisel järgmised juhised:

Miks nii? Esiteks on mõned operaatorid esialgu piiratud. Üldiselt, kui operaatori defineerimisel pole semantilist erinevust, siis on parem kujundada see klassifunktsioonina, et rõhutada seost, lisaks on funktsioon inline. Lisaks võib mõnikord tekkida vajadus esitada vasakpoolset operandi mõne teise klassi objektina. Ilmselt kõige ilmekam näide on ümberdefineerimine<< и >> I/O voogude jaoks.

Kirjandus

Bruce Eckel – C++ filosoofia. Sissejuhatus standardisse C++.

Sildid:

  • C++
  • operaatorite ülekoormus
  • operaatori ülekoormus
Lisa märksõnu

Operaatori ülekoormamise põhitõed

C#-l, nagu igal programmeerimiskeelel, on sooritamiseks kasutatud žetoonide komplekt põhitoimingudüle sisseehitatud tüüpide. Näiteks on teada, et + operatsiooni saab rakendada kahele täisarvule, et saada nende summa:

// Tehe + täisarvudega. int a = 100; int b = 240; int c = a + b; //s on nüüd võrdne 340-ga

Siin pole midagi uut, aga kas olete kunagi mõelnud, et sama + operatsiooni saab rakendada enamikule C# sisseehitatud andmetüüpidele? Näiteks kaaluge seda koodi:

// Operatsioon + stringidega. string si = "Tere"; string s2 = "maailm!"; string s3 = si + s2; // s3 sisaldab nüüd "Tere maailm!"

Põhimõtteliselt põhineb + ​​operatsiooni funktsionaalsus üheselt esitatud andmetüüpidel (antud juhul stringidel või täisarvudel). Kui + toimingut rakendatakse numbrilised tüübid, saame aritmeetiline summa operandid Kui aga rakendatakse sama toimingut stringi tüübid, saame stringide konkatenatsiooni.

C#-keel annab võimaluse luua eriklasse ja -struktuure, mis reageerivad unikaalselt ka samale põhimärkide komplektile (nagu operaator +). Pidage meeles, et absoluutselt iga sisseehitatud C#-operaatorit ei saa üle koormata. Järgmine tabel kirjeldab põhitoimingute ülekoormusvõimalusi.

C# operatsioon Ülekoormuse võimalus
+, -, !, ++, --, õige, vale See unaarsete operaatorite komplekt võib olla üle koormatud
+, -, *, /, %, &, |, ^, > Need binaartoimingud võivad olla ülekoormatud
==, !=, <, >, <=, >= Need võrdlusoperaatorid võivad olla ülekoormatud. C# nõuab "meeldib" operaatorite jagatud ülekoormamist (st.< и >, <= и >=, == ja!=)
Toimingut ei saa üle koormata. Kuid indekseerijad pakuvad sarnaseid funktsioone
() Toimingut () ei saa üle koormata. Spetsiaalsed teisendusmeetodid pakuvad aga sama funktsiooni
+=, -=, *=, /=, %=, &=, |=, ^=, >= Lühiülesannete operaatoreid ei saa üle koormata; aga saad need automaatselt vastava binaartehte ülekoormamisega

Operaatori ülekoormus on tihedalt seotud meetodi ülekoormamisega. Operaatori ülekoormuse korral kasutage märksõna operaator, mis määratleb operaatori meetodi, mis omakorda määratleb operaatori tegevuse tema klassi suhtes. Operaatorimeetodeid on kahte tüüpi: üks unaartehtetele, teine ​​​​binaartehtetele. Allpool on nende meetodite iga variatsiooni üldvorm.

// Üldine vorm operaatori ühekordsed ülekoormused. public static return_type operaator op(parameetri_tüüp operaand) ( // operatsioonid ) // Binaarse operaatori ülekoormuse üldvorm. avalik staatiline return_type operaator op(parameetri_tüüp1 operaand1, parameetri_tüüp2 operaand2) ( // toimingud )

Siin asendatakse op ülekoormatud operaatoriga, näiteks + või / ja tagastamise_tüüp tähistab konkreetne tüüp määratud toimingu poolt tagastatav väärtus. See väärtus võib olla mis tahes tüüpi, kuid sageli määratakse see sama tüüpi klassiga, mille puhul operaator on ülekoormatud. See korrelatsioon muudab avaldistes ülekoormatud operaatorite kasutamise lihtsamaks. Unaarsetele operaatoritele operand tähistab edastatavat operandi ja kahendtehtete puhul tähistatakse sama operand1 Ja operand2. Pange tähele, et operaatorimeetoditel peavad olema nii avalikud kui ka staatilised tüübispetsifikaadid.

Kahendoperaatorite ülekoormus

Vaatame binaarse operaatori ülekoormuse kasutamist lihtsa näite abil:

Süsteemi kasutamine; kasutades System.Collections.Generic; kasutades System.Linq; kasutades System.Text; namespace ConsoleApplication1 ( klass MyArr ( // Punkti koordinaadid kolmemõõtmelises ruumis public int x, y, z; public MyArr(int x = 0, int y = 0, int z = 0) ( this.x = x; see.y = y; this.z = z; binaarne operaator+ avalik staatiline MyArr operaator +(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 x ; WriteLine("Esimese punkti koordinaadid: " + Punkt1.x + " " + Punkt1.y + " " + Punkt1.z .WriteLine("Teise punkti koordinaadid: " + Punkt2.x + " " + Punkt2); .y + " " + Punkt2.z + "\n"); ; Console.WriteLine("\nPoint1 - Point2 = " + Point3.x + " " + Point3.y + " " + Point3.z);

Unaarsete operaatorite ülekoormamine

Unaartehteid on ülekoormatud samamoodi nagu binaartehteid. Peamine erinevus on muidugi see, et neil on ainult üks operaand. Moderniseerime eelmist näidet, lisades operaatorite ülekoormused ++, --, -:

Süsteemi kasutamine; kasutades System.Collections.Generic; kasutades System.Linq; kasutades System.Text; namespace ConsoleApplication1 ( klass MyArr ( // Punkti koordinaadid kolmemõõtmelises ruumis 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 ) // Binaarne operaator + avalik staatiline MyArr operaator +(MyArr obj1, MyArr obj2) ( MyArr arr = new MyArr(); arr.x = obj1.x + obj2; .x arr y = obj1.y + obj2.y; ( MyArr arr = new MyArr (); arr.x = obj1.x - obj2.x; arr.y = obj1.y - obj2.y; arr.z = obj1.z - obj2.z ) // Unaari ülekoormamine operaator - avalik staatiline MyArr operaator -(MyArr obj1) ( MyArr arr = new MyArr(); arr.x = -obj1.x; arr.y = -obj1.y; arr.z = -obj1.z; return arr; ) // Unaaroperaatori ülekoormamine ++ avalik staatiline MyArr operaator ++(MyArr obj1) ( obj1.x += 1; obj1.y += 1; obj1.z +=1; return obj1; ) // Unaaroperaatori ülekoormamine operaator -- avalik staatiline MyArr operaator ---(MyArr obj1) ( obj1.x -= 1; obj1.y -= 1; obj1.z -= 1; tagasta obj1; ) ) klass Program ( staatiline void Main(string args) ( MyArr Point1 = new MyArr(1, 12, -4); MyArr Point2 = new MyArr(0, -3, 18); Console.WriteLine("Esimese koordinaadid punkt: " + Punkt1.x + " " + Punkt1.y + " " + Punkt1.z); Console.WriteLine("Teise punkti koordinaadid: " + Punkt2.x + " " + Punkt2.y + " " + Punkt2.z + „\n”); WriteLine("Punkt1 - Punkt2 = " + Punkt3.x + " " + Punkt3.y + " " + Punkt3.z Console.WriteLine("-Punkt1 = " + Punkt3.x + " " + Punkt3.y + "); " + Punkt3.z); Punkt2++; Console.WriteLine("Punkt2++ = " + Punkt2.x + " " + Punkt2.y + " " + Punkt2.z); Punkt2--; Konsool. WriteLine("Punkt2-- = " + Punkt2.x + " " + Punkt2.y + " " + Punkt2.z());

Viimane uuendus: 20.10.2017

Operaatori ülekoormus võimaldab teil määratleda operaatori sooritatavad toimingud. Ülekoormamine hõlmab funktsiooni loomist, mille nimi sisaldab sõna operaator ja ülekoormatud operaatori sümbolit. Operaatori funktsiooni saab määratleda klassi liikmena või väljaspool klassi.

Üle koormata saab ainult neid operaatoreid, mis on juba C++-s defineeritud. Uusi operaatoreid luua ei saa.

Kui operaatorfunktsioon on defineeritud eraldi funktsioonina ja see ei ole klassi liige, siis on sellise funktsiooni parameetrite arv sama, mis operandi operandide arv. Näiteks funktsioonil, mis esindab unaarset operaatorit, on üks parameeter ja funktsioonil, mis esindab binaarset operaatorit, on kaks parameetrit. Kui operaator võtab kaks operandi, siis esimene operand edastatakse funktsiooni esimesele parameetrile ja teine ​​operand teisele parameetrile. Sel juhul peab vähemalt üks parameetritest esindama klassi tüüpi

Vaatame näidet klassiga Counter, mis tähistab stopperit ja salvestab sekundite arvu:

#kaasa << seconds << " seconds" << std::endl; } int seconds; }; Counter operator + (Counter c1, Counter c2) { return Counter(c1.seconds + c2.seconds); } int main() { Counter c1(20); Counter c2(10); Counter c3 = c1 + c2; c3.display(); // 30 seconds return 0; }

Siin ei kuulu operaatori funktsioon loenduri klassi ja on määratletud väljaspool seda. See funktsioon koormab loenduri tüübi liitmisoperaatorit üle. See on binaarne, seega on vaja kahte parameetrit. Sel juhul lisame kaks loenduri objekti. Funktsioon tagastab ka loenduri objekti, mis salvestab sekundite koguarvu. See tähendab, et sisuliselt taandub siinne liitmisoperatsioon mõlema objekti sekundite lisamisele:

Loenduri operaator + (loendur c1, loendur c2) (tagasi loendur(c1.sekundit + c2.sekundit); )

Klassiobjekti tagastamine pole vajalik. See võib olla ka sisseehitatud primitiivset tüüpi objekt. Samuti saame määratleda täiendavaid operaatori ülekoormusi:

Int operaator + (loendur c1, int s) ( tagastab c1.sekundit + s; )

See versioon lisab numbrile loenduri objekti ja tagastab samuti numbri. Seetõttu peab toimingu vasak operandi tüüp olema Counter ja parem operandi tüüp int. Ja näiteks saame seda operaatori versiooni rakendada järgmiselt:

loendur c1(20); int sekundit = c1 + 25; // 45 std::cout<< seconds << std::endl;

Samuti saab operaatorfunktsioone määratleda klasside liikmetena. Kui operaatorfunktsioon on määratletud klassi liikmena, pääseb vasakpoolne operandi juurde selle osuti kaudu ja see esindab praegust objekti ning parem operand edastatakse ainsa parameetrina sarnasele funktsioonile:

#kaasa klass Loendur ( public: Counter(int sec) ( sekundit = s; ) void display() ( std::cout<< seconds << " seconds" << std::endl; } Counter operator + (Counter c2) { return Counter(this->sekundit + c2.sekundit); ) int operaator + (int s) ( tagasta see->sekundid + s; ) int sekundid; ); int main() (loendur c1(20); loendur c2(10); loendur c3 = c1 + c2; c3.display(); // 30 sekundit int sekundit = c1 + 25; // 45 return 0; )

Sel juhul pääseme operaatori funktsioonide vasakpoolsele operandile selle kursi kaudu.

Millised operaatorid tuleks kus ümber määratleda? Omistamise, indekseerimise (), kutsumise (()), klassi liikmele kursori (->) kaudu juurdepääsu operaatorid tuleks määratleda klassiliikme funktsioonidena. Klassiliikme funktsioonidena määratletakse tavaliselt ka operaatoreid, mis muudavad objekti olekut või on objektiga otseselt seotud (increment, decrement,). Kõiki teisi operaatoreid määratletakse sageli pigem üksikute funktsioonidena kui klassi liikmetena.

Võrdlusoperaatorid

Mitmed operaatorid on paarikaupa ülekoormatud. Näiteks kui defineerime == operaatori, siis peame defineerima ka operaatori !=. Ja operaatori määratlemisel< надо также определять функцию для оператора >. Näiteks laadime need operaatorid üle:

Booli operaator == (Loendur c1, Loendur c2) ( tagastab c1.sekundit == c2.sekundit; ) tõeväärtuse operaator != (Loendur c1, loendur c2) ( tagastab c1.sekundit != c2.sekundit; ) tõeväärtuse operaator > ( Loendur c1, loendur c2) ( tagastab c1.sekundit > c2.sekundit; ) booli operaator< (Counter c1, Counter c2) { return c1.seconds < c2.seconds; } int main() { Counter c1(20); Counter c2(10); bool b1 = c1 == c2; // false bool b2 = c1 >c2; // tõene std::cout<< b1 << std::endl; std::cout << b2 << std::endl; return 0; }

Määramise operaatorid

#kaasa klass Loendur ( public: Counter(int sec) ( sekundit = s; ) void display() ( std::cout<< seconds << " seconds" << std::endl; } Counter& operator += (Counter c2) { seconds += c2.seconds; return *this; } int seconds; }; int main() { Counter c1(20); Counter c2(10); c1 += c2; c1.display(); // 30 seconds return 0; }

Kasvamis- ja kahandamistehted

Kasvamis- ja kahandamistehte ümberdefineerimine võib olla eriti keeruline, kuna peame nende operaatorite jaoks määratlema nii ees- kui ka järelliite vormid. Määratleme loenduri tüübi jaoks sarnased operaatorid:

#kaasa klass Loendur ( public: Counter(int sec) ( sekundit = s; ) void display() ( std::cout<< seconds << " seconds" << std::endl; } // префиксные операторы Counter& operator++ () { seconds += 5; return *this; } Counter& operator-- () { seconds -= 5; return *this; } // постфиксные операторы Counter operator++ (int) { Counter prev = *this; ++*this; return prev; } Counter operator-- (int) { Counter prev = *this; --*this; return prev; } int seconds; }; int main() { Counter c1(20); Counter c2 = c1++; c2.display(); // 20 seconds c1.display(); // 25 seconds --c1; c1.display(); // 20 seconds return 0; }

Loendur& operaator++ () ( sekundit += 5; tagasta *see; )

Funktsioonis endas saate määratleda väärtuse suurendamise loogika. Sel juhul suureneb sekundite arv 5 võrra.

Postfixi operaatorid peavad tagastama objekti väärtuse enne juurdekasvu, st objekti eelmise oleku. Et postfiksi vorm prefiksi vormist erineks, saavad postfixi versioonid lisaparameetri tüübiga int, mida ei kasutata. Kuigi põhimõtteliselt saame seda kasutada.

Loenduri operaator++ (int) ( Loendur eelmine = *see; ++*see; tagasta eelmine; )