Poslední aktualizace: 12.08.2018

Spolu s metodami můžeme přetížit i operátory. Řekněme například, že máme následující třídu Counter:

Class Counter ( public int Value ( get; set; ) )

Tato třída představuje čítač, jehož hodnota je uložena ve vlastnosti Value.

A řekněme, že máme dva objekty třídy Counter - dva čítače, které chceme porovnat nebo přidat na základě jejich vlastnosti Value, pomocí standardních operací porovnávání a sčítání:

Čítač c1 = nový Čítač(Hodnota = 23); Čítač c2 = nový čítač ( Hodnota = 45 ); bool výsledek = c1 > c2; Čítač c3 = c1 + c2;

Ale dál tento moment Pro objekty Counter není k dispozici srovnání ani sčítání. Tyto operace lze použít na řadě primitivních typů. Ve výchozím nastavení můžeme například přidat číselné hodnoty, ale kompilátor neví, jak přidat objekty složitých typů - třídy a struktury. A k tomu potřebujeme přetížit operátory, které potřebujeme.

Přetěžování operátorů spočívá v definování speciální metody ve třídě, pro jejíž objekty chceme definovat operátor:

Veřejný statický operátor typu return_type operátor (parametry) ( )

Tato metoda musí mít veřejné statické modifikátory, protože přetížení operátora bude použito pro všechny objekty této třídy. Následuje název návratového typu. Návratový typ představuje typ, jehož objekty chceme přijímat. Například v důsledku přidání dvou objektů Counter očekáváme získání nového objektu Counter. A jako výsledek porovnání těchto dvou chceme získat objekt typu bool, který udává, zda je podmíněný výraz pravdivý nebo nepravdivý. Ale v závislosti na úkolu mohou být návratové typy jakékoli.

Pak je místo názvu metody operátor klíčového slova a samotný operátor. A pak jsou parametry uvedeny v závorkách. Binární operátory berou dva parametry, unární operátory jeden parametr. A v každém případě jeden z parametrů musí představovat typ – třídu nebo strukturu, ve které je operátor definován.

Například přetížíme řadu operátorů pro třídu Counter:

Class Counter ( public int Value ( get; set; ) public static Operátor čítače +(Counter c1, Counter c2) ( return new Counter ( Value = c1.Value + c2.Value ); ) public static bool operátor >(Counter c1, Čítač c2) ( návrat c1.Value > c2.Value; ) veřejný statický boolův operátor<(Counter c1, Counter c2) { return c1.Value < c2.Value; } }

Protože všechny přetížené operátory jsou binární, to znamená, že se provádějí na dvou objektech, existují dva parametry pro každé přetížení.

Protože v případě operace sčítání chceme přidat dva objekty třídy Counter, operátor akceptuje dva objekty této třídy. A protože chceme jako výsledek sčítání získat nový objekt Counter, používá se tato třída také jako návratový typ. Všechny akce tohoto operátora vedou k vytvoření nového objektu, jehož vlastnost Value kombinuje hodnoty vlastnosti Value obou parametrů:

Operátor veřejného statického počítadla + (Počítadlo c1, Počítadlo c2) ( návrat nového počítadla ( Hodnota = c1.Value + c2.Value ); )

Předefinovány byly také dva srovnávací operátory. Pokud přepíšeme jeden z těchto operátorů porovnání, musíme přepsat i druhý z těchto operátorů. Samotné porovnávací operátory porovnávají hodnoty vlastností Value a v závislosti na výsledku porovnání vrátí buď true nebo false.

Nyní v programu používáme přetížené operátory:

Static void Main(string args) ( Counter c1 = nové Counter ( Hodnota = 23 ); Counter c2 = New Counter ( Value = 45 ); bool result = c1 > c2; Console.WriteLine(result); // false Counter c3 = c1 + c2; Console.WriteLine(c3.Value); // 23 + 45 = 68 Console.ReadKey(); )

Stojí za zmínku, že vzhledem k tomu, že definice operátoru je v podstatě metoda, můžeme i tuto metodu přetížit, tedy vytvořit pro ni další verzi. Například do třídy Counter přidejte další operátor:

Veřejný statický operátor int + (Počítadlo c1, hodnota int) ( návrat c1.Value + hodnota; )

Tato metoda přidá hodnotu vlastnosti Value a určité číslo a vrátí jejich součet. A můžeme také použít tento operátor:

Čítač c1 = nový Čítač(Hodnota = 23); int d = cl + 27; // 50 Console.WriteLine(d);

Je třeba vzít v úvahu, že při přetížení by se ty objekty, které jsou operátorovi předávány prostřednictvím parametrů, neměly měnit. Například můžeme definovat operátor přírůstku pro třídu Counter:

Veřejný statický operátor počítadla ++(Počítadlo c1) ( c1.Hodnota += 10; návrat c1; )

Protože je operátor unární, bere pouze jeden parametr - objekt třídy, ve které je operátor definován. Toto je však nesprávná definice přírůstku, protože operátor by neměl měnit hodnoty svých parametrů.

A správnější přetížení operátoru přírůstku by vypadalo takto:

Operátor veřejného statického počítadla ++(Počítadlo c1) (vrácení nového počítadla (Hodnota = c1.Value + 10); )

To znamená, že je vrácen nový objekt, který obsahuje zvýšenou hodnotu ve vlastnosti Value.

V tomto případě nemusíme definovat samostatné operátory pro inkrementaci prefixu a postfixu (stejně jako dekrementaci), protože jedna implementace bude fungovat v obou případech.

Například používáme operaci zvýšení předpony:

Counter counter = new Counter() ( Hodnota = 10 ); Console.WriteLine($"(counter.Value)"); // 10 Console.WriteLine($"((++counter).Value)"); // 20 Console.WriteLine($"(counter.Value)"); // 20

Výstup konzole:

Nyní použijeme přírůstek postfixu:

Counter counter = new Counter() ( Hodnota = 10 ); Console.WriteLine($"(counter.Value)"); // 10 Console.WriteLine($"((counter++).Value)"); // 10 Console.WriteLine($"(counter.Value)"); // 20

Výstup konzole:

Za zmínku také stojí, že můžeme přepsat pravdivé a nepravdivé operátory. Definujme je například ve třídě Counter:

Class Counter ( public int Value ( get; set; ) public static bool operator true(Counter c1) ( return c1.Value != 0; ) public static bool operator false(Counter c1) ( return c1.Value == 0; ) // zbytek obsahu třídy)

Tyto operátory jsou přetížené, když chceme použít objekt typu jako podmínku. Například:

Čítač čítač = new Counter() ( Hodnota = 0 ); if (counter) Console.WriteLine(true); else Console.WriteLine(false);

Při přetěžování operátorů musíte počítat s tím, že ne všechny operátory lze přetížit. Zejména můžeme přetížit následující operátory:

    unární operátory +, -, !, ~, ++, --

    binární operátory +, -, *, /, %

    srovnávací operace ==, !=,<, >, <=, >=

    logické operátory &&, ||

    operátory přiřazení +=, -=, *=, /=, %=

A existuje řada operátorů, které nelze přetížit, například operátor rovnosti = nebo ternární operátor?:, a také řada dalších.

Kompletní seznam přetížených operátorů najdete v dokumentaci msdn

Při přetěžování operátorů je také potřeba pamatovat na to, že nemůžeme změnit prioritu operátoru ani jeho asociativitu, nemůžeme vytvořit nový operátor ani změnit logiku operátorů v typech, které jsou v .NET výchozí.

Dobrý den!

Touha napsat tento článek se objevila po přečtení příspěvku, protože mnoho důležitých témat v něm nebylo pokryto.

Nejdůležitější je zapamatovat si, že přetížení operátora je jen více pohodlný způsob volání funkcí, takže se nenechte unést přetížením operátora. Mělo by se používat pouze tehdy, když to usnadní psaní kódu. Ale ne tolik, aby to ztěžovalo čtení. Koneckonců, jak víte, kód se čte mnohem častěji, než je zapsán. A nezapomeňte, že nikdy nebudete moci přetěžovat operátory v tandemu s vestavěnými typy; možnost přetížení je k dispozici pouze pro uživatelem definované typy/třídy.

Syntaxe přetížení

Syntaxe přetížení operátora je velmi podobná definici funkce nazvané operátor@, kde @ je identifikátor operátora (například +, -,<<, >>). Uvažujme nejjednodušší příklad:
class Integer ( private: int value; public: Integer(int i): value(i) () const Integer operator+(const Integer& rv) const ( return (value + rv.value); ) );
V tomto případě je operátor orámován jako člen třídy, argument určuje hodnotu umístěnou na pravé straně operátoru. Obecně existují dva hlavní způsoby přetížení operátorů: globální funkce, které jsou přátelské ke třídě, nebo inline funkce třídy samotné. Která metoda je pro kterého operátora lepší, zvážíme na konci tématu.

Ve většině případů operátory (kromě podmíněných) vracejí objekt nebo odkaz na typ, ke kterému patří jeho argumenty (pokud se typy liší, pak se rozhodnete, jak interpretovat výsledek vyhodnocení operátorů).

Přetížení unárních operátorů

Podívejme se na příklady přetížení unárních operátorů pro třídu Integer definovanou výše. Zároveň je definujme ve formě přátelských funkcí a zvažte operátory dekrementace a inkrementace:
class Integer ( private: int value; public: Integer(int i): value(i) () //unary + friend const Integer& operator+(const Integer& i); //unary - friend const Integer operator-(const Integer& i) ; //přírůstek předpony friend const Integer& operator++(Integer& i); //přírůstek s příponou přítel const Integer operator++(Integer& i, int); //předpona snížení friend const Integer& operator--(Integer& i); //postfix snížení přítele const Operátor celého čísla--(Integer& i, int); ); //unární plus nedělá nic. const Integer& operator+(const Integer& i) ( return i.value; ) const Integer operator-(const Integer& i) ( return Integer(-i.value); ) //verze prefixu vrátí hodnotu po přírůstku const Integer& operator++(Integer& i) ( i.value++; return i; ) //verze postfixu vrátí hodnotu před přírůstkem const Integer operator++(Integer& i, int) ( Integer oldValue(i.value); i.value++; return oldValue; ) //verze prefixu vrátí hodnota po dekrementaci const Integer& operátor--(Celé číslo& i) ( i.hodnota--; return i; ) //verze postfixu vrátí hodnotu před snížením const Integer operator--(Integer& i, int) ( Integer oldValue(i. hodnota); i .value--;vrátit starouValue; )
Nyní víte, jak kompilátor rozlišuje mezi prefixovou a postfixovou verzí dekrementace a inkrementace. V případě, že vidí výraz ++i, zavolá se operátor funkce ++(a). Pokud vidí i++, zavolá se operátor ++(a, int). To znamená, že se volá funkce přetíženého operátora++ a k tomu slouží parametr dummy int ve verzi postfixu.

Binární operátory

Podívejme se na syntaxi přetěžování binárních operátorů. Pojďme přetížit jeden operátor, který vrací l-hodnotu, one podmíněný operátor a jeden operátor vytvářející novou hodnotu (definujme je globálně):
class Integer ( private: int value; public: Integer(int i): value(i) () friend const Integer operator+(const Integer& left, const Integer& right); friend Integer& operator+=(Integer& left, const Integer& right); friend bool operator==(const Integer& left, const Integer& right); ); const Integer operator+(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; )
Ve všech těchto příkladech jsou operátoři přetíženi pro stejný typ, to však není nutné. Můžete například přetížit sčítání našeho typu Integer a Float definovaného jeho podobností.

Argumenty a návratové hodnoty

Jak vidíte, příklady používají různé cesty předávání argumentů funkcím a vracení hodnot operátorů.
  • Pokud není argument modifikován operátorem, například v případě unárního plus, musí být předán jako odkaz na konstantu. Obecně to platí téměř pro všechny aritmetické operátory(sčítání, odčítání, násobení...)
  • Typ návratové hodnoty závisí na povaze operátora. Pokud musí operátor vrátit novou hodnotu, musí být vytvořen nový objekt (jako v případě binárního plus). Pokud chcete zabránit tomu, aby byl objekt upraven jako l-hodnota, musíte jej vrátit jako konstantu.
  • Operátoři přiřazení musí vrátit odkaz na změněný prvek. Také, pokud chcete použít operátor přiřazení v konstrukcích jako (x=y).f(), kde je funkce f() volána pro proměnnou x, po jejím přiřazení k y, pak nevracejte odkaz na konstantní, stačí vrátit odkaz.
  • Logické operátory by měly v nejhorším případě vrátit int a v lepším případě bool.

Optimalizace návratové hodnoty

Při vytváření nových objektů a jejich vracení z funkce byste měli použít zápis podobný výše popsanému příkladu binárního operátoru plus.
return Integer(left.value + right.value);
Abych byl upřímný, nevím, jaká situace je relevantní pro C++ 11; všechny další argumenty jsou platné pro C++98.
Na první pohled to vypadá podobně jako syntaxe pro vytvoření dočasného objektu, to znamená, že mezi výše uvedeným kódem a tímto není žádný rozdíl:
Integer temp(left.value + right.value); návratová teplota;
Ale ve skutečnosti se v tomto případě na prvním řádku zavolá konstruktor, pak se zavolá kopírovací konstruktor, který objekt zkopíruje, a pak při rozbalování zásobníku se zavolá destruktor. Při použití první položky kompilátor nejprve vytvoří objekt v paměti, do které je třeba jej zkopírovat, čímž ušetří volání konstruktoru a destruktoru kopírování.

Speciální operátoři

C++ má operátory, které mají specifickou syntaxi a metody přetěžování. Například operátor indexování. Je vždy definován jako člen třídy, a protože indexovaný objekt se má chovat jako pole, měl by vrátit odkaz.
Čárkový operátor
Mezi "speciální" operátory patří také operátor čárka. Je volána u objektů, které mají vedle sebe čárku (ale není volána na seznamech argumentů funkcí). Vymyslet smysluplný případ použití pro tohoto operátora není snadné. Habrauser v komentářích k předchozímu článku o přetížení.
Operátor dereference ukazatele
Přetížení těchto operátorů může být ospravedlnitelné pro třídy inteligentních ukazatelů. Tento operátor je nutně definován jako funkce třídy a jsou na něj kladena určitá omezení: musí vracet buď objekt (nebo odkaz) nebo ukazatel, který umožňuje přístup k objektu.
Operátor přiřazení
Operátor přiřazení je nutně definován jako funkce třídy, protože je vnitřně propojen s objektem nalevo od "=". Definování operátoru přiřazení v globálně by umožnilo přepsat výchozí chování operátoru "=". Příklad:
class Integer ( private: int value; public: Integer(int i): value(i) () Integer& operator=(const Integer& right) ( //kontrola samopřiřazení if (this == &right) ( return *this; ) value = right.value; return *this; ) );

Jak vidíte, na začátku funkce je provedena kontrola samopřiřazení. Obecně je v tomto případě sebepřivlastňování neškodné, ale ne vždy je situace tak jednoduchá. Pokud je objekt například velký, můžete ztrácet spoustu času zbytečným kopírováním nebo při práci s ukazateli.

Nepřetížitelné operátory
Některé operátory v C++ nejsou vůbec přetížené. Zřejmě se tak stalo z bezpečnostních důvodů.
  • Operátor výběru členů třídy ".".
  • Operátor pro dereferencování ukazatele na člena třídy ".*"
  • V C++ není žádný operátor umocňování (jako ve Fortranu) "**".
  • Je zakázáno definovat vlastní operátory (může dojít k problémům s určením priorit).
  • Priority operátora nelze změnit
Jak jsme již zjistili, existují dva způsoby operátorů - jako třídní funkce a jako přátelská globální funkce.
Rob Murray ve své knize C++ Strategies and Tactics definuje následující pokyny pro výběr formy operátora:

proč tomu tak je? Za prvé, někteří operátoři jsou zpočátku omezeni. Obecně platí, že pokud neexistuje žádný sémantický rozdíl v tom, jak je operátor definován, je lepší jej navrhnout jako funkci třídy pro zdůraznění spojení a navíc funkce bude inline. Navíc někdy může být potřeba reprezentovat levý operand jako objekt jiné třídy. Asi nejvýraznějším příkladem je redefinice<< и >> pro I/O streamy.

V kapitole 15 se podíváme na dva typy speciálních funkcí: přetížené operátory a uživatelem definované konverze. Umožňují používat objekty tříd ve výrazech stejně intuitivním způsobem jako objekty vestavěných typů. V této kapitole nejprve nastíníme obecné koncepty pro návrh přetížených operátorů. Dále představíme koncept přátel třídy se speciálními přístupovými právy a prodiskutujeme, proč se používají, přičemž budeme věnovat zvláštní pozornost tomu, jak jsou implementovány některé přetížené operátory: přiřazení, index, volání, šipka člena třídy, zvýšení a snížení a specializované operátory. pro operátory třídy new a delete. Další kategorií speciálních funkcí probíraných v této kapitole jsou funkce pro převod členů (převodníky), které tvoří sadu standardních převodů pro typ třídy. Jsou použity implicitně kompilátorem, když jsou objekty třídy použity jako skutečné argumenty funkce nebo operandy vestavěných nebo přetížených operátorů. Kapitola končí podrobným představením pravidel pro řešení přetížení funkcí s přihlédnutím k předávání objektů jako argumentů, funkcí členů třídy a přetížených operátorů.

15.1. Přetížení operátora

Již v předchozích kapitolách jsme si ukázali, že přetížení operátorů umožňuje programátorovi zavést vlastní verze předdefinovaných operátorů (viz kapitola 4) pro operandy typu třídy. Například třída String z oddílu 3.15 má mnoho přetížených operátorů. Níže je jeho definice:

#zahrnout třída String; istream& operator>>(istream &, const String &); ostream & 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; };

Třída String má tři sady přetížených operátorů. První je sada operátorů přiřazení:

Nejprve přichází operátor přiřazení kopírování. (Tyto jsou podrobně popsány v části 14.7.) Následující příkaz podporuje přiřazení C-řetězce znaků k objektu zadejte řetězec:

Název řetězce; jméno = "Sherlock"; // použití operátoru operator=(char *)

(Na jiné operátory přiřazení než operátory kopírování se podíváme v sekci 15.3.)

Ve druhé sadě je pouze jeden operátor - vezme index:

// přetížený indexový operátor char& operator(int);

Umožňuje programu indexovat objekty třídy String stejným způsobem jako pole objektů vestavěného typu:

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

(Tento operátor je podrobně popsán v části 15.4.)

Třetí sada definuje přetížené operátory rovnosti pro objekty třídy String. Program může zkontrolovat rovnost dvou takových objektů nebo objektu a řetězce C:

// sada přetížených operátorů rovnosti // str1 == str2; bool operator==(const char *); bool operator==(const String &);

Přetížené operátory vám umožňují používat objekty typu třídy s operátory definovanými v kapitole 4 a manipulovat s nimi stejně intuitivně jako s objekty vestavěných typů. Pokud bychom například chtěli definovat operaci zřetězení dvou objektů String, mohli bychom ji implementovat jako členskou funkci concat(). Ale proč concat() a ne, řekněme, append()? Název, který jsme zvolili, je logický a snadno zapamatovatelný, ale uživatel stále může zapomenout, jak jsme funkci pojmenovali. Často je snazší zapamatovat si název, pokud definujete přetížený operátor. Například místo concat() bychom zavolali operátor nové operace+=(). Tento operátor se používá následovně:

#include "String.h" int main() ( Název řetězce1 "Sherlock"; Název řetězce2 "Holmes"; jméno1 += " "; jméno1 += jméno2; if (! (jméno1 == "Sherlock Holmes")) cout< < "конкатенация не сработала\n"; }

Přetížený operátor je v těle třídy deklarován stejně jako běžná členská funkce, skládá se pouze z jeho názvu klíčové slovo operátor následovaný jedním z mnoha operátorů předdefinovaných v jazyce C++ (viz tabulka 15.1). Takto můžete deklarovat operátor+=() ve třídě String:

Class String ( public: // sada přetížených operátorů += String& operator+=(const String &); String& operator+=(const char *); // ... private: // ... );

a definuj to takto:

#zahrnout inline String& String::operator+=(const String &rhs) ( // Pokud řetězec, na který odkazuje rhs, není prázdný if (rhs._string) ( String tmp(*this); // přidělte oblast paměti // dostatečné pro uložení zřetězených řetězců _size += rhs._size; delete _string; _string = new char[ _size + 1 ]; // nejprve zkopírujte původní řetězec do vybrané oblasti // poté připojte na konec řetězec, na který odkazuje rhs strcpy (_string, tmp._string) ; strcpy(_string + tmp._size, rhs._string); ) return *this; ) inline String& String::operator+=(const char *s) ( // Pokud ukazatel s nemá hodnotu null if (s) ( String tmp(*this ); // přidělte dostatečnou paměťovou oblast // pro uložení zřetězených řetězců _size += strlen(s); delete _string; _string = nový znak[ _velikost + 1 ]; // první kopie původní řetězec do alokované oblasti // poté připojte na konec řetězce C, na který odkazuje s strcpy(_string, tmp._string); strcpy(_string + tmp._size, s); ) return *this; )

15.1.1. Členové a nečlenové třídy

Podívejme se blíže na operátory rovnosti v naší třídě String. První operátor vám umožňuje nastavit rovnost mezi dvěma objekty a druhý operátor vám umožňuje nastavit rovnost mezi objektem a C-řetězcem:

#include "String.h" int main() ( Řetězec květina; // napište něco do proměnné květina if (květ == "lilie") // opravte // ... else if ("tulipán" == květina ) // chyba // ... )

Při prvním použití operátoru rovnosti v main() se zavolá přetížený operátor==(const char *) třídy String. Ve druhém příkazu if však kompilátor vyvolá chybovou zprávu. Co se děje?

Přetížený operátor, který je členem třídy, se používá pouze v případě, že levý operand je objektem této třídy. Protože ve druhém případě levý operand nepatří do třídy String, kompilátor se snaží najít vestavěný operátor, pro který levý operand může být C-řetězec a pravý operand může být objekt třídy String. Samozřejmě neexistuje, takže kompilátor hlásí chybu.

Ale můžete vytvořit objekt třídy String z C-řetězce pomocí konstruktoru třídy. Proč by kompilátor neprovedl následující převod implicitně:

If (String("tulipán") == květina) //správně: je volán člen operátora

Důvodem je jeho neúčinnost. Přetížené operátory nevyžadují, aby oba operandy byly stejného typu. Třída Text například definuje následující operátory rovnosti:

Class Text ( public: Text(const char * = 0); Text(const Text &); // sada přetížených operátorů rovnosti bool operator==(const char *) const; bool operator==(const String &) const; bool operator==(const Text &) const; // ... );

a výraz v main() lze přepsat takto:

If (Text("tulipán") == květina) // volá Text::operator==()

Aby tedy kompilátor našel vhodný operátor rovnosti pro srovnání, bude muset projít všechny definice tříd a hledat konstruktor, který dokáže přetypovat levý operand na nějaký typ třídy. Poté pro každý z těchto typů musíte zkontrolovat všechny související přetížené operátory rovnosti, abyste zjistili, zda některý z nich může provést srovnání. A pak se kompilátor musí rozhodnout, která z nalezených kombinací konstruktoru a operátoru rovnosti (pokud existuje) nejlépe odpovídá operandu na pravé straně! Pokud požadujete, aby kompilátor provedl všechny tyto akce, doba překladu programů C++ se dramaticky prodlouží. Místo toho se kompilátor dívá pouze na přetížené operátory definované jako členy třídy levého operandu (a jejích základních tříd, jak ukážeme v kapitole 19).

Je však povoleno definovat přetížené operátory, které nejsou členy třídy. Při analýze řádku v main(), který způsobil chybu kompilace, byly takové příkazy brány v úvahu. Porovnání, ve kterém se C-řetězec objevuje na levé straně, lze tedy opravit nahrazením operátorů rovnosti, které jsou členy třídy String, operátory rovnosti deklarovanými v rozsahu jmenného prostoru:

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

Všimněte si, že tyto globální přetížené operátory mají o jeden parametr více než členské operátory. Pokud je operátor členem třídy, pak je tento ukazatel implicitně předán jako první parametr. To znamená pro členské operátory výraz

Květina == "lilie"

je kompilátorem přepsán jako:

Flower.operator==("lilie")

a pomocí tohoto lze odkazovat na levý operand květiny v definici přetíženého členu operátoru. (Indikátor this byl zaveden v části 13.4.) V případě přetížení globálního operátora musí být parametr reprezentující levý operand specifikován explicitně.

Pak ten výraz

Květina == "lilie"

hovory operátora

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

Není jasné, který operátor je volán pro druhé použití operátoru rovnosti:

"tulipán" == květina

Takto přetížený operátor jsme nedefinovali:

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

Ale to je volitelné. Když je přetíženým operátorem funkce ve jmenném prostoru, pak se uvažují možné konverze jak pro jeho první, tak pro druhý parametr (pro levý a pravý operand), tzn. kompilátor interpretuje druhé použití operátoru rovnosti jako

Operátor==(String("tulipán"), květina);

a zavolá následující přetížený operátor, aby provedl porovnání: bool operator==(const String &, const String &);

Proč jsme ale poskytli druhý přetížený operátor: bool operator==(const String &, const char *);

Převod typu z C-řetězce na třídu String lze použít i na pravý operand. Funkce main() se zkompiluje bez chyb, pokud jednoduše definujete přetížený operátor ve jmenném prostoru, který trvá dva operandy String:

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

Mám uvést pouze tohoto operátora nebo dva další:

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

závisí na tom, jak velké jsou náklady na převod z řetězce C na řetězec za běhu, tedy na „ceně“ dalších volání konstruktoru v programech, které používají naši třídu String. Pokud se operátor rovnosti bude často používat k porovnání řetězců C a objektů, je lepší poskytnout všechny tři možnosti. (K otázce efektivity se vrátíme v části o přátelích.

O přetypování na typ třídy pomocí konstruktorů si povíme více v sekci 15.9; Část 15.10 pojednává o povolení přetížení funkcí pomocí popsaných transformací a část 15.12 pojednává o povolení přetížení operátora.)

Jaký je tedy základ pro rozhodnutí, zda operátor učinit členem třídy nebo členem jmenného prostoru? V některých případech programátor prostě nemá na výběr:

  • Pokud je přetížený operátor členem třídy, je volán pouze v případě, že levý operand je členem této třídy. Pokud je levý operand jiného typu, musí být operátor členem jmenného prostoru;
  • Jazyk vyžaduje, aby operátor přiřazení ("="), index (""), volání ("()") a přístupová šipka člena ("->") byly definovány jako členové třídy. V opačném případě se zobrazí chybová zpráva kompilace:
// chyba: musí být členem třídy char& operator(String &, int ix);

(Operátor přiřazení je podrobněji probrán v části 15.3, operátor indexu v části 15.4, operátor volání v části 15.5 a operátor přístupu člena šipky v části 15.6.)

V ostatních případech rozhoduje návrhář třídy. Symetrické operátory, jako je operátor rovnosti, se nejlépe definují ve jmenném prostoru, pokud může být členem třídy jakýkoli operand (jako v případě String).

Než dokončíme tuto podsekci, definujme operátory rovnosti pro třídu String ve jmenném prostoru:

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. Přetížená jména operátorů

Přetížit lze pouze předdefinované operátory jazyka C++ (viz tabulka 15.1).

Tabulka 15.1. Přetížitelné operátory

+ - * / % ^ & | ~ ! , = < > <= >= ++ -- << >> == != && || += -= /= %= ^= &= |= *= <= >>= () -> ->* nový nový smazat smazat

Návrhář třídy nemá právo prohlásit operátor s jiným jménem za přetížený. Pokud se tedy pokusíte deklarovat operátor ** pro umocnění, kompilátor vyvolá chybovou zprávu.

Následující čtyři operátory C++ nelze přetížit:

// nepřetížitelné operátory:: .* . ?:

Předdefinované přiřazení operátora nelze u vestavěných typů změnit. Není například povoleno přepsat vestavěný operátor sčítání celých čísel pro kontrolu přetečení.

// chyba: nelze přepsat vestavěný operátor sčítání int int operator+(int, int);

Nemůžete také definovat další operátory pro vestavěné datové typy, jako je přidání operátoru+ do sady vestavěných operátorů pro přidání dvou polí.

Přetížený operátor je definován výlučně pro operandy typu třídy nebo výčtu a může být deklarován pouze jako člen třídy nebo jmenného prostoru s alespoň jedním parametrem typu třídy nebo výčtu (předávaným hodnotou nebo odkazem).

Předdefinované priority operátora (viz část 4.13) nelze změnit. Bez ohledu na typ třídy a implementaci operátoru v příkazu

X == y + z;

operator+ se vždy provede jako první a potom operator==; můžete však změnit pořadí pomocí závorek.

Rovněž musí být zachována předdefinovaná arita operátorů. Například unární logický operátor NOT nelze definovat jako binární operátor pro dva objekty typu String. Následující implementace je nesprávná a bude mít za následek chybu kompilace:

// špatně: ! je unární operátor bool operátor!(const String &s1, const String &s2) ( return (strcmp(s1.c_str(), s2.c_str()) != 0); )

U vestavěných typů se čtyři předdefinované operátory ("+", "-", "*" a "&") používají buď jako unární nebo binární operátory. V kterékoli z těchto kapacit mohou být přetíženy.

Všechny přetížené operátory kromě operator() nepovolují výchozí argumenty.

15.1.3. Navrhování přetížených operátorů

Operátory přiřazení, adresy a čárky mají předdefinovaný význam, pokud jsou operandy objekty typu třídy. Mohou být ale také přetížené. Sémantika všech ostatních operátorů při aplikaci na takové operandy musí být explicitně specifikována vývojářem. Volba poskytovaných operátorů závisí na očekávaném využití třídy.

Měli byste začít definováním jeho veřejného rozhraní. Sada veřejných členských funkcí je tvořena na základě operací, které musí třída poskytovat uživatelům. Poté se rozhodne, které funkce by měly být implementovány jako přetížené operátory.

Po definování veřejného rozhraní třídy zkontrolujte, zda existuje logický soulad mezi operacemi a příkazy:

  • isEmpty() se stane operátorem LOGICAL NOT, operátorem!().
  • isEqual() se stane operátorem rovnosti, operator==().
  • copy() se stane operátorem přiřazení, operator=().

Každý operátor má nějakou přirozenou sémantiku. Binární + je tedy vždy spojeno se sčítáním a jeho mapování na podobnou operaci s třídou může být pohodlný a stručný zápis. Například pro typ matice je přidání dvou matic dokonale vhodným rozšířením binárního plus.

Příkladem nesprávného použití přetížení operátoru je definování operátoru+() jako operace odčítání, což je nesmyslné: neintuitivní sémantika je nebezpečná.

Takový operátor podporuje několik různých interpretací stejně dobře. Dokonale jasné a dobře odůvodněné vysvětlení toho, co operátor+() dělá, pravděpodobně neuspokojí uživatele třídy String, kteří si myslí, že slouží ke zřetězení řetězců. Pokud sémantika přetíženého operátoru není zřejmá, je lepší ji neposkytovat.

Ekvivalence sémantiky složeného operátoru a odpovídající sekvence jednoduchých operátorů pro vestavěné typy (například ekvivalence operátoru + následovaného = a složeného operátoru +=) musí být také explicitně zachována pro třídu. . Předpokládejme, že String má definované operátory +() i operátor=(), aby podporovaly operace zřetězení a kopírování po členech:

Řetězec s1("C"); Řetězec s2("++"); s1 = s1 + s2; // s1 == "C++"

To však nestačí k podpoře operátoru složeného přiřazení

SI + = s2;

Měl by být definován explicitně, aby podporoval očekávanou sémantiku.

Cvičení 15.1

Proč následující srovnání nevolá přetížený operátor==(const String&, const String&):

"dláždění" == "kámen"

Cvičení 15.2

Napište přetížené operátory nerovnosti, které lze použít v takových srovnáních:

Řetězec != Řetězec Řetězec != Řetězec C C-řetězec != Řetězec

Vysvětlete, proč jste se rozhodli implementovat jedno nebo více prohlášení.

Cvičení 15.3

Identifikujte ty členské funkce třídy Screen implementované v kapitole 13 (části 13.3, 13.4 a 13.6), které mohou být přetíženy.

Cvičení 15.4

Vysvětlete, proč jsou přetížené vstupní a výstupní operátory definované pro třídu String v části 3.15 deklarovány jako globální funkce, nikoli členské funkce.

Cvičení 15.5

Implementujte přetížené vstupní a výstupní operátory pro třídu Screen z kapitoly 13.

15.2. Přátelé

Podívejme se znovu na přetížené operátory rovnosti pro třídu String, definované v oboru názvů. Operátor rovnosti pro dva objekty typu String vypadá takto:

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: skutečný; )

Porovnejte tuto definici s definicí stejného operátoru jako členská funkce:

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

Museli jsme upravit způsob, jakým přistupujeme k soukromým členům třídy String. Protože nový operátor rovnosti je globální funkce není členská funkce, nemá přístup k soukromým členům třídy String. Členské funkce size() a c_str() se používají k získání velikosti objektu String a jeho základního řetězce znaků C.

Alternativní implementací je prohlásit operátory globální rovnosti za přátele třídy String. Pokud je funkce nebo operátor deklarován tímto způsobem, získá přístup k neveřejným členům.

Deklarace přítele (začíná klíčovým slovem friend) se vyskytuje pouze v rámci definice třídy. Protože přátelé nejsou členy třídy, která deklaruje přátelský vztah, nezáleží na tom, zda jsou deklarováni veřejně, soukromě nebo chránění. V níže uvedeném příkladu jsme se rozhodli umístit všechny takové deklarace bezprostředně za záhlaví třídy:

Class String ( friend bool operator==(const String &, const String &); friend bool operator==(const char *, const String &); friend bool operator==(const String &, const char *); public: // ... zbytek třídy String);

Tyto tři řádky deklarují tři přetížené porovnávací operátory, které patří do globálního rozsahu, jako přátele třídy String, a proto jejich definice mohou přímo přistupovat k soukromým členům třídy:

// přátelské operátory přímo přistupují k soukromým členům // třídy String 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; ) // atd.

Dalo by se namítnout, že přímý přístup k členům _size a _string není v tomto případě nutný, protože vestavěné funkce c_str() a size() jsou stejně účinné a stále zachovávají zapouzdření, což znamená, že není potřeba deklarovat operátory rovnosti pro třídu String její přátelé.

Jak víte, zda udělat z nečlenského operátora přítele třídy nebo použít funkce přístupového objektu? Obecně by měl vývojář snížit na minimum počet deklarovaných funkcí a operátorů, které mají přístup k vnitřní reprezentaci třídy. Pokud existují přístupové funkce, které poskytují stejnou účinnost, pak by měly být upřednostněny, čímž se operátory v oboru názvů izolují od změn reprezentace třídy, jak se to dělá u jiných funkcí. Pokud návrhář třídy neposkytuje přístupové funkce pro některé členy a operátor deklarovaný ve jmenném prostoru musí k těmto členům přistupovat, pak se použití mechanismu přátel stává nevyhnutelným.

Nejběžnějším použitím tohoto mechanismu je umožnit přetíženým operátorům, kteří nejsou členy třídy, přístup k jejím soukromým členům. Pokud by nebylo potřeba zajistit symetrii levého a pravého operandu, přetíženým operátorem by byla členská funkce s plnými přístupovými právy.

Ačkoli se deklarace přátel obvykle používají k odkazování na operátory, existují případy, kdy funkce ve jmenném prostoru, členská funkce jiné třídy nebo dokonce celá třída musí to takto deklarovat. Pokud je jedna třída prohlášena za přítele druhé třídy, pak všechny členské funkce první třídy mají přístup k neveřejným členům druhé třídy. Podívejme se na to na příkladu pomocí neoperátorských funkcí.

Třída musí deklarovat jako přítele každou z mnoha přetížených funkcí, kterým chce udělit neomezená přístupová práva:

Externí ostream& storeOn(ostream &, Obrazovka &); extern BitMap& storeOn(BitMap &, Screen &); // ... class Screen ( friend ostream& storeOn(ostream &, Screen &); friend BitMap& storeOn(BitMap &, Screen &); // ... );

Pokud funkce manipuluje s objekty dvou různé třídy a potřebuje přístup k jejich neveřejným členům, pak lze takovou funkci buď prohlásit za přítele obou tříd, nebo učinit členem jedné a přítelem druhé.

Deklarování funkce jako přítele dvou tříd by mělo vypadat takto:

Okno třídy; // toto je jen deklarační třída Screen ( friend bool is_equal(Screen &, Window &); // ... ); class Window ( friend bool is_equal(Screen &, Window &); // ... );

Pokud se rozhodneme učinit funkci členem jedné třídy a přítelem druhé třídy, pak budou deklarace vytvořeny následovně:

Okno třídy; class Screen ( // copy() - člen třídy Screen Screen& copy(Window &); // ... ); class Window ( // Screen::copy() - přítel třídy Window friend Screen& Screen::copy(Window &); // ... ); Screen& Screen::copy(Window &) ( /* ... */ )

Členská funkce jedné třídy nemůže být prohlášena za přítele jiné, pokud kompilátor neviděl svou vlastní definici třídy. To není vždy možné. Předpokládejme, že by Screen deklaroval některé členské funkce Window jako své přátele a Window by stejným způsobem deklarovalo některé členské funkce Screen. V tomto případě je celá třída Window prohlášena za přítele Screen:

Okno třídy; class Screen ( Friend class Window; // ... );

K soukromým členům třídy Screen lze nyní přistupovat z jakékoli členské funkce okna.

Cvičení 15.6

Implementujte vstupní a výstupní operátory definované pro třídu Screen ve cvičení 15.5 jako přátelé a upravte jejich definice tak, aby přímo přistupovaly k soukromým členům. Která implementace je lepší? Vysvětli proč.

15.3. Provozovatel =

Přiřazení jednoho objektu jinému objektu stejné třídy se provádí pomocí operátoru přiřazení kopie. (Tento zvláštní případ byl diskutován v části 14.7.)

Pro třídu lze definovat další operátory přiřazení. Pokud je třeba objektům třídy přiřadit hodnoty typu odlišného od této třídy, pak je povoleno definovat operátory, které přijímají podobné parametry. Například pro podporu přiřazení řetězce C k objektu String:

String car("Volks"); auto = "Studebaker";

poskytujeme operátor, který přijímá parametr typu const char*. Tato operace již byla v naší třídě deklarována:

Class String ( public: // operátor přiřazení pro char* String& operator=(const char *); // ... private: int _size; char *string; );

Tento operátor je implementován následovně. Pokud je objektu String přiřazen ukazatel null, stane se „null“. Jinak je mu přiřazena kopie C-řetězce:

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

Řetězec odkazuje na kopii řetězce C, na který ukazuje sobj. Proč kopie? Protože nemůžete přímo přiřadit sobj k členu _string:

String = sobj; // chyba: neshoda typu

sobj je ukazatel na const, a proto nemůže být přiřazen k ukazateli na "non-const" (viz část 3.5). Změňme definici operátoru přiřazení:

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

Nyní _string přímo odkazuje na řetězec C adresovaný sobj. To však vyvolává další problémy. Připomeňme, že řetězec C je typu const char*. Definování parametru jako ukazatele na non-const znemožňuje přiřazení:

Auto = "Studebaker"; // není povoleno s operator=(char *) !

Takže není na výběr. Chcete-li přiřadit řetězec C k objektu typu String, musí být parametr typu const char*.

Uložení přímého odkazu na řetězec C adresovaný sobj v _string vytváří další komplikace. Nevíme, na co přesně sobj ukazuje. Může to být pole znaků, které je upraveno způsobem, který objekt String nezná. Například:

Char ia = ("d", "a", "n", "c", "e", "r"); Strunová past = ia; // trap._string odkazuje na ia ia = "g"; // ale toto nepotřebujeme: // ia i trap jsou upraveny._string

Pokud by trap._string přímo odkazoval na ia, pak by objekt trap vykazoval zvláštní chování: jeho hodnota by se mohla změnit bez volání členských funkcí třídy String. Proto se domníváme, že přidělení oblasti paměti pro uložení kopie hodnoty C-string je méně nebezpečné.

Všimněte si, že operátor přiřazení používá delete. Člen _string obsahuje odkaz na pole znaků umístěné v haldě. Aby se zabránilo únikům paměti, paměť přidělená pro starý řádek se uvolní pomocí příkazu delete před přidělením paměti pro nový řádek. Protože _string adresuje pole znaků, měla by být použita verze pole delete (viz oddíl 8.4).

Poslední poznámka k operátorovi přiřazení. Jeho návratový typ je odkazem na třídu String. Proč přesně ten odkaz? Jde o to, že u vestavěných typů lze operátory přiřazení řetězit:

// zřetězení operátorů přiřazení int iobj, jobj; iobj = jobj = 63;

Jsou sdruženy zprava doleva, tzn. v předchozím příkladu se úkoly dělají takto:

Iobj = (jobj = 63);

To je také výhodné při práci s objekty třídy String: například je podporována následující konstrukce:

Řetězec ver, podstatné jméno; sloveso = podstatné jméno = "počítat";

První přiřazení z tohoto řetězce volá dříve definovaný operátor pro const char*. Typ výsledného výsledku musí být takový, aby jej bylo možné použít jako argument pro operátor přiřazení kopie třídy String. Proto i když parametr tohoto operátora je typu const char *, ale je vrácen odkaz na String.

Operátoři přiřazení mohou být přetíženi. Například v naší třídě String máme následující sadu:

// sada přetížených operátorů přiřazení String& operator=(const String &); String& operator=(const char *);

Pro každý typ, který může být přiřazen k objektu String, může existovat samostatný operátor přiřazení. Všechny takové operátory však musí být definovány jako členské funkce třídy.

15.4. Indexový operátor

Operátor indexu() lze definovat pro třídy, které představují abstrakci kontejneru, ze kterého se získávají jednotlivé prvky. Příklady takových kontejnerů zahrnují naši třídu String, třídu IntArray představenou v kapitole 2 nebo šablonu vektorové třídy definovanou ve standardní knihovně C++. Operátor indexu musí být členskou funkcí třídy.

Uživatelé String musí být schopni číst a zapisovat jednotlivé znaky člena _string. Chceme podporovat následující způsob použití objektů této třídy:

String entry("extravagantní"); Mycopy řetězce; for (int ix = 0; ix< entry.size(); ++ix) mycopy[ ix ] = entry[ ix ];

Operátor indexu se může objevit nalevo nebo napravo od operátoru přiřazení. Aby byl na levé straně, musí vrátit hodnotu l indexovaného prvku. Chcete-li to provést, vrátíme odkaz:

#zahrnout inine char& String::operator(int elem) const ( asse(elem >= 0 && elem)< _size); return _string[ elem ]; }

Následující úryvek přiřadí znak "V" prvku nula barevného pole:

Barva řetězce("fialová"); barva[ 0 ] = "V";

Upozorňujeme, že definice operátoru kontroluje, zda index přesahuje hranice pole. K tomu slouží funkce knihovny Casses() . Je také možné vyvolat výjimku označující, že hodnota elem je menší než 0 nebo větší než délka řetězce C, na který odkazuje _string. (Nastavování a zacházení s výjimkami bylo diskutováno v kapitole 11.)

15.5. Operátor volání funkcí

Operátor volání funkce může být přetížen pro objekty typu třídy. (Už jsme viděli, jak se používá, když jsme diskutovali o funkčních objektech v sekci 12.3.) Pokud je definována třída, která představuje operaci, je odpovídající operátor přetížen, aby ji mohl zavolat. Chcete-li například získat absolutní hodnotu int, můžete definovat třídu absInt:

Třída absInt ( public: int operator())(int val) ( int vysledek = val< 0 ? -val: val; return result; } };

Přetížený operátor operator() musí být deklarován jako členská funkce s libovolným počtem parametrů. Parametry a návratová hodnota mohou být libovolného typu povoleného pro funkce (viz sekce 7.2, 7.3 a 7.4). operator() se volá aplikací seznamu argumentů na objekt třídy, ve které je definován. Podíváme se, jak se používá v jednom z obecných algoritmů popsaných v kapitole . V následujícím příkladu je volán generický algoritmus transform() k aplikaci operace definované v absInt na každý prvek vektoru ivec, tj. nahradit prvek jeho absolutní hodnotou.

#zahrnout #zahrnout int main() ( int ia = (-0, 1, -1, -2, 3, 5, -5, 8); vektor ivec(ia, ia+8); // nahradí každý prvek jeho absolutní hodnotou transform(ivec.begin(), ivec.end(), ivec.begin(), absInt()); //...)

První a druhý argument pro transform() omezují rozsah prvků, na které se použije operace absInt. Třetí označuje začátek vektoru, kam bude uložen výsledek aplikace operace.

Čtvrtý argument je dočasný objekt absInt vytvořený pomocí výchozího konstruktoru. Konkretizace generického algoritmu transform() volaného z main() může vypadat takto:

Typedef vektor ::iterator iter_type; // instanciation transform() // operace absInt je aplikována na vektorový prvek int iter_type transform(iter_type iter, iter_type last, iter_type result, absInt func) ( while (iter != last) *result++ = func(*iter++); // voláno absInt::operator() return iter; )

func je objekt třídy, který poskytuje operaci absInt, která nahrazuje int jeho absolutní hodnotou. Používá se k volání přetíženého operátoru() třídy absInt. Tomuto operátoru je předán argument *iter, který ukazuje na prvek vektoru, pro který chceme získat absolutní hodnotu.

15.6. Operátor šipky

Operátor šipky, který umožňuje přístup k členům, může být přetížen pro objekty třídy. Musí být definována jako členská funkce a poskytovat sémantiku ukazatele. Tento operátor se nejčastěji používá ve třídách, které poskytují „chytrý ukazatel“, který se chová podobně jako ty vestavěné, ale zároveň podporuje některé další funkce.

Řekněme, že chceme definovat typ třídy reprezentující ukazatel na objekt Screen (viz kapitola 13):

Class ScreenPtr ( // ... private: Screen *ptr; );

Definice ScreenPtr musí být taková, aby objekt této třídy zaručeně ukazoval na objekt Screen: na rozdíl od vestavěného ukazatele nemůže být null. Aplikace pak může používat objekty typu ScreenPtr, aniž by kontrolovala, zda ukazují na nějaký objekt Screen. Chcete-li to provést, musíte definovat třídu ScreenPtr s konstruktorem, ale bez výchozího konstruktoru (konstruktory byly podrobně popsány v části 14.2):

Class ScreenPtr ( public: ScreenPtr(const Screen &s) : ptr(&s) ( ) // ... );

Jakákoli definice objektu třídy ScreenPtr musí obsahovat inicializátor – objekt třídy Screen, na který bude objekt ScreenPtr odkazovat:

ScreenPtr p1; // chyba: třída ScreenPtr nemá výchozí konstruktor Screen myScreen(4, 4); ScreenPtr ps(myScreen); // Že jo

Aby se třída ScreenPtr chovala jako vestavěný ukazatel, musíte definovat některé přetížené operátory – operátory dereference (*) a šipky pro přístup k členům:

// přetížené operátory pro podporu třídy chování ukazatele ScreenPtr ( public: Screen& operator*() ( return *ptr; ) Screen* operator->() ( return ptr; ) // ... ); Operátor přístupu členů je unární, takže se mu nepředávají žádné parametry. Při použití jako součást výrazu závisí jeho výsledek pouze na typu levého operandu. Například v instrukci point->action(); zkoumá se typ bodu. Pokud se jedná o ukazatel na nějaký typ třídy, pak platí sémantika vestavěného operátoru přístupu členů. Pokud se jedná o objekt nebo odkaz na objekt, pak se kontroluje, zda tato třída nemá přetížený operátor přístupu. Když je definován přetížený šipkový operátor, je volán na bodovém objektu, jinak je příkaz neplatný, protože tečkový operátor musí být použit pro přístup k členům samotného objektu (včetně odkazu). Přetížený operátor šipky musí vrátit buď ukazatel na typ třídy nebo objekt třídy, ve které je definován. Pokud je vrácen ukazatel, použije se na něj sémantika vestavěného operátoru šipky. Jinak proces pokračuje rekurzivně, dokud není získán ukazatel nebo není detekována chyba. Například takto můžete použít objekt ps třídy ScreenPtr pro přístup k členům obrazovky: ps->move(2, 3); Protože vlevo od šipkového operátoru je objekt typu ScreenPtr, je použit přetížený operátor této třídy, který vrací ukazatel na objekt Screen. Vestavěný šipkový operátor je pak aplikován na výslednou hodnotu pro volání členské funkce move(). Níže je malý program k otestování třídy ScreenPtr. Objekt typu ScreenPtr se používá stejným způsobem jako jakýkoli objekt typu Screen*: #include #zahrnout #include "Screen.h" void printScreen(const ScreenPtr &ps) ( cout<< "Screen Object (" << ps->výška()<< ", " << ps->šířka()<< ")\n\n"; for (int ix = 1; ix <= ps->výška(); ++ix) (pro (int iy = 1; iy<= ps->šířka(); ++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->výška(); ++ix) pro (int iy = 1; iy<= ps->šířka(); ++iy) ( ps->move(ix, iy); ps->set(init[ initpos++ ]); ) // Tisk obsahu obrazovky printScreen(ps); návrat 0; )

Samozřejmě, že takové manipulace s ukazateli na objekty třídy nejsou tak efektivní jako práce s vestavěnými ukazateli. Inteligentní ukazatel proto musí poskytovat další funkce, které jsou pro aplikaci důležité, aby ospravedlnily složitost jeho použití.

15.7. Operátory inkrementace a dekrementace

Pokračujeme ve vývoji implementace třídy ScreenPtr představené v předchozí části, zvážíme dva další operátory, které jsou podporovány pro vestavěné ukazatele a které je žádoucí mít pro náš inteligentní ukazatel: inkrementovat (++) a dekrementovat (- -). Chcete-li použít třídu ScreenPtr k odkazování na prvky pole objektů Screen, budete muset přidat několik dalších členů.

Nejprve definujeme nový člen size, který obsahuje buď nulu (označující, že objekt ScreenPtr ukazuje na jeden objekt), nebo velikost pole adresovaného objektem ScreenPtr. Potřebujeme také člen offsetu, který ukládá offset od začátku daného pole:

Class ScreenPtr ( public: // ... private: int size; // velikost pole: 0, pokud je jediným objektem int offset; // ptr offset od začátku pole Screen *ptr; );

Upravme konstruktor třídy ScreenPtr s ohledem na jeho novou funkcionalitu a další členy. Uživatel naší třídy musí konstruktoru předat další argument, pokud vytvářený objekt ukazuje na pole:

Class ScreenPtr ( public: ScreenPtr(Screen &s, int arraySize = 0) : ptr(&s), size (arraySize), offset(0) ( ) private: int size; int offset; Screen *ptr; );

Tento argument nastavuje velikost pole. Aby byla zachována stejná funkce, poskytneme pro ni výchozí hodnotu nula. Pokud je tedy druhý argument konstruktoru vynechán, člen size bude 0, a proto bude objekt ukazovat na jeden objekt Screen. Objekty nové třídy ScreenPtr lze definovat takto:

Screen myScreen(4, 4); ScreenPtr pobj(myScreen); // správně: ukazuje na jeden objekt const int arrSize = 10; Screen *parray = new Screen[ arrSize ]; ScreenPtr parr(*parray, arrSize); // správně: ukazuje na pole

Nyní jsme připraveni definovat přetížené operátory inkrementace a dekrementace v ScreenPtr. Existují však dva typy: prefix a postfix. Naštěstí lze identifikovat obě možnosti. Pro operátor předpony deklarace neobsahuje nic neočekávaného:

Class ScreenPtr ( public: Screen& operator++(); Screen& operator--(); // ... );

Takové operátory jsou definovány jako unární operátorové funkce. Operátor přírůstku předpony můžete použít například takto: const int arrSize = 10; Screen *parray = new Screen[ arrSize ]; ScreenPtr parr(*parray, arrSize); for (int ix = 0; ix

Definice těchto přetížených operátorů jsou uvedeny níže:

Screen& ScreenPtr::operator++() ( if (size == 0) ( cerr<<"не могу инкрементировать указатель для одного объекта\n"; return *ptr; } if (offset >= velikost - 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; }

Pro odlišení prefixů od postfixových operátorů mají deklarace těchto operátorů další parametr typu int. Následující úryvek deklaruje varianty předpony a přípony operátorů inkrementace a dekrementu pro třídu ScreenPtr:

Třída ScreenPtr ( public: Screen& operator++(); // prefixové operátory Screen& operator--(); Screen& operator++(int); // postfixové operátory Screen& operator--(int); // ... );

Níže je možná implementace postfixových operátorů:

Screen& ScreenPtr::operator++(int) ( if (size == 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--; }

Vezměte prosím na vědomí, že není nutné uvádět název druhého parametru, protože se nepoužívá v definici operátoru. Kompilátor sám pro něj poskytuje výchozí hodnotu, kterou lze ignorovat. Zde je příklad použití operátoru postfix:

Const int arrSize = 10; Screen *parray = new Screen[ arrSize ]; ScreenPtr parr(*parray, arrSize); for (int ix = 0; ix

Pokud jej zavoláte explicitně, musíte stále předat hodnotu druhého celočíselného argumentu. V případě naší třídy ScreenPtr je tato hodnota ignorována, takže to může být cokoliv:

Parr.operator++(1024); // volání operátora postfix++

Přetížené operátory inkrementace a dekrementace mohou být deklarovány jako přátelské funkce. Změňme odpovídajícím způsobem definici třídy ScreenPtr:

Třída ScreenPtr ( // deklarace nečlena friend Screen& operator++(Screen &); // prefix operator friend Screen& operator--(Screen &); friend Screen& operator++(Screen &, int); // postfix operator friend Screen& operator-- ( Screen &, int); public: // definice členů );

Cvičení 15.7

Napište definice přetížených operátorů inkrementace a dekrementu pro třídu ScreenPtr za předpokladu, že jsou deklarováni jako přátelé třídy.

Cvičení 15.8

Pomocí ScreenPtr můžete reprezentovat ukazatel na pole objektů třídy Screen. Upravte přetížení operátoru*() a operátoru >() (viz část 15.6), aby ukazatel nikdy neadresoval prvek před začátkem nebo za koncem pole. Tip: Tyto operátory by měly používat nové členy velikosti a posunu.

15.8. Operátoři nové a smazat

Ve výchozím nastavení se alokace objektu třídy z haldy a uvolnění paměti, kterou zabíral, provádí pomocí globálních operátorů new() a delete() definovaných ve standardní knihovně C++. (O těchto operátorech jsme hovořili v sekci 8.4.) Třída však může implementovat svou vlastní strategii správy paměti tím, že poskytne stejnojmenné členské operátory. Pokud jsou definovány ve třídě, jsou volány namísto globálních operátorů, aby přidělily a uvolnily paměť pro objekty této třídy.

Pojďme definovat operátory new() a delete() v naší třídě Screen.

Členský operátor new() musí vrátit hodnotu typu void* a vzít jako svůj první parametr hodnotu typu size_t, kde size_t je definice typu definovaná v systémovém hlavičkovém souboru. Zde je jeho oznámení:

Když se new() použije k vytvoření objektu typu třídy, kompilátor zkontroluje, zda je takový operátor v dané třídě definován. Pokud ano, pak se zavolá k přidělení paměti pro objekt, jinak se zavolá globální operátor new(). Například následující návod

Obrazovka *ps = nová obrazovka;

vytvoří v haldě objekt Screen, a protože tato třída má operátor new(), volá se. Parametr operátora size_t je automaticky inicializován na hodnotu rovnou velikosti obrazovky v bajtech.

Přidání nebo odebrání new() do třídy nemá žádný vliv na uživatelský kód. Call to new vypadá stejně pro globálního i členského operátora. Pokud by třída Screen neměla vlastní new(), pak by volání zůstalo správné, pouze by se místo členského operátora volal globální operátor.

Pomocí globálního operátoru rozlišení rozsahu můžete volat globální new(), i když třída Screen definuje svou vlastní verzi:

Obrazovka *ps = ::nová Obrazovka;

Když je operand delete ukazatel na objekt typu třídy, kompilátor zkontroluje, zda je v této třídě definován operátor delete(). Pokud ano, pak se zavolá k uvolnění paměti, jinak se zavolá globální verze operátoru. Další pokyny

Smazat ps;

Uvolní paměť obsazenou objektem Screen, na který ukazuje ps. Protože Screen má člena operátora delete(), používá se toto. Operátorský parametr typu void* se automaticky inicializuje na hodnotu ps. Přidání delete() do třídy nebo její odstranění z třídy nemá žádný vliv na uživatelský kód. Volání k odstranění vypadá stejně pro globálního operátora i pro člena operátora. Pokud by třída Screen neměla svůj vlastní operátor delete(), pak by volání zůstalo správné, pouze by se místo členského operátora volal globální operátor.

Pomocí globálního operátoru rozlišení rozsahu můžete volat globální delete(), i když má Screen definovanou vlastní verzi:

::smazat ps;

Obecně platí, že použitý operátor delete() se musí shodovat s operátorem new(), kterým byla paměť přidělena. Pokud například ps ukazuje na oblast paměti přidělenou globálním new(), pak by se k jeho uvolnění mělo použít globální delete().

Operátor delete() definovaný pro typ třídy může mít dva parametry místo jednoho. První parametr musí být stále typu void* a druhý musí být předdefinovaného typu size_t (nezapomeňte zahrnout hlavičkový soubor):

Class Screen ( public: // nahrazuje // void operator delete(void *); void operator delete(void *, size_t); );

Pokud je přítomen druhý parametr, kompilátor jej automaticky inicializuje s hodnotou rovnou velikosti v bajtech objektu adresovaného prvním parametrem. (Tato možnost je důležitá v hierarchii tříd, kde může být operátor delete() zděděn odvozenou třídou. Dědičnost je podrobněji popsána v kapitole.)

Podívejme se na implementaci operátorů new() a delete() ve třídě Screen podrobněji. Naše strategie alokace paměti bude založena na propojeném seznamu objektů Screen, počínaje členem freeStore. Pokaždé, když je zavolán členský operátor new(), je vrácen další objekt v seznamu. Při volání delete() se objekt vrátí do seznamu. Pokud je při vytváření nového objektu seznam adresovaný freeStore prázdný, pak se zavolá globální operátor new(), aby získal blok paměti dostatečný pro uložení objektů screenChunk třídy Screen.

ScreenChunk i freeStore jsou zajímavé pouze pro Screen, takže z nich uděláme soukromé členy. Navíc pro všechny vytvořené objekty naší třídy musí být hodnoty těchto členů stejné, a proto musí být deklarovány jako statické. Pro podporu struktury propojeného seznamu objektů Screen potřebujeme třetího dalšího člena:

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

Zde je jedna možná implementace operátoru new() pro třídu Screen:

#include "Screen.h" #include // statické členy jsou inicializovány // ve zdrojových souborech programu, nikoli v hlavičkových souborech Screen *Screen::freeStore = 0; const int Screen::screenChunk = 24; void *Screen::operator new(size_t size) ( Screen *p; if (!freeStore) ( // propojený seznam je prázdný: získat nový blok // globální operátor se nazývá new size_t chunk = screenChunk * size; freeStore = p = reinterpret_cast< Screen* >(nový znak[ chunk ]); // zahrnout přijatý blok do seznamu pro (; p != &freeStore[ screenChunk - 1 ]; ++p) p->next = p+1; p->další = 0; ) p = freeStore; freeStore = freeStore->další; vrátit p; ) A zde je implementace operátoru delete(): void Screen::operator delete(void *p, size_t) ( // vložte „smazaný“ objekt zpět // do volného seznamu (static_cast< Screen* >(p))->další = freeStore; freeStore = static_cast< Screen* >(p); )

Operátor new() lze deklarovat ve třídě bez odpovídajícího delete(). V tomto případě jsou objekty uvolněny pomocí stejnojmenného globálního operátoru. Je také povoleno deklarovat operátor delete() bez new(): objekty budou vytvořeny pomocí stejnojmenného globálního operátoru. Obvykle jsou však tyto operátory implementovány současně, jako ve výše uvedeném příkladu, protože vývojář třídy obvykle potřebuje oba.

Jsou to statické členy třídy, i když je programátor výslovně nedeklaruje jako takové, a podléhají obvyklým omezením pro takové členské funkce: nejsou předávány ukazatelem this, a proto mohou přistupovat pouze přímo. statické členy. (Viz diskusi o statických členských funkcích v sekci 13.5.) Důvodem, proč jsou tyto operátory statické, je to, že jsou volány buď před vytvořením objektu třídy (new()) nebo po jeho zničení (delete()).

Alokace paměti pomocí operátoru new(), například:

Obrazovka *ptr = new Screen(10, 20);

// Pseudokód v C++ ptr = Screen::operator new(sizeof(Screen)); Screen::Screen(ptr, 10, 20);

Jinými slovy, nejprve je zavolán operátor new() třídy, aby alokoval paměť pro objekt, a poté je objekt inicializován konstruktorem. Pokud new() selže, vyvolá se výjimka typu bad_alloc a konstruktor se nezavolá.

Uvolnění paměti pomocí operátoru delete(), například:

Smazat ptr;

je ekvivalentní postupnému provádění následujících pokynů:

// Pseudokód v C++ Screen::~Screen(ptr); Screen::operator delete(ptr, sizeof(*ptr));

Když je tedy objekt zničen, je nejprve zavolán destruktor třídy a poté je zavolán operátor delete() definovaný ve třídě, aby se uvolnila paměť. Pokud je ptr 0, nezavolá se ani destruktor ani delete().

15.8.1. Operátoři nové a smazat

Operátor new() definovaný v předchozí podsekci se volá pouze tehdy, když je paměť alokována pro jeden objekt. V této instrukci se tedy new() třídy Screen nazývá:

// s názvem Screen::operator new() Screen *ps = new Screen(24, 80);

zatímco níže je volán globální operátor new(), který alokuje paměť z haldy pro pole objektů typu Screen:

// s názvem Screen::operator new() Screen *psa = new Screen;

Třída může také deklarovat operátory new() a delete() pro práci s poli.

Členský operátor new() musí vrátit hodnotu typu void* a jako svůj první parametr převzít hodnotu typu size_t. Zde je jeho oznámení pro Screen:

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

Při použití new k vytvoření pole objektů typu třídy kompilátor zkontroluje, zda má třída definovaný operátor new(). Pokud ano, pak se zavolá k přidělení paměti pro pole, jinak se zavolá globální new(). Následující příkaz vytvoří pole deseti objektů Screen v hromadě:

Obrazovka *ps = nová obrazovka;

Tato třída má operátor new(), proto je volána k alokaci paměti. Jeho parametr size_t se automaticky inicializuje na množství paměti v bajtech, které je potřeba k uložení deseti objektů Screen.

I když má třída členský operátor new(), programátor může zavolat globální new() k vytvoření pole pomocí globálního operátoru rozlišení rozsahu:

Obrazovka *ps = ::nová Obrazovka;

Operátor delete(), který je členem třídy, musí být typu void a jako svůj první parametr mít void*. Takto vypadá jeho reklama na obrazovce:

Class Screen ( public: void operator delete(void *); );

Chcete-li odstranit pole objektů třídy, musí být delete zavoláno takto:

Smazat ps;

Když je operand delete ukazatel na objekt typu třídy, kompilátor zkontroluje, zda je v této třídě definován operátor delete(). Pokud ano, pak se zavolá k uvolnění paměti, jinak se zavolá jeho globální verze. Parametr typu void* se automaticky inicializuje na hodnotu adresy začátku oblasti paměti, ve které se pole nachází.

I když má třída operátor člena delete(), programátor může volat globální delete() pomocí operátoru rozlišení globálního rozsahu:

::smazat ps;

Přidání nebo odstranění operátorů new() nebo delete() do třídy neovlivní uživatelský kód: volání globálních i členských operátorů vypadají stejně.

Při vytváření pole se nejprve zavolá new() k přidělení potřebné paměti a poté se každý prvek inicializuje pomocí výchozího konstruktoru. Pokud má třída alespoň jeden konstruktor, ale žádný výchozí konstruktor, je volání operátoru new() považováno za chybu. Při vytváření pole tímto způsobem neexistuje žádná syntaxe pro určení inicializátorů prvků pole nebo argumentů konstruktoru třídy.

Když je pole zničeno, je nejprve zavolán destruktor třídy, aby zničil prvky, a poté je zavolán operátor delete() k uvolnění veškeré paměti. Je důležité použít správnou syntaxi. Pokud pokyny

Smazat ps;

ps ukazuje na pole objektů třídy, pak absence hranatých závorek způsobí, že destruktor bude volán pouze pro první prvek, i když se paměť zcela uvolní.

Členský operátor delete() může mít dva parametry místo jednoho a druhý musí být typu size_t:

Class Screen ( public: // nahrazuje // void operator delete(void*); void operator delete(void*, size_t); );

Pokud je přítomen druhý parametr, kompilátor jej automaticky inicializuje s hodnotou rovnou množství paměti přidělené pro pole v bajtech.

15.8.2. Operátor přidělení new() a operátor delete()

Členský operátor new() může být přetížen, pokud mají všechny deklarace různé seznamy parametrů. První parametr musí být typu size_t:

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

Zbývající parametry jsou inicializovány alokačními argumenty uvedenými při volání new:

Void func(Obrazovka *start) ( // ... )

Část výrazu, která následuje za novým klíčovým slovem a je uzavřena v závorkách, představuje alokační argumenty. Výše uvedený příklad volá operátor new(), který přebírá dva parametry. První se automaticky inicializuje na velikost třídy Screen v bajtech a druhý na hodnotu argumentu počátečního umístění.

Členský operátor delete() můžete také přetížit. Takový operátor však není nikdy volán z výrazu delete. Přetížené delete() je voláno implicitně kompilátorem, pokud konstruktor volaný při provádění operátoru new (to není překlep, máme na mysli nový) vyvolá výjimku. Podívejme se na použití delete() blíže.

Posloupnost akcí při vyhodnocování výrazu

Obrazovka *ps = nová (startovní) obrazovka;

  1. Zavolá se operátor new(size_t, Screen*) definovaný ve třídě.
  2. K inicializaci vytvořeného objektu je volán výchozí konstruktor třídy Screen.

Proměnná ps je inicializována adresou nového objektu Screen.

Předpokládejme, že operátor třídy new(size_t, Screen*) alokuje paměť pomocí globálního new(). Jak může vývojář zajistit, že se paměť uvolní, pokud konstruktor volaný v kroku 2 vyvolá výjimku? Chcete-li chránit uživatelský kód před úniky paměti, měli byste poskytnout přetížený operátor delete(), který se volá pouze v této situaci.

Pokud má třída přetížený operátor s parametry, jejichž typy se shodují s typy new(), kompilátor jej automaticky zavolá, aby uvolnil paměť. Předpokládejme, že máme následující výraz s novým operátorem přidělení:

Obrazovka *ps = nová (startovní) obrazovka;

Pokud výchozí konstruktor třídy Screen vyvolá výjimku, kompilátor hledá delete() v rozsahu Screen. Aby byl takový operátor nalezen, typy jeho parametrů musí odpovídat typům parametrů volání new(). Vzhledem k tomu, že první parametr new() je vždy typu size_t a parametr operátoru delete() je void*, první parametry se při porovnání neberou v úvahu. Kompilátor hledá následující operátor delete() ve třídě Screen:

Void operator delete(void*, Screen*);

Pokud je takový operátor nalezen, je volán k uvolnění paměti v případě, že new() vyvolá výjimku. (Jinak se to nejmenuje.)

Návrhář třídy se rozhodne, zda poskytne delete() odpovídající new() v závislosti na tom, zda tento operátor new() alokuje paměť sám nebo používá tu, která již byla přidělena. V prvním případě musí být povoleno delete() pro uvolnění paměti, pokud konstruktor vyvolá výjimku; jinak to není potřeba.

Můžete také přetížit operátor alokace new() a operátor delete() pro pole:

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

Operátor new() se používá, když jsou ve výrazu obsahujícím new specifikovány příslušné argumenty pro přidělení pole:

Void func(Screen *start) ( // s názvem Screen::operator new(size_t, Screen*) Screen *ps = new (start) Screen; // ... )

Pokud konstruktor vyvolá výjimku během operace operátoru new, pak se automaticky zavolá odpovídající delete().

Cvičení 15.9

Vysvětlete, která z následujících inicializací je nesprávná:

Třída iStack ( public: iStack(int kapacita) : _stack(kapacita), _top(0) () // ... private: int _top; vatcor< int>_zásobník; ); (a) iStack *ps = nový iStack(20); (b) iStack *ps2 = new const iStack(15); (c) iStack *ps3 = nový iStack[ 100];

Cvičení 15.10

Co se stane v následujících výrazech obsahujících new a delete?

Třídní cvičení ( public: Cvičení(); ~Cvičení(); ); Cvičení *pe = nové Cvičení; smazat ps;

Upravte tyto výrazy tak, aby byly volány globální operátory new() a delete().

Cvičení 15.11

Vysvětlete, proč by měl vývojář třídy poskytnout operátor delete().

15.9. Uživatelsky definované transformace

Už jsme viděli, jak se převody typů vztahují na operandy vestavěných typů: Část 4.14 se na tento problém podívala pomocí operandů vestavěných operátorů jako příklad a část 9.3 se zabývala skutečnými argumenty volané funkce k přetypování. k formálním typům parametrů. Z tohoto hlediska zvažte následujících šest operací sčítání:

Char ch; krátké sh;, int ival; /* v každé operaci jeden operand * vyžaduje konverzi typu */ ch + ival; ival + ch; ch + sh; ch + ch; ival + sh; sh + ival;

Operandy ch a sh jsou rozšířeny na typ int. Při provádění operace se přidají dvě hodnoty int. Rozšíření typů provádí implicitně kompilátor a je pro uživatele transparentní.

V této části se podíváme na to, jak může vývojář definovat vlastní transformace pro objekty typu třídy. Takové uživatelem definované konverze jsou také automaticky volány kompilátorem podle potřeby. Abychom ukázali, proč jsou potřeba, podívejme se znovu na třídu SmallInt představenou v sekci 10.9.

Připomeňme, že SmallInt umožňuje definovat objekty, které mohou ukládat hodnoty ze stejného rozsahu jako znak bez znaménka, tj. od 0 do 255 a zachycuje chyby, které přesahují jeho hranice. Ve všech ostatních ohledech se tato třída chová přesně jako unsigned char.

Abychom mohli sčítat a odečítat objekty SmallInt s jinými objekty stejné třídy nebo s hodnotami vestavěných typů, implementujeme šest operátorských funkcí:

Třída SmallInt ( přítel operátor+(const SmallInt &, int); přítel operátor-(const SmallInt &, int); přítel operátor-(int, const SmallInt &); přítel operátor+(int, const SmallInt &); public: SmallInt(int ival) : value(ival) ( ) operator+(const SmallInt &); operator-(const SmallInt &); // ... private: int value; );

Členové operátory poskytují možnost přidat a odečíst dva objekty SmallInt. Operátory globálního přítele vám umožňují provádět tyto operace s objekty dané třídy a objekty vestavěných aritmetických typů. Je potřeba pouze šest operátorů, protože jakýkoli vestavěný aritmetický typ lze přetypovat na int. Například výraz

vyřešeno ve dvou krocích:

  1. Konstanta 3,14159 typu double se převede na celé číslo 3.
  2. Zavolá se operátor+(const SmallInt &,int), který vrátí hodnotu 6.

Pokud chceme podporovat bitové a logické operátory, stejně jako operátory porovnání a operátory složeného přiřazení, jak velké přetížení operátorů je potřeba? Nemůžeš to hned spočítat. Mnohem pohodlnější je automatický převod objektu třídy SmallInt na objekt typu int.

Jazyk C++ má mechanismus, který umožňuje jakékoli třídě specifikovat sadu transformací použitelných na její objekty. Pro SmallInt definujeme přetypování objektu na typ int. Zde je jeho implementace:

Třída SmallInt ( public: SmallInt(int ival) : value(ival) ( ) // převodník // SmallInt ==> operátor int int() ( návratová hodnota; ) // přetížené operátory nejsou potřeba private: int value; );

Operátor int() je konvertor, který implementuje uživatelem definovanou konverzi, v tomto případě přetypování typu třídy na daný typ int. Definice konvertoru popisuje, co konverze znamená a co musí kompilátor udělat, aby ji použil. U objektu SmallInt je účelem převodu na int vrátit hodnotu int uloženou v hodnotovém členu.

Nyní lze objekt třídy SmallInt použít všude tam, kde je povoleno int. Za předpokladu, že již neexistují žádné přetížené operátory a v SmallInt je definován převodník na int, operace sčítání

SmallInt si(3); si+3,14159

vyřešeno ve dvou krocích:

  1. Zavolá se převodník třídy SmallInt, který vrátí celé číslo 3.
  2. Celé číslo 3 je rozšířeno na 3,0 a přičteno ke konstantě dvojnásobné přesnosti 3,14159, výsledkem je 6,14159.

Toto chování je konzistentnější s chováním operandů vestavěných typů ve srovnání s dříve definovanými přetíženými operátory. Když se k double přidá int, sečtou se dva double (protože int se rozšíří na double) a výsledkem je číslo stejného typu.

Tento program ilustruje použití třídy SmallInt:

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

Zkompilovaný program dává následující výsledky:

Zadejte prosím SmallInt: 127

Přečíst hodnotu 127

To se rovná 127

Zadejte SmallInt prosím (Ctrl-d pro ukončení): 126

Je to méně než 127

Zadejte SmallInt prosím (Ctrl-d pro ukončení): 128

Je větší než 127

Zadejte SmallInt prosím (Ctrl-d pro ukončení): 256

***Chyba rozsahu SmallInt: 256***

#zahrnout class SmallInt ( přítel istream& operátor>(istream &is, SmallInt &s); přítel 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; };

Níže jsou uvedeny definice členských funkcí mimo tělo třídy:

Istream& operator>>(istream &is, SmallInt &si) ( int ix; is >> ix; si = ix; // SmallInt::operator=(int) return is; ) int SmallInt::rangeCheck(int i) ( /* pokud je nastaven alespoň jeden bit kromě prvních osmi *, pak je hodnota příliš velká; nahlaste a okamžitě ukončete */ if (i & ~0377) ( cerr< <"\n*** Ошибка диапазона SmallInt: " << i << " ***" << endl; exit(-1); } return i; }

15.9.1. Převodníky

Převaděč je speciální případ funkce člena třídy, která implementuje uživatelem definovaný převod objektu na nějaký jiný typ. Převaděč je deklarován v těle třídy zadáním operátoru klíčového slova následovaného cílovým typem převodu.

Název následující za klíčovým slovem nemusí být názvem jednoho z vestavěných typů. Třída Token uvedená níže definuje několik převodníků. Jeden z nich používá typ tName k určení názvu typu a druhý používá typ třídy SmallInt.

#include "SmallInt.h" typedef char *tName; class Token ( public: Token(char *, int); operátor SmallInt() ( návratová hodnota; ) operátor tName() ( návratový název; ) operátor int() ( návratová hodnota; ) // ostatní veřejní členové private: SmallInt val; char *jméno; );

Všimněte si, že definice převodníků na typy SmallInt a int jsou stejné. Převaděč Token::operator int() vrací hodnotu členu val. Protože val je typu SmallInt, SmallInt::operator int() se implicitně používá k převodu val na typ int. Samotný Token::operator int() je implicitně používán kompilátorem k převodu objektu typu Token na hodnotu typu int. Tento konvertor se například používá k implicitnímu přetypování skutečných argumentů t1 a t2 typu Token pro zadání int formálního parametru funkce print():

#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; }

Po zkompilování a spuštění program vypíše následující řádky:

Tisk (int): 127 tisk (int): 255

Celkový pohled na převodník je následující:

Typ operátora();

kde typ může být vestavěný typ, typ třídy nebo název definice typu. Převaděče, kde typem je pole nebo typ funkce, nejsou povoleny. Převodník musí být členská funkce. Jeho deklarace nesmí uvádět ani návratový typ, ani seznam parametrů:

Operátor int(SmallInt &); // chyba: není členem třídy SmallInt ( public: operátor int int(); // chyba: zadaný typ návratu int(int = 0); // chyba: zadán seznam parametrů // ... );

Převodník je volán jako výsledek explicitní konverze typu. Pokud má převáděná hodnota typ třídy, která má převodník, a typ tohoto převodníku je zadán v operaci přetypování, pak se nazývá:

#include "Token.h" Token tok("funkce", 78); // funkční zápis: voláno Token::operátor SmallInt() SmallInt tokVal = SmallInt(tok); // static_cast: voláno Token::operator tName() char *tokName = static_cast< char * >(tok);

Převaděč Token::operator tName() může mít nežádoucí vedlejší účinek. Pokus o přímý přístup k soukromému členovi Token::name je kompilátorem označen jako chyba:

Char *tokName = tok.name; // chyba: Token::name je soukromý člen

Náš konvertor však tím, že umožňuje uživatelům přímo měnit Token::name, dělá přesně to, před čím jsme chtěli chránit. S největší pravděpodobností to nepůjde. Zde je například uvedeno, jak by k takové změně mohlo dojít:

#include "Token.h" Token tok("funkce", 78); char *tokName = tok; // správně: implicitní konverze *tokname = "P"; // ale nyní má člen jména Punction!

Máme v úmyslu povolit přístup pouze pro čtení k převedenému objektu třídy Token. Proto musí konvertor vrátit typ const char*:

Typedef const char *cchar; class Token ( public: operator cchar() ( return name; ) // ... ); // chyba: převod char* na const char* není povolen char *pn = tok; const char *pn2 = tok; // Že jo

Dalším řešením je nahradit typ char* v definici tokenu typem řetězce ze standardní knihovny C++:

Token třídy ( public: Token(řetězec, int); operátor SmallInt() ( návratová hodnota; ) operátor string() ( návratový název; ) operátor int() ( návratová hodnota; ) // ostatní veřejní členové private: SmallInt hodnota; řetězec název; );

Sémantika převodníku Token::operator string() je vrátit kopii hodnoty (nikoli ukazatel na hodnotu) řetězce představujícího název tokenu. Tím se zabrání náhodné změně člena soukromého jména třídy Token.

Musí cílový typ přesně odpovídat typu převodníku? Bude například následující kód volat převodník int() definovaný ve třídě Token?

Exter void calc(double); Token tok("konstanta", 44); // Je volán operátor int()? Ano // je použita standardní konverze int --> double calc(tok);

Pokud se cílový typ (v tomto případě double) přesně neshoduje s typem převodníku (v našem případě int), bude převodník stále volán za předpokladu, že existuje sekvence standardních převodů, která vede k cílovému typu z převodníku typ. (Tyto sekvence jsou popsány v části 9.3.) Když je volána calc(), je volána Token::operator int() pro převod tok z typu Token na typ int. Standardní převod se pak použije k přetypování výsledku z int na double.

Po uživatelsky definované transformaci jsou povoleny pouze standardní. Pokud je k dosažení cílového typu potřeba další vlastní konverze, kompilátor žádné konverze nepoužije. Za předpokladu, že třída Token nedefinuje operátor int(), následující volání selže:

Externí void calc(int); Token tok("ukazatel", 37); // pokud Token::operator int() není definován, // toto volání má za následek chybu kompilace calc(tok);

Pokud převodník Token::operator int() není definován, přetypování tok na typ int by vyžadovalo volání dvou převodníků definovaných uživatelem. Za prvé, skutečný argument tok by musel být převeden z typu Token na SmallInt pomocí převodníku

Token::operator SmallInt()

a poté výsledek převeďte na typ int – také pomocí vlastního převodníku

Token::operator int()

Volání calc(tok) je kompilátorem označeno jako chyba, protože nedochází k žádné implicitní konverzi z tokenu na int.

Pokud neexistuje žádná logická korespondence mezi typem převodníku a typem třídy, účel převodníku nemusí být čtenáři programu jasný:

Datum třídy ( public: // zkuste uhodnout, který člen se vrací! operátor int(); private: int měsíc, den, rok; );

Jakou hodnotu by měl vrátit převodník třídy Date int()? Bez ohledu na to, jak dobré důvody pro konkrétní rozhodnutí jsou, bude čtenář v rozpacích, jak používat objekty Date, protože mezi nimi a celými čísly neexistuje žádná zřejmá logická korespondence. V takových případech je lepší převodník vůbec nedefinovat.

15.9.2. Konstruktor jako konvertor

Sada konstruktorů tříd, které přebírají jeden parametr, jako je SmallInt(int) třídy SmallInt, definují sadu implicitních převodů na hodnoty typu SmallInt. Konstruktor SmallInt(int) tedy převádí hodnoty typu int na hodnoty typu SmallInt.

Exter void calc(SmallInt); int i; // je nutné převést i na hodnotu typu SmallInt // toho dosáhneme pomocí SmallInt(int) calc(i); Při volání calc(i) se číslo i převede na hodnotu typu SmallInt pomocí konstruktoru SmallInt(int), který je volán kompilátorem, aby vytvořil dočasný objekt požadovaného typu. Kopie tohoto objektu je poté předána do calc(), jako by bylo volání funkce zapsáno ve tvaru: // C++ pseudokód // vytvoří dočasný objekt typu SmallInt ( SmallInt temp = SmallInt(i); calc(temp) ;)

Složené závorky v tomto příkladu označují životnost tohoto objektu: při ukončení funkce je zničen.

Typ parametru konstruktoru může být typ nějaké třídy:

Class Number ( public: // vytvoření hodnoty typu Number z hodnoty typu SmallInt Number(const SmallInt &); // ... );

V tomto případě lze hodnotu typu SmallInt použít všude tam, kde je platná hodnota typu Number:

Exter void func(Number); SmallInt si(87); int main() ( // volané Číslo(const SmallInt &) func(si); // ... )

Pokud se konstruktor používá k provedení implicitní konverze, musí typ jeho parametru přesně odpovídat typu hodnoty, která má být převedena? Volal by například následující kód SmallInt(int), definovaný ve třídě SmallInt, k přetypování dobj na SmallInt?

Exter void calc(SmallInt); dvojité dobj; // se volá SmallInt(int)? Ano // dobj se převede z double na int // pomocí standardního převodu calc(dobj);

V případě potřeby se na skutečný argument před voláním konstruktoru, který provede uživatelem definovanou konverzi, aplikuje sekvence standardních převodů. Při volání funkce calc() se používá standardní převod dobj z typu double na typ int. K přetypování výsledku na typ SmallInt se zavolá SmallInt(int).

Kompilátor implicitně používá konstruktor s jediným parametrem k převodu jeho typu na typ třídy, do které konstruktor patří. Někdy je však výhodnější, aby byl konstruktor Number(const SmallInt&) volán pouze k inicializaci objektu typu Number na hodnotu typu SmallInt a nikdy neprováděl implicitní převody. Abychom se takovému použití konstruktoru vyhnuli, deklarujme jej explicitně:

Číslo třídy ( public: // nikdy nepoužívejte pro implicitní převody explicitní číslo(const SmallInt &); // ... );

Kompilátor nikdy nepoužívá explicitní konstruktory k provádění implicitních převodů typů:

Exter void func(Number); SmallInt si(87); int main() ( // chyba: neexistuje žádná implicitní konverze z SmallInt na Number func(si); // ... )

Takový konstruktor však lze stále použít pro konverzi typu, pokud je výslovně požadován ve formě operátoru přetypování:

SmallInt si(87); int main() ( // chyba: neexistuje žádná implicitní konverze z SmallInt na Number func(si); func(Number(si)); // správně: cast func(static_cast< Number >(si)); // správně: casting )

15.10. Výběr transformace A

Uživatelsky definovaný převod je implementován jako převodník nebo konstruktor. Jak již bylo zmíněno, poté, co převodník provede převod, můžete použít standardní převod k přetypování vrácené hodnoty na cílový typ. Transformaci provedené konstruktorem může také předcházet standardní konverze k přetypování typu argumentu na typ formálního parametru konstruktoru.

Sekvence uživatelem definovaných transformací je kombinací definované uživatelem a standardní převod, který je nutný k přetypování hodnoty na cílový typ. Tato sekvence vypadá takto:

Posloupnost standardních transformací ->

Uživatelsky definovaná transformace ->

Posloupnost standardních transformací

kde je uživatelsky definovaný převod implementován konvertorem nebo konstruktorem.

Je možné, že existují dvě různé sekvence vlastních převodů pro transformaci zdrojové hodnoty na cílový typ a kompilátor pak musí vybrat tu lepší. Podívejme se, jak se to dělá.

Třída může definovat mnoho převodníků. Například naše třída Number má dvě z nich: operator int() a operator float(), které oba dokážou převést objekt typu Number na hodnotu typu float. Pro přímou transformaci můžete samozřejmě použít převodník Token::operator float(). Ale Token::operator int() je také vhodný, protože výsledek jeho aplikace je typu int a lze jej tedy převést na float pomocí standardní konverze. Je transformace nejednoznačná, pokud existuje několik takových sekvencí? Nebo může být jeden z nich upřednostněn před ostatními?

Číslo třídy ( public: operator float(); operator int(); // ...); Číslo číslo; float ff = počet; // jaký převodník? operátor float()

V takových případech je výběr nejlepší sekvence uživatelem definovaných transformací založen na analýze sekvence transformací, která je aplikována za převodníkem. V předchozím příkladu můžete použít následující dvě sekvence:

  1. operátor float() -> přesná shoda
  2. operátor int() -> standardní konverze

Jak je uvedeno v části 9.3, přesná shoda je lepší než standardní převod. Proto je první sekvence lepší než druhá, což znamená, že je vybrán převodník Token::operator float().

Může se stát, že se pro převod hodnoty na cílový typ použijí dva různé konstruktory. V tomto případě je analyzována sekvence standardních transformací předcházejících volání konstruktoru:

Třída SmallInt ( public: SmallInt(int ival) : value(ival) ( ) SmallInt(double dval) : value(static_cast< int >(dval)); ( )); extern void manip(const SmallInt &); int main() ( double dobj; manip(dobj); // správně: SmallInt(double) )

Třída SmallInt zde definuje dva konstruktory – SmallInt(int) a SmallInt(double), které lze použít ke změně hodnoty typu double na objekt typu SmallInt: SmallInt(double) transformuje double přímo na SmallInt a SmallInt( int) pracuje s výsledkem standardního převodu double na int. Existují tedy dvě sekvence uživatelsky definovaných transformací:

  1. přesná shoda -> SmallInt(double)
  2. standardní převod -> SmallInt(int)

Protože přesná shoda je lepší než standardní převod, je vybrán konstruktor SmallInt(double).

Ne vždy je možné rozhodnout, která sekvence je nejlepší. Může se stát, že jsou všechny stejně dobré, v takovém případě říkáme, že transformace je nejednoznačná. V tomto případě kompilátor nepoužije žádné implicitní transformace. Pokud má například třída Number dva převodníky:

Číslo třídy ( public: operator float(); operator int(); // ...);

pak není možné implicitně převést objekt typu Number na typ long. Následující instrukce způsobí chybu při kompilaci, protože volba sekvence převodů definovaných uživatelem je nejednoznačná:

// chyba: můžete použít float() i int() long lval = num;

Pro transformaci num na hodnotu typu long jsou použitelné následující dvě sekvence:

  1. operátor float() -> standardní konverze
  2. operátor int() -> standardní konverze

Vzhledem k tomu, že v obou případech po použití převodníku následuje použití standardního převodu, jsou obě sekvence stejně dobré a překladač nemůže volit jednu před druhou.

Pomocí explicitního přetypování je programátor schopen specifikovat požadovanou změnu:

// správně: explicitní cast long lval = static_cast (počet);

Kvůli této specifikaci je vybrán převodník Token::operator int() následovaný standardním převodem na long.

Nejednoznačnost ve výběru posloupnosti transformací může také nastat, když dvě třídy definují transformace do sebe. Například:

Class SmallInt ( public: SmallInt(const Number &); // ... ); class Number ( public: operator SmallInt(); // ... ); extern void compute(SmallInt); externí Číslo číslo; vypočítat(počet); // chyba: jsou možné dvě konverze

Argument num je převeden na SmallInt dvěma různé způsoby: pomocí konstruktoru SmallInt::SmallInt(const Number&) nebo pomocí převodníku Number::operator SmallInt(). Protože jsou obě změny stejně dobré, je volání považováno za chybu.

Pro vyřešení nejednoznačnosti může programátor explicitně zavolat převodník třídy Number:

// správně: explicitní volání je jednoznačné compute(num.operator SmallInt());

K vyřešení nejednoznačnosti byste však neměli používat explicitní přetypování, protože při výběru převodů vhodných pro přetypování se bere v úvahu jak převodník, tak konstruktor:

Compute(SmallInt(num)); // chyba: stále nejednoznačné

Jak vidíte, přítomnost velké číslo Takové převodníky a konstruktéry nejsou bezpečné, takže jsou. by měl být používán s opatrností. Můžete omezit použití konstruktorů při provádění implicitních převodů (a snížit tak pravděpodobnost neočekávaných efektů) jejich deklarováním jako explicitní.

15.10.1. Ještě jednou o povolení přetížení funkcí

Kapitola 9 podrobně popisuje, jak se řeší přetížené volání funkce. Pokud jsou skutečné argumenty volání typu třídy, ukazatele na typ třídy nebo ukazatele na členy třídy, pak se za možné kandidáty považuje více funkcí. V důsledku toho přítomnost takových argumentů ovlivňuje první krok postupu řešení přetížení – výběr sady kandidátských funkcí.

Ve třetím kroku tohoto postupu se vybere nejlepší shoda. V tomto případě se řadí transformace typů aktuálních argumentů na typy formálních parametrů funkce. Jsou-li argumenty a parametry typu třídy, pak by sada možných transformací měla zahrnovat sekvence uživatelem definovaných transformací, které také klasifikují.

V této části se podrobně podíváme na to, jak skutečné argumenty a formální parametry typu třídy ovlivňují výběr kandidátských funkcí a jak sekvence uživatelem definovaných transformací ovlivňují výběr nejvhodnější funkce.

15.10.2. Kandidátské funkce

Kandidátská funkce je funkce se stejným názvem jako volaná funkce. Předpokládejme, že existuje takové volání:

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

Kandidátská funkce se musí jmenovat add. Které deklarace add() jsou brány v úvahu? Ty, které jsou viditelné v místě volání.

Například obě funkce add() deklarované v globálním rozsahu by byly kandidáty pro následující volání:

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

Zvažování funkcí, jejichž deklarace jsou viditelné v bodě volání, není omezeno na volání s argumenty typu třídy. Pro ně se však vyhledávání reklam provádí ve dvou dalších oblastech viditelnosti:

  • pokud je skutečným argumentem objekt typu třídy, ukazatel nebo odkaz na typ třídy nebo ukazatel na člena třídy a tento typ je deklarován v uživatelském jmenném prostoru, pak funkce deklarované ve stejném prostoru a mající stejné jmenuj se a volal:
jmenný prostor NS ( class SmallInt ( /* ... */ ); class String ( /* ... */ ); String add(const String &, const String &); ) int main() ( // si má typ class SmallInt: // třída deklarovaná ve jmenném prostoru NS NS::SmallInt si(15); add(si, 566); // NS::add() - kandidátní funkce vrátí 0; )

Argument si je typu SmallInt, tzn. Typ třídy deklarovaný v oboru názvů NS. Proto add(const String &, const String &) deklarované v tomto jmenném prostoru je přidáno do sady kandidátských funkcí;

  • pokud je skutečným argumentem objekt typu třídy, ukazatel nebo odkaz na třídu nebo ukazatel na člena třídy a tato třída má přátele, kteří mají stejné jméno jako volaná funkce, jsou přidáni do sada kandidátských funkcí:
  • jmenný prostor NS ( třída SmallInt ( přítel SmallInt add(SmallInt, int) ( /* ... */ ) ); ) int main() ( NS::SmallInt si(15); add(si, 566); // funkce -friend add() - kandidát vrátí 0; )

    Argument funkce si je typu SmallInt. Funkce přátel třídy SmallInt add(SmallInt, int) je členem jmenného prostoru NS, i když není deklarována přímo ve jmenném prostoru NS. Normální vyhledávání v NS nenajde funkci přítele. Když se však add() zavolá s argumentem typu třídy SmallInt, přátelé této třídy uvedení v jejím seznamu členů jsou také vzati v úvahu a přidáni do sady kandidátů.

    Pokud tedy skutečný seznam argumentů funkce obsahuje objekt, ukazatel nebo odkaz na třídu a ukazatele na členy třídy, pak se sada kandidátských funkcí skládá ze sady funkcí viditelných v bodě volání nebo deklarovaných ve stejném jmenný prostor, kde je definován typ třídy, nebo deklarovaní přátelé této třídy.

    Zvažte následující příklad:

    Jmenný prostor NS ( class SmallInt ( friend SmallInt add(SmallInt, int) ( /* ... */ ) ); class String ( /* ... */ ); String add(const String &, const String &); ) const matrix& add(const matrix &, int); double add(double, double); int main() ( // si je typu class SmallInt: // třída je deklarována v jmenném prostoru NS NS::SmallInt si(15); add(si, 566); // funkce friend se nazývá return 0; )

    Zde jsou kandidáti:

    • globální funkce:
    const matrix& add(const matrix &, int) double add(double, double)
  • funkce z jmenného prostoru:
  • NS::add(const String &, const String &)
  • funkce přítele:
  • NS::add(SmallInt, int)

    Když je přetížení vyřešeno, je jako nejvhodnější zvolena spřátelená funkce třídy SmallInt NS::add(SmallInt, int): oba aktuální argumenty přesně odpovídají daným formálním parametrům.

    Volaná funkce může mít samozřejmě několik argumentů typu třídy, ukazatel nebo odkaz na třídu nebo ukazatel na člena třídy. Pro každý z těchto argumentů jsou povoleny různé typy tříd. Hledání kandidátních funkcí pro ně se provádí ve jmenném prostoru, kde je třída definována, a mezi funkcemi přátel třídy. Proto výsledná sada kandidátů pro volání funkce s takovými argumenty obsahuje funkce z různých jmenných prostorů a přátelské funkce deklarované v různých třídách.

    15.10.3. Kandidátské funkce pro volání funkce v rozsahu třídy

    Při volání funkce jako

    se vyskytuje v rozsahu třídy (například uvnitř členské funkce), pak může první část kandidátní sady popsané v předchozím pododdílu (tj. sada, která obsahuje deklarace funkcí viditelných v bodě volání) obsahovat více než jen členské funkce třídy. Ke konstrukci takové množiny se používá rozlišení názvů. (Toto téma bylo podrobně probráno v částech 13.9 – 13.12.)

    Podívejme se na příklad:

    Jmenný prostor NS ( struct myClass ( void k(int); static void k(char*); void mf(); ); int k(double); ); void h(char); void NS::myClass::mf() ( h("a"); // je voláno globální h(char) k(4); // je voláno myClass::k(int))

    Jak je uvedeno v části 13.11, kvalifikátory NS::myClass:: se prohledávají v obráceném pořadí: nejprve se ve třídě myClass hledá viditelná deklarace názvu použitého v definici členské funkce mf() a poté se hledá ve jmenném prostoru NS. . Podívejme se na první hovor:

    Při řešení názvu h() v definici členské funkce mf() jsou nejprve vyhledány členské funkce myClass. Protože v oboru této třídy není žádná členská funkce se stejným názvem, pokračuje hledání ve jmenném prostoru NS. Není tam ani funkce h(), takže se přesuneme do globálního rozsahu. Výsledkem je globální funkce h(char), jediná kandidátská funkce viditelná v bodě volání.

    Jakmile je nalezen vhodný inzerát, vyhledávání se zastaví. Sada tedy obsahuje pouze ty funkce, jejichž deklarace jsou v oborech, kde bylo překlad názvů úspěšný. To lze pozorovat na příkladu konstrukce množiny kandidátů na volání

    Nejprve se provádí vyhledávání v rozsahu třídy myClass. V tomto případě jsou nalezeny dvě členské funkce k(int) a k(char*). Vzhledem k tomu, že kandidátní sada obsahuje pouze funkce deklarované v oboru, kde bylo řešení úspěšné, není prohledáván jmenný prostor NS a funkce k(double) není v této sadě zahrnuta.

    Pokud se zjistí, že volání je nejednoznačné, protože v sadě není žádná nejvhodnější funkce, kompilátor vydá chybovou zprávu. Kandidáti, kteří lépe odpovídají skutečným argumentům, se v přiložených oborech nehledají.

    15.10.4. Pořadí sekvencí uživatelem definovaných transformací

    Argument skutečné funkce lze implicitně přetypovat na typ formálního parametru pomocí sekvence uživatelem definovaných převodů. Jak to ovlivní řešení přetížení? Pokud je například zadáno následující volání funkce calc(), která funkce bude volána?

    Třída SmallInt ( public: SmallInt(int); ); extern void calc(double); extern void calc(SmallInt); int ival; int main() ( calc(ival); // který calc() se nazývá?)

    Vybere se funkce, jejíž formální parametry nejlépe odpovídají typům skutečných argumentů. To se nazývá funkce nejlepšího přizpůsobení nebo nejlepšího postavení. Pro výběr takové funkce se seřadí implicitní transformace aplikované na skutečné argumenty. Za nejlepší přežívající funkci je považována ta, u které nejsou změny aplikované na argumenty horší než u jakékoli jiné přežívající funkce a alespoň pro jeden argument jsou lepší než pro všechny ostatní funkce.

    Sekvence standardních transformací je vždy lepší než sekvence transformací definovaných uživatelem. Takže při volání calc() z příkladu výše jsou obě funkce calc() stále platné. calc(double) přežije, protože existuje standardní převod z typu skutečného argumentu, int, na typ formálního parametru double, a calc(SmallInt) přežije, protože existuje uživatelsky definovaný převod z int na SmallInt, který používá konstruktor SmallInt(int). Proto je nejlepší přežívající funkce calc(double).

    Jak se porovnávají dvě sekvence uživatelsky definovaných transformací? Pokud používají různé konvertory nebo různé konstruktory, pak jsou obě takové sekvence považovány za stejně dobré:

    Číslo třídy ( public: operator SmallInt(); operator int(); // ... ); extern void calc(int); extern void calc(SmallInt); externí Číslo číslo; calc(num); // chyba: nejednoznačnost

    calc(int) i calc(SmallInt) přežijí; první proto, že převodník Number::operator int() převádí skutečný argument typu Number na formální parametr typu int, a druhý proto, že převodník Number::operator SmallInt() převádí skutečný argument typu Number na formální parametr typu SmallInt. Protože sekvence uživatelsky definovaných transformací mají vždy stejnou hodnost, kompilátor si nemůže vybrat, která z nich je lepší. Toto volání funkce je tedy nejednoznačné a vede k chybě kompilace.

    Existuje způsob, jak vyřešit nejednoznačnost explicitním zadáním převodu:

    // explicitní určení přetypování odlišuje calc(static_cast< int >(počet));

    Explicitní přetypování nutí kompilátor převést argument num na typ int pomocí převodníku Number::operator int(). Vlastní argument pak bude typu int, který přesně odpovídá funkci calc(int), která je vybrána jako nejlepší.

    Řekněme, že převodník Number::operator int() není definován ve třídě Number. Bude pak výzva?

    // pouze Cislo::operator SmallInt() calc(num); // stále nejednoznačné?

    stále nejednoznačné? Připomeňme, že SmallInt má také konvertor, který dokáže převést hodnotu typu SmallInt na int.

    Třída SmallInt ( public: operator int(); // ...);

    Můžeme předpokládat, že funkce calc() je volána tak, že nejprve převedeme skutečný argument num z typu Number na typ SmallInt pomocí převodníku Number::operator SmallInt() a poté přetypujeme výsledek na typ int pomocí SmallInt::operator SmallInt( ). Nicméně není. Připomeňme, že sekvence uživatelem definovaných transformací může zahrnovat několik standardních transformací, ale pouze jednu vlastní. Není-li převodník Number::operator int() definován, funkce calc(int) se nepovažuje za funkční, protože neexistuje implicitní převod z typu skutečného argumentu num na typ formálního parametru int.

    Proto při absenci převodníku Number::operator int() je jedinou přežívající funkcí calc(SmallInt), v jejímž prospěch se volání vyřeší.

    Pokud dvě sekvence uživatelem definovaných transformací používají stejný převodník, pak výběr té nejlepší závisí na sekvenci standardních transformací provedených po jejím volání:

    Třída SmallInt ( public: operator int(); // ...); void manip(int); void manip(char); SmallInt si(68); main() ( manip(si); // volá manip(int) )

    Manip(int) i manip(char) jsou dobře zavedené funkce; první proto, že převodník SmallInt::operator int() převádí skutečný argument typu SmallInt na typ formálního parametru int, a druhý proto, že stejný převodník převádí SmallInt na int, načež je výsledek přetypován na char pomocí standardní konverze. Sekvence uživatelsky definovaných transformací vypadají takto:

    Manip(int) : operátor int()->přesná shoda manip(int) : operátor int()->standardní převod

    Protože obě sekvence používají stejný převodník, analyzuje se pořadí standardních převodů, aby se určilo, která z nich je lepší. Vzhledem k tomu, že přesná shoda je lepší než konverze, nejlepší přežívající funkcí je manip(int).

    Zdůrazňujeme, že takové výběrové kritérium je akceptováno pouze tehdy, je-li použit stejný převodník v obou sekvencích uživatelem definovaných transformací. To se liší od příkladu na konci sekce 15.9, kde jsme ukázali, jak kompilátor zvolil uživatelem definovanou konverzi nějaké hodnoty na daný cílový typ: zdrojový a cílový typ byly pevně dané a kompilátor si musel vybrat mezi různými uživateli -definované převody z jednoho typu na druhý. Zde uvažujeme dvě různé funkce s různými typy formálních parametrů a cílové typy jsou různé. Pokud pro dva odlišné typy parametry vyžadují různé uživatelem definované převody, je možné vybrat jeden typ před druhým pouze v případě, že je v obou sekvencích použit stejný převodník. Pokud tomu tak není, vyhodnotí se standardní konverze po aplikaci konvertoru, aby se vybral nejlepší typ cíle. Například:

    Třída SmallInt ( public: operator int(); operator float(); // ...); void compute(float); void compute(char); SmallInt si(68); main() ( compute(si); // nejednoznačnost )

    Compute(float) i compute(int) jsou dobře zavedené funkce. compute(float) – protože převodník SmallInt::operator float() převádí argument typu SmallInt na parametr typu float a compute(char) – protože SmallInt::operator int() převádí argument typu SmallInt na int type, po kterém je výsledek standardně přetypován na typ char. Existují tedy sekvence:

    Compute(float) : operátor float()->přesná shoda compute(char) : operátor char()->standardní převod

    Protože používají různé převodníky, není možné určit, která funkce má formální parametry, které lépe odpovídají volání. Pro výběr lepší z těchto dvou se nepoužívá pořadí posloupnosti standardních transformací. Volání je kompilátorem označeno jako nejednoznačné.

    Cvičení 15.12

    Třídy standardní knihovny C++ nemají definice převodníků a většina konstruktorů, které přebírají jeden parametr, je deklarována jako explicitní. Je však definováno mnoho přetížených operátorů. Proč si myslíte, že k tomuto rozhodnutí došlo během návrhu?

    Cvičení 15.13

    Proč není operátor přetíženého vstupu pro třídu SmallInt definovaný na začátku této části implementován takto:

    Istream& operator>>(istream &is, SmallInt &si) ( return (is >> is.value); )

    Cvičení 15.14

    Uveďte možné sekvence uživatelem definovaných transformací pro následující inicializace. Jaký bude výsledek každé inicializace?

    Třída LongDouble ( operátor double(); operátor float(); ); externí LongDouble ldObj; (a) int ex1 = ldObj; (b) float ex2 = ldObj;

    Cvičení 15.15

    Pojmenujte tři sady kandidátských funkcí, které se berou v úvahu při povolení přetížení funkce, když je alespoň jeden z jejích argumentů typu třídy.

    Cvičení 15.16

    Která funkce calc() je v tomto případě vybrána jako nejlepší přežívající funkce? Ukažte posloupnost transformací potřebných k volání každé funkce a vysvětlete, proč je jedna lepší než druhá.

    Třída LongDouble ( public: LongDouble(double); // ... ); extern void calc(int); extern void calc(LongDouble); dvojitý dval; int main() ( calc(dval); // jakou funkci?)

    15.11. Řešení přetížení a členské funkce A

    Členové funkce mohou být také přetíženy, v takovém případě se postup řešení přetížení také používá k výběru toho nejlepšího, který přežije. Toto rozlišení je velmi podobné stejnému postupu pro běžné funkce a skládá se ze stejných tří kroků:

    1. Výběr kandidátských funkcí.
    2. Výběr zavedených funkcí.

    Existují však drobné rozdíly v algoritmech pro generování sady kandidátů a výběr zbývajících členských funkcí. Na tyto rozdíly se podíváme v této části.

    15.11.1. Deklarace přetížených členských funkcí

    Funkce členů třídy mohou být přetíženy:

    Class myClass ( public: void f(double); char f(char, char); // přetěžuje myClass::f(double) // ... );

    Stejně jako u funkcí deklarovaných ve jmenném prostoru mohou mít členské funkce stejný název, pokud se jejich seznamy parametrů liší, ať už počtem parametrů, nebo jejich typy. Pokud se deklarace dvou členských funkcí liší pouze návratovým typem, pak je druhá deklarace považována za chybu kompilace:

    Class myClass ( public: void mf(); double mf(); // chyba: toto nelze přetížit // ... );

    Na rozdíl od funkcí ve jmenných prostorech musí být členské funkce deklarovány pouze jednou. I když jsou návratový typ a seznamy parametrů dvou členských funkcí stejné, kompilátor interpretuje druhou deklaraci jako nesprávnou opakovanou deklaraci:

    Class myClass ( public: void mf(); void mf(); // chyba: opakovaná deklarace // ... );

    Všechny funkce ze sady přetížení musí být deklarovány ve stejném rozsahu. Proto členské funkce nikdy nepřetěžují funkce deklarované v oboru názvů. Navíc, protože každá třída má svůj vlastní rozsah, funkce, které jsou členy různých tříd, se navzájem nepřetěžují.

    Sada přetížených členských funkcí může obsahovat statické i nestatické funkce:

    Class myClass ( public: void mcf(double); static void mcf(int*); // přetěžuje myClass::mcf(double) // ...);

    Volání statické nebo nestatické členské funkce závisí na výsledku řešení přetížení. Proces řešení v situaci, kdy přežívají statické i nestatické členy, bude podrobně popsán v další části.

    15.11.2. Kandidátské funkce

    Podívejme se na dva typy volání členských funkcí:

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

    kde mc je výraz typu myClass a pmc je výraz typu "ukazatel na typ myClass". Kandidátní sada pro obě volání se skládá z funkcí nalezených v rozsahu třídy myClass při hledání deklarace mf().

    Podobně pro volání funkce formuláře

    MyClass::mf(arg);

    kandidátní sada se také skládá z funkcí nalezených v rozsahu třídy myClass při hledání deklarace mf(). Například:

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

    Kandidáti na volání funkce v main() jsou všechny tři členské funkce mf() deklarované v myClass:

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

    Pokud by v myClass nebyla deklarována žádná členská funkce s názvem mf(), pak by sada kandidátů byla prázdná. (Funkce ze základních tříd by byly ve skutečnosti také zváženy. O tom, jak se do této sady dostanou, si povíme v sekci 19.3.) Pokud neexistují žádní kandidáti na volání funkce, kompilátor vydá chybovou zprávu.

    15.11.3. Starší funkce

    Přežívající funkce je funkce ze sady kandidátů, kterou lze volat s danými aktuálními argumenty. Aby přežil, musí existovat implicitní konverze mezi typy skutečných argumentů a formálními parametry. Například: class myClass ( public: void mf(double); void mf(char, char = "\n"); static void mf(int*); // ... ); int main() ( myClass mc; int iobj; mc.mf(iobj); // která členská funkce mf() je? Nejednoznačné)

    V tomto úryvku pro volání mf() z main() jsou dvě stálé funkce:

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

    • mf(double) přežilo, protože má pouze jeden parametr a existuje standardní převod z argumentu iobj typu int na parametr typu double;
    • mf(char,char) přežil, protože existuje výchozí hodnota pro druhý parametr a existuje standardní převod z argumentu int iobj na typ char prvního formálního parametru.

    Při výběru nejlepšího argumentu jsou seřazeny funkce převodu typu použité na každý skutečný argument. Za nejlepší je považována ta, pro kterou nejsou všechny použité transformace horší než pro jakoukoli jinou existující funkci a alespoň pro jeden argument je taková transformace lepší než pro všechny ostatní funkce.

    V předchozím příkladu každá ze dvou starších funkcí použila standardní převod k přetypování typu skutečného argumentu na typ formálního parametru. Volání je považováno za nejednoznačné, protože obě členské funkce jej řeší stejně dobře.

    Bez ohledu na typ volání funkce lze do zbývající sady zahrnout statické i nestatické členy:

    Class myClass ( public: static void mf(int); char mf(char); ); int main() ( char cobj; myClass::mf(cobj); // která členská funkce přesně?)

    Zde je volána členská funkce mf() s názvem třídy a operátorem rozlišení rozsahu myClass::mf(). Nebyl však zadán ani objekt (s operátorem tečka), ani ukazatel na objekt (s operátorem šipka). Navzdory tomu je nestatická členská funkce mf(char) stále zahrnuta v přežívající sadě spolu se statickým členem mf(int).

    Proces řešení přetížení pak pokračuje seřazením převodů typů použitých na skutečné argumenty, aby se vybrala nejlepší přežívající funkce. Argument cobj typu char přesně odpovídá formálnímu parametru mf(char) a lze jej rozšířit na formální parametr typu mf(int). Protože je úroveň přesné shody vyšší, je vybrána funkce mf(char).

    Tato členská funkce však není statická, a proto ji lze volat pouze prostřednictvím objektu nebo ukazatele na objekt třídy myClass pomocí jednoho z přístupových operátorů. V takové situaci, pokud objekt není specifikován a volání funkce je tedy nemožné (přesně náš případ), kompilátor to považuje za chybu.

    Další vlastností členských funkcí, kterou je třeba vzít v úvahu při vytváření sady stojících funkcí, je přítomnost konst nebo volatile specifikátorů na nestatických členech. (O nich pojednává oddíl 13.3.) Jak ovlivňují proces řešení přetížení? Nechte třídu myClass mít následující členské funkce:

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

    Potom jsou v sadě kandidátů pro volání uvedené níže zahrnuty jak statická členská funkce mf(int*), const funkce mf(int), tak non-const funkce mf(double). Ale která z nich bude zahrnuta do mnoha, které přežijí?

    Int main() ( const myClass mc; double dobj; mc.mf(dobj); // která členská funkce je mf()?)

    Při zkoumání transformací, které je třeba aplikovat na skutečné argumenty, zjistíme, že funkce mf(double) a mf(int) přežijí. Typ double aktuálního argumentu dobj přesně odpovídá typu formálního parametru mf(double) a lze jej přetypovat na typ parametru mf(int) pomocí standardní konverze.

    Pokud při volání členské funkce použijete přístupové operátory tečka nebo šipka, vezme se při výběru funkcí do přežívající množiny v úvahu typ objektu nebo ukazatele, na kterém je funkce volána.

    mc je objekt const, na kterém lze volat pouze nestatické členské funkce const. Tím je nekonstantní členská funkce mf(double) vyloučena z množiny přeživších a zůstává v ní jediná funkce mf(int), která je volána.

    Co když je objekt const použit k volání statické členské funkce? Koneckonců, pro takovou funkci nemůžete specifikovat const nebo volatile specifikátor, takže ji lze volat přes konstantní objekt?

    Class myClass ( public: static void mf(int); char mf(char); ); int main() ( const myClass mc; int iobj; mc.mf(iobj); // je možné volat statickou členskou funkci? )

    Statické členské funkce jsou společné pro všechny objekty stejné třídy. Mají přímý přístup pouze ke statickým členům třídy. Nestatické členy konstantního objektu mc tedy nejsou dostupné statickému mf(int). Z tohoto důvodu je přípustné volat statickou členskou funkci na objektu const pomocí operátorů tečka nebo šipka.

    Statické členské funkce tedy nejsou vyloučeny z množiny přežívajících funkcí, i když jsou na objektu, na kterém jsou volány, specifikátory const nebo volatile. Statické členské funkce jsou považovány za odpovídající libovolnému objektu nebo ukazateli na objekt své třídy.

    Ve výše uvedeném příkladu je mc const objekt, takže členská funkce mf(char) je vyloučena ze sady, která existuje. Členská funkce mf(int) v něm ale zůstává, protože je statická. Protože se jedná o jedinou dochovanou funkci, ukazuje se jako nejlepší.

    15.12. Rozlišení přetížení a A prohlášení

    Přetížené operátory a převodníky lze deklarovat ve třídách. Předpokládejme, že během inicializace narazíte na operátor přidání:

    SomeClass sc; int iobj = sc + 3;

    Jak se kompilátor rozhodne, zda zavolá přetížený operátor na SomeClass nebo převede operand sc na vestavěný typ a poté použije vestavěný operátor?

    Odpověď závisí na mnoha přetížených operátorech a převodnících definovaných v SomeClass. Když vyberete operátor, který provede sčítání, použije se proces rozlišení přetížení funkce. V tato sekce ukážeme si, jak vám tento proces umožňuje vybrat správný operátor, když jsou operandy objekty typu třídy.

    Řešení přetížení se řídí stejným tříkrokovým postupem uvedeným v části 9.2:

    • Výběr kandidátských funkcí.
    • Výběr zavedených funkcí.
    • Výběr nejlepší existující funkce.
    • Podívejme se na tyto kroky podrobněji.

      Rozlišení přetížení funkcí se nepoužije, pokud jsou všechny operandy vestavěných typů. V tomto případě je zaručeno použití vestavěného pohonu. (Použití operátorů s vestavěnými operandy typu je popsáno v kapitole 4.) Například:

    class SmallInt ( public: SmallInt(int); ); SmallInt operátor+ (const SmallInt &, const SmallInt &); void func() ( int i1, i2; int i3 = i1 + i2; )

    Protože operandy i1 a i2 jsou typu int a nikoli typu class, sčítání používá vestavěný operátor +. Přetížení operátoru+ (const SmallInt &, const SmallInt &) je ignorováno, ačkoli operandy lze přetypovat na SmallInt pomocí uživatelem definovaného převodu ve formě konstruktoru SmallInt(int). Proces řešení přetížení popsaný níže v těchto situacích neplatí.

    Kromě toho se rozlišení přetížení operátora používá pouze při použití syntaxe operátora:

    Void func() ( SmallInt si(98); int iobj = 65; int res = si + iobj; // použitá syntaxe operátoru)

    Pokud místo toho použijete syntaxi volání funkce: int res = operator+(si, iobj); // syntaxe volání funkce

    pak se použije postup řešení přetížení pro funkce ve jmenném prostoru (viz oddíl 15.10). Pokud se použije syntaxe pro volání členské funkce:

    // syntaxe pro volání členské funkce int res = si.operator+(iobj);

    pak funguje odpovídající postup pro členské funkce (viz část 15.11).

    15.12.1. Kandidát na funkce operátora

    Funkce operátora je kandidátem, pokud má stejný název jako volaná funkce. Při použití následujícího operátoru sčítání

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

    Funkce kandidáta na operátora je operátor+. Která prohlášení operátora+ se berou v úvahu?

    Potenciálně, pokud je syntaxe operátoru použita s operandy typu třídy, je vytvořeno pět kandidátních sad. První tři jsou stejné jako při volání běžných funkcí s argumenty typu třídy:

    • na místě volání je vidět více operátorů. Kandidáti jsou deklarace funkce operátor+() viditelné v místě použití operátoru. Například operátor+() deklarovaný v globálním rozsahu je kandidátem při použití operátoru+() uvnitř main():
    SmallInt operátor+ (const SmallInt &, const SmallInt &); int main() ( SmallInt si(98); int iobj = 65; int res = si + iobj; // ::operator+() - kandidátská funkce )
  • sada operátorů deklarovaných ve jmenném prostoru, ve kterém je definován typ operandu. Pokud je operand typu třídy a tento typ je deklarován v uživatelském jmenném prostoru, pak jsou za kandidáty považovány operátorové funkce deklarované ve stejném jmenném prostoru a mající stejný název jako použitý operátor:
  • jmenný prostor NS ( třída SmallInt ( /* ... */ ); operátor SmallInt+ (const SmallInt&, double); ) int main() ( // si je typu SmallInt: // tato třída je deklarována ve jmenném prostoru NS NS: :SmallInt si(15); // NS::operator+() - kandidátská funkce int res = si + 566; návrat 0; )

    Operand si je typu SmallInt, deklarovaný ve jmenném prostoru NS. Proto se do kandidátní sady přidá přetížení operator+(const SmallInt, double) deklarované ve stejném prostoru;

  • sada operátorů deklarovaných jako přátele tříd, do kterých operandy patří. Pokud operand patří k typu třídy a definice této třídy obsahuje spřátelené funkce se stejným názvem jako použitý operátor, pak jsou přidány do sady kandidátů:
  • jmenný prostor NS ( třída SmallInt ( přítel SmallInt operátor+(const SmallInt&, int) ( /* ... */ ) ); ) int main() ( NS::SmallInt si(15); // operátor funkce přítele+() - kandidát int res = si + 566; návrat 0; )

    Operand si je typu SmallInt. Operátorová funkce operator+(const SmallInt&, int), která je přítelem této třídy, je členem jmenného prostoru NS, i když není deklarována přímo v tomto prostoru. Normální vyhledávání NS tuto funkci operátora nenajde. Když však použijete operátor+() s argumentem typu SmallInt, funkce přátel deklarované v oboru této třídy jsou zahrnuty do úvahy a přidány do sady kandidátů. Tyto tři sady kandidátských funkcí operátorů jsou vytvořeny přesně stejným způsobem jako pro volání běžných funkcí s argumenty typu třídy. Při použití syntaxe operátoru jsou však vytvořeny další dvě sady:

    • sada členských operátorů deklarovaných v levé třídě operandů. Pokud je takový operand operátor+() typu třídy, pak deklarace operátoru+(), které jsou členy této třídy, jsou zahrnuty do sady kandidátských funkcí:
    class myFloat ( myFloat(double); ); class SmallInt ( public: SmallInt(int); operátor SmallInt+ (const myFloat &); ); int main() ( SmallInt si(15); int res = si + 5,66; // člen operátor operátor+() je kandidátem )

    Členský operátor SmallInt::operator+(const myFloat &), definovaný v SmallInt, je součástí sady kandidátských funkcí umožňujících volání operátoru+() v main();

  • mnoho vestavěných operátorů. Vzhledem k typům, které lze použít s vestavěným operátorem + (), kandidáty jsou také:
  • operátor int+(int, int); double operator+(double, double); operátor T*+(T*, I); T* operátor+(I, T*);

    První deklarace je pro vestavěný operátor pro sčítání dvou hodnot celočíselných typů, druhá je pro operátora pro sčítání hodnot typů s plovoucí desetinnou čárkou. Třetí a čtvrtý odpovídá vestavěnému operátoru sčítání typů ukazatelů, který se používá k přidání celého čísla k ukazateli. Poslední dvě deklarace jsou uvedeny v symbolické formě a popisují celou rodinu vestavěných operátorů, které může kompilátor vybrat jako kandidáty pro zpracování operací sčítání.

    Kterákoli z prvních čtyř sad může být prázdná. Pokud například mezi členy třídy SmallInt není žádná funkce s názvem operator+(), bude čtvrtá sada prázdná.

    Celá sada funkcí kandidáta na operátora je spojením pěti podmnožin popsaných výše:

    Jmenný prostor NS ( class myFloat ( myFloat(double); ); class SmallInt ( přítel SmallInt operátor+(const SmallInt &, int) ( /* ... */ ) public: SmallInt(int); operátor int(); SmallInt operátor+ ( const myFloat &); // ... ); SmallInt operator+ (const SmallInt &, double); ) int main() ( // typ si - třída SmallInt: // Tato třída je deklarována v jmenném prostoru NS NS::SmallInt si (15); int res = si + 5,66; // který operátor+()? vrátí 0; )

    Těchto pět sad obsahuje sedm kandidátských funkcí operátora pro roli operátora+() v main():

      první sada je prázdná. V globálním rozsahu, konkrétně v něm se používá operátor+(). hlavní funkce(), žádná deklarace přetíženého operátora operátor+();
    • druhá sada obsahuje operátory deklarované v jmenném prostoru NS, kde je definována třída SmallInt. V tomto prostoru je jeden operátor: NS::SmallInt NS::operator+(const SmallInt &, double);
    • třetí sada obsahuje operátory deklarované jako přátele třídy SmallInt. To zahrnuje NS::SmallInt NS::operator+(const SmallInt &, int);
    • čtvrtá sada obsahuje operátory deklarované jako členy SmallInt. Existuje také tento: NS::SmallInt NS::SmallInt::operator+(const myFloat &);
    • pátá sada obsahuje vestavěné binární operátory:
    operátor int+(int, int); double operator+(double, double); T* operátor+(T*, I); T* operátor+(I, T*);

    Ano, generování více kandidátů pro vyřešení operátoru používaného pomocí syntaxe operátoru je únavné. Ale poté, co je zkonstruována, jsou stabilní funkce a ta nejlepší nalezena, jako dříve, analýzou transformací aplikovaných na operandy vybraných kandidátů.

    15.12.2. Starší funkce

    Množina zavedených operátorských funkcí je tvořena z množiny kandidátů výběrem pouze těch operátorů, které lze volat s danými operandy. Který ze sedmi výše nalezených kandidátů například přežije? Operátor se používá v následujícím kontextu:

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

    Levý operand je typu SmallInt a pravý je dvojitý.

    Prvním kandidátem je stálá funkce dané použití operátor+():

    Levý operand typu SmallInt jako inicializátor přesně odpovídá formálnímu referenčnímu parametru tohoto přetížení operátoru. Ten pravý, který je typu double, také přesně odpovídá druhému formálnímu parametru.

    Následující kandidátská funkce také přežije:

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

    Levý operand si typu SmallInt jako inicializátor přesně odpovídá formálnímu referenčnímu parametru přetíženého operátoru. Pravý je typu int a lze jej přetypovat na typ druhého formálního parametru pomocí standardní konverze.

    Třetí kandidátská funkce také obstojí:

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

    Levý operand si je typu SmallInt, tzn. typ třídy, jejímž členem je přetížený operátor. Pravý je typu int a je přetypován do třídy myFloat pomocí uživatelem definované konverze ve formě konstruktoru myFloat(double).

    Čtvrtá a pátá zavedená funkce jsou vestavěné operátory:

    Int operátor+(int, int); double operator+(double, double);

    Třída SmallInt obsahuje konvertor, který dokáže přetypovat hodnotu typu SmallInt na typ int. Tento převodník se používá ve spojení s prvním vestavěným operátorem pro převod levého operandu na int. Druhý operand typu double je převeden na typ int pomocí standardní konverze. Pokud jde o druhý vestavěný operátor, převodník přetypuje levý operand z typu SmallInt na typ int, načež se výsledek standardně převede na double. Druhý operand typu double přesně odpovídá druhému parametru.

    Nejlepší z těchto pěti dochovaných funkcí je první, operator+(), deklarovaná ve jmenném prostoru NS:

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

    Oba jeho operandy přesně odpovídají parametrům.

    15.12.3. Dvojznačnost

    Přítomnost převodníků, které provádějí implicitní převody na vestavěné typy a přetížené operátory ve stejné třídě, může vést k nejednoznačnosti při výběru mezi nimi. Existuje například následující definice třídy String s porovnávací funkcí:

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

    a toto použití operátoru operátor==:

    Řetězová květina("tulipán"); void foo(const char *pf) ( // přetížený operátor String::operator==() se volá if (flower == pf) cout<< pf <<" is a flower!\en"; // ... }

    Pak při porovnávání

    Květina == pf

    operátor rovnosti třídy String se nazývá:

    Pro transformaci pravého operandu pf z typu const char* na typ String parametru operator==() se použije uživatelsky definovaná konverze, která zavolá konstruktor:

    Řetězec (const char *)

    Pokud do definice třídy String přidáte konvertor k typu const char*:

    Class String ( // ... public: String(const char * = 0); bool operator== (const String &) const; operator const char*(); // new converter );

    pak se ilustrované použití operátoru==() stává nejednoznačným:

    // kontrola rovnosti se již nezkompiluje! if (květ == pf)

    Díky přidání převodníku operátoru const char*() je vestavěný operátor porovnání

    je také považován za zavedenou vlastnost. S jeho pomocí lze levý operand květu typu String převést na typ const char *.

    Nyní existují dvě zavedené operátorové funkce pro použití operator==() v foo(). První

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

    vyžaduje uživatelem definovanou konverzi správného operandu pf z const char* na String. Druhý

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

    vyžaduje vlastní konverzi levého operandu květiny z String na const char*.

    První přežívající funkce je tedy lepší pro levý operand a druhá je lepší pro pravý. Protože neexistuje žádná nejlepší funkce, je volání kompilátorem označeno jako nejednoznačné.

    Při navrhování rozhraní třídy, které obsahuje deklarace přetížených operátorů, konstruktorů a převodníků, musíte být velmi opatrní. Konverze definované uživatelem jsou implicitně aplikovány kompilátorem. To může způsobit selhání vestavěných operátorů, když je přetížení vyřešeno pro operátory s operandy typu třídy.

    Cvičení 15.17

    Vyjmenujte pět sad kandidátních funkcí, které se berou v úvahu při řešení přetížení operátorů operandy typu třídy.

    Cvičení 15.18

    Který operátor+() by byl vybrán jako nejlepší operátor pro operátor přidání v main()? Vypište všechny kandidátské funkce, všechny přežívající funkce a převody typů, které se mají použít na argumenty pro každou přežívající funkci.

    Jmenný prostor NS ( class complex ( complex(double); // ... ); class LongDouble ( friend LongDouble operator+(LongDouble &, int) ( /* ... */ ) public: LongDouble(int); operator double() ; Operátor LongDouble+(const complex &); // ... ); Operátor LongDouble

    Základy přetížení operátora

    C#, jako každý programovací jazyk, má připravenou sadu tokenů používaných k provádění základních operací na vestavěných typech. Je například známo, že operaci + lze použít na dvě celá čísla a získat jejich součet:

    // Operace + s celými čísly. int a = 100; int b = 240; int c = a + b; //s se nyní rovná 340

    Není zde nic nového, ale napadlo vás někdy, že stejnou operaci + lze použít na většinu vestavěných datových typů C#? Zvažte například tento kód:

    // Operace + s řetězci. string si = "Ahoj"; řetězec s2 = "svět!"; řetězec s3 = si + s2; // s3 nyní obsahuje "Ahoj světe!"

    Funkčnost operace + je v podstatě jednoznačně založena na reprezentovaných datových typech (v tomto případě řetězců nebo celých čísel). Když je operace + aplikována na číselné typy, získáme aritmetický součet operandů. Když se však stejná operace použije na typy řetězců, výsledkem je zřetězení řetězců.

    Jazyk C# poskytuje možnost vytvářet speciální třídy a struktury, které také jedinečně reagují na stejnou sadu základních tokenů (jako je operátor +). Mějte na paměti, že absolutně každý vestavěný operátor C# nemůže být přetížen. Následující tabulka popisuje možnosti přetížení základních operací:

    Operace C# Možnost přetížení
    +, -, !, ++, --, pravda, nepravda Tato sada unárních operátorů může být přetížena
    +, -, *, /, %, &, |, ^, > Tyto binární operace mohou být přetížené
    ==, !=, <, >, <=, >= Tyto porovnávací operátory mohou být přetížené. C# vyžaduje sdílené přetěžování „podobných“ operátorů (tj.< и >, <= и >=, == a!=)
    Provoz nemůže být přetížen. Indexery však nabízejí podobnou funkcionalitu
    () Operaci () nelze přetížit. Speciální metody převodu však poskytují stejnou funkcionalitu
    +=, -=, *=, /=, %=, &=, |=, ^=, >= Operátory krátkého přiřazení nelze přetížit; získáte je však automaticky přetížením odpovídající binární operace

    Přetížení operátora úzce souvisí s přetížením metody. Chcete-li přetížit operátor, použijte klíčové slovo operátor, který definuje operátorovou metodu, která zase definuje akci operátoru vzhledem k jeho třídě. Existují dvě formy operátorových metod: jedna pro unární operátory, druhá pro binární operátory. Níže je uveden obecný formulář pro každou variantu těchto metod:

    // Obecná forma přetížení unárního operátoru. public static return_type operator op(operand typ_parametru) ( // operace ) // Obecná forma přetěžování binárního operátoru. public static return_type operátor op(parametr_typ1 operand1, parametr_typ2 operand2) ( // operace )

    Zde je op nahrazen přetíženým operátorem, například + nebo / a návratový_typ označuje konkrétní typ hodnoty vrácené zadanou operací. Tato hodnota může být libovolného typu, ale často je specifikována jako stejný typ jako třída, pro kterou je operátor přetížen. Tato korelace usnadňuje použití přetížených operátorů ve výrazech. Pro unární operátory operand označuje předávaný operand a pro binární operátory se označuje totéž operand1 A operand2. Všimněte si, že operátorské metody musí mít veřejné i statické specifikátory typu.

    Přetížení binárních operátorů

    Podívejme se na použití přetěžování binárních operátorů na jednoduchém příkladu:

    Použití systému; pomocí System.Collections.Generic; pomocí System.Linq; pomocí System.Text; jmenný prostor ConsoleApplication1 ( class MyArr ( // Souřadnice bodu v trojrozměrném prostoru 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; ) // Přetížení binárního operátoru + veřejný statický operátor MyArr +(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; ) // Přetížení binárního operátoru - veřejný statický operátor MyArr -(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; ) ) class Program ( static void Main (string args) ( MyArr Point1 = new MyArr(1, 12, -4); MyArr Point2 = new MyArr(0, -3, 18); Console.WriteLine("Souřadnice prvního bodu: " + Bod1.x + " " + Bod1.y + " " + Bod1.z); Console.WriteLine("Souřadnice druhého bodu: " + Bod2.x + " " + Bod2.y + " " + Bod2. z + "\n"); MyArr Bod3 = Bod1 + Bod2; Console.WriteLine("\nBod1 + Bod2 = " + Bod3.x + " " + Bod3.y + " " + Bod3.z); Bod3 = Bod1 - Bod2; Console.WriteLine("\nBod1 - Bod2 = " + Bod3.x + " " + Bod3.y + " " + Bod3.z); Console.ReadLine(); )))

    Přetížení unárních operátorů

    Unární operátory jsou přetíženy stejným způsobem jako binární operátory. Hlavní rozdíl je samozřejmě v tom, že mají pouze jeden operand. Pojďme modernizovat předchozí příklad přidáním přetížení operátorů ++, --, -:

    Použití systému; pomocí System.Collections.Generic; pomocí System.Linq; pomocí System.Text; jmenný prostor ConsoleApplication1 ( class MyArr ( // Souřadnice bodu v trojrozměrném prostoru 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; ) // Přetížení binárního operátoru + veřejný statický operátor MyArr +(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; ) // Přetížení binárního operátoru - veřejný statický operátor MyArr -(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; ) / / Přetížení unárního operátoru - veřejný statický operátor MyArr -(MyArr obj1) ( MyArr arr = new MyArr(); arr.x = -obj1.x; arr.y = -obj1.y; arr.z = -obj1.z ; return arr; ) // Přetížení unárního operátoru ++ veřejný statický operátor MyArr ++(MyArr obj1) ( obj1.x += 1; obj1.y += 1; obj1.z +=1; return obj1; ) / / Přetížení unárního operátoru -- veřejný statický operátor MyArr ---(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("Souřadnice prvního bod: " + Bod1.x + " " + Bod1.y + " " + Bod1.z); Console.WriteLine("Souřadnice druhého bodu: " + Bod2.x + " " + Bod2.y + " " + Bod2.z + "\n"); MyArr Bod3 = Bod1 + Bod2; Console.WriteLine("\nBod1 + Bod2 = " + Bod3.x + " " + Bod3.y + " " + Bod3.z); Bod3 = Bod1 – Bod2; Console.WriteLine("Bod1 - Bod2 = " + Bod3.x + " " + Bod3.y + " " + Bod3.z); Bod3 = -Bod1; Console.WriteLine("-Point1 = " + Bod3 .x + " " + Bod3.y + " " + Bod3.z); Bod2++; Console.WriteLine("Point2++ = " + Bod2.x + " " + Bod2.y + " " + Bod2.z); Bod2- -; Console. WriteLine("Point2-- = " + Bod2.x + " " + Bod2.y + " " + Bod2.z); Console.ReadLine(); ) ) )

    Mnoho programovacích jazyků používá operátory: minimálně přiřazení (=, := nebo podobné) a aritmetické operátory (+, -, * a /). Ve většině staticky typovaných jazyků jsou tyto operátory vázány na typy. Například v Javě je sčítání pomocí operátoru + možné pouze pro celá čísla, čísla s plovoucí desetinnou čárkou a řetězce. Pokud definujeme vlastní třídy pro matematické objekty, jako jsou matice, můžeme implementovat metodu pro jejich sčítání, ale můžeme ji volat pouze takto: a = b.add(c) .

    V C++ takové omezení neexistuje – přetížit můžeme téměř jakýkoli známý operátor. Možnosti jsou nekonečné: můžete si vybrat libovolnou kombinaci typů operandů, jediným omezením je, že musí být přítomen alespoň jeden uživatelsky definovaný typ operandu. To znamená definovat nový operátor nad vestavěnými typy nebo přepsat existující je to zakázáno.

    Kdy přetížit operátory?

    Pamatujte na hlavní věc: přetěžujte operátory tehdy a jen tehdy, když to dává smysl. Tedy pokud je smysl přetížení zřejmý a neobsahuje skrytá překvapení. Přetížení operátoři se musí chovat stejně jako jejich základní verze. Výjimky jsou samozřejmě povoleny, ale pouze v případech, kdy jsou doprovázeny jasným vysvětlením. Dobrým příkladem jsou operátoři<< и >> standardní knihovna iostream, které se zjevně nechovají jako běžné operátory.

    Zde jsou dobré a špatné příklady přetěžování operátorů. Výše uvedené přidání matice je ilustrativní případ. Zde je přetížení operátoru sčítání intuitivní a pokud je implementováno správně, nevyžaduje vysvětlení:

    Matice a, b; Matice c = a + b;

    Příkladem špatného přetížení operátora přidání by bylo přidání dvou objektů „hráčů“ ve hře. Co tím tvůrce třídy myslel? Jaký bude výsledek? Nevíme, co operace dělá, a proto je použití tohoto operátoru nebezpečné.

    Jak přetížit operátory?

    Přetěžování operátorů je podobné přetěžování funkcí se speciálními názvy. Ve skutečnosti, když kompilátor vidí výraz, který obsahuje operátor a uživatelsky definovaný typ, nahradí výraz voláním odpovídající funkce přetíženého operátoru. Většina jejich názvů začíná operátorem klíčového slova, za kterým následuje označení odpovídajícího operátoru. Pokud se zápis neskládá ze speciálních znaků, například v případě převodu typu nebo operátoru správy paměti (nový, smazat atd.), musí být slovo operátor a zápis operátoru odděleny mezerou (operátor nový) , jinak může být mezera ignorována (operátor+ ).

    Většina operátorů může být přetížena jak metodami tříd, tak i jednoduché funkce, ale existuje několik výjimek. Když je přetížený operátor metodou třídy, typ prvního operandu musí být tato třída (vždy *this) a druhý musí být deklarován v seznamu parametrů. Příkazy metody navíc nejsou statické, s výjimkou příkazů správy paměti.

    Když přetížíte operátor v metodě třídy, získá přístup k soukromým polím třídy, ale skrytý převod prvního argumentu není dostupný. Binární funkce jsou proto obvykle přetěžovány jako volné funkce. Příklad:

    Class Rational ( public: //Konstruktor lze použít pro implicitní převod z int: Rational(int čitatel, int jmenovatel = 1); Racionální operátor+(Rational const& rhs) const;); int main() ( Racionální a, b, c; int i; a = b + c; //ok, není nutná konverze a = b + i; //ok, implicitní konverze druhého argumentu a = i + c; //CHYBA: první argument nelze implicitně převést)

    Když jsou unární operátory přetíženy jako volné funkce, je jim k dispozici konverze skrytých argumentů, která se však obvykle nepoužívá. Na druhou stranu je tato vlastnost nezbytná pro binární operátory. Proto by hlavní rada byla následující:

    Implementujte unární operátory a binární operátory jako „ X=” ve formě metod tříd a dalších binárních operátorů – ve formě volných funkcí.

    Kteří operátoři mohou být přetíženi?

    Můžeme přetížit téměř jakýkoli operátor C++, s výhradou následujících výjimek a omezení:

    • Nemůžete definovat nový operátor, například operátor** .
    • Následující operátory nelze přetížit:
      1. ?: (ternární operátor);
      2. :: (přístup k vnořeným názvům);
      3. . (přístup k polím);
      4. .* (přístup k polím pomocí ukazatele);
      5. Operátory sizeof , typeid a cast.
    • Následující operátory lze přetížit pouze jako metody:
      1. = (zadání);
      2. -> (přístup k polím pomocí ukazatele);
      3. () (volání funkce);
      4. (přístup pomocí indexu);
      5. ->* (přístup k ukazateli na pole ukazatelem);
      6. operátory konverze a správy paměti.
    • Počet operandů, pořadí provádění a asociativita operátorů je určena standardní verzí.
    • Alespoň jeden operand musí být typu uživatele. Typedef se nepočítá.

    Další část představí přetížení operátorů C++, ve skupinách i jednotlivě. Každý úsek je charakterizován sémantikou, tzn. očekávané chování. Kromě toho budou ukázány typické způsoby deklarování a implementace operátorů.