Tak więc wiemy już, jak deklarować, definiować i używać funkcji w programach. W tym rozdziale porozmawiamy o ich specjalnej formie - przeciążonych funkcjach. Dwie funkcje są nazywane przeciążonymi, jeśli mają taką samą nazwę, są zadeklarowane w tym samym zakresie, ale mają różne listy parametrów formalnych. Wyjaśnimy, jak takie funkcje są deklarowane i dlaczego są przydatne. Następnie rozważymy kwestię ich rozwiązania, tj. o tym, która z kilku przeciążonych funkcji jest wywoływana podczas wykonywania programu. Ten problem jest jednym z najtrudniejszych w C++. Dla tych, którzy chcą zagłębić się w szczegóły, zainteresuje się przeczytanie dwóch sekcji na końcu rozdziału, które bardziej szczegółowo omawiają temat konwersji typów argumentów i rozwiązywania przeciążeń.

9.1. Przeciążone deklaracje funkcji

Teraz, nauczywszy się deklarować, definiować i używać funkcji w programach, zapoznamy się z: przeciążać to kolejny aspekt w C++. Przeciążanie pozwala mieć kilka funkcji o tej samej nazwie, które wykonują podobne operacje na argumentach różnych typów.
Korzystałeś już z predefiniowanej funkcji przeciążonej. Na przykład, aby ocenić wyrażenie

wywoływana jest operacja dodawania liczb całkowitych, natomiast obliczenie wyrażenia

1.0 + 3.0

wykonuje dodawanie zmiennoprzecinkowe. Wybór tej lub innej operacji jest niedostrzegalny dla użytkownika. Operator dodawania jest przeciążony, aby pomieścić operandy różnych typów. To kompilator, a nie programista, jest odpowiedzialny za rozpoznanie kontekstu i zastosowanie operacji odpowiednich dla typów operandów.
W tym rozdziale pokażemy, jak zdefiniować własne przeciążone funkcje.

9.1.1. Dlaczego musisz przeciążyć nazwę funkcji?

Podobnie jak w przypadku wbudowanej operacji dodawania, możemy potrzebować zestawu funkcji, które wykonują tę samą akcję, ale na parametrach różnych typów. Załóżmy, że chcemy zdefiniować funkcje, które zwracają największą z przekazanych wartości parametrów. Gdyby nie było przeciążenia, każda taka funkcja musiałaby mieć unikalną nazwę. Na przykład rodzina funkcji max() może wyglądać tak:

int i_max(int, int); int vi_max(stały wektor &); int matrix_max(const matrix &);

Jednak wszystkie robią to samo: zwracają największą z wartości parametrów. Z punktu widzenia użytkownika jest tu tylko jedna operacja - wyliczenie maksimum i szczegóły jego wykonania są mało interesujące.
Zauważona złożoność leksykalna odzwierciedla ograniczenie środowiska programistycznego: każda nazwa występująca w tym samym zakresie musi odnosić się do unikalnej jednostki (obiektu, funkcji, klasy itp.). Takie ograniczenie w praktyce stwarza pewne niedogodności, ponieważ programista musi zapamiętać lub jakoś znaleźć wszystkie nazwy. Przeciążanie funkcji pomaga w tym problemie.
Używając przeciążenia, programista może napisać coś takiego:

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

Takie podejście okazuje się niezwykle przydatne w wielu sytuacjach.

9.1.2. Jak przeciążyć nazwę funkcji?

W C++ dwie lub więcej funkcji może mieć taką samą nazwę, pod warunkiem, że ich listy parametrów różnią się liczbą parametrów lub ich typami. W ten przykład deklarujemy przeciążoną funkcję max():

intmax(int, int); int max(stały wektor &); int max(const macierz &);

Każda przeciążona deklaracja wymaga osobnej definicji funkcji max() z odpowiednią listą parametrów.
Jeśli nazwa funkcji jest zadeklarowana więcej niż raz w pewnym zakresie, to druga (i kolejne) deklaracja jest interpretowana przez kompilator w następujący sposób:

  • jeśli listy parametrów dwóch funkcji różnią się liczbą lub typami parametrów, to funkcje są uważane za przeciążone: // funkcje przeciążone void print(const string &); pusty druk (wektor &);
  • jeśli zwracany typ i listy parametrów w deklaracjach dwóch funkcji są takie same, to druga deklaracja jest uważana za powtórzoną: // deklaracje tej samej funkcji void print(const string &str); void print(const string &); Nazwy parametrów nie są brane pod uwagę podczas porównywania deklaracji;
    jeśli listy parametrów dwóch funkcji są takie same, ale typy zwracane są różne, to druga deklaracja jest uznawana za niepoprawną (niezgodną z pierwszą) i jest oflagowana przez kompilator jako błąd: unsigned int max(int ​​i1, wew i2); int max(int ​​i1, int i2);
    // błąd: tylko typy się różnią
    // zwróć wartości

Przeciążone funkcje nie mogą różnić się tylko typami zwracanymi; jeśli listy parametrów dwóch funkcji różnią się tylko domyślnymi wartościami argumentów, to druga deklaracja jest uważana za powtórzoną:

// deklaracje tej samej funkcji int max (int *ia, int sz); int max (int * ia, int = 10);

Słowo kluczowe typedef tworzy alternatywną nazwę dla już istniejącej typ danych, nie jest tworzony żaden nowy typ. Dlatego, jeśli listy parametrów dwóch funkcji różnią się tylko tym, że jedna używa typedef, a druga używa typu, dla którego typedef jest aliasem, listy są uważane za takie same, jak w następujących dwóch deklaracjach funkcji calc(). W takim przypadku druga deklaracja da błąd kompilacji, ponieważ zwracana wartość jest inna niż określona wcześniej:

// typedef nie wprowadza nowego typedef double DOLLAR; // błąd: te same listy parametrów, ale różne // typy zwracane extern DOLLAR calc(DOLLAR); extern int calc(podwójny);

W takim porównaniu nie są brane pod uwagę specyfikatory const lub volatile. W związku z tym następujące dwie deklaracje są uważane za takie same:

// zadeklaruj tę samą funkcję void f(int); void f(const int);

Specyfikator const jest ważny tylko w definicji funkcji: wskazuje, że zmiana wartości parametru w ciele funkcji jest zabroniona. Jednak argument przekazany przez wartość może być użyty w treści funkcji jak zwykła zmienna wyzwalana: poza funkcją zmiany nie są widoczne. (Sposoby przekazywania argumentów, w szczególności przekazywanie przez wartość, zostały omówione w rozdziale 7.3.) Dodanie specyfikatora const do parametru przekazywanego przez wartość nie wpływa na jego interpretację. Do funkcji zadeklarowanej jako f(int) można przekazać dowolną wartość typu int, podobnie jak funkcja f(const int). Ponieważ oba przyjmują ten sam zestaw wartości argumentów, powyższe deklaracje nie są uważane za przeciążone. f() można zdefiniować jako

Pustka f(int i) ( )

Pusta f(const int i) ( )

Obecność tych dwóch definicji w jednym programie jest błędem, ponieważ ta sama funkcja jest zdefiniowana dwukrotnie.
Jeśli jednak do parametru typu wskaźnika lub referencji zostanie zastosowany specyfikator const lub volatile, jest on brany pod uwagę podczas porównywania deklaracji.

// różne funkcje są deklarowane void f(int*); void f(const int*); // i tutaj zadeklarowane są różne funkcje
nieważne f(int&);
void f(const int&);

9.1.3. Kiedy nie przeciążać nazwy funkcji?

W jakich przypadkach przeciążanie nazw nie jest korzystne? Na przykład przypisywanie różnych nazw do funkcji ułatwia czytanie programu. Oto kilka przykładów. Poniższe funkcje działają na tym samym abstrakcyjnym typie daty. Na pierwszy rzut oka są dobrymi kandydatami na przeciążenie:

void setDate(Data&, int, int, int); Data &convertDate(const string &); void printDate(const Date&);

Funkcje te działają na tym samym typie danych, klasie Date, ale wykonują semantycznie różne akcje. W tym przypadku złożoność leksykalna związana z użyciem różnych nazw wynika z przyjętej przez programistę konwencji dostarczania zestawu operacji na typie danych i nazywania funkcji zgodnie z semantyką tych operacji. To prawda, że ​​mechanizm klas C++ sprawia, że ​​taka konwencja jest zbędna. Powinniśmy uczynić takie funkcje członkami klasy Date, ale pozostawić różne nazwy, które odzwierciedlają znaczenie operacji:

#włączać class Date ( public: set(int, int, int); Date& convert(const string &); void print(); // ...
};

Weźmy inny przykład. Następujące pięć funkcji składowych Screen wykonuje różne operacje na kursorze ekranowym, który należy do tej samej klasy. Rozsądne może wydawać się przeciążenie tych funkcji pod ogólną nazwą move():

Ekran&przesuńDom(); Screen& moveAbs(int, int); Screen& moveRel(int, int, char *kierunek); Ekran& moveX(int); Ekran& przesuńY(int);

Jednak dwie ostatnie funkcje nie mogą być przeciążone, ponieważ mają te same listy parametrów. Aby podpis był wyjątkowy, połączmy je w jedną funkcję:

// funkcja łącząca moveX() i moveY() Screen& move(int, char xy);

Teraz wszystkie funkcje mają różne listy parametrów, więc można je przeciążać pod nazwą move(). Nie należy tego jednak robić: różne nazwy zawierają informacje, bez których program będzie trudniejszy do zrozumienia. Na przykład operacje ruchu kursora wykonywane przez te funkcje są różne. Na przykład moveHome() wykonuje specjalny rodzaj ruchu w lewo górny róg ekran. Które z dwóch poniższych wywołań jest bardziej przyjazne dla użytkownika i łatwiejsze do zapamiętania?

// które wywołanie jest jaśniejsze? mojeEkran.dom(); // myślimy, że to! mojeEkran.przenieś();

W niektórych przypadkach nie trzeba przeciążać nazwy funkcji ani nadawać różnych nazw: użycie domyślnych wartości argumentów pozwala połączyć kilka funkcji w jedną. Na przykład funkcje sterowania kursorem

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

różnią się obecnością trzeciego parametru typu char*. Jeśli ich implementacje są podobne i można znaleźć rozsądną wartość domyślną dla trzeciego argumentu, to obie funkcje można zastąpić jednym. W tym przypadku wskaźnik o wartości 0 nadaje się do roli wartości domyślnej:

Przenieś(int, int, char* = 0);

Należy używać pewnych funkcji, gdy jest to wymagane przez logikę aplikacji. Nie jest konieczne dołączanie przeładowanych funkcji do programu tylko dlatego, że one istnieją.

9.1.4. Przeciążenie i zakres A

Wszystkie przeciążone funkcje są zadeklarowane w tym samym zakresie. Na przykład funkcja zadeklarowana lokalnie nie przeciąża, ale po prostu ukrywa funkcję globalną:

#włączać void print(const string &); voidprint(podwójny); // przeciąża print() void fooBar(int ival)
{
// oddzielny zakres: ukrywa obie implementacje print()
zewnętrzny void print(int); // błąd: print(const string &) nie jest widoczny w tym obszarze
print("Wartość: ");
drukuj(ival); // poprawnie: print(int) jest widoczny
}

Ponieważ każda klasa definiuje swój własny zakres, funkcje należące do dwóch różnych klas nie przeciążają się nawzajem. (Funkcje składowe klasy omówiono w rozdziale 13. Rozwiązywanie przeciążeń funkcji składowych klasy omówiono w rozdziale 15.)
Dozwolone jest również deklarowanie takich funkcji w przestrzeni nazw. Każdy z nich ma również skojarzony z nim własny zakres, dzięki czemu funkcje zadeklarowane w różnych zakresach nie przeciążają się nawzajem. Na przykład:

#włączać przestrzeń nazw IBM ( extern void print(const string &); extern void print(double); // przeciąża print() ) przestrzeń nazw Disney ( // oddzielny zakres: // nie przeciąża funkcji print() z przestrzeni nazw IBM extern void drukuj (int); )

Korzystanie z deklaracji using i dyrektyw using ułatwia udostępnianie członków przestrzeni nazw w innych zakresach. Mechanizmy te mają pewien wpływ na deklaracje przeciążonych funkcji. (Korzystanie z deklaracji i używanie dyrektyw zostało omówione w rozdziale 8.6.)

Jak using-declaration wpływa na przeciążanie funkcji? Przypomnij sobie, że wprowadza alias dla elementu członkowskiego przestrzeni nazw w zakresie, w którym występuje deklaracja. Co robią takie deklaracje w poniższym programie?

Przestrzeń nazw libs_R_us ( int max(int, int); int max(double, double); extern void print(int);
zewnętrzny pusty wydruk (podwójny);
) // używanie-deklaracji
używając libs_R_us::max;
używając libs_R_us::print(double); // błąd void func()
{
maks. (87, 65); // wywołuje libs_R_us::max(int, int)
maks. (35,5; 76,6); // wywołuje libs_R_us::max(double, double)

Pierwsza deklaracja using przenosi obie funkcje libs_R_us::max do zasięgu globalnego. Teraz każda z funkcji max() może być wywołana wewnątrz func(). Typy argumentów określają, którą funkcję należy wywołać. Druga deklaracja using to błąd: nie może mieć listy parametrów. Funkcja libs_R_us::print() jest deklarowana tylko w następujący sposób:

Korzystanie z libs_R_us::print;

Deklaracja using zawsze udostępnia wszystkie przeciążone funkcje o określonej nazwie. To ograniczenie zapewnia, że ​​interfejs przestrzeni nazw libs_R_us nie zostanie naruszony. Oczywiste jest, że w przypadku połączenia

Drukuj(88);

autor przestrzeni nazw oczekuje wywołania funkcji libs_R_us::print(int). Jeśli pozwolisz użytkownikowi selektywnie uwzględnić tylko jedną z kilku przeciążonych funkcji w zakresie, zachowanie programu stanie się nieprzewidywalne.
Co się stanie, jeśli deklaracja using wprowadzi w zakres funkcji o już istniejącej nazwie? Te funkcje wyglądają tak, jakby zostały zadeklarowane w miejscu, w którym występuje deklaracja using. Dlatego wprowadzone funkcje uczestniczą w procesie rozwiązywania nazw wszystkich przeciążonych funkcji występujących w danym zakresie:

#włączać przestrzeń nazw libs_R_us ( extern void print(int); extern void print(double); ) extern void print(const string &); // libs_R_us::print(int) i libs_R_us::print(double)
// przeciążenie print(const string &)
używając libs_R_us::print; void fooBar(int ival)
{
// print(const string &)
}

Deklaracja using dodaje dwie deklaracje do zakresu globalnego: jedną dla print(int) i jedną dla print(double). Są to aliasy w przestrzeni libs_R_us i są zawarte w zestawie przeciążonych funkcji o nazwie print, gdzie globalny print(const string &) już istnieje. Podczas rozwiązywania problemu przeciążenia drukowania w fooBar brane są pod uwagę wszystkie trzy funkcje.
Jeśli deklaracja using wprowadza jakąś funkcję do zakresu, który ma już funkcję o tej samej nazwie i tej samej liście parametrów, jest to traktowane jako błąd. Deklaracja using nie może aliasować funkcji print(int) w przestrzeni nazw libs_R_us, jeśli print(int) już istnieje w zasięgu globalnym. Na przykład:

Przestrzeń nazw libs_R_us ( void print(int); void print(double); ) void print(int); używając libs_R_us::print; // błąd: powtórzona deklaracja print(int) void fooBar(int ival)
{
drukuj(ival); // który druk? ::drukuj lub libs_R_us::drukuj
}

Pokazaliśmy, jak powiązane są deklaracje using i funkcje przeciążone. Przyjrzyjmy się teraz szczegółom korzystania z dyrektywy using. Dyrektywa using powoduje, że członkowie przestrzeni nazw pojawiają się zadeklarowani poza tą przestrzenią, dodając je do nowego zakresu. Jeśli w tym zakresie istnieje już funkcja o tej samej nazwie, wystąpi przeciążenie. Na przykład:

#włączać przestrzeń nazw libs_R_us ( extern void print(int); extern void print(double); ) extern void print(const string &); // używanie dyrektywy
// print(int), print(double) i print(const string &) są elementami
// ten sam zestaw przeciążonych funkcji
za pomocą przestrzeni nazw libs_R_us; void fooBar(int ival)
{
print("Wartość: "); // wywołuje funkcję globalną
// print(const string &)
drukuj(ival); // wywołuje libs_R_us::print(int)
}

Dotyczy to również sytuacji, gdy istnieje wiele dyrektyw using. Funkcje o tej samej nazwie, które są członkami różnych przestrzeni, są zawarte w tym samym zestawie:

Przestrzeń nazw IBM ( int print(int); ) przestrzeń nazw Disney ( double print(double); ) // using-directive // ​​​​generuje wiele przeciążonych funkcji z różnych // przestrzeni nazw używając namespace IBM; za pomocą przestrzeni nazw Disney; długi podwójny nadruk (długi podwójny); int main() (
druk(1); // o nazwie IBM::print(int)
drukuj(3.1); // zadzwoń do Disney::print(double)
zwróć 0;
}

Zestaw przeciążonych funkcji o nazwie print w zakresie globalnym obejmuje funkcje print(int), print(double) i print(long double). Wszystkie z nich są uwzględniane w funkcji main() podczas rozwiązywania przeciążenia, chociaż pierwotnie zostały zdefiniowane w różnych przestrzeniach nazw.
Tak więc ponownie przeciążone funkcje znajdują się w tym samym zakresie. W szczególności trafiają tam w wyniku użycia deklaracji i dyrektyw, które udostępniają nazwy z innych zakresów.

9.1.5. dyrektywa extern "C" i przeciążone funkcje A

Widzieliśmy w sekcji 7.7, że dyrektywa bind extern "C" może być użyta w programie C++ do wskazania, że ​​jakiś obiekt znajduje się w części C. Jak ta dyrektywa wpływa na przeciążone deklaracje funkcji? Czy funkcje napisane w C++ i C mogą znajdować się w tym samym zestawie?
Dyrektywa bind może określać tylko jedną z wielu przeciążonych funkcji. Na przykład następujący program jest niepoprawny:

// błąd: dyrektywa określona dla dwóch przeciążonych funkcji extern "C" extern "C" void print(const char*); extern "C" void print(int);

Poniższy przykład przeciążonej funkcji calc() ilustruje typowe użycie dyrektywy extern "C":

ClassSmallInt(/* ... */); klasa DużaNum(/* ... */); // funkcja napisana w C może być wywołana zarówno z programu,
// napisane w C lub z programu napisanego w C++.
// Funkcje C++ obsługują parametry będące klasami
zewnętrzne "C" podwójne obliczenie (podwójne);
extern SmallInt calc(const SmallInt&);
extern BigNum calc(const BigNum&);

Funkcja calc() napisana w C może być wywoływana zarówno z C, jak iz programu C++. Pozostałe dwie funkcje przyjmują klasę jako parametr i dlatego mogą być używane tylko w programie C++. Kolejność deklaracji nie jest ważna.
Dyrektywa bind nie ma znaczenia przy podejmowaniu decyzji, którą funkcję wywołać; ważne są tylko typy parametrów. Wybierana jest funkcja, która najlepiej pasuje do typów przekazanych argumentów:

Smallint si = 8; int main() ( calc(34); // wywołanie funkcji C calc(double) calc(si); // wywołanie funkcji C++ calc(const SmallInt &) // ... return 0; )

9.1.6. Wskaźniki do przeciążonych funkcji A

Możesz zadeklarować wskaźnik do jednej z wielu przeciążonych funkcji. Na przykład:

zewnętrzna pustka ff(wektor ); extern void ff(unsigned int); // na jaką funkcję wskazuje pf1?
void (*pf1)(unsigned int) =

Ponieważ funkcja ff() jest przeciążona, sam inicjator &ff nie wystarczy do wybrania poprawna opcja. Aby zrozumieć, która funkcja inicjuje wskaźnik, kompilator szuka w zestawie wszystkich przeciążonych funkcji jednej, która ma ten sam typ zwracany i listę parametrów, co funkcja, do której odwołuje się wskaźnik. W naszym przypadku zostanie wybrana funkcja ff(unsigned int).
Ale co, jeśli nie ma funkcji, która dokładnie pasuje do typu wskaźnika? Następnie kompilator wyświetli komunikat o błędzie:

zewnętrzna pustka ff(wektor ); extern void ff(unsigned int); // błąd: nie znaleziono dopasowania: nieprawidłowa lista parametrów void (*pf2)(int) = // błąd: nie znaleziono dopasowania: zły typ zwracany double (*pf3)(vector ) = &ff;

Zadanie działa w podobny sposób. Jeśli wartość wskaźnika powinna być adresem przeciążonej funkcji, to typ wskaźnika do funkcji służy do wybierania operandu po prawej stronie operatora przypisania. A jeśli kompilator nie znajdzie funkcji, która dokładnie pasuje do żądanego typu, wyświetla komunikat o błędzie. W związku z tym konwersja typu między wskaźnikami funkcji nigdy nie jest wykonywana.

Obliczanie macierzy (macierz const &); intcalc(int, int); int (*pc1)(int, int) = 0;
int (*pc2)(int, double) = 0; //...
// poprawnie: wybrano funkcję calc(int, int)
pc1 = // błąd: brak dopasowania: nieprawidłowy drugi typ parametru
szt2=

9.1.7. Bezpieczne wiązanie A

Używając przeciążania można odnieść wrażenie, że program może mieć kilka funkcji o tej samej nazwie z różnymi listami parametrów. Ta wygoda leksykalna istnieje jednak tylko na poziomie tekstu źródłowego. W większości systemów kompilacji programy przetwarzające ten tekst w celu wygenerowania kodu wykonywalnego wymagają, aby wszystkie nazwy były różne. Edytory linków zwykle pozwalają Zewnętrzne linki leksykalnie. Jeśli taki edytor napotka nazwę print dwa lub więcej razy, nie może ich rozróżnić na podstawie analizy typu (w tym momencie informacje o typie są zwykle tracone). Więc po prostu wypisuje komunikat o przedefiniowanym wydruku znaku i kończy działanie.
Aby rozwiązać ten problem, nazwa funkcji wraz z listą jej parametrów jest dekorowana tak, aby nadać unikalną nazwę wewnętrzną. Programy wywoływane po kompilatorze widzą tylko tę wewnętrzną nazwę. To, jak dokładnie odbywa się to rozpoznawanie nazw, zależy od implementacji. Ogólną ideą jest reprezentowanie liczby i typów parametrów jako ciągu znaków i dołączanie go do nazwy funkcji.
Jak wspomniano w sekcji 8.2, takie kodowanie gwarantuje w szczególności, że dwie deklaracje funkcji o tej samej nazwie z różnymi listami parametrów umieszczonych w różnych plikach nie są postrzegane przez linker jako deklaracje tej samej funkcji. Ponieważ ta metoda pomaga odróżnić przeciążone funkcje podczas fazy edycji łącza, mówimy o bezpiecznym łączeniu.
Dekoracja nazw nie dotyczy funkcji zadeklarowanych za pomocą dyrektywy extern "C", ponieważ tylko jedna z wielu przeciążonych funkcji może zostać napisana w czystym C. Dwie funkcje z różnymi listami parametrów zadeklarowane jako extern "C" są traktowane przez linker jako jeden i ten sam charakter.

Ćwiczenie 9.1

Dlaczego miałbyś deklarować przeciążone funkcje?

Ćwiczenie 9.2

Jak zadeklarować przeciążone wersje funkcji error(), aby następujące wywołania były poprawne:

Indeks międzynarodowych; int górna granica; char selectVal; // ... error("Tablica poza granicami: ", index, upperBound); error("Dzielenie przez zero"); error("Nieprawidłowy wybór", selectVal);

Ćwiczenie 9.3

Wyjaśnij wpływ drugiej deklaracji w każdym z poniższych przykładów:

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

Ćwiczenie 9.4

Która z poniższych inicjalizacji kończy się błędem? Czemu?

(a) reset nieważności (int *); nieważne (*pf)(nieważne *) = zresetuj; (b) intcalc(int, int); int (*pf1)(int, int) = oblicz; (c) extern "C" int obliczyć(int *, int); int (*pf3)(int*, int) = obliczyć; (d) void (*pf4)(const matrix &) = 0;

9.2. Trzy kroki rozwiązywania problemów z przeciążeniem

Rozdzielczość przeciążenia funkcji nazywamy procesem wyboru funkcji ze zbioru przeciążonych, którą należy wywołać. Ten proces jest oparty na argumentach określonych podczas wywołania. Rozważ przykład:

Tt1, t2; nieważne f(int, int); void f(float, float); int main() (
f(t1, t2);
zwróć 0;
}

Tutaj podczas procesu rozwiązywania przeciążeń, w zależności od typu T, określa się, czy funkcja f(int,int) czy f(float,float) zostanie wywołana podczas przetwarzania wyrażenia f(t1,t2) czy błędu zostanie nagrany.
Rozpoznawanie przeciążenia funkcji jest jednym z najbardziej złożonych aspektów języka C++. Próbując zrozumieć wszystkie szczegóły, początkujący programiści napotkają poważne trudności. Dlatego w ta sekcja będziemy tylko prezentować krótka recenzja jak działa rozdzielczość przeciążenia, aby uzyskać przynajmniej pewne wrażenie procesu. Dla tych, którzy chcą wiedzieć więcej, kolejne dwie sekcje zawierają bardziej szczegółowy opis.
Proces rozwiązywania przeciążenia funkcji składa się z trzech kroków, które pokażemy w poniższym przykładzie:

nieważne f(); nieważne f(int); nieważne f(podwójne, podwójne = 3,4); void f(znak *, znak *); nieważne główne() (
f(5,6);
zwróć 0;
}

Podczas rozwiązywania przeciążenia funkcji podejmowane są następujące kroki:

  1. Podświetlony jest zestaw przeciążonych funkcji dla danego wywołania, a także właściwości listy argumentów przekazanych do funkcji.
  2. Te z przeciążonych funkcji, które można wywołać za pomocą podanych argumentów, są wybierane, biorąc pod uwagę ich liczbę i typy.
  3. Znaleziono funkcję, która najlepiej pasuje do wywołania.

Rozważmy kolejno każdą pozycję.
Pierwszym krokiem jest zidentyfikowanie zestawu przeciążonych funkcji, które będą brane pod uwagę w tym wywołaniu. Funkcje zawarte w tym zestawie nazywane są kandydatami. Funkcja kandydująca to funkcja o tej samej nazwie co wywołana, a jej deklaracja jest widoczna w momencie wywołania. W naszym przykładzie mamy czterech takich kandydatów: f(), f(int), f(double, double) i f(char*, char*).
Następnie identyfikowane są właściwości listy przekazanych argumentów, tj. ich liczba i rodzaje. W naszym przykładzie lista składa się z dwóch podwójnych argumentów.
W drugim kroku, spośród zbioru kandydatów, wybierane są wykonalne - te, które można wywołać z podanymi argumentami.Funkcja trwała ma albo tyle parametrów formalnych, ile rzeczywistych argumentów przekazanych do wywoływanej funkcji, albo więcej, ale potem dla każdego dodatkowego parametru wartość domyślna. Aby funkcja została uznana za trwałą, każdy rzeczywisty argument przekazany w wywołaniu musi mieć konwersję na typ parametru formalnego określonego w deklaracji.

W naszym przykładzie istnieją dwie ustalone funkcje, które można wywołać z podanymi argumentami:

  • funkcja f(int) przetrwała, ponieważ ma tylko jeden parametr i nastąpiła konwersja rzeczywistego podwójnego argumentu na formalny parametr int;
  • funkcja f(double,double) przetrwała, ponieważ istnieje wartość domyślna drugiego argumentu, a pierwszy parametr formalny jest typu double, który jest dokładnie typem rzeczywistego argumentu.

Jeśli po drugim kroku nie zostaną znalezione stabilne funkcje, wywołanie zostanie uznane za błędne. W takich przypadkach mówimy, że brakuje korespondencji.
Trzecim krokiem jest wybranie funkcji, która najlepiej pasuje do kontekstu połączenia. Taka funkcja nazywana jest najlepszym stanem (lub najlepszym dopasowaniem). Na tym etapie konwersje używane do rzutowania typów rzeczywistych argumentów na typy parametrów formalnych ustalonej funkcji są uszeregowane. Za najbardziej odpowiednią funkcję uważa się tę, dla której spełnione są następujące warunki:
przekształcenia zastosowane do rzeczywistych argumentów nie są gorsze niż przekształcenia wymagane do wywołania jakiejkolwiek innej dobrze ustalonej funkcji;
w przypadku niektórych argumentów zastosowane konwersje są lepsze niż konwersje wymagane do rzutowania tych samych argumentów w wywołaniach do innych dobrze ugruntowanych funkcji.
Konwersje typów i ich ranking omówiono bardziej szczegółowo w rozdziale 9.3. Tutaj tylko pokrótce przyjrzymy się transformacjom rankingowym dla naszego przykładu. W przypadku ustanowionej funkcji f(int) należy zastosować standardowe rzutowanie rzeczywistego argumentu typu double na int. Dla ustalonej funkcji f(double,double) typ rzeczywistego argumentu double dokładnie odpowiada typowi parametru formalnego. Ponieważ dokładne dopasowanie jest lepsze niż standardowa konwersja (żadna konwersja nie jest zawsze lepsza niż posiadanie jednej), f(double,double) jest uważana za najbardziej odpowiednią funkcję dla tego wywołania.
Jeśli na trzecim etapie nie można znaleźć jedynej najlepszej z ustalonych funkcji, innymi słowy, nie ma takiej ustalonej funkcji, która pasowałaby bardziej niż wszystkie inne, wówczas wywołanie uważa się za niejednoznaczne, tj. błędny.
(Sekcja 9.4 bardziej szczegółowo omawia wszystkie kroki rozwiązywania przeciążeń. Proces rozwiązywania jest również używany podczas wywoływania przeciążonej funkcji składowej klasy i przeciążonego operatora. W sekcji 15.10 omówiono reguły rozwiązywania przeciążeń, które mają zastosowanie do funkcji elementów klasy, a w sekcji 15.11 omówiono reguły dla (przeciążone operatory. Rozdzielczość przeciążenia powinna również uwzględniać funkcje utworzone z szablonów. Sekcja 10.8 omawia wpływ szablonów na to rozwiązanie.)

Ćwiczenie 9.5

Co się dzieje w ostatnim (trzecim) kroku procesu rozwiązywania problemu przeciążenia funkcji?

9.3. Konwersje typów argumentów A

W drugim kroku procesu rozwiązywania przeciążenia funkcji kompilator identyfikuje i klasyfikuje konwersje, które należy zastosować do każdego rzeczywistego argumentu wywoływanej funkcji, aby rzutować go na typ odpowiedniego parametru formalnego dowolnego z powszechnie znanych Funkcje. Ranking może dać jeden z trzech możliwych wyników:

  • dokładne dopasowanie. Typ rzeczywistego argumentu dokładnie odpowiada typowi parametru formalnego. Na przykład, jeśli zestaw przeładowanych funkcji print() ma następujące wartości: void print(unsigned int); void print(const char*); voidprint(znak);
  • wtedy każde z następujących trzech wywołań daje dokładne dopasowanie:
    niepodpisany int;
print("a"); // pasuje do print(char); print("a"); // pasuje do print(const char*); druk(a); // pasuje do print(unsigned int);
  • dopasowanie z konwersją typu. Typ aktualnego argumentu nie jest zgodny z typem parametru formalnego, ale można go na niego przekonwertować: void ff(char); ff(0); // argument typu int jest rzutowany na typ char
  • brak zgodności. Typ rzeczywistego argumentu nie może być rzutowany na typ parametru formalnego w deklaracji funkcji, ponieważ wymagana konwersja nie istnieje. Nie ma dopasowania dla każdego z następujących dwóch wywołań funkcji print():
  • // funkcje print() są zadeklarowane jak powyżej int *ip; klasa SmallInt ( /* ... */ ); SmallInt si; drukuj(ip); // błąd: brak dopasowania
    drukuj(si); // błąd: brak dopasowania
  • Aby ustalić dokładne dopasowanie, typ rzeczywistego argumentu nie musi być zgodny z typem parametru formalnego. Do argumentu można zastosować kilka trywialnych przekształceń, a mianowicie:

    • konwersja l-wartości na r-wartość;
    • konwersja tablicy na wskaźnik;
    • konwersja funkcji na wskaźnik;
    • konwersje specyfikatorów.

    (Są one omówione bardziej szczegółowo poniżej.) Kategoria zgodności z konwersją typu jest najbardziej złożona. Istnieje kilka rodzajów takiego rzutowania do rozważenia: rozszerzenia typu (promocje), standardowe konwersje i konwersje zdefiniowane przez użytkownika. (Rozszerzenia typów i konwersje standardowe są omówione w tym rozdziale. Konwersje zdefiniowane przez użytkownika zostaną wprowadzone później, po szczegółowym omówieniu klas; są one wykonywane przez konwerter, funkcję składową, która umożliwia zdefiniowanie własnego zestawu „standardowych przekształcenia w klasie. W rozdziale 15 przyjrzymy się takim konwerterom i ich wpływowi na rozdzielczość przeciążenia funkcji.)
    Wybierając najlepszą ugruntowaną funkcję dla danego wywołania, kompilator szuka funkcji, dla której przekształcenia zastosowane do rzeczywistych argumentów są „najlepsze”. Konwersje typu są uszeregowane w następujący sposób: dopasowanie ścisłe jest lepsze niż rozszerzenie typu, rozszerzenie typu jest lepsze niż konwersja standardowa, a to z kolei jest lepsze niż konwersja zdefiniowana przez użytkownika. Wrócimy do rankingu w sekcji 9.4, ale na razie proste przykłady Pokażmy, jak pomaga wybrać najbardziej odpowiednią funkcję.

    9.3.1. Dowiedz się więcej o dokładnym dopasowaniu

    Najprostszy przypadek występuje, gdy typy rzeczywistych argumentów są takie same jak typy parametrów formalnych. Na przykład, poniżej pokazane są dwie przeciążone funkcje max(). Następnie każde z wywołań max() dokładnie pasuje do jednej z deklaracji:

    int max(int, int); podwójne max(podwójne, podwójne); int i1; void calc(podwójne d1) (
    max(56, i1); // dokładnie pasuje do max(int, int);
    max(d1, 66,9); // dokładnie pasuje do max(double, double);
    }

    Typ wyliczeniowy dokładnie pasuje tylko do tych, które są w nim zdefiniowane. wyliczenie elementów, a także obiekty, które zostały zadeklarowane jako tego typu:

    Wylicz tokeny ( INLINE = 128; VIRTUAL = 129; ); Tokeny curTok = INLINE; wyliczenie Stat( Niepowodzenie, Zaliczenie ); extern void ff(Tokeny);
    extern void ff(Stat);
    zewnętrzna pustka ff(int); int main() (
    ff(Pass); // dokładnie pasuje do ff(Statystyka)
    ff(0); // dokładnie pasuje do ff(int)
    ff(curTok); // dokładnie pasuje do ff(Tokeny)
    // ...
    }

    Wspomniano powyżej, że rzeczywisty argument może dokładnie odpowiadać parametrowi formalnemu, nawet jeśli do rzutowania ich typów konieczna jest pewna trywialna konwersja, z których pierwszym jest konwersja l-wartości na r-wartość. L-wartość to obiekt, który spełnia następujące warunki:

    • możesz uzyskać adres obiektu;
    • możesz uzyskać wartość przedmiotu;
    • ta wartość jest łatwa do zmodyfikowania (chyba że w deklaracji obiektu istnieje specyfikator const).

    Natomiast wartość r to wyrażenie, którego wartość jest oceniana, lub wyrażenie oznaczające obiekt tymczasowy, dla którego nie można uzyskać adresu i którego wartość nie może być modyfikowana. Oto prosty przykład:

    intkalc(int); int main() ( int lval, res; lval = 5; // lwartość: lval; rwartość: 5
    res = oblicz(lwal);
    // lwartość:res
    // rvalue: tymczasowy obiekt do przechowywania wartości,
    // zwracane przez funkcję calc()
    zwróć 0;
    }

    W pierwszej instrukcji przypisania zmienna lval jest l-wartością, a literał 5 jest r-wartością. W drugiej instrukcji przypisania res jest wartością l, a obiekt tymczasowy przechowujący wynik zwrócony przez funkcję calc() jest wartością r.
    W niektórych sytuacjach, w kontekście, w którym oczekiwana jest wartość, można użyć wyrażenia będącego l-wartością:

    inbj1; inbj2; int main() (
    // ...
    intlocal = obj1 + obj2;
    zwróć 0;
    }

    Tutaj obj1 i obj2 są l-wartościami. Aby jednak wykonać dodawanie w funkcji main(), ich wartości są wyodrębniane ze zmiennych obj1 i obj2. Czynność wyodrębniania wartości obiektu reprezentowanego przez wyrażenie o wartości l nazywana jest konwersją wartości l na wartość r.
    Gdy funkcja oczekuje argumentu przekazywanego przez wartość, jeśli argument jest l-wartością, jest on konwertowany na r-wartość:

    #włączać stringcolor("fioletowy"); voidprint(ciąg); int main() (
    drukuj(kolor); // dopasowanie ścisłe: konwersja lvalue
    // w rwartości
    zwróć 0;
    }

    Ponieważ argument w wywołaniu print(color) jest przekazywany przez wartość, wartość l jest konwertowana na wartość r, aby wyodrębnić wartość koloru i przekazać ją do funkcji prototypowej print(string). Jednak nawet jeśli takie rzutowanie miało miejsce, zakłada się, że rzeczywisty argument koloru dokładnie pasuje do deklaracji print(string).
    Podczas wywoływania funkcji nie zawsze jest konieczne zastosowanie takiej konwersji do argumentów. Odniesieniem jest l-wartość; jeśli funkcja ma parametr referencyjny, to po wywołaniu funkcji otrzymuje l-wartość. Dlatego opisana transformacja nie jest stosowana do rzeczywistego argumentu, któremu odpowiada formalny parametr odniesienia. Załóżmy na przykład, że zadeklarowana jest następująca funkcja:

    #włączać nieważne drukowanie (lista &);

    W poniższym wywołaniu li jest l-wartością reprezentującą obiekt listy , przekazany do funkcji print():

    Lista li(20); int main() (
    // ...
    drukuj(li); // dopasowanie ścisłe: brak konwersji z lwartości na
    // rwartość
    zwróć 0;
    }

    Dopasowanie li do parametru referencyjnego jest uważane za dokładne dopasowanie.
    Druga konwersja, która wciąż naprawia dokładne dopasowanie, to konwersja tablicy na wskaźnik. Jak zauważono w sekcji 7.3, parametr funkcji nigdy nie jest typem tablicowym, zamiast tego przekształca się we wskaźnik do pierwszego elementu. Podobnie, rzeczywisty argument typu tablicy z NT (gdzie N to liczba elementów w tablicy, a T to typ każdego elementu) jest zawsze rzutowany na wskaźnik na T. Ta konwersja typu rzeczywistego argumentu nazywana jest tablicą na -konwersja wskaźnika. Mimo to uważa się, że rzeczywisty argument dokładnie odpowiada formalnemu parametrowi typu „wskaźnik do T”. Na przykład:

    int ai; void putValues(int*); int main() (
    // ...
    putValues(ai); // dokładne dopasowanie: przekonwertuj tablicę na
    // wskaźnik
    zwróć 0;
    }

    Przed wywołaniem funkcji putValues() tablica jest konwertowana na wskaźnik, w wyniku czego rzeczywisty argument ai (tablica trzech liczb całkowitych) jest rzutowany na wskaźnik do int. Chociaż formalnym parametrem funkcji putValues() jest wskaźnik, a rzeczywisty argument jest konwertowany po wywołaniu, ustalana jest dokładna zgodność między nimi.
    Przy ustalaniu dokładnego dopasowania można również przekonwertować funkcję na wskaźnik. (Wspomniano o tym w rozdziale 7.9.) Podobnie jak parametr tablicowy, parametr funkcji staje się wskaźnikiem do funkcji. Rzeczywisty argument typu „funkcja” jest również automatycznie rzutowany na typ wskaźnika funkcji. Ta konwersja typu rzeczywistego argumentu nazywana jest konwersją funkcji na wskaźnik. Podczas wykonywania transformacji rzeczywisty argument jest uważany za dokładnie zgodny z parametrem formalnym. Na przykład:

    Int lexicoCompare(const string &, const string &); typedef int (*PFI)(const string &, const string &);
    void sort(ciąg *, ciąg *, PFI); ciąg jako; int main()
    {
    // ...
    sortuj (jak,
    as + sizeof(as)/sizeof(as - 1),
    lexicoCompare // dopasowanie ścisłe
    // przekonwertuj funkcję na wskaźnik
    ); zwróć 0;
    }

    Przed wywołaniem sort() stosowana jest konwersja funkcji do wskaźnika, która rzutuje argument lexicoCompare z typu „funkcja” na typ „wskaźnik do funkcji”. Chociaż formalnym argumentem funkcji jest wskaźnik, a rzeczywistym argumentem jest nazwa funkcji, a zatem funkcja została przekonwertowana na wskaźnik, zakłada się, że rzeczywisty argument jest dokładnie trzecim formalnym parametrem funkcji sort() .
    Ostatnim z powyższych jest przekształcenie specyfikatorów. Dotyczy to tylko wskaźników i polega na dodaniu specyfikatorów const lub volatile (lub obu) do typu, który odnosi się do danego wskaźnika:

    Int a = (4454, 7864, 92, 421, 938); int*pi = a; bool is_equal(const int * , const int *); void func(int *parm) ( // dokładne dopasowanie między pi i parm: konwersja specyfikatorów
    if (is_equal(pi, parm))
    // ... zwróć 0;
    }

    Przed wywołaniem funkcji is_equal() rzeczywiste argumenty pi i parm są konwertowane z typu „wskaźnik na int” na typ „wskaźnik na stałą int”. Ta transformacja polega na dodaniu specyfikatora const do adresowanego typu i dlatego należy do kategorii transformacji specyfikatora. Mimo że funkcja oczekuje, że otrzyma dwa wskaźniki do stałej int, a rzeczywiste argumenty są wskaźnikami do int, zakłada się, że istnieje dokładne dopasowanie między formalnymi i rzeczywistymi parametrami funkcji is_equal().
    Konwersja specyfikatora dotyczy tylko typu, do którego adresowany jest wskaźnik. Nie jest używany, gdy parametr formalny ma specyfikator const lub volatile, ale rzeczywisty argument nie.

    extern void takeCI(const int); int main() (
    int ii = ...;
    wziąćCI(ii); // konwersja specyfikatora nie jest stosowana
    zwróć 0;
    }

    Chociaż formalny parametr funkcji takeCI() jest typu const int i jest wywoływany z argumentem typu ii typu int, nie ma konwersji specyfikatora: istnieje dokładne dopasowanie między rzeczywistym argumentem a parametrem formalnym.
    Wszystko powyższe jest również prawdziwe w przypadku, gdy argument jest wskaźnikiem, a specyfikatory const lub volatile odnoszą się do tego wskaźnika:

    extern void init(int *const); wewn. zewn. *pi; int main() (
    // ...
    init(pi); // konwersja specyfikatora nie jest stosowana
    zwróć 0;
    }

    Specyfikator const w parametrze formalnym funkcji init() odnosi się do samego wskaźnika, a nie do typu, który adresuje. W związku z tym kompilator nie uwzględnia tego specyfikatora podczas analizowania konwersji, które mają zostać zastosowane do rzeczywistego argumentu. Do argumentu pi nie jest stosowana konwersja specyfikatora: ten argument i parametr formalny są uważane za dokładnie dopasowane.
    Pierwsze trzy z tych przekształceń (l-wartość na r-wartość, tablica na wskaźnik i funkcja na wskaźnik) są często określane jako przekształcenia l-wartości. (W sekcji 9.4 zobaczymy, że chociaż zarówno transformacje l-wartości, jak i transformacje specyfikujące należą do kategorii transformacji, które nie naruszają dokładnego dopasowania, ich stopień jest uważany za wyższy w przypadku, gdy potrzebna jest tylko pierwsza transformacja. W następnej sekcji , porozmawiamy o tym bardziej szczegółowo. .)
    Dokładne dopasowanie można wymusić za pomocą jawnego rzutowania typu. Na przykład, jeśli istnieją dwie przeciążone funkcje:

    zewnętrzna pustka ff(int); extern void ff(void *);

    Ff(0xffbc); // wywołanie ff(int)

    będzie dokładnie pasować do ff(int), nawet jeśli literał 0xffbc jest zapisany jako stała szesnastkowa. Programista może zmusić kompilator do wywołania funkcji ff(void *) przez jawne wykonanie operacji rzutowania:

    Ff(reinterpretuj_cast (0xffbc)); // zadzwoń ff(nieważne*)

    Jeśli takie rzutowanie zostanie zastosowane do rzeczywistego argumentu, uzyska typ, na który jest konwertowane. Jawne konwersje typów pomagają kontrolować proces rozpoznawania przeciążenia. Na przykład, jeśli rozpoznawanie przeciążenia daje niejednoznaczny wynik (rzeczywiste argumenty pasują równie dobrze do dwóch lub więcej dobrze ustalonych funkcji), można użyć jawnego rzutowania do rozwiązania niejednoznaczności, zmuszając kompilator do wybrania określonej funkcji.

    9.3.2. Więcej o rozszerzeniu typu

    Rozszerzenie typu to jedna z następujących konwersji:

    • rzeczywisty argument typu char, unsigned char lub short jest rozwijany do typu int. Rzeczywisty argument typu unsigned short jest rozszerzany do typu int, jeśli rozmiar maszyny int jest większy niż rozmiar short, aw przeciwnym razie do typu unsigned int;
    • argument typu float jest rozwijany do typu double;
    • wyliczeniowy argument typu rozwija się do pierwszego z następujących typów, który może reprezentować wszystkie wartości elementów członkowskich wyliczenia: int, unsigned int, long, unsigned long;
    • argument bool jest rozwijany do typu int.

    Podobne rozszerzenie jest stosowane, gdy typ rzeczywistego argumentu jest jednym z wymienionych typów, a parametr formalny jest odpowiedniego typu rozszerzonego:

    zewnętrzny manip pusty (int); int main() (
    manip("a"); // wpisz char rozwija się do int
    zwróć 0;
    }

    Literał znakowy jest typu char. Rozszerza się do int. Ponieważ typ rozszerzony odpowiada typowi parametru formalnego funkcji manip(), mówimy, że wywołanie go wymaga rozszerzenia typu argumentu.
    Rozważmy następujący przykład:

    extern void print(unsigned int); zewnętrzny void print(int); extern void print(char); znak niepodpisany;
    drukuj(uc); // drukuj(int); UC potrzebuje tylko rozszerzenia typu!

    Na platformie sprzętowej, na której unsigned char zajmuje jeden bajt pamięci, a int zajmuje cztery bajty, rozszerzenie konwertuje unsigned char na int, ponieważ może reprezentować wszystkie wartości unsigned char. Dla takiej architektury maszynowej spośród wielu przeciążonych funkcji pokazanych w przykładzie print(int) zapewnia najlepsze dopasowanie dla argumentu unsigned char. W przypadku pozostałych dwóch funkcji dopasowanie wymaga standardowego rzutowania.
    Poniższy przykład ilustruje rozwinięcie rzeczywistego argumentu typu wyliczeniowego:

    Wyliczenie Stat (Niepowodzenie, Zaliczenie); zewnętrzna pustka ff(int);
    extern void ff(char); int main() (
    // poprawnie: enum member Pass rozwija się do typu int
    ff(Pass); // ff(int)
    ff(0); // ff(int)
    }

    Czasami rozszerzanie wyliczeń przynosi niespodzianki. Kompilatory często wybierają reprezentację wyliczenia na podstawie wartości jego elementów. Załóżmy, że powyższa architektura (jeden bajt na char i cztery bajty na int) definiuje wyliczenie w ten sposób:

    Wyliczenie e1 ( a1, b1, c1 );

    Ponieważ istnieją tylko trzy elementy a1, b1 i c1 o wartościach odpowiednio 0, 1 i 2 - i ponieważ wszystkie te wartości mogą być reprezentowane przez typ char, kompilator zwykle wybierze char do reprezentowania typu e1. Rozważmy jednak wyliczenie e2 z następującym zestawem elementów:

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

    Ponieważ jedna ze stałych ma wartość 0x80000000, kompilator musi wybrać reprezentację e2 z typem wystarczającym do przechowywania wartości 0x80000000, czyli unsigned int.
    Tak więc, chociaż zarówno e1 jak i e2 są wyliczeniami, ich reprezentacje są różne. Z tego powodu e1 i e2 rozszerzają się na różne typy:

    #włączać format ciągu (int);
    format ciągu (bez znaku int); int main() (
    format(a1); // format wywołania(int)
    format(a2); // format wywołania(unsigned int)
    zwróć 0;
    }

    Przy pierwszym wywołaniu format() rzeczywisty argument jest rozwijany do typu int, ponieważ znak jest używany do reprezentowania typu e1 i dlatego wywoływana jest przeciążona funkcja format(int). Przy drugim wywołaniu typ rzeczywistego argumentu e2 to unsigned int, a argument jest rozszerzany do unsigned int, co powoduje wywołanie przeciążonej funkcji format(unsigned int). Dlatego należy pamiętać, że zachowanie dwóch wyliczeń w odniesieniu do procesu rozwiązywania przeciążeń może być różne i zależeć od wartości elementów, które określają sposób rozwijania typu.

    9.3.3. Dowiedz się więcej o konwersji standardowej

    Istnieje pięć typów standardowych przekształceń, a mianowicie:

    1. konwersje typu całkowitoliczbowego: rzutowanie z typu całkowitoliczbowego lub wyliczenia na dowolny inny typ całkowitoliczbowy (z wyjątkiem przekształceń, które zostały powyżej skategoryzowane jako rozszerzenia typu);
    2. konwersje typu zmiennoprzecinkowego: rzutowanie z dowolnego typu zmiennoprzecinkowego na dowolny inny typ zmiennoprzecinkowy (z wyłączeniem przekształceń, które zostały sklasyfikowane powyżej jako rozszerzenia typu);
    3. konwersje między typem całkowitym a zmiennoprzecinkowym: rzutowanie z dowolnego typu zmiennoprzecinkowego na dowolny typ całkowity lub odwrotnie;
    4. konwersje wskaźników: rzutowanie wartości całkowitej 0 na typ wskaźnika lub przekształcanie wskaźnika dowolnego typu na typ void*;
    5. konwersje na typ bool: rzutowanie z dowolnego typu liczb całkowitych, typu zmiennoprzecinkowego, typu wyliczeniowego lub typu wskaźnika na typ bool.

    Oto kilka przykładów:

    extern void print(void*); zewnętrzny pusty wydruk (podwójny); int main() (
    wew;
    drukuj(i); // pasuje do print(double);
    // przechodzę standardową konwersję z int na double
    drukuj(&i); // pasuje do print(void*);
    // &i przechodzi standardową konwersję
    // od int* do void*
    zwróć 0;
    }

    Konwersje należące do grup 1, 2 i 3 są potencjalnie niebezpieczne, ponieważ typ docelowy może nie reprezentować wszystkich wartości typu źródłowego. Na przykład floaty nie mogą odpowiednio reprezentować wszystkich wartości int. Z tego powodu przekształcenia zawarte w tych grupach są klasyfikowane jako przekształcenia standardowe, a nie rozszerzenia typu.

    wew; voidcalc(liczba zmiennoprzecinkowa); int main() ( calc(i); // standardowa konwersja między typem całkowitym a // zmiennoprzecinkowym jest potencjalnie niebezpieczna w zależności // od wartości i zwraca 0; )

    Gdy funkcja calc() jest wywoływana, stosowana jest standardowa konwersja z typu integer na typ zmiennoprzecinkowy float. W zależności od wartości zmiennej i, może nie być możliwe przechowywanie jej jako liczby zmiennoprzecinkowej bez utraty precyzji.
    Zakłada się, że wszystkie standardowe zmiany wymagają takiej samej ilości pracy. Na przykład konwersja z char na unsigned char nie ma wyższego priorytetu niż konwersja z char na double. Bliskość typów nie jest brana pod uwagę. Jeśli dwie ustanowione funkcje wymagają standardowej transformacji rzeczywistego argumentu do dopasowania, wywołanie jest uważane za niejednoznaczne i oznaczane przez kompilator jako błąd. Na przykład, biorąc pod uwagę dwie przeciążone funkcje:

    zewnętrzny manip pustki (długi); zewnętrzny manewr pustej przestrzeni (pływak);

    wtedy następujące wywołanie jest niejednoznaczne:

    int main() ( manip(3.14); // błąd: niejednoznaczność // manip(float) nie jest lepszy niż manip(int) return 0; )

    Stała 3.14 jest typu double. Za pomocą jednej lub drugiej standardowej konwersji można ustalić korespondencję z dowolną przeciążoną funkcją. Ponieważ do celu prowadzą dwie transformacje, wywołanie jest uważane za niejednoznaczne. Żadna transformacja nie ma pierwszeństwa przed drugą. Programista może rozwiązać niejednoznaczność albo przez jawne rzutowanie typu:

    Manip(static_cast (3.14)); // manip(długi)

    lub za pomocą przyrostka oznaczającego, że stała jest typu float:

    Manipulator (3.14F)); // manip(pływak)

    Oto kilka przykładów niejednoznacznych wywołań, które są oznaczane jako błędy, ponieważ pasują do wielu przeciążonych funkcji:

    extern void farith(unsigned int); extern void farith(float); int main() (
    // każde z poniższych wywołań jest niejednoznaczne
    farith("a"); // argument jest typu char
    farith(0); // argument jest typu int
    farith(2ul); // argument jest typu unsigned long
    farith(3.14159); // argument jest typu double
    farith(prawda); // argument jest typu bool
    }

    Konwersje wskaźników standardowych są czasami sprzeczne z intuicją. W szczególności wartość 0 jest rzutowana na wskaźnik do dowolnego typu; tak otrzymany wskaźnik nazywa się null. Wartość 0 może być reprezentowana jako stałe wyrażenie typu integer:

    zestaw pusty (int*); int main() (
    // konwersja wskaźnika z 0 na int* zastosowana do argumentów
    // w obu rozmowach
    zestaw(0L);
    zestaw (0x00);
    zwróć 0;
    }

    Wyrażenie stałe 0L (wartość 0 typu long int) i wyrażenie stałe 0x00 (wartość szesnastkowa liczba całkowita 0) są typu całkowitego i dlatego można je przekonwertować na wskaźnik zerowy typu int*.
    Ale ponieważ wyliczenia nie są typami całkowitymi, element równy 0 nie jest rzutowany na typ wskaźnika:

    Wyliczenie EN ( zr = 0 ); zbiór(zr); // błąd: zr nie można przekonwertować na typ int*

    Wywołanie funkcji set() jest błędem, ponieważ nie ma konwersji między wartością zr elementu wyliczenia a parametrem formalnym typu int*, mimo że zr wynosi 0.
    Zauważ, że wyrażenie stałe 0 jest typu int. Aby rzutować go na typ wskaźnika, wymagana jest standardowa konwersja. Jeśli w zestawie przeciążonych funkcji znajduje się funkcja z parametrem formalnym typu int, to przeciążenie będzie dozwolone na jej korzyść w przypadku, gdy rzeczywisty argument wynosi 0:

    Unieważnij wydruk(int); voidprint(nieważny*); void set(const char*);
    pusty zestaw(znak*); int main()(
    drukuj(0); // nazywamy print(int);
    zestaw(0); // niejasność
    zwróć 0;
    }

    Wywołanie print(int) jest dokładnym dopasowaniem, podczas gdy wywołanie print(void*) wymaga rzutowania wartości 0 na typ wskaźnika. Ponieważ dopasowanie jest lepsze niż transformacja, aby rozstrzygnąć to wezwanie, wybiera się funkcja drukowania(wew.). Wywołanie set() jest niejednoznaczne, ponieważ 0 dopasowuje parametry formalne obu przeciążonych funkcji przez zastosowanie standardowej transformacji. Ponieważ obie funkcje są równie dobre, ustalona jest niejednoznaczność.
    Ostatnia możliwa konwersja wskaźnika umożliwia rzutowanie wskaźnika dowolnego typu na typ void*, ponieważ void* jest ogólnym wskaźnikiem do dowolnego typu danych. Oto kilka przykładów:

    #włączać zewnętrzny reset pustki (unieważniony *); void func(int *pi, string *ps) (
    // ...
    zresetuj(pi); // konwersja wskaźnika: int* na void*
    /// ...
    reset (ps); // konwersja wskaźnika: string* na void*
    }

    Tylko wskaźniki do typów danych mogą być rzutowane na void* przy użyciu standardowej konwersji, wskaźników do funkcji nie można wykonać w ten sposób:

    Typedef int(*PFV)(); zewnętrzne przypadki testowe PFV; // tablica wskaźników do funkcji extern void reset(void *); int main() (
    // ...
    zresetuj (tekst); // błąd: brak standardowej konwersji
    // między int(*)() a void*
    zwróć 0;
    }

    9.3.4. Spinki do mankietów

    Rzeczywisty argument lub formalny parametr funkcji może być referencjami. Jak to wpływa na reguły konwersji typów?
    Zastanów się, co się dzieje, gdy odniesienie jest rzeczywistym argumentem. Jego typ nigdy nie jest typem referencyjnym. Argument referencyjny jest traktowany jako l-wartość, której typ jest taki sam jak typ odpowiadającego obiektu:

    wew; int& ri = i; voidprint(int); int main() (
    drukuj(i); // argument jest lwartością typu int
    drukuj(ri); // to samo
    zwróć 0;
    }

    Rzeczywisty argument w obu wywołaniach jest typu int. Użycie referencji do przekazania jej w drugim wywołaniu nie wpływa na typ samego argumentu.
    Standardowe konwersje i rozszerzenia typów rozważane przez kompilator są takie same, gdy rzeczywisty argument jest odwołaniem do typu T i gdy sam jest tego typu. Na przykład:

    wew; int& ri = i; voidcalc(podwójny); int main() (
    oblicz(i); // standardowa konwersja między typem całkowitym
    // i typ zmiennoprzecinkowy
    oblicz(ri); // to samo
    zwróć 0;
    }

    A jak formalny parametr odniesienia wpływa na przekształcenia zastosowane do rzeczywistego argumentu? Porównanie daje następujące wyniki:

    • rzeczywisty argument jest odpowiedni jako inicjator parametru referencyjnego. W tym przypadku mówimy, że jest między nimi dokładne dopasowanie: void swap(int &, int &); void manip(int i1, int i2) (
      // ...
      zamiana(i1, i2); // poprawnie: wywołanie swap(int &, int &)
      // ...
      zwróć 0;
      }
    • rzeczywisty argument nie może zainicjować parametru referencyjnego. W takiej sytuacji nie ma dokładnego dopasowania, a argument nie może być użyty do wywołania funkcji. Na przykład:
    • int obj; nieważne Fred (podwójne i); int main() ( frd(obj); // błąd: parametr musi być typu const double & return 0; )
    • Wywołanie funkcji frd() jest błędem. Rzeczywisty argument jest typu int i musi zostać przekonwertowany na typ double, aby pasował do formalnego parametru odwołania. Wynikiem tej transformacji jest zmienna tymczasowa. Ponieważ odwołanie nie ma specyfikatora const, takich zmiennych nie można użyć do jego inicjalizacji.
      Oto kolejny przykład, w którym nie ma dopasowania między formalnym parametrem referencji a rzeczywistym argumentem:
    • klasa B; nieważne wziąćB(B&); DajB(); int main() (
      weźB(dajB()); // błąd: parametr musi być typu const B &
      zwróć 0;
      }
    • Wywołanie funkcji takeB() jest błędem. Faktycznym argumentem jest wartość zwracana, tj. zmienna tymczasowa, której nie można użyć do zainicjowania odwołania bez specyfikatora const.
      W obu przypadkach widzimy, że jeśli formalny parametr odniesienia ma specyfikator const, to można ustalić dokładne dopasowanie między nim a rzeczywistym argumentem.

    Należy zauważyć, że zarówno konwersja wartości l na wartość r, jak i inicjalizacja odwołania są uważane za dokładne dopasowania. W tym przykładzie pierwsze wywołanie funkcji powoduje błąd:

    Unieważnij wydruk(int); voidprint(int&); intioobj;
    int &ri = iobj; int main() (
    drukuj(iobj); // błąd: niejednoznaczność
    drukuj(ri); // błąd: niejednoznaczność
    drukuj(86); // poprawnie: wywołaj print(int)
    zwróć 0;
    }

    Obiekt iobj jest argumentem, który można zmapować do obu funkcji print(), tj. wywołanie jest niejednoznaczne. To samo dotyczy następnego wiersza, gdzie odwołanie ri oznacza obiekt odpowiadający obu funkcjom print(). Jednak przy trzecim wezwaniu wszystko jest w porządku. Dla niego print(int&) nie jest dobrze ugruntowany. Stała całkowita jest wartością r, więc nie może zainicjować parametru referencyjnego. Jedyną ustaloną funkcją do wywołania print(86) jest print(int), dlatego jest wybierana w rozdzielczości przeciążenia.
    Krótko mówiąc, jeśli formalny argument jest odwołaniem, rzeczywisty argument jest dokładnym dopasowaniem, jeśli może zainicjować odwołanie, a nie inaczej.

    Ćwiczenie 9.6

    Wymień dwie trywialne przekształcenia, które są dozwolone podczas ustalania dokładnego dopasowania.

    Ćwiczenie 9.7

    Jaka jest ranga każdej konwersji argumentów w następujących wywołaniach funkcji:

    (a) void print(int *, int); wewn arr; drukuj(arr, 6); // wywołanie funkcji (b) void manip(int, int); manip("a", "z"); // wywołanie funkcji (c) int calc(int, int); podwójny dobj; double = calc(55.4, dobj) // wywołanie funkcji (d) void set(const int *); int*pi; zestaw(pi); // wywołanie funkcji

    Ćwiczenie 9.8

    Które z tych wywołań są błędne, ponieważ nie ma konwersji między typem rzeczywistego argumentu a parametrem formalnym:

    (a) wyliczenie Stat (Niepowodzenie, Zaliczenie); test nieważności (Stat); tekst(0); // wywołanie funkcji (b) void reset(void *); zresetuj(0); // wywołanie funkcji (c) void set(void *); int*pi; zestaw(pi); // wywołanie funkcji (d) #include lista opera(); void print(oper()); // wywołanie funkcji (e) void print(const int); intioobj; drukuj(iobj); // wywołanie funkcji

    9.4. Szczegóły rozdzielczości przeciążenia funkcji

    Wspomnieliśmy już w sekcji 9.2, że proces rozwiązywania problemów z przeciążeniem funkcji składa się z trzech kroków:

    1. Ustaw zestaw funkcji kandydujących do rozwiązania danego wywołania, a także właściwości rzeczywistej listy argumentów.
    2. Wybierz ze zbioru kandydatów ustalone funkcje - te, które można wywołać z podaną listą rzeczywistych argumentów, biorąc pod uwagę ich liczbę i rodzaje.
    3. Wybierz funkcję, która najlepiej pasuje do wywołania, klasyfikując przekształcenia, które mają być zastosowane do rzeczywistych argumentów, tak aby odpowiadały formalnym parametrom ustanowionej funkcji.

    Jesteśmy teraz gotowi do bardziej szczegółowego zbadania tych kroków.

    9.4.1. Funkcje kandydata

    Funkcja kandydująca to funkcja, która ma taką samą nazwę jak wywołana. Kandydaci wybierani są na dwa sposoby:

    • deklaracja funkcji jest widoczna w momencie wywołania. W poniższym przykładzie
      nieważne f(); nieważne f(int); nieważne f(podwójne, podwójne = 3,4); void f(znak*, znak*); int main() (
      f(5,6); // jest czterech kandydatów do rozstrzygnięcia tego wezwania
      zwróć 0;
      }
    • wszystkie cztery funkcje f() spełniają ten warunek. Dlatego zbiór kandydatów zawiera cztery elementy;
    • jeśli typ rzeczywistego argumentu jest zadeklarowany w jakiejś przestrzeni nazw, to funkcje składowe tej przestrzeni, które mają taką samą nazwę jak wywoływana funkcja, są dodawane do zbioru kandydującego: namespace NS ( class C ( /* ... */ ); void takeC( C&); ) // type cobj to klasa C zadeklarowana w przestrzeni nazw NS
      NS::Cobj; int main() (
      // żadna z funkcji takeC() nie jest widoczna w punkcie wywołania
      weźC(cobj); // poprawnie: NS::takeC(C&) jest wywoływane,
      // ponieważ argument jest typu NS::C, więc
      // brana jest pod uwagę funkcja takeC(),
      // zadeklarowane w przestrzeni nazw NS
      zwróć 0;
      }

    Tak więc zbiorem kandydatów jest związek zestaw funkcji, widoczne w punkcie wywołania, oraz zestaw funkcji zadeklarowanych w tej samej przestrzeni nazw, co rzeczywiste typy argumentów.
    Podczas identyfikowania zestawu przeciążonych funkcji, które są widoczne w momencie wywołania, obowiązują zasady omówione wcześniej.
    Funkcja zadeklarowana w zakresie zagnieżdżonym ukrywa, a nie przeciąża funkcję o tej samej nazwie w zakresie zewnętrznym. W takiej sytuacji kandydatami będą tylko funkcje z zakresu zagnieżdżonego, czyli te, które nie są ukryte po wywołaniu. W poniższym przykładzie kandydujące funkcje widoczne w punkcie wywołania to format(double) i format(char*):

    Format znaku*(int); void g() ( char *format(double); char* format(char*); format(3); // wywołanie format(double)
    }

    Ponieważ format(int) zadeklarowany w zasięgu globalnym jest ukryty, nie jest uwzględniony w zestawie funkcji kandydujących.
    Kandydatów można przedstawić za pomocą deklaracji widocznych w miejscu wywołania:

    Przestrzeń nazw libs_R_us ( int max(int, int); double max(double, double); ) char max(char, char); funkcja nieważna()
    {
    // funkcje z przestrzeni nazw są niewidoczne
    // wszystkie trzy wywołania są rozstrzygane na korzyść funkcji globalnej max(char, char)
    maks. (87, 65);
    maks. (35,5; 76,6);
    max("J", "L");
    }

    Funkcje max() zdefiniowane w przestrzeni nazw libs_R_us są niewidoczne w punkcie wywołania. Jedyną widoczną jest funkcja max() z zasięgu globalnego; tylko znajduje się w zestawie funkcji kandydujących i jest wywoływana przy każdym z trzech wywołań funkcji func(). Możemy użyć deklaracji using do ujawnienia funkcji max() z przestrzeni nazw libs_R_us. Gdzie umieścić deklarację using? Jeśli uwzględnisz go w zakresie globalnym:

    Maks. znak(znak, znak); używając libs_R_us::max; // używanie-deklaracji

    następnie funkcje max() z libs_R_us są dodawane do zestawu przeciążonych funkcji, które już zawierają max() zadeklarowane w zasięgu globalnym. Teraz wszystkie trzy funkcje są widoczne wewnątrz funkcji func() i stają się kandydatami. W tej sytuacji wywołania func() są rozwiązywane w następujący sposób:

    Void func() ( max(87, 65); // nazwane libs_R_us::max(int, int) max("J", "L"); // nazwane::max(char, char) )

    Ale co się stanie, jeśli wstawimy deklarację using do lokalnego zakresu funkcji func(), jak pokazano w tym przykładzie?

    void func() ( // używanie deklaracji przy użyciu libs_R_us::max; // te same wywołania funkcji jak powyżej
    }

    Która z funkcji max() zostanie uwzględniona w zestawie kandydującym? Przypomnij sobie, że deklaracje using są zagnieżdżone w sobie. Jeśli istnieje taka deklaracja w zakresie lokalnym funkcja globalna max(char, char) jest ukryty, więc tylko

    Libs_R_us::max(int, int); libs_R_us::max(podwójne, podwójne);

    To są kandydaci. Teraz wywołania func() są rozwiązywane w następujący sposób:

    Void func() ( // using-declaration // globalna funkcja max(char, char) jest ukrywana za pomocą libs_R_us::max; max(87, 65); // libs_R_us::max(int, int) jest wywoływana
    maks. (35,5; 76,6); // libs_R_us::max(double, double) jest wywoływany
    max("J", "L"); // zadzwoń do libs_R_us::max(int, int)
    }

    Dyrektywy using wpływają również na skład zestawu funkcji kandydujących. Załóżmy, że zdecydujemy się ich użyć, aby funkcje max() z przestrzeni nazw libs_R_us były widoczne w func(). Jeśli umieścimy następującą dyrektywę using w zasięgu globalnym, to zbiór funkcji kandydujących będzie składał się z funkcji globalnej max(char, char) oraz funkcji max(int, int) i max(double, double) zadeklarowanych w libs_R_us:

    Przestrzeń nazw libs_R_us ( int max(int, int); double max(double, double); ) char max(char, char);
    za pomocą przestrzeni nazw libs_R_us; // użycie dyrektywy void func()
    {
    maks. (87, 65); // zadzwoń do libs_R_us::max(int, int)
    maks. (35,5; 76,6); // libs_R_us::max(double, double) jest wywoływany
    }

    Co się stanie, jeśli umieścisz dyrektywę using w zakresie lokalnym, jak w poniższym przykładzie?

    Void func() ( // using-directive używającej przestrzeni nazw libs_R_us; // te same wywołania funkcji jak powyżej
    }

    Która z funkcji max() znajdzie się wśród kandydatów? Przypomnij sobie, że dyrektywa using sprawia, że ​​elementy członkowskie przestrzeni nazw są widoczne, tak jakby zostały zadeklarowane poza tą przestrzenią, w punkcie, w którym taka dyrektywa jest umieszczona. W naszym przykładzie elementy libs_R_us są widoczne w lokalnym zasięgu funkcji func(), tak jakby zostały zadeklarowane poza przestrzenią - w zasięgu globalnym. Wynika z tego, że zestaw przeciążonych funkcji widocznych wewnątrz funkcji func() jest taki sam jak poprzednio, tj. zawiera

    Max(znak, znak); libs_R_us::max(int, int); libs_R_us::max(podwójne, podwójne);

    Dyrektywa using pojawia się w zasięgu lokalnym lub globalnym, nie ma to wpływu na rozdzielczość wywołań funkcji func():

    Void func() ( używając przestrzeni nazw libs_R_us; max(87, 65); // wywołaj libs_R_us::max(int, int)
    maks. (35,5; 76,6); // libs_R_us::max(double, double) jest wywoływany
    max("J", "L"); // nazwane::max(int, int)
    }

    Tak więc zbiór kandydatów składa się z funkcji widocznych w punkcie wywołania, w tym wprowadzonych przez using-declarations i using-dyrektywy, a także funkcji zadeklarowanych w przestrzeniach nazw związanych z rzeczywistymi typami argumentów. Na przykład:

    Przestrzeń nazw basicLib ( int print(int); double print(double); ) przestrzeń nazw matrixLib ( macierz klas ( /* ... */ ); void print(const maxtrix &); ) void display() ( przy użyciu basicLib::print ; matrixLib::matrix mObj;
    drukuj(mObj); // wywołanie maxtrixLib::print(const maxtrix &) print(87); // basicLib::print(const maxtrix &) jest wywoływana
    }

    Kandydatami do print(mObj) są deklaracje using wewnątrz display() funkcji basicLib::print(int) i basicLib::print(double), ponieważ są one widoczne w punkcie wywołania. Ponieważ rzeczywisty argument funkcji jest typu matrixLib::matrix, funkcja print() zadeklarowana w przestrzeni nazw matrixLib również byłaby kandydatem. Jakie są funkcje kandydujące dla print(87)? Tylko basicLib::print(int) i basicLib::print(double) widoczne w punkcie wywołania. Ponieważ argument jest typu int, podczas wyszukiwania innych kandydatów nie jest uwzględniana żadna dodatkowa przestrzeń nazw.

    9.4.2. Ustanowione funkcje

    Jednym z kandydatów jest ugruntowana funkcja. Lista jej parametrów formalnych ma albo taką samą liczbę elementów jak lista rzeczywistych argumentów wywoływanej funkcji, albo więcej. W tym drugim przypadku dla opcje dodatkowe podane są wartości domyślne, w przeciwnym razie funkcja nie może być wywołana z podaną liczbą argumentów. Aby funkcja została uznana za stabilną, musi istnieć konwersja z każdego rzeczywistego argumentu na typ odpowiedniego parametru formalnego. (Takie przekształcenia zostały omówione w rozdziale 9.3.)
    W poniższym przykładzie wywołanie f(5.6) ma dwie ustalone funkcje: f(int) i f(double).

    nieważne f(); nieważne f(int); nieważne f(podwójne); void f(znak*, znak*); int main() (
    f(5,6); // 2 ustalone funkcje: f(int) i f(double)
    zwróć 0;
    }

    Funkcja f(int) przetrwała, ponieważ ma tylko jeden parametr formalny, który odpowiada liczbie rzeczywistych argumentów w wywołaniu. Dodatkowo istnieje standardowa konwersja argumentu typu double na int. Przetrwała również funkcja f(double); ma również jeden parametr typu double i dokładnie pasuje do rzeczywistego argumentu. Funkcje kandydujące f() i f(char*, char*) są wykluczone z listy funkcji, które przetrwały, ponieważ nie można ich wywołać za pomocą jednego argumentu.
    W poniższym przykładzie jedyną ustaloną funkcją do wywołania format(3) jest format(double). Chociaż kandydata format(char*) można wywołać z pojedynczym argumentem, nie ma konwersji typu rzeczywistego argumentu int na typ parametru formalnego char*, a zatem funkcji nie można uznać za dobrze ustaloną.

    Format znaku*(int); void g() ( // globalna funkcja format(int) jest ukryta char* format(double); char* format(char*); format(3); // istnieje tylko jedna ustanowiona funkcja: format(double) )

    W poniższym przykładzie wszystkie trzy kandydujące funkcje są w stanie wywołać max() wewnątrz funkcji func(). Wszystkie z nich można wywołać dwoma argumentami. Ponieważ rzeczywiste argumenty są typu int, odpowiadają dokładnie formalnym parametrom funkcji libs_R_us::max(int, int) i mogą być rzutowane na typy parametrów funkcji libs_R_us::max(double, double) poprzez konwersję liczb całkowitych na liczby zmiennoprzecinkowe, a także na typy parametrów funkcji libs_R_us::max(char, char) poprzez konwersję typu liczb całkowitych.


    używając libs_R_us::max; znak max(znak, znak);
    funkcja nieważna()
    {
    // wszystkie trzy funkcje max() są dobrze ugruntowane
    maks. (87, 65); // wywołane przy użyciu libs_R_us::max(int, int)
    }

    Należy zauważyć, że funkcja kandydująca z wieloma parametrami jest usuwana, gdy tylko okaże się, że jednego z rzeczywistych argumentów nie można rzutować na typ odpowiedniego parametru formalnego, nawet jeśli taka konwersja istnieje dla wszystkich innych argumentów. W poniższym przykładzie funkcja min(char *, int) jest wykluczona ze zbioru pozostałych, ponieważ nie jest możliwe przekonwertowanie typu pierwszego argumentu int na typ odpowiadającego parametru char *. Dzieje się tak pomimo faktu, że drugi argument dokładnie pasuje do drugiego parametru.

    zewnętrzne podwójne min (podwójne, podwójne); extern double min(char*, int); funkcja nieważna()
    {
    // jedna funkcja kandydująca min(double, double)
    min (87, 65); // zadzwoń min(podwójne, podwójne)
    }

    Jeżeli po wykluczeniu ze zbioru kandydatów wszystkich funkcji z nieodpowiednią liczbą parametrów oraz tych, dla których nie było odpowiedniej transformacji, nie ma stałych, to przetwarzanie wywołania funkcji kończy się błędem kompilacji. W tym przypadku mówi się, że nie znaleziono dopasowania.

    Unieważnij druk (niepodpisany int); voidprint(znak*); voidprint(znak); int*ip;
    klasa SmallInt ( /* ... */ );
    SmallInt si; int main() (
    drukuj(ip); // błąd: brak ustalonych funkcji: nie znaleziono dopasowania
    drukuj(si); // błąd: brak ustalonych funkcji: nie znaleziono dopasowania
    zwróć 0;
    }

    9.4.3. Najlepsza uznana funkcja

    Za najlepszą uważa się tę z ustalonych funkcji, której parametry formalne najbardziej odpowiadają rodzajom rzeczywistych argumentów. W przypadku każdej takiej funkcji konwersje typu zastosowane do każdego argumentu są klasyfikowane w celu określenia, jak dobrze pasuje do parametru. (Sekcja 6.2 opisuje obsługiwane konwersje typów). Najlepiej ustaloną funkcją jest funkcja, która jednocześnie spełnia dwa warunki:

    • przekształcenia zastosowane do argumentów nie są gorsze niż przekształcenia wymagane do wywołania jakiejkolwiek innej dobrze ustalonej funkcji;
    • dla co najmniej jednego argumentu zastosowana transformacja jest lepsza niż dla tego samego argumentu w jakiejkolwiek innej dobrze ustalonej funkcji.

    Może się zdarzyć, że aby rzutować rzeczywisty argument na typ odpowiedniego parametru formalnego, trzeba wykonać kilka konwersji. Tak więc w poniższym przykładzie

    przykł. wewn.; void putValues(const int *); int main() (
    putValues(arr); // Potrzebne 2 konwersje
    // tablica do wskaźnika + konwersja specyfikatora
    zwróć 0;
    }

    aby rzutować argument arr z typu „array of three ints” na typ „pointer to const int”, stosowana jest sekwencja konwersji:

    1. Konwersja tablicy na wskaźnik, która przekształca tablicę trzech wartości typu int na wskaźnik na int.
    2. Konwersja specyfikatora, która przekształca wskaźnik na int we wskaźnik na const int.

    Dlatego bardziej słuszne byłoby stwierdzenie, że w celu rzucenia rzeczywistego argumentu na typ parametru formalnego ustalonej funkcji wymagana jest sekwencja konwersji. Ponieważ stosowana jest nie jedna, ale wiele transformacji, trzeci krok procesu rozwiązywania przeciążenia funkcji faktycznie klasyfikuje sekwencje transformacji.
    Za rangę takiego ciągu uważa się rangę najgorszej z zawartych w nim przekształceń. Jak wyjaśniono w rozdziale 9.2, konwersje typu mają następującą rangę: dopasowanie ścisłe jest lepsze niż rozszerzenie typu, a rozszerzenie typu jest lepsze niż konwersja standardowa. W poprzednim przykładzie obie zmiany mają ścisły ranking dopasowania. Dlatego cała sekwencja ma tę samą rangę.
    Taki zbiór składa się z kilku przekształceń zastosowanych w pokazanej kolejności:

    konwersja l-wartości -> rozszerzenie typu lub konwersja standardowa -> konwersja specyfikatora

    Termin konwersja wartości l odnosi się do pierwszych trzech przekształceń z dokładnym dopasowaniem omówionych w sekcji 9.2: konwersja l-wartość na wartość r, konwersja tablica-wskaźnik i konwersja funkcji-wskaźnik. Sekwencja przekształceń składa się z konwersji zero lub jednej l-wartości, po której następuje zero lub jedno rozszerzenie typu lub konwersja standardowa, a na końcu zero lub jedna konwersja specyfikatora. Do rzutowania rzeczywistego argumentu na typ parametru formalnego można zastosować tylko jedną transformację każdego rodzaju.

    Opisana sekwencja nazywana jest sekwencją standardowych przekształceń. Istnieje również sekwencja konwersji zdefiniowanych przez użytkownika, która jest skojarzona z funkcją konwertera elementu członkowskiego. (Konwertery i sekwencje konwersji zdefiniowanych przez użytkownika omówiono w rozdziale 15.)

    Jakie są sekwencje, w których zmieniają się rzeczywiste argumenty w poniższym przykładzie?

    Przestrzeń nazw libs_R_us ( int max(int, int); double max(double, double); ) // using-declaration
    używając libs_R_us::max; funkcja nieważna()
    {
    znak c1, c2;
    max(c1, c2); // zadzwoń do libs_R_us::max(int, int)
    }

    Argumenty wywołania funkcji max() są typu char. Sekwencja przekształceń argumentów podczas wywoływania funkcji libs_R_us::max(int,int) jest następująca:

    1a. Ponieważ argumenty są przekazywane przez wartość, konwersja wartości l na wartość r wyodrębnia wartości argumentów c1 i c2.

    2a. Argumenty są konwertowane z char na int przy użyciu rozszerzenia typu.
    Sekwencja przekształceń argumentów podczas wywoływania funkcji libs_R_us::max(double,double) jest następująca:
    1b. Konwertując wartość l na wartość r, wyodrębniane są wartości argumentów c1 i c2.

    2b. Standardowa konwersja między typami całkowitymi i zmiennoprzecinkowymi rzutuje argumenty z typu char na typ double.

    Ranga pierwszej sekwencji jest rozszerzeniem typu (najgorsza zastosowana zmiana), natomiast ranga drugiej to standardowa konwersja. Ponieważ rozszerzenie typu jest lepsze niż konwersja typu, funkcja libs_R_us::max(int,int) jest wybierana jako najlepiej dopasowana do tego wywołania.
    Jeśli rankingowe sekwencje transformacji argumentów nie mogą ujawnić jednej dobrze ustalonej funkcji, wywołanie jest uważane za niejednoznaczne. W tym przykładzie oba wywołania funkcji calc() wymagają następującej sekwencji:

    1. Przekształć l-wartość na r-wartość, aby wyodrębnić wartości argumentów i oraz j.
    2. Standardowa konwersja służąca do rzutowania rzeczywistych argumentów na odpowiednie parametry formalne.

    Ponieważ nie można powiedzieć, która z tych sekwencji jest lepsza od drugiej, wywołanie jest niejednoznaczne:

    Int i, j; extern long calc(długi, długi); zewnętrzne podwójne obliczenie (podwójne, podwójne); void jj() ( // błąd: niejednoznaczność, brak najlepszego dopasowania
    oblicz(i, j);
    }

    Konwersja specyfikatora (dodanie specyfikatora const lub volatile do typu, który odnosi się do wskaźnika) ma rangę dokładnego dopasowania. Jeśli jednak dwie sekwencje transformacji różnią się tylko tym, że jedna z nich ma na końcu dodatkową transformację specyfikatora, to sekwencja bez niej jest uważana za lepszą. Na przykład:

    nieważny reset (int *); void reset(const int *); int*pi; int main() (
    zresetuj(pi); // bez konwertowania specyfikatorów jest lepsze:
    // wybierz zresetuj(int *)
    zwróć 0;
    }

    Sekwencja standardowych konwersji zastosowanych do rzeczywistego argumentu dla pierwszej kandydującej funkcji reset(int*) jest dokładnym dopasowaniem, wystarczy przejść od l-wartości do r-wartości, aby wyodrębnić wartość argumentu. W przypadku drugiej kandydującej funkcji reset(const int *) , stosowana jest również transformacja l-wartości na r-wartość, ale po niej następuje również konwersja specyfikatora w celu rzutowania wynikowej wartości ze wskaźnika na int na wskaźnik na const int. Obie sekwencje są dokładnym dopasowaniem, ale nie ma dwuznaczności. Ponieważ druga sekwencja różni się od pierwszej obecnością transformacji specyfikującej na końcu, sekwencja bez takiej transformacji jest uważana za najlepszą. Dlatego reset(int*) jest najlepszą funkcją stojącą.
    Oto kolejny przykład, w którym specyfikatory rzutowania wpływają na wybraną sekwencję:

    wyciąg z int(unieważniony*);
    int extract(const void *);

    int main() (
    ekstrakt(pi); // wybierz ekstrakt(unieważnij *)
    zwróć 0;
    }

    Istnieją dwie ustalone funkcje do wywołania: extract(void*) i extract(const void*). Sekwencja przekształceń funkcji extract(void*) polega na przekształceniu wartości l na wartość r w celu wyodrębnienia wartości argumentów, po której następuje standardowa konwersja wskaźnika: ze wskaźnika na int na wskaźnik na void. W przypadku funkcji extract(const void*) ta sekwencja różni się od pierwszej dodatkową konwersją specyfikatorów w celu rzutowania typu wyniku ze wskaźnika na void na wskaźnik na const void. Ponieważ sekwencje różnią się tylko tą transformacją, pierwsza z nich jest wybierana jako bardziej odpowiednia, a zatem funkcja extract(const void*) będzie najlepiej zachowana.
    Specyfikatory const i volatile wpływają również na ranking inicjalizacji parametrów referencyjnych. Jeśli dwie takie inicjalizacje różnią się tylko dodaniem specyfikatora const i volatile, wówczas inicjalizacja bez dodatkowego specyfikatora jest uważana za lepszą w rozdzielczości przeciążenia:

    #włączać manipulacja pustki (wektor &); void manip (const vector) &); wektor f();
    wektor zewnętrzny vec; int main() (
    manip(vec); // wybierz manip(wektor &)
    manip(f()); // wybierz manip(const vector &)
    zwróć 0;
    }

    W pierwszym wywołaniu inicjalizacja odwołania dla dowolnego wywołania funkcji jest dokładnym dopasowaniem. Ale to wyzwanie nadal nie będzie jednoznaczne. Ponieważ obie inicjalizacje sątakie same z wyjątkiem obecności dodatkowej specyfikacji const w drugim przypadku, inicjalizacja bez takiej specyfikacji jest uważana za lepszą, więc przeciążenie zostanie rozwiązane na korzyść dobrze ustalonego manip(vector &).
    W drugim wywołaniu istnieje tylko jedna dobrze ustalona funkcja manip(const vector &). Ponieważ aktualny argument jest zmienną tymczasową zawierającą wynik zwracany przez f(), taki argument jest wartością r, która nie może zostaćużyta do zainicjowania niestałego formalnego parametru referencji manip(vector &). Dlatego jedynym ugruntowanym manipem (const vector &).
    Oczywiście funkcje mogą mieć wiele rzeczywistych argumentów. Wybór najlepszych z aktualnych powinien być dokonany z uwzględnieniem rankingu sekwencji przekształceń wszystkich argumentów. Rozważ przykład:

    extern int ff(znak*, int); extern int ff(int, int); int main() ( ff(0, "a"); // ff(int, int)
    zwróć 0;
    }

    Funkcja ff(), która przyjmuje dwa argumenty int, została wybrana jako najlepiej działająca funkcja z następujących powodów:

    1. jej pierwszy argument jest lepszy. 0 pasuje dokładnie do formalnego parametru int, podczas gdy standardowa konwersja wskaźnika jest wymagana do dopasowania z parametrem typu char *;
    2. jego drugi argument ma tę samą rangę. Do argumentu "a" typu char, aby ustalić zgodność z drugim parametrem formalnym dowolnej z dwóch funkcji, należy zastosować sekwencję konwersji, która ma rangę rozszerzenia typu.

    Oto kolejny przykład:

    int obliczyć(const int&, short); int obliczyć(int&, double); zewnętrzne intioobj;
    int main() (
    obliczyć(iobj, "c"); // obliczyć(int&, double)
    zwróć 0;
    }

    Obie funkcje compute(const int&, short) i compute(int&, double) przetrwały. Drugi jest wybierany jako najlepszy z następujących powodów:

    1. jej pierwszy argument jest lepszy. Inicjalizacja odniesienia dla pierwszej ustanowionej funkcji jest gorsza, ponieważ wymaga dodania specyfikatora const, który nie jest potrzebny dla drugiej funkcji;
    2. jego drugi argument ma tę samą rangę. Do argumentu „c” typu char, w celu ustalenia zgodności z drugim parametrem formalnym dowolnej z dwóch funkcji, należy zastosować sekwencję przekształceń o randze transformacji standardowej.

    9.4.4. Argumenty z wartościami domyślnymi

    Posiadanie argumentów z wartościami domyślnymi może rozszerzyć wiele dobrze ugruntowanych funkcji. Residual to funkcje, które są wywoływane z podaną listą rzeczywistych argumentów. Ale taka funkcja może mieć więcej parametrów formalnych niż rzeczywiste podane argumenty, w przypadku gdy istnieje jakaś wartość domyślna dla każdego nieokreślonego parametru:

    zewnętrzna pustka ff(int); extern void ff(long, int = 0); int main() (
    ff(2L); // pasuje do ff(long, 0); ff(0, 0); // pasuje do ff(long, int);
    ff(0); // pasuje do ff(int);
    ff(3.14); // błąd: niejednoznaczność
    }

    Dla pierwszego i trzeciego wywołania funkcja ff() jest ustalana, nawet jeśli przekazano tylko jeden rzeczywisty argument. Wynika to z następujących powodów:

    1. istnieje wartość domyślna drugiego parametru formalnego;
    2. pierwszy parametr typu long odpowiada dokładnie rzeczywistemu argumentowi w pierwszym wywołaniu i może być rzutowany na typ argumentu w trzecim wywołaniu przez sekwencję, która ma rangę standardowej konwersji.

    Ostatnie wywołanie jest niejednoznaczne, ponieważ obie ustanowione funkcje można wybrać, stosując standardową transformację do pierwszego argumentu. Funkcja ff(int) nie jest preferowana tylko dlatego, że ma jeden parametr.

    Ćwiczenie 9.9

    Wyjaśnij, co się stanie, gdy rozwiążesz przeciążenie, aby wywołać metodę compute() wewnątrz funkcji main(). Jakie cechy mają kandydaci? Który z nich stanie po pierwszym kroku? Jaką sekwencję przekształceń należy zastosować do rzeczywistego argumentu, aby odpowiadał formalnemu parametrowi każdej ustalonej funkcji? Która funkcja będzie najlepsza?

    Przestrzeń nazw primerLib ( void compute(); void compute(const void *); ) przy użyciu primerLib::compute;
    nieważne obliczenie(int);
    void compute(double, double = 3.4);
    void obliczyć(char*, char* = 0); int main() (
    obliczyć(0);
    zwróć 0;
    }

    Co się stanie, jeśli deklaracja using zostanie umieszczona wewnątrz funkcji main() przed wywołaniem funkcji compute()? Odpowiedz na te same pytania.

    Kiedy definiujesz funkcje w swoich programach, musisz określić typ zwracany przez funkcję, a także liczbę parametrów i typ każdego parametru. W przeszłości (jeśli programowałeś w C), gdy miałeś funkcję o nazwie add_values, która działała na dwóch wartościach całkowitych i chciałeś użyć podobnej funkcji do dodania trzech wartości całkowitych, powinieneś utworzyć funkcję z inna nazwa. Na przykład możesz użyć add_two_values ​​i add_three_values. Podobnie, jeśli chcesz użyć podobnej funkcji do dodawania wartości zmiennoprzecinkowych, potrzebujesz innej funkcji o innej nazwie. Aby uniknąć powielania funkcji, C++ umożliwia zdefiniowanie wielu funkcji o tej samej nazwie. Podczas kompilacji C++ bierze pod uwagę liczbę argumentów używanych przez każdą funkcję, a następnie wywołuje dokładnie wymaganą funkcję. Nadanie kompilatorowi wyboru spośród wielu funkcji nazywa się przeciążaniem. W tym samouczku dowiesz się, jak korzystać z przeciążonych funkcji. Pod koniec tej lekcji opanujesz następujące podstawowe pojęcia:

    Przeciążanie funkcji umożliwia używanie tej samej nazwy dla wielu funkcji z różne rodzaje parametry.

    Aby przeciążyć funkcje, po prostu zdefiniuj dwie funkcje o tej samej nazwie i typie zwracanym, które różnią się liczbą parametrów lub ich typem.

    Przeciążanie funkcji jest cechą języka C++, której nie ma w C. Jak zobaczysz, przeciążanie funkcji jest całkiem wygodne i może poprawić czytelność twoich programów.

    PIERWSZE WPROWADZENIE DO PRZECIĄŻENIA FUNKCJI

    Przeciążanie funkcji umożliwia programom zdefiniowanie wielu funkcji o tej samej nazwie i typie zwracanym. Na przykład poniższy program przeciąża funkcję o nazwie add_values. Pierwsza definicja funkcji dodaje dwie wartości int. Druga definicja funkcji dodaje trzy wartości. Podczas kompilacji C++ poprawnie określa funkcję, która ma być użyta:

    #włączać

    int add_values(int a,int b)

    {
    powrót(a+b);
    )

    int add_values ​​(int a, int b, int c)

    (
    powrót(a+b+c);
    )

    {
    Cout<< «200 + 801 = » << add_values(200, 801) << endl;
    Cout<< «100 + 201 + 700 = » << add_values(100, 201, 700) << endl;
    }

    Jak widać, program definiuje dwie funkcje o nazwie add_values ​​Pierwsza funkcja dodaje dwie wartości int, a druga trzy wartości. Nie musisz robić nic specjalnie, aby ostrzec kompilator o przeciążeniu, po prostu go użyj. Kompilator ustali, której funkcji użyć na podstawie opcji, które udostępnia program.

    Podobnie następujący program MSG_OVR.CPP przeciąża funkcję show_message. Pierwsza funkcja o nazwie show_message wyświetla standardowy komunikat, do którego nie są przekazywane żadne parametry. Drugi wypisuje przekazaną do niego wiadomość, a trzeci wyprowadza dwie wiadomości:

    #włączać

    nieważna wiadomość_pokazu (nieważna)

    {
    Cout<< «Стандартное сообщение: » << «Учимся программировать на C++» << endl;
    }

    void show_message(char *wiadomość)

    {
    Cout<< message << endl;
    }

    void show_message(znak *pierwszy, znak *drugi)

    {
    Cout<< first << endl;
    Cout<< second << endl;
    }

    {
    Pokaż wiadomość();
    show_message("Naucz się programować w C++!");
    show_message("W C++ nie ma uprzedzeń!", "Przeładowanie jest fajne!");
    }

    KIEDY PRZECIĄŻENIE JEST NIEZBĘDNE

    Jednym z najczęstszych przypadków użycia przeciążania jest użycie funkcji w celu uzyskania określonego wyniku przy różnych parametrach. Załóżmy na przykład, że twój program ma funkcję o nazwie day_of_week, która zwraca bieżący dzień tygodnia (0 dla niedzieli, 1 dla poniedziałku, ..., 6 dla soboty). Twój program może przeciążyć tę funkcję, aby poprawnie zwracał dzień tygodnia, jeśli jako parametr podano dzień juliański lub jeśli podano dzień, miesiąc i rok:

    int day_of_week(int julian_day)

    {
    // Operatorzy
    }

    int day_of_week(int miesiąc, int dzień, int rok)

    {
    // Operatorzy
    }

    Podczas odkrywania programowania obiektowego w C++ w poniższych lekcjach będziesz używać przeciążania funkcji, aby zwiększyć możliwości swoich programów.

    Przeciążanie funkcji poprawia czytelność programu

    Przeciążanie funkcji C++ umożliwia programom zdefiniowanie wielu funkcji o tej samej nazwie. Funkcje przeciążone muszą zwracać wartości tego samego typu*, ale mogą różnić się liczbą i rodzajem parametrów. Przed pojawieniem się przeciążania funkcji w C++ programiści C musieli tworzyć wiele funkcji o prawie tej samej nazwie. Niestety programiści chcący korzystać z takich funkcji musieli pamiętać, jaka kombinacja parametrów odpowiada której funkcji. Z drugiej strony przeciążanie funkcji ułatwia programistom zadanie zapamiętania tylko jednej nazwy funkcji.* Przeciążone funkcje nie muszą zwracać wartości tego samego typu, ponieważ kompilator jednoznacznie identyfikuje funkcję po nazwie i jej nazwie. zestaw argumentów. Dla kompilatora funkcje o tej samej nazwie, ale różne typy argumentów są różnymi funkcjami, więc typ zwracany jest przywilejem każdej funkcji. - Przybliż.przeł.

    CO MUSISZ WIEDZIEĆ

    Przeciążanie funkcji umożliwia określenie wielu definicji tej samej funkcji. Podczas kompilacji C++ określi, której funkcji użyć na podstawie liczby i typu przekazanych parametrów. W tym samouczku dowiedziałeś się, że łatwo jest przeciążyć funkcje. W lekcji 14 dowiesz się, jak odwołania C++ ułatwiają zmianę parametrów w funkcjach. Zanim jednak przejdziesz do lekcji 14, upewnij się, że nauczyłeś się następujących podstawowych pojęć:

    1. Przeciążanie funkcji zapewnia wiele „widoków” tej samej funkcji w programie.
    2. Aby przeciążyć funkcje, po prostu zdefiniuj wiele funkcji o tej samej nazwie i zwracanym typie, które różnią się tylko liczbą i typem parametrów.
    3. Podczas kompilacji C++ określi, którą funkcję wywołać na podstawie liczby i typu przekazanych parametrów.
    4. Przeciążanie funkcji upraszcza programowanie, umożliwiając programistom pracę tylko z jedną nazwą funkcji.

    Adnotacja: Wykład omawia koncepcje, deklarację i zastosowanie funkcji inline i przeciążanych w programach C++, mechanizmy wykonywania podstawień i przeciążania funkcji, zalecenia dotyczące poprawy wydajności programów poprzez przeciążanie lub podstawienie funkcji.

    Cel wykładu: naucz się funkcji wbudowanych (osadzanych) i przeciążeń funkcji, naucz się tworzyć programy wykorzystujące przeciążanie funkcji w C++.

    Funkcje wbudowane

    Wywołanie funkcji, przekazanie do niej wartości, zwrócenie wartości - te operacje zajmują dość dużo czasu procesora. Zwykle podczas definiowania funkcji kompilator rezerwuje tylko jeden blok komórek w pamięci do przechowywania swoich instrukcji. Po wywołaniu funkcji sterowanie programem jest przekazywane tym operatorom, a po powrocie z funkcji wykonywanie programu wznawia się od wiersza następującego po wywołaniu funkcji.

    Przy powtarzających się wywołaniach za każdym razem program będzie przetwarzał ten sam zestaw poleceń, bez tworzenia kopii dla każdego wywołania z osobna.

    Każdy skok do obszaru pamięci zawierającego instrukcje funkcji spowalnia wykonanie programu. Jeśli funkcja jest mała, możesz zaoszczędzić czas przy wielu wywołaniach, nakazując kompilatorowi wbudowanie kodu funkcji bezpośrednio do programu w miejscu wywołania. Takie funkcje nazywają się podstawiony. W tym przypadku, mówiąc o wydajności, zakłada się przede wszystkim szybkość wykonania programu.

    Funkcje wbudowane lub wbudowane to funkcje, których kod jest wstawiany przez kompilator bezpośrednio w witrynie wywołania, zamiast przekazywania sterowania do pojedynczego wystąpienia funkcji.

    Jeśli funkcja jest inline, kompilator nie tworzy danej funkcji w pamięci, ale kopiuje jej ciągi bezpośrednio do kodu programu w miejscu wywołania. Jest to równoważne zapisaniu odpowiednich bloków w programie zamiast wywołań funkcji. Więc specyfikator wbudowany definiuje dla funkcji tzw wiązanie wewnętrzne, który polega na tym, że kompilator zastępuje polecenia swojego kodu zamiast wywoływać funkcję. Funkcje wbudowane są używane, jeśli treść funkcji składa się z kilku instrukcji.

    Takie podejście pozwala na zwiększenie szybkości wykonywania programu, ponieważ polecenia są wykluczone z programu. mikroprocesor Wymagane do przekazania argumentów i wywołania funkcji.

    Na przykład:

    /*funkcja zwraca odległość od punktu o współrzędnych (x1,y1) do punktu o współrzędnych (x2,y2)*/ inline float Line(float x1,float y1,float x2, float y2) ( return sqrt(pow(x1-x2 ,2)+pow(y1-y2,2)); )

    Należy jednak zauważyć, że wykorzystanie funkcji wbudowanych nie zawsze prowadzi do pozytywnego efektu. Jeżeli taka funkcja zostanie kilkakrotnie wywołana w kodzie programu, to w czas kompilacji do programu zostanie wstawionych tyle kopii tej funkcji, ile jest wywołań. Nastąpi znaczny wzrost rozmiaru kodu programu, w wyniku czego oczekiwany wzrost wydajności wykonywania programu w czasie może nie nastąpić.

    Przykład 1.

    #include "stdafx.h" #include używając standardowej przestrzeni nazw; wbudowany int Cube(int x); int _tmain(int argc, _TCHAR* argv)( int x=2; float y=3; double z=4; cout<

    Podajemy powody, dla których funkcja ze specyfikatorem inline będzie traktowana jako zwykła funkcja nieinline:

    • funkcja inline jest rekurencyjna;
    • funkcje, których wywołanie znajduje się przed jego definicją;
    • funkcje, które w wyrażeniu są wywoływane więcej niż raz;
    • funkcje zawierające pętle, przełączniki i operatorzy skoku;
    • funkcje, które są zbyt duże, aby umożliwić podstawianie.

    Ograniczenia dotyczące wykonywania zastępowania są w większości zależne od implementacji. Jeśli dla funkcji ze specyfikatorem wbudowany kompilator nie może wykonać podstawienia ze względu na kontekst, w którym znajduje się wywołanie, wtedy funkcja jest uznawana za statyczną i jest wydawana Wiadomość ostrzegawcza.

    Inną cechą funkcji wbudowanych jest niemożność ich zmiany bez ponownej kompilacji wszystkich części programu, w których te funkcje są wywoływane.

    Przeciążenie funkcji

    Definiując funkcje w programach konieczne jest określenie typu wartości zwracanej przez funkcję oraz ilości parametrów i typu każdego z nich. Gdyby język C++ miał funkcję o nazwie add_values, która działała na dwóch wartościach całkowitych, a program musiał użyć podobnej funkcji do przekazania trzech wartości całkowitych, to należałoby stworzyć funkcję o innej nazwie. Na przykład add_two_values ​​i add_three_values. Podobnie, jeśli chcesz użyć podobnej funkcji do pracy z wartościami zmiennoprzecinkowymi, potrzebujesz innej funkcji o innej nazwie. Aby uniknąć powielania funkcji, C++ umożliwia zdefiniowanie wielu funkcji o tej samej nazwie. Podczas kompilacji C++ bierze pod uwagę liczbę argumentów używanych przez każdą funkcję, a następnie wywołuje dokładnie wymaganą funkcję. Nadanie kompilatorowi wyboru spośród wielu funkcji nazywa się przeciążaniem.

    Przeciążenie funkcji to tworzenie kilku funkcji o tej samej nazwie, ale o różnych parametrach. Różne parametry oznaczają to, co powinno być inne liczba argumentów funkcje i/lub ich funkcje typ. Oznacza to, że przeciążanie funkcji umożliwia zdefiniowanie wielu funkcji o tej samej nazwie i typie zwracanym.

    Przeciążanie funkcji jest również nazywane polimorfizm funkcji. „Poly” oznacza dużo, „morf” - forma, czyli funkcja polimorficzna to funkcja wyróżniająca się różnorodnością form.

    Polimorfizm funkcji rozumiany jest jako istnienie w programie kilku przeciążonych wersji funkcji, które mają różne wartości. Zmieniając liczbę lub typ parametrów, możesz nadać dwóm lub większej liczbie funkcji tę samą nazwę. W takim przypadku nie będzie zamieszania, ponieważ pożądana funkcja jest określana przez zbieżność użytych parametrów. Pozwala to na stworzenie funkcji, która może pracować z liczbami całkowitymi, liczbami zmiennoprzecinkowymi lub innymi typami wartości bez konieczności tworzenia osobnych nazw dla każdej funkcji.

    Tak więc dzięki zastosowaniu przeładowane funkcje, nie należy się martwić wywołaniem właściwej funkcji w programie, która pasuje do typu przekazywanych zmiennych. Po wywołaniu przeciążonej funkcji kompilator automatycznie określi, która wersja funkcji powinna zostać użyta.

    Na przykład poniższy program przeciąża funkcję o nazwie add_values. Pierwsza definicja funkcji dodaje dwie wartości typu int . Druga definicja funkcji dodaje trzy wartości typu int . Podczas kompilacji C++ poprawnie określa funkcję, która ma być użyta:

    #include "stdafx.h" #include używając standardowej przestrzeni nazw; int add_values(int a,int b); int add_values ​​(int a, int b, int c); int _tmain(int argc, _TCHAR* argv)( cout<< "200+801=" << add_values(200,801) << "\n"; cout << "100+201+700=" << add_values(100,201,700) << "\n"; system("pause"); return 0; } int add_values(int a,int b) { return(a + b); } int add_values (int a, int b, int c) { return(a + b + c); }

    W ten sposób program definiuje dwie funkcje o nazwie add_values. Pierwsza funkcja dodaje dwie wartości, natomiast druga dodaje trzy wartości tego samego typu int. Kompilator C++ określa, której funkcji użyć na podstawie opcji udostępnianych przez program.

    Korzystanie z przeciążenia funkcji

    Jednym z najczęstszych przypadków użycia przeciążania jest użycie funkcji w celu uzyskania określonego wyniku przy różnych parametrach. Załóżmy na przykład, że program ma funkcję o nazwie dzień_tygodnia, która zwraca bieżący dzień tygodnia (0 oznacza niedzielę, 1 oznacza poniedziałek, ... , 6 oznacza sobotę). Program może przeciążyć tę funkcję, aby poprawnie zwracała dzień tygodnia, jeśli jako parametr podano dzień juliański lub jeśli podano dzień, miesiąc i rok.

    int day_of_week(int julian_day) ( // operatory ) int day_of_week(int miesiac, int dzien, int rok) ( // operatory )

    Za pomocą przeładowane funkcje często popełnianych jest wiele błędów. Na przykład, jeśli funkcje różnią się tylko typem zwracanym, ale nie typami argumentów, takie funkcje nie mogą mieć tej samej nazwy. Następująca opcja przeciążenia jest również nieprawidłowa:

    int nazwa_funkcji(int nazwa_argumentu); int nazwa_funkcji(int nazwa_argumentu); /* nieprawidłowe przeciążenie nazwy: argumenty mają ten sam numer i ten sam typ */

    Korzyści z przeciążania funkcji:

    • poprawia się przeciążenie funkcji czytelność programy;
    • Przeciążanie funkcji C++ umożliwia programom definiowanie wielu funkcji o tej samej nazwie;
    • przeładowane funkcje zwracają wartości tego samego typu, ale mogą różnić się liczbą i rodzajem parametrów;
    • przeciążanie funkcji upraszcza pracę programistów, wymagając od nich zapamiętania tylko jednej nazwy funkcji, ale potem muszą wiedzieć, która kombinacja parametrów odpowiada której funkcji.

    Przykład 2.

    /*Przeciążone funkcje mają tę samą nazwę, ale różne listy parametrów i zwracane wartości*/ #include "stdafx.h" #include używając standardowej przestrzeni nazw; int średnia(int pierwszy numer, int drugi numer, int trzeci numer); int średnia(int pierwszy numer, int drugi numer); int _tmain(int argc, _TCHAR* argv)(// funkcja główna int liczba_A = 5, liczba_B = 3, liczba_C = 10; cout<< "Целочисленное среднее чисел " << number_A << " и "; cout << number_B << " равно "; cout << average(number_A, number_B) << ".\n\n"; cout << "Целочисленное среднее чисел " << number_A << ", "; cout << number_B << " и " << number_C << " равно "; cout << average(number_A, number_B, number_C) << ".\n"; system("PAUSE"); return 0; }// конец главной функции /*функция для вычисления целочисленного среднего значения 3-х целых чисел*/ int average(int first_number, int second_number, int third_number) { return((first_number + second_number + third_number)/3); } // конец функции /*функция для вычисления целочисленного среднего значения 2-х целых чисел*/ int average(int first_number, int second_number) { return((first_number + second_number)/2); } // конец функции



    Jak osiągnąć przeciążenie funkcji w C? (dziesięć)

    Czy istnieje sposób na przeciążenie funkcji w C? Patrzę na proste funkcje, które mogą być przeładowane jak

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

    Myślę, że nie ma bezpośredniej drogi; Szukam obejścia, jeśli takie istnieją.

    Mam nadzieję, że poniższy kod pomoże Ci zrozumieć przeciążenie funkcji

    #włączać #włączać int zabawa(int a, ...); int main(int argc, char *argv)( fun(1,10); fun(2,"cquestionbank"); return 0; ) int fun(int a, ...)( va_list vl; va_start(vl,a if(a==1) printf("%d",va_arg(vl,int)); else printf("\n%s",va_arg(vl,char *)); )

    To znaczy, masz na myśli - nie, nie możesz.

    Możesz zadeklarować funkcję va_arg jako

    void moja_funkcja (znak* format, ...);

    Ale będziesz musiał przekazać trochę informacji o liczbie zmiennych i ich typach w pierwszym argumencie - jak printf() .

    Tak jak.

    Tutaj podajesz przykład:

    void printA(int a)( printf("Witaj świecie z printA: %d\n",a); ) void printB(const char *buff)( printf("Witaj świecie z printB: %s\n",buff) ; ) #define Max_ITEMS() 6, 5, 4, 3, 2, 1, 0 #define __VA_ARG_N(_1, _2, _3, _4, _5, _6, N, ...) N #define _Num_ARGS_(... ) __VA_ARG_N(__VA_ARGS__) #define NUM_ARGS(...) (_Num_ARGS_(_0, ## __VA_ARGS__, Max_ITEMS()) - 1) #define CHECK_ARGS_MAX_LIMIT(t) if(NUM_ARGS(args)>t) #define CHECK_ARGS_MIN_ if(NUM_ARGS(args) #define print(x , args ...) \ CHECK_ARGS_MIN_LIMIT(1) printf("błąd");fflush(stdout); \ CHECK_ARGS_MAX_LIMIT(4) printf("błąd");fflush(stdout) ; \ (( \ if (__builtin_types_compatible_p (typeof (x), int)) \ printA(x, ##args); \ else \ printB (x,##args); \ )) int main(int argc, char* * argv) ( int a=0; print(a); print("cześć"); return (EXIT_SUCCESS); )

    Wypisze 0 i cześć z printA i printB.

    Jeśli Twoim kompilatorem jest gcc i nie masz nic przeciwko ręcznym aktualizacjom za każdym razem, gdy dodajesz nowe przeciążenie, możesz utworzyć makromasę i uzyskać wynik, który chcesz z punktu widzenia wywołującego, nie jest to zbyt przyjemne do pisania... ale jest możliwy

    spójrz na __builtin_types_compatible_p, a następnie użyj go do zdefiniowania makra, które robi coś takiego

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

    ale tak, paskudnie, po prostu nie

    EDYTOWAĆ: C1X otrzyma wsparcie dla wyrażeń typu, które wyglądają tak:

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

    Jak już wspomniano, przeciążanie w sensie, o którym mówisz, nie jest obsługiwane przez C. Zwykłym idiomem rozwiązywania tego problemu jest przyjmowanie przez funkcję tagowanej unii . Jest to implementowane przy użyciu parametru struct, gdzie sama struktura składa się z pewnego typu wskaźnika typu, takiego jak enum i unii różnych typów wartości. Przykład:

    #włączać typedef enum ( T_INT, T_FLOAT, T_CHAR, ) mój_typ; typedef struct ( my_type typ; union ( int a; float b; char c; ) my_union; ) my_struct; void set_overload (my_struct *cokolwiek) ( switch (cokolwiek->typ) ( case T_INT: cokolwiek->my_union.a = 1; break; case T_FLOAT: cokolwiek->my_union.b = 2.0; break; case T_CHAR: cokolwiek-> my_union.c = "3"; ) ) void printf_overload (my_struct *cokolwiek) ( switch (cokolwiek->typ) ( case T_INT: printf("%d\n", cokolwiek->my_union.a); break; case T_FLOAT : printf("%f\n", cokolwiek->my_union.b); break; case T_CHAR: printf("%c\n", cokolwiek->my_union.c); break; ) ) int main (int argc, char* argv) ( my_struct s; s.type=T_INT; set_overload(&s); printf_overload(&s); s.type=T_FLOAT; set_overload(&s); printf_overload(&s); s.type=T_CHAR; set_overload(&s) ; printf_overload(&s; )

    Czy nie możesz po prostu używać C++ i nie używać wszystkich innych funkcji C++ poza tym?

    Jeśli do tej pory nie było ścisłego C, zalecałbym zamiast tego funkcje wariadyczne.

    Poniższe podejście jest podobne do a2800276, ale z niektórymi makrami C99:

    // potrzebujemy `size_t` #include // typy argumentów do przyjęcia enum sum_arg_types ( SUM_LONG, SUM_ULONG, SUM_DOUBLE ); // struktura do przechowywania argumentu struct sum_arg ( enum sum_arg_types typ; union ( long as_long; unsigned long as_ulong; double as_double; ) wartość; ); // określ rozmiar tablicy #define count(TABLICA) ((sizeof (TABLICA))/(sizeof *(TABLICA))) // tak nasza funkcja będzie nazywana #define sum(...) _sum( count(sum_args(__VA_ARGS__)), sum_args(__VA_ARGS__)) // tworzenie tablicy `struct sum_arg` #define sum_args(...) ((struct sum_arg )( __VA_ARGS__ )) // tworzenie inicjatorów dla argumentów #define sum_long (WARTOŚĆ) ( SUM_LONG, ( .as_long = (WARTOŚĆ)) ) #define sum_ulong(WARTOŚĆ) ( SUM_ULONG, ( .as_ulong = (WARTOŚĆ) ) ) #define sum_double(WARTOŚĆ) ( SUM_DOUBLE, ( .as_double = (WARTOŚĆ)) ) ) // nasza funkcja polimorficzna long double _sum(size_t count, struct sum_arg * args) ( long double value = 0; for(size_t i = 0; i< count; ++i) { switch(args[i].type) { case SUM_LONG: value += args[i].value.as_long; break; case SUM_ULONG: value += args[i].value.as_ulong; break; case SUM_DOUBLE: value += args[i].value.as_double; break; } } return value; } // let"s see if it works #include int main() ( unsigned long foo = -1; long double value = sum(sum_long(42), sum_ulong(foo), sum_double(1e10)); printf("%Le\n", value); return 0; )

    Na razie _Generic, ponieważ _Generic pytania, standardowe C (bez rozszerzeń) jest skutecznie Odebrane wsparcie dla funkcji przeciążających (zamiast operatorów) dzięki dodaniu słowa _Generic _Generic w C11. (obsługiwane w GCC od wersji 4.9)

    (Przeciążenie nie jest tak naprawdę „wbudowane” w sposób pokazany w pytaniu, ale łatwo jest zniszczyć coś, co działa w ten sposób).

    Generic to operator czasu kompilacji w tej samej rodzinie co sizeof i _Alignof . Zostało to opisane w standardowym rozdziale 6.5.1.1. Przyjmuje dwa główne parametry: wyrażenie (które nie będzie oceniane w czasie wykonywania) oraz listę skojarzeń typu/wyrażenia, która przypomina trochę blok przełączników. _Generic pobiera ogólny typ wyrażenia, a następnie "przełącza się" na niego, aby wybrać z listy końcowe wyrażenie wynikowe dla jego typu:

    Generic(1, float: 2.0, char *: "2", int: 2, domyślnie: get_two_object());

    Powyższe wyrażenie ma wartość 2 — typem wyrażenia sterującego jest int , więc jako wartość wybiera wyrażenie skojarzone z int. Nic z tego nie pozostaje w czasie wykonywania. (Klauzula default jest obowiązkowa: jeśli jej nie określisz, a typ się nie zgadza, spowoduje to błąd kompilacji).

    Technika, która jest przydatna do przeciążania funkcji, polega na tym, że może ona zostać wstawiona przez preprocesor C i wybrać wyrażenie wynikowe na podstawie typu argumentów przekazanych do makra sterującego. A więc (przykład ze standardu C):

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

    To makro implementuje operację przeciążenia cbrt, przekazując typ argumentu do makra, wybierając odpowiednią funkcję implementacji, a następnie przekazując oryginalne makro do tej funkcji.

    Aby zaimplementować twój oryginalny przykład, możemy zrobić to:

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

    W tym przypadku moglibyśmy użyć default: binding dla trzeciego przypadku, ale to nie pokazuje, jak rozszerzyć zasadę na wiele argumentów. Efektem końcowym jest to, że możesz użyć foo(...) w swoim kodzie bez martwienia się (dużo) o typ swoich argumentów.

    W bardziej złożonych sytuacjach, takich jak funkcje przeciążające więcej argumentów lub zmieniające się liczby, można użyć makr narzędziowych do automatycznego generowania statycznych struktur dyspozytorskich:

    void print_ii(int a, int b) ( printf("int, int\n"); ) void print_di(double a, int b) ( printf("double, int\n"); ) void print_iii(int a, int b, int c) ( printf("int, int, int\n"); ) void print_default(void) ( printf("nieznane argumenty\n"); ) #define print(...) PRZECIĄŻENIE(print, (__VA_ARGS__), \ (print_ii, (int, int)), \ (print_di, (double, int)), \ (print_iii, (int, int, int)) \) #define OVERLOAD_ARG_TYPES (int, double) #define OVERLOAD_FUNCTIONS (print) #include "activate-overloads.h" int main(void) ( print(44, 47); // drukuje "int, int" print(4.4, 47); // drukuje "double, int" print (1, 2, 3); // wyświetla "int, int, int" print(""); // wyświetla "nieznane argumenty" )

    (wdrożenie tutaj). Z pewnym wysiłkiem możesz zredukować boilerplate, aby wyglądał bardzo podobnie do języka z wbudowaną obsługą przeciążania.

    Poza tym było już możliwe przeciążenie ilość argumenty (zamiast typu) w C99.

    Zauważ, że sposób oceny C może Cię poruszyć. Spowoduje to wybranie foo_int, jeśli na przykład spróbujesz przekazać do niego znak literału, a potrzebujesz trochę foo_int, jeśli chcesz, aby przeciążenia obsługiwały literały ciągów. Jednak ogólnie całkiem fajnie.

    Odpowiedź Leushenko jest naprawdę fajna: tylko przykład foo nie kompiluje się z GCC, co nie działa na foo(7) , uderzając w PIERWSZE makro i rzeczywiste wywołanie funkcji ((_1, __VA_ARGS__) , pozostając z dodatkowym przecinkiem. napotkać problemy, jeśli chcemy zapewnić dodatkowe przeciążenia, takie jak foo(double) .

    Postanowiłem więc odpowiedzieć na to pytanie bardziej szczegółowo, włączając w to dopuszczenie pustego przeciążenia (foo(void) - co przysporzyło trochę kłopotów...).

    Pomysł teraz jest taki: zdefiniuj więcej niż jeden rodzajowy w różnych makrach i pozwól, aby ten właściwy został wybrany na podstawie liczby argumentów!

    Liczba argumentów jest dość prosta, opierając się na tej odpowiedzi:

    #define foo(...) SELECT(__VA_ARGS__)(__VA_ARGS__) #define SELECT(...) CONCAT(SELECT_, NARG(__VA_ARGS__))(__VA_ARGS__) #define CONCAT(X,Y) CONCAT_(X,Y) # zdefiniuj CONCAT_(X, Y) X ## Y

    To dobrze, wybieramy SELECT_1 lub SELECT_2 (lub więcej argumentów, jeśli ich potrzebujesz), więc potrzebujemy tylko odpowiednich definicji:

    #define SELECT_0() foo_void #define SELECT_1(_1) _Generic ((_1), \ int: foo_int, \ char: foo_char, \ double: foo_double \) #define SELECT_2(_1, _2) _Generic((_1), \ double : _Generic((_2), \ int: foo_double_int \) \)

    Po pierwsze, puste wywołanie makra (foo()) nadal tworzy token, ale jest on pusty. Tak więc makro count faktycznie zwraca 1 zamiast 0, nawet jeśli makro jest nazywane pustym. Możemy "łatwo" naprawić ten problem, jeśli __VA_ARGS__ z przecinkiem po __VA_ARGS__ warunkowo, w zależności od tego, czy lista jest pusta, czy nie:

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

    to wyglądałłatwe, ale makro przecinek jest dość ciężkie; na szczęście ten temat jest już omówiony na blogu Jensa Gustedta (dzięki Jens). Główną sztuczką jest to, że makra funkcji nie rozszerzają się, chyba że są poprzedzone nawiasami, zobacz blog Jensa po dalsze wyjaśnienia... Musimy tylko trochę zmodyfikować makra dla naszych potrzeb (będę używał krótszych nazw i mniejszej liczby argumentów dla zwięzłości) .

    #define ARGN(...) ARGN_(__VA_ARGS__) #define ARGN_(_0, _1, _2, _3, N, ...) N #define HAS_COMMA(...) ARGN(__VA_ARGS__, 1, 1, 1, 0 ) #define SET_COMMA(...) , #define PRZECINEK(...) SELECT_COMMA \ (\ HAS_COMMA(__VA_ARGS__), \ HAS_COMMA(__VA_ARGS__ ()), \ HAS_COMMA(SET_COMMA __VA_ARGS__), \ HAS_COMMA(SET_COMMA __))VA_ARGS__ \) #define SELECT_COMMA(_0, _1, _2, _3) SELECT_COMMA_(_0, _1, _2, _3) #define SELECT_COMMA_(_0, _1, _2, _3) COMMA_ ## _0 ## _1 ## _2 ## _3 # define COMMA_0000 , #define COMMA_0001 #define COMMA_0010 , // ... (wszystkie inne z przecinkiem) #define COMMA_1111 ,

    A teraz wszystko w porządku...

    Pełny kod w jednym bloku:

    /* * demo.c * * Utworzono: 14.09.2017 * Autor: sboehler */ #include void foo_void(void) ( puts("void"); ) void foo_int(int c) ( printf("int: %d\n", c); ) void foo_char(char c) ( printf("char: %c \n", c); ) void foo_double(double c) ( printf("double: %.2f\n", c); ) void foo_double_int(double c, int d) ( printf("double: %.2f, int: %d\n", c, d); ) #define foo(...) SELECT(__VA_ARGS__)(__VA_ARGS__) #define SELECT(...) CONCAT(SELECT_, NARG(__VA_ARGS__))(__VA_ARGS__) # define CONCAT(X, Y) CONCAT_(X, Y) #define CONCAT_(X, Y) X ## Y #define SELECT_0() foo_void #define SELECT_1(_1) _Generic ((_1), \ int: foo_int, \ char : foo_char, \ double: foo_double \) #define SELECT_2(_1, _2) _Generic((_1), \ double: _Generic((_2), \ int: foo_double_int \) \) #define ARGN(...) ARGN_( __VA_ARGS__) #define ARGN_(_0, _1, _2, N, ...) N #define NARG(...) ARGN(__VA_ARGS__ PRZECINEK(__VA_ARGS__) 3, 2, 1, 0) #define HAS_COMMA(...) ARGN(__VA_ARGS__, 1, 1, 0) #define SET_COMMA(...) , #define PRZECINEK(...) SELECT_COMMA \ (\ HAS_COMMA(__VA_ARGS__), \ HAS_COMMA(__VA_ARGS__ ()), \ HAS_C OMMA(SET_COMMA __VA_ARGS__), \ HAS_COMMA(SET_COMMA __VA_ARGS__ ()) \) #define SELECT_COMMA(_0, _1, _2, _3) SELECT_COMMA_(_0, _1, _2, _3) #define SELECT_COMMA_(_0, _3), _2 COMMA_ ## _0 ## _1 ## _2 ## _3 COMMA_1001 , #define COMMA_1010 , #define COMMA_1011 , #define COMMA_1100 , #define COMMA_1101 , #define COMMA_1110 , #define COMMA_1111 , int main(int argv, char** ) ( bla(); foo(7); foo(10.12); foo(12.10, 7); foo((char)"s"); zwróć 0; )

    C++ pozwala określić więcej niż jedną definicję dla Funkcje imię lub operator w tym samym obszarze co funkcja przeciążenia oraz przeciążenia operatora odpowiednio.

    Deklaracja przeciążona to deklaracja zadeklarowana o tej samej nazwie co wcześniej zadeklarowana deklaracja w tym samym zakresie, z tą różnicą, że obie deklaracje mają różne argumenty i oczywiście inną definicję (implementację).

    Kiedy dzwonisz przeładowany funkcjonować lub operator, kompilator określa najbardziej odpowiednią definicję do użycia, porównując typy argumentów użyte do wywołania funkcji lub operatora z typami parametrów określonymi w definicjach. Proces wyboru najbardziej odpowiedniej przeciążonej funkcji lub operatora nazywa się rozdzielczość przeciążenia .

    Przeciążanie funkcji w C++

    Możesz mieć wiele definicji dla tej samej nazwy funkcji w tym samym zakresie. Definicja funkcji musi różnić się od siebie pod względem typów i/lub liczby argumentów na liście argumentów. Nie można przeciążać deklaracji funkcji, które różnią się tylko typem zwracanym.

    Poniżej znajduje się przykład, w którym ta sama funkcja wydrukować() służy do drukowania różnych typów danych -

    #włączać używając standardowej przestrzeni nazw; class printData ( public: void print(int i) ( cout<< "Printing int: " << i << endl; } void print(double f) { cout << "Printing float: " << f << endl; } void print(char* c) { cout << "Printing character: " << c << endl; } }; int main(void) { printData pd; // Call print to print integer pd.print(5); // Call print to print float pd.print(500.263); // Call print to print character pd.print("Hello C++"); return 0; }

    Drukowanie int: 5 Drukowanie float: 500.263 Drukowanie znaku: Hello C++

    Przeciążanie operatorów w C++

    Możesz zastąpić lub przeciążyć większość wbudowanych operatorów dostępnych w C++. W ten sposób programista może również używać operatorów o typach zdefiniowanych przez użytkownika.

    Przeciążone operatory to funkcje o specjalnych nazwach: słowo kluczowe „operator”, po którym następuje znak definiowanego operatora. Jak każda inna funkcja, przeciążony operator ma typ zwracany i listę parametrów.

    Operator skrzynki+(const Box&);

    deklaruje operator append, którego można użyć do wzbogacenie dwa obiekty Box i zwraca końcowy obiekt Box. Większość przeciążonych operatorów można zdefiniować jako zwykłe funkcje niebędące członkami lub jako funkcje składowe klasy. W przypadku, gdy zdefiniujemy powyższą funkcję jako funkcję nienależącą do klasy, musielibyśmy przekazać dwa argumenty dla każdego operandu w następujący sposób:

    Operator skrzynki+(const Box&, const Box&);

    Poniżej znajduje się przykład przedstawiający koncepcję operatora podczas ładowania przy użyciu funkcji członkowskiej. Tutaj obiekt jest przekazywany jako argument, którego właściwości będą dostępne za pomocą tego obiektu, obiekt, który wywoła ten operator, można uzyskać za pomocą ten operator jak opisano poniżej -

    #włączać używając standardowej przestrzeni nazw; class Box ( public: double getVolume(void) ( zwraca długość * szerokość * wysokość; ) void setLength(double len) ( length = len; ) void setBreadth(double bre) ( width = bre; ) void setHeight(double hei) ( height = hei; ) // Przeciążenie + operator, aby dodać dwa obiekty Box.Operator Box+(const Box& b) ( Box box; box.length = this->length + b.length; box.breadth = this->szerokość + b .breadth; box.height = this->height + b.height; return box; ) private: podwójna długość; // długość pudełka podwójna szerokość; // szerokość pudełka podwójna wysokość; // wysokość pudełka ) ; // Główna funkcja programu int main() ( Box Box1; // Zadeklaruj Box1 typu Box Box Box2; // Zadeklaruj Box2 typu Box Box Box3; // Zadeklaruj Box3 typu Box double volume = 0.0; // Store objętość pudełka tutaj // specyfikacja pudełka 1 Box1.setLength(6.0); Box1.setBreadth(7.0); Box1.setHeight(5.0); // specyfikacja pudełka 2 Box2.setLength(12.0); ;Box2.setHeight(10.0) ); // objętość pudełka 1 objętość = Box1.getVolume(); cout<< "Volume of Box1: " << volume <

    Kiedy powyższy kod jest kompilowany i wykonywany, daje następujące dane wyjściowe:

    Objętość pudełka 1: 210 Objętość pudełka 2: 1560 Objętość pudełka 3: 5400

    Przeciążalność / Nieprzeciążalność Operatorzy

    Poniżej znajduje się lista operatorów, które mogą być przeciążone.