Sisäiset (sulautetut) toiminnot. Toiminnan ylikuormitus



Kuinka saavuttaa toimintojen ylikuormitus C:ssä? (10)

Onko mahdollista saavuttaa toimintojen ylikuormitus C:ssä? Tarkastelen yksinkertaisia ​​toimintoja, jotka voivat olla ylikuormitettuja, kuten

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

Mielestäni ei ole suoraa tietä; Etsin ratkaisuja, jos sellaisia ​​on olemassa.

Toivon, että alla oleva koodi auttaa sinua ymmärtämään toimintojen ylikuormituksen

#sisältää #sisältää int fun(int a, ...); int main(int argc, char *argv)( fun(1,10); fun(2"kysymyspankki"); 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 *)); )

Tarkoitan, tarkoitat - ei, et voi.

Voit määrittää va_arg-funktion muodossa

void my_func(char*-muoto, ...);

Mutta sinun on välitettävä joitakin tietoja muuttujien lukumäärästä ja niiden tyypeistä ensimmäisessä argumentissa - kuten printf() .

Kyllä, kuten.

Tässä annat esimerkin:

void printA(int a)( printf("Hei maailma printA:sta: %d\n",a); ) void printB(const char *buff)( printf("Hei maailma printB:stä: %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)_CHMINKARGS_t(args)>t) if(NUM_ARGS(args) #define print(x , args ...) \ CHECK_ARGS_MIN_LIMIT(1) printf("virhe");fflush(stdout); \ CHECK_ARGS_MAX_LIMIT(4) printf("virhe");fflush(stdout) [ * argv) ( int a=0; print(a); print("hei"); return (EXIT_SUCCESS); )

Se tulostaa 0 ja hello kohdista printA ja printB.

Jos kääntäjäsi on gcc, etkä välitä tehdä manuaalisia päivityksiä aina, kun lisäät uuden ylikuormituksen, voit tehdä makromassan ja saada haluamasi tuloksen soittajan näkökulmasta, ei niin mukavaa kirjoittaa... mutta se on mahdollista

katso __builtin_types_yhteensopiva_p ja käytä sitä makron määrittämiseen, joka tekee jotain

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

mutta joo, ilkeää, ei vain

MUOKATA: C1X saa tuen tyyppilausekkeille, jotka näyttävät tältä:

#define cbrt(X) _Yleinen((X), pitkä double: cbrtl, \ oletus: cbrt, \ float: cbrtf)(X)

Kuten jo todettiin, C ei tue ylikuormitusta siinä mielessä, mitä tarkoitat. Tavallinen idioomi ongelman ratkaisemiseksi on, että funktio ottaa koodatun liiton . Tämä toteutetaan struct-parametrilla, jossa itse rakenne koostuu jonkin tyyppisestä tyyppi-indikaattorista, kuten enum , ja eri arvotyyppien liitosta. Esimerkki:

#sisältää typedef enum ( T_INT, T_FLOAT, T_CHAR, ) my_type; typedef struct ( my_type type; union ( int a; float b; char c; ) my_union; ) my_struct; void set_overload (my_struct *whatever) ( kytkin (mikä tahansa->tyyppi) ( tapaus T_INT: whatever->my_union.a = 1; break; case T_FLOAT: whatever->my_union.b = 2.0; break; case T_CHAR: whatever-> my_union.c = "3"; ) ) void printf_overload (my_struct *whatever) (kytkin (mitä tahansa->tyyppi) ( case T_INT: printf("%d\n", whatever->my_union.a); break; case T_FLOAT : printf("%f\n", whatever->my_union.b); break; case T_CHAR: printf("%c\n", whatever->my_union.c); break; ) ) int main (int argc, char* argv) ( my_struct s; s.type=T_INT; set_overload(&s); printf_overload(&s); s.type=T_FLOAT; set_overload(&s); printf_overload(&s); s.type=T_CHAR; set_overload(&s) ; printf_overload(&s); )

Etkö voi vain käyttää C++:aa etkä käyttää kaikkia muita C++-ominaisuuksia tämän lisäksi?

Jos tiukkaa C:tä ei ole toistaiseksi ollut, suosittelen sen sijaan variadic-funktioita.

Seuraava lähestymistapa on samanlainen kuin a2800276, mutta joissain C99-makroissa:

// tarvitsemme `size_t` #include // argumenttityypit hyväksymään enum sum_arg_types ( SUM_LONG, SUM_ULONG, SUM_DOUBLE ); // rakenne, joka sisältää argumentin struct sum_arg ( enum sum_arg_types type; union ( long as_long; unsigned long as_ulong; double as_double; ) arvo; ); // määrittää taulukon koon #define count(ARRAY) ((koko (ARRAY))/(koko *(ARRAY))) // näin funktiotamme kutsutaan #define sum(...) _sum( count(sum_args(__VA_ARGS__)), sum_args(__VA_ARGS__)) // luo taulukko `struct sum_arg` #define sum_args(...) ((struct sum_arg )( __VA_ARGS__ )) // luo alustajat sum_long argumenteille #define (ARVO) ( SUMMA_PITKÄ, ( .as_long = (ARVO) ) ) #define sum_ulong(ARVO) ( SUM_ULONG, ( .as_ulong = (ARVO) ) ) #define sum_double(ARVO) ( SUMMA_KAKSININEN, ( .as_double = (ARVO) ) ) // polymorfinen funktiomme 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() ( etumerkitön pitkä foo = -1; pitkä kaksoisarvo = summa(summa_pitkä(42), sum_ulong(foo), summa_double(1e10)); printf("%Le\n", arvo); return 0; )

Toistaiseksi _Yleinen, koska kysymyksen _Yleinen, standardi C (ei laajennuksia) on tehokkaasti sain tuki toimintojen ylikuormitukselle (operaattoreiden sijaan), koska C11:ssä on lisätty sana _Generic _Generic. (tuettu GCC:ssä versiosta 4.9 lähtien)

(Ylikuormitus ei todellakaan ole "sisäänrakennettu" kysymyksessä esitetyllä tavalla, mutta näin toimivaa on helppo tuhota.)

Generic on käännösaikaoperaattori samassa perheessä sizeof:n ja _Alignof:n kanssa. Se on kuvattu standardin kohdassa 6.5.1.1. Se vaatii kaksi pääparametria: lausekkeen (jota ei arvioida ajon aikana) ja luettelon tyyppi-/lausekeassosiaatioista, joka on vähän kuin kytkinlohko. _Generic hakee lausekkeen yleisen tyypin ja sitten "vaihtaa" siihen valitakseen tyypilleen lopputuloksen lausekkeen luettelosta:

Generic(1, float: 2.0, char *: "2", int: 2, oletus: get_two_object());

Yllä oleva lauseke laskee arvoksi 2 - ohjauslausekkeen tyyppi on int , joten se valitsee arvoksi lausekkeen, joka liittyy lausekkeeseen int. Mikään näistä ei jää ajon aikana. (Oletuslauseke on pakollinen: jos et määritä sitä ja tyyppi ei täsmää, se aiheuttaa käännösvirheen.)

Tekniikka, joka on hyödyllinen funktion ylikuormituksessa, on se, että C-esiprosessori voi lisätä sen ja valita tuloslausekkeen ohjaavalle makrolle välitettävien argumenttien tyypin perusteella. Joten (esimerkki C-standardista):

#define cbrt(X) _Yleinen((X), \ long double: cbrtl, \ oletus: cbrt, \ float: cbrtf \)(X)

Tämä makro toteuttaa cbrt overloaded -toiminnon välittämällä argumentin tyypin makrolle, valitsemalla sopivan toteutusfunktion ja välittämällä sitten alkuperäisen makron kyseiselle funktiolle.

Joten toteuttaaksemme alkuperäisen esimerkkisi voisimme tehdä näin:

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(() FIRST(__VA_ARGS__,)), \int: foo_float_int))(_1, __VA_ARGS__) #define FIRST(A, ...) A

Tässä tapauksessa voisimme käyttää oletusarvoa: sitova kolmannelle tapaukselle, mutta se ei osoita, kuinka periaatetta voidaan laajentaa useisiin argumentteihin. Lopputuloksena on, että voit käyttää koodissasi koodia foo(...) murehtimatta (paljon) argumenttien tyypistä.

Monimutkaisemmissa tilanteissa, kuten funktioissa, jotka ylikuormittavat enemmän argumentteja tai vaihtelevia lukuja, voit käyttää apuohjelmamakroja luomaan automaattisesti staattisia lähetysrakenteita:

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("tuntemattomat argumentit\n"); ) #define print(...) OVERLOAD(print, (__VA_ARGS__), \ (print_ii, (int, int)), \ (print_di, (double, int)), \ (print_iii, (int, int, int)) \) #define OVERLOAD_ARG_TYPES (int, double) #define OVERLOAD_FUNCTIONS (tulosta) #include "activate-overloads.h" int main(void) ( print(44, 47); // tulostaa "int, int" print(4.4, 47); // tulostaa "double, int" print (1, 2, 3); // tulostaa "int, int, int" print(""); // tulostaa "tuntemattomat argumentit" )

(toteutus tästä). Pienellä vaivalla voit pienentää kattilalevyn näyttämään melko samanlaiselta kuin kieli, jossa on sisäänrakennettu ylikuormitustuki.

Sitä paitsi ylikuormittaminen oli jo mahdollista määrä argumentit (eikä tyyppi) C99:ssä.

Huomaa, että tapa, jolla C arvioidaan, voi liikuttaa sinua. Tämä valitsee foo_int, jos yrität välittää sille esimerkiksi literaalimerkin, ja tarvitset foo_int, jos haluat ylikuormituksen tukevan merkkijonoliteraaaleja. Kuitenkin kaiken kaikkiaan aika siistiä.

Leushenkon vastaus on todella siisti: vain esimerkki foo ei käänny GCC:llä, joka epäonnistuu foo(7) :ssä, osuu ENSIMMÄISEN makroon ja varsinaiseen funktiokutsuun ((_1, __VA_ARGS__) , jää ylimääräiseen pilkkuun. kohtaa ongelmia, jos haluamme tarjota ylimääräisiä ylikuormituksia, kuten foo(double).

Joten päätin vastata tähän kysymykseen yksityiskohtaisemmin, mukaan lukien tyhjän ylikuormituksen salliminen (foo(void) - mikä aiheutti ongelmia...).

Ajatuksena nyt on: määrittele useampi kuin yksi yleinen eri makroissa ja anna oikea valita argumenttien lukumäärän mukaan!

Argumenttien määrä on melko yksinkertainen tämän vastauksen perusteella:

#define foo(...) SELECT(__VA_ARGS__)(__VA_ARGS__) #define SELECT(...) CONCAT(SELECT_, NARG(__VA_ARGS__))(__VA_ARGS__) #define CONCAT(X, Y) CONCAT_(X, Y) # määrittele CONCAT_(X, Y) X ## Y

Se on hyvä, olemme päättämässä joko SELECT_1 tai SELECT_2 (tai useampi argumentti, jos haluat/tarvitset niitä), joten tarvitsemme vain asianmukaiset määritelmät:

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

Ensinnäkin tyhjä makrokutsu (foo()) luo silti tunnuksen, mutta se on tyhjä. Joten laskentamakro itse asiassa palauttaa 1:n 0:n sijaan, vaikka makroa kutsutaan tyhjäksi. Voimme "helposti" korjata tämän ongelman, jos merkitsemme __VA_ARGS__ pilkulla kohdan __VA_ARGS__ jälkeen ehdollisesti, riippuen siitä, onko luettelo tyhjä vai ei:

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

Tämä katsoin helppoa, mutta COMMA-makro on melko raskas; onneksi tätä aihetta käsitellään jo Jens Gustedtin blogissa (kiitos, Jens). Päätemppu on, että funktiomakrot eivät laajene, ellei niitä seuraa sulkeita, katso Jensin blogista lisäselvityksiä... Meidän täytyy vain muokata makroja hieman tarpeidemme mukaan (käytän lyhyempiä nimiä ja vähemmän argumentteja lyhyyden vuoksi) .

#define ARGN(...) ARGN_(__VA_ARGS__) #define ARGN_(_0, _1, _2, _3, N, ...) N #define HAS_COMMA(...) ARGN(__VA_ARGS__, 1, 1, 1, 0 ) #define SET_COMMA(...) , #define COMMA(...) SELECT_COMMA \ (\ HAS_COMMA(__VA_ARGS__), \ HAS_COMMA(__VA_ARGS__ ()), \ HAS_COMMA(SET_COMMA __VA_ARGS__), \ HAS_COMMA(_)_(_VA_COMMA) \) #define SELECT_COMMA(_0, _1, _2, _3) SELECT_COMMA_(_0, _1, _2, _3) #define SELECT_COMMA_(_0, _1, _2, _3) COMMA_ ## _0 ## _1 ## _3 ## define COMMA_0000 , #define COMMA_0001 #define COMMA_0010 , // ... (kaikki muut pilkulla) #define COMMA_1111 ,

Ja nyt meillä on kaikki hyvin...

Koko koodi yhdessä lohkossa:

/* * demo.c * * Luotu: 14.9.2017 * Kirjoittaja: 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) _Yleinen((_1), \ double: _Yleinen((_2), \ int: foo_double_int \) \) #define ARGN(...) ARGN_( __VA_ARGS__) #määrittää ARGN_(_0, _1, _2, N, ...) N #määrittää NARG(...) ARGN(__VA_ARGS__ COMMA(__VA_ARGS__) 3, 2, 1, 0) #määrittää HAS_COMMA(...) ARGN(__VA_ARGS__, 1, 1, 0) #define SET_COMMA(...) , #define COMMA(...) 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) #MA_1, SELECT_COM COMMA_ ## _0 ## _1 ## _2 ## _3 COMMA_1001 , #define COMMA_1010 , #define COMMA_1011 , #define COMMA_1100 , #define COMMA_1101 , #define COMMA_1101 , #define COMMA_1101 , #define COMMA_1110 in** COMMA_11110, COMMA_1110 (foo(); foo(7); foo(10.12); foo(12.10; 7); foo((merkki)"s"); paluu 0; )

Joten tiedämme jo kuinka julistaa, määritellä ja käyttää funktioita ohjelmissa. Tässä luvussa puhumme niiden erikoismuodosta - ylikuormitetuista toiminnoista. Kahta funktiota kutsutaan ylikuormitukseksi, jos niillä on sama nimi, ne on ilmoitettu samassa laajuudessa, mutta niillä on erilaiset muodollisten parametrien luettelot. Selitämme, kuinka tällaiset funktiot ilmoitetaan ja miksi ne ovat hyödyllisiä. Sitten pohditaan kysymystä niiden ratkaisusta, ts. mitä useista ylikuormitetuista funktioista kutsutaan ohjelman suorituksen aikana. Tämä ongelma on yksi vaikeimmista C++:ssa. Niiden, jotka haluavat perehtyä yksityiskohtiin, on mielenkiintoista lukea luvun lopussa olevat kaksi osiota, jotka käsittelevät argumenttityypin muuntamista ja ylikuormituksen ratkaisua yksityiskohtaisemmin.

9.1. Ylikuormitetut funktioilmoitukset

Nyt, kun olemme oppineet ilmoittamaan, määrittelemään ja käyttämään toimintoja ohjelmissa, tutustumme niihin ylikuormitus on toinen näkökohta C++:ssa. Ylikuormituksen avulla voit käyttää useita samannimiä toimintoja, jotka suorittavat samanlaisia ​​toimintoja erityyppisille argumenteille.
Olet jo hyödyntänyt ennalta määritettyä ylikuormitustoimintoa. Esimerkiksi lausekkeen arvioimiseksi

Kokonaisluvun yhteenlaskuoperaatiota kutsutaan, kun taas lausekkeen arviointi

1.0 + 3.0

suorittaa liukulukujen lisäyksen. Tämän tai toisen toiminnon valinta tehdään käyttäjälle huomaamattomasti. Summausoperaattori on ylikuormitettu erityyppisten operandien mukauttamiseksi. Kääntäjän, ei ohjelmoijan, vastuulla on tunnistaa konteksti ja soveltaa operandityypeille sopivaa toimintoa.
Tässä luvussa näytämme, kuinka voit määrittää omat ylikuormitetut toiminnot.

9.1.1. Miksi funktion nimi täytyy ylikuormittaa

Kuten sisäänrakennetussa lisäystoiminnossa, saatamme tarvita joukon toimintoja, jotka suorittavat saman toiminnon, mutta erityyppisillä parametreilla. Oletetaan, että haluamme määritellä funktioita, jotka palauttavat suurimman syötetyistä parametriarvoista. Jos ylikuormitusta ei olisi, jokaiselle tällaiselle funktiolle olisi annettava yksilöllinen nimi. Esimerkiksi max()-funktioperhe voi näyttää tältä:

int i_max(int, int); int vi_max(vakiovektori &); int matrix_max(const matriisi &);

Ne kaikki tekevät kuitenkin saman asian: ne palauttavat suurimman parametriarvoista. Käyttäjän näkökulmasta tässä on vain yksi operaatio - maksimin laskeminen ja sen toteuttamisen yksityiskohdat eivät kiinnosta.
Huomattu leksikaalinen monimutkaisuus kuvastaa ohjelmointiympäristön rajoitusta: minkä tahansa samassa laajuudessa esiintyvän nimen on viitattava ainutlaatuiseen kokonaisuuteen (objekti, funktio, luokka jne.). Tällainen rajoitus aiheuttaa käytännössä tiettyjä haittoja, koska ohjelmoijan täytyy muistaa tai jollakin tavalla löytää kaikki nimet. Toimintojen ylikuormitus auttaa tähän ongelmaan.
Ylikuormitusta käyttämällä ohjelmoija saattaa kirjoittaa jotain tällaista:

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

Tämä lähestymistapa osoittautuu erittäin hyödylliseksi monissa tilanteissa.

9.1.2. Kuinka ylikuormittaa funktion nimi

C++:ssa kahdelle tai useammalle funktiolle voidaan antaa sama nimi, jos niiden parametriluettelot eroavat joko parametrien lukumäärän tai tyypin osalta. Tässä esimerkissä ilmoitamme ylikuormitetun max()-funktion:

intmax(int, int); int max(vakiovektori &); int max(const matriisi &);

Jokainen ylikuormitettu ilmoitus vaatii erillisen max()-funktion määritelmän sopivalla parametriluettelolla.
Jos funktion nimi ilmoitetaan useammin kuin kerran jossain laajuudessa, kääntäjä tulkitsee toisen (ja sen jälkeisen) ilmoituksen seuraavasti:

  • jos kahden funktion parametriluettelot eroavat parametrien lukumäärän tai tyypin osalta, funktioita pidetään ylikuormitettuina: // ylikuormitetut funktiot void print(const string &); void print (vektori &);
  • jos palautustyyppi ja parametriluettelot kahden funktion ilmoituksissa ovat samat, niin toinen ilmoitus katsotaan toistuvaksi: // saman funktion ilmoitukset void print(const string &str); void print(const string &); Parametrien nimiä ei oteta huomioon ilmoituksia verrattaessa;
    jos kahden funktion parametriluettelot ovat samat, mutta palautustyypit ovat erilaisia, toista ilmoitusta pidetään virheellisenä (epäyhdenmukaisena ensimmäisen kanssa) ja kääntäjä merkitsee sen virheeksi: unsigned int max(int ​​​​i1, int i2); int max(int ​​i1, int i2);
    // virhe: vain tyypit eroavat
    // palauttaa arvot

Ylikuormitetut funktiot eivät voi erota vain palautustyypeillään; jos kahden funktion parametriluettelot eroavat vain oletusargumenttiarvoistaan, toista ilmoitusta pidetään toistuvana:

// saman funktion ilmoitukset int max (int *ia, int sz); int max (int *ia, int = 10);

Typedef-avainsana luo vaihtoehtoisen nimen olemassa olevalle tietotyypille; se ei luo uutta tyyppiä. Siksi, jos kahden funktion parametriluettelot eroavat toisistaan ​​vain siinä, että toinen käyttää typedef-funktiota ja toinen tyyppiä, jolle typedef on alias, luetteloita pidetään samoina, kuten kahdessa seuraavassa calc()-funktion ilmoituksessa. Tässä tapauksessa toinen ilmoitus antaa käännösvirheen, koska palautusarvo on eri kuin aiemmin määritetty:

// typedef ei ota käyttöön uutta typedef-tyyppiä double DOLLAR; // virhe: samat parametriluettelot, mutta eri // paluutyypit extern DOLLAR calc(DOLLAR); extern int calc(double);

Const- tai volatile-määritteitä ei oteta huomioon tällaisessa vertailussa. Näin ollen seuraavia kahta ilmoitusta pidetään samana:

// julistaa sama funktio void f(int); void f(const int);

Const-määritelmä on tärkeä vain funktion määritelmän sisällä: se osoittaa, että parametrin arvon muuttaminen funktion rungossa on kielletty. Arvon välittämää argumenttia voidaan kuitenkin käyttää funktion rungossa tavallisen laukaistun muuttujan tavoin: funktion ulkopuolella muutokset eivät ole näkyvissä. (Argumenttien välitysmenetelmiä, erityisesti arvon ohittamista, käsitellään luvussa 7.3.) Const-määritteen lisääminen arvon välittämään parametriin ei vaikuta sen tulkintaan. Mikä tahansa int-tyyppinen arvo voidaan välittää funktiolle, joka on ilmoitettu muodossa f(int), samoin kuin funktio f(const int). Koska ne molemmat ottavat saman joukon argumenttiarvoja, yllä olevia ilmoituksia ei pidetä ylikuormitettuina. f() voidaan määritellä muodossa

Tyhjä f(int i) ( )

Void f(const int i) ( )

Näiden kahden määritelmän esiintyminen yhdessä ohjelmassa on virhe, koska sama funktio määritellään kahdesti.
Jos osoittimen tai viitetyypin parametriin kuitenkin sovelletaan const- tai volatile-spesifikaattoria, se otetaan huomioon ilmoitusten vertailussa.

// eri funktiot julistetaan void f(int*); void f(const int*); // ja eri funktiot on ilmoitettu tässä
void f(int&);
void f(const int&);

9.1.3. Milloin funktion nimeä ei saa ylikuormittaa

Missä tapauksissa nimen ylikuormitus ei ole hyödyllistä? Esimerkiksi erilaisten nimien antaminen funktioille helpottaa ohjelman lukemista. Tässä on joitain esimerkkejä. Seuraavat toiminnot toimivat samalla abstraktilla päivämäärätyypillä. Päällisin puolin ne ovat hyviä ehdokkaita ylikuormitukseen:

void setDate(Päivämäärä&, int, int, int); Päivämäärä &convertDate(const string &); void printDate(const Date&);

Nämä toiminnot toimivat samalla tietotyypillä, Date-luokalla, mutta suorittavat semanttisesti erilaisia ​​toimintoja. Tässä tapauksessa erilaisten nimien käyttöön liittyvä leksikaalinen monimutkaisuus johtuu ohjelmoijan omaksumasta käytännöstä tarjota tietotyypille operaatioita ja nimetä toimintoja näiden operaatioiden semantiikan mukaisesti. Totta, C++-luokan mekanismi tekee tällaisesta käytännöstä tarpeettoman. Tällaiset funktiot tulisi tehdä Date-luokan jäseniksi, mutta jättää erilaiset nimet, jotka kuvastavat toiminnon merkitystä:

#sisältää class Päivämäärä ( public: set(int, int, int); Date& convert(const string &); void print(); // ...
};

Otetaan toinen esimerkki. Seuraavat viisi Screen-jäsentoimintoa suorittavat erilaisia ​​toimintoja näytön kohdistimelle, joka on saman luokan jäsen. Saattaa tuntua järkevältä ylikuormittaa nämä funktiot yleisnimellä move():

Screen& moveHome(); Screen& moveAbs(int, int); Screen& moveRel(int, int, char *suunta); Screen& moveX(int); Screen& moveY(int);

Kahta viimeistä toimintoa ei kuitenkaan voi ylikuormittaa, koska niillä on samat parametriluettelot. Jotta allekirjoitus olisi ainutlaatuinen, yhdistetään ne yhdeksi funktioksi:

// funktio, joka yhdistää liikkuaX() ja moveY() Screen& move(int, char xy);

Nyt kaikilla funktioilla on eri parametrilistat, joten ne voidaan ylikuormittaa nimellä move(). Näin ei kuitenkaan pidä tehdä: eri nimet sisältävät tietoa, jota ilman ohjelmaa on vaikeampi ymmärtää. Esimerkiksi näiden toimintojen suorittamat kohdistimen liiketoiminnot ovat erilaisia. Esimerkiksi moveHome() suorittaa erityisen siirron näytön vasempaan yläkulmaan. Kumpi alla olevista kahdesta kutsusta on käyttäjäystävällisempi ja helpompi muistaa?

// kumpi kutsu on selkeämpi? myScreen.home(); // Ajattelemme tätä! myScreen.move();

Joissakin tapauksissa sinun ei tarvitse ylikuormittaa funktion nimeä tai antaa eri nimiä: oletusargumenttiarvojen avulla voit yhdistää useita toimintoja yhdeksi. Esimerkiksi kohdistimen ohjaustoiminnot

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

eroavat kolmannen char*-tyypin parametrin läsnäolosta. Jos niiden toteutukset ovat samanlaisia ​​ja kolmannelle argumentille löytyy kohtuullinen oletusarvo, molemmat funktiot voidaan korvata yhdellä. Tässä tapauksessa osoitin, jonka arvo on 0, sopii oletusarvon rooliin:

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

Sinun tulee käyttää tiettyjä ominaisuuksia, kun sovelluslogiikka sitä vaatii. Ylikuormitettuja toimintoja ei tarvitse sisällyttää ohjelmaan vain siksi, että ne ovat olemassa.

9.1.4. Ylikuormitus ja laajuus A

Kaikki ylikuormitetut toiminnot ilmoitetaan samassa laajuudessa. Esimerkiksi paikallisesti ilmoitettu funktio ei ylikuormita, vaan yksinkertaisesti piilottaa globaalin:

#sisältää void print(const string &); voidprint (kaksois); // ylikuormittaa print() void fooBar(int ival)
{
// erillinen laajuus: piilottaa molemmat print()-toteutukset
extern void print(int); // virhe: print(const string &) ei näy tällä alueella
print("Arvo: ");
tulostaa (ival); // oikein: print(int) on näkyvissä
}

Koska jokainen luokka määrittelee oman laajuutensa, funktiot, jotka ovat kahden eri luokan jäseniä, eivät ylikuormita toisiaan. (Luokan jäsenfunktioita käsitellään luvussa 13. Luokan jäsentoimintojen ylikuormituksen ratkaisu käsitellään luvussa 15.)
Tällaisten funktioiden ilmoittaminen nimiavaruudessa on myös sallittua. Jokaiseen niistä on myös liitetty oma soveltamisalansa, jotta eri laajuuksiin ilmoitetut toiminnot eivät ylikuormittaisi toisiaan. Esimerkiksi:

#sisältää nimitila IBM ( extern void print(const string &); extern void print(double); // overloads print() ) nimiavaruus Disney ( // erillinen laajuus: // ei ylikuormita print()-funktiota IBM:n nimiavaruudesta extern void tulosta (int); )

Deklaraatioiden ja ohjeiden käyttö auttaa saamaan nimitilan jäseniä saataville muissa laajuuksissa. Näillä mekanismeilla on jonkin verran vaikutusta ylikuormitettujen toimintojen ilmoittamiseen. (Deklaraatioiden käyttöä ja käskyjen käyttöä käsiteltiin kohdassa 8.6.)

Miten ilmoituksen käyttö vaikuttaa toimintojen ylikuormitukseen? Muista, että se lisää aliaksen nimiavaruuden jäsenelle alueella, jossa ilmoitus esiintyy. Mitä tällaiset ilmoitukset tekevät seuraavassa ohjelmassa?

Nimiavaruus libs_R_us ( int max(int, int); int max(double, double); extern void print(int);
extern void print (double);
) // käyttämällä-ilmoituksia
käyttäen libs_R_us::max;
käyttäen libs_R_us::print(double); // virhe void func()
{
max(87, 65); // kutsuu libs_R_us::max(int, int)
max(35,5; 76,6); // kutsuu libs_R_us::max(double, double)

Ensimmäinen käyttävä määritys tuo molemmat libs_R_us::max-funktiot globaaliin laajuuteen. Nyt mitä tahansa max()-funktioista voidaan kutsua func() sisällä. Argumenttien tyypit määräävät, mitä funktiota kutsutaan. Toinen käyttöilmoitus on virhe: sillä ei voi olla parametriluetteloa. Funktio libs_R_us::print() määritellään vain seuraavasti:

Käyttämällä libs_R_us::print;

Use-ilmoitus antaa aina kaikki ylikuormitetut funktiot saataville määritetyllä nimellä. Tämä rajoitus varmistaa, että nimitilan libs_R_us käyttöliittymää ei rikota. On selvää, että puhelun tapauksessa

Tulosta(88);

nimitilan kirjoittaja odottaa libs_R_us::print(int)-funktion kutsuvan. Jos annat käyttäjän sisällyttää valikoivasti vain yhden useista ylikuormitetuista funktioista soveltamisalaan, ohjelman käyttäytyminen muuttuu arvaamattomaksi.
Mitä tapahtuu, jos use-ilmoitus tuo piiriin funktion, jolla on jo olemassa oleva nimi? Nämä funktiot näyttävät olevan ilmoitettu juuri siellä, missä use-ilmoitus tapahtuu. Siksi käyttöönotetut toiminnot osallistuvat kaikkien annetussa laajuudessa olevien ylikuormitettujen toimintojen nimien selvittämiseen:

#sisältää nimiavaruus libs_R_us ( extern void print(int); extern void print(double); ) extern void print(const string &); // libs_R_us::print(int) ja libs_R_us::print(double)
// overload print(const string &)
käyttäen libs_R_us::print; void fooBar (in ival)
{
// print(const string &)
}

Use-ilmoitus lisää kaksi ilmoitusta yleiseen laajuuteen: yksi print(int) ja yksi print(double). Ne ovat aliaksia libs_R_us-tilassa ja sisältyvät ylikuormitettujen funktioiden joukkoon print, jossa globaali print(const string &) on jo olemassa. Kun fooBarin tulostusylikuormitusta ratkaistaan, kaikki kolme toimintoa otetaan huomioon.
Jos use-lause lisää jonkin funktion soveltamisalaan, jolla on jo samanniminen funktio ja sama parametriluettelo, se katsotaan virheeksi. Use-declaration ei voi aliasa print(int)-funktiota nimiavaruudessa libs_R_us, jos print(int) on jo olemassa globaalissa laajuudessa. Esimerkiksi:

Nimiavaruus libs_R_us ( void print(int); void print(double); ) void print(int); käyttäen libs_R_us::print; // virhe: toistuva ilmoitus print(int) void fooBar(int ival)
{
tulostaa (ival); // mikä printti? ::print tai libs_R_us::print
}

Olemme osoittaneet, kuinka käyttö-ilmoitukset ja ylikuormitetut funktiot liittyvät toisiinsa. Katsotaanpa nyt use-direktiivin käytön yksityiskohtia. Käyttöohje saa nimiavaruuden jäsenet näkymään kyseisen tilan ulkopuolella ja lisää ne uuteen soveltamisalaan. Jos tässä laajuudessa on jo samanniminen funktio, tapahtuu ylikuormitus. Esimerkiksi:

#sisältää nimiavaruus libs_R_us ( extern void print(int); extern void print(double); ) extern void print(const string &); // käyttämällä ohjetta
// print(int), print(double) ja print(const string &) ovat elementtejä
// sama joukko ylikuormitettuja toimintoja
käyttäen nimiavaruutta libs_R_us; void fooBar (in ival)
{
print("Arvo: "); // kutsuu globaalia funktiota
// print(const string &)
tulostaa (ival); // kutsuu libs_R_us::print(int)
}

Tämä pätee myös silloin, kun direktiivejä on useita. Samannimiset funktiot, jotka ovat eri tilojen jäseniä, sisältyvät samaan joukkoon:

Nimiavaruus IBM ( int print(int); ) nimiavaruus Disney ( double print (double); ) // käyttämällä-direktiiveä // luo useita ylikuormitettuja toimintoja eri // nimiavaruuksista käyttämällä nimiavaruutta IBM; käyttämällä nimiavaruutta Disney; pitkä kaksoispainatus (pitkä kaksoispainatus); int main() (
tulostaa (1); // nimeltään IBM::print(int)
tulostaa (3.1); // soita Disneylle::print(double)
paluu 0;
}

Ylikuormitettujen funktioiden joukko, jonka nimi on print maailmanlaajuisessa laajuudessa, sisältää print(int), print(double) ja print(long double) -funktiot. Kaikki ne huomioidaan main():ssa ylikuormitusratkaisun aikana, vaikka ne alun perin määriteltiin eri nimiavaruuksiin.
Joten jälleen ylikuormitetut toiminnot ovat samassa laajuudessa. Etenkin ne päätyvät sinne käyttämällä ilmoituksia ja direktiivejä, jotka tuovat muiden ulottuvuuksien nimiä saataville.

9.1.5. ulkoinen "C"-direktiivi ja ylikuormitetut A-funktiot

Kohdassa 7.7 näimme, että ulkoista "C"-sidosdirektiiviä voidaan käyttää C++-ohjelmassa osoittamaan, että jokin olio on C-osassa. Miten tämä direktiivi vaikuttaa ylikuormitettuihin funktiomäärittelyihin? Voivatko sekä C++:ssa että C:ssä kirjoitetut funktiot olla samassa joukossa?
Sidosdirektiivi saa määrittää vain yhden monista ylikuormitetuista funktioista. Esimerkiksi seuraava ohjelma on virheellinen:

// virhe: direktiivi määritetty kahdelle ylikuormitetulle funktiolle extern "C" extern "C" void print(const char*); extern "C" void print(int);

Seuraava esimerkki ylikuormitetusta calc()-funktiosta havainnollistaa ulkoisen "C"-direktiivin tyypillistä käyttöä:

ClassSmallInt(/* ... */); luokka BigNum(/* ... */); // C-kielellä kirjoitettu funktio voidaan kutsua sekä ohjelmasta,
// kirjoitettu C:llä tai C++:lla kirjoitetusta ohjelmasta.
// C++-funktiot käsittelevät parametreja, jotka ovat luokkia
extern "C" double calc(double);
extern SmallInt calc(const SmallInt&);
extern BigNum calc(const BigNum&);

C:ssä kirjoitettua calc()-funktiota voidaan kutsua sekä C:stä että C++-ohjelmasta. Kaksi muuta funktiota ottavat luokan parametrina ja siksi niitä voidaan käyttää vain C++-ohjelmassa. Ilmoitusten järjestyksellä ei ole merkitystä.
Sidosdirektiivillä ei ole merkitystä päätettäessä, mitä toimintoa kutsutaan; vain parametrityypit ovat tärkeitä. Valitaan funktio, joka parhaiten vastaa välitettyjen argumenttien tyyppejä:

Smallint si = 8; int main() ( calc(34); // kutsu C-funktio calc(double) calc(si); // kutsu C++-funktio calc(const SmallInt &) // ... return 0; )

9.1.6. Osoittimet ylikuormitettuihin toimintoihin A

Voit ilmoittaa osoittimen yhteen monista ylikuormitetuista funktioista. Esimerkiksi:

ulkoinen void ff(vektori ); extern void ff(allekirjoittamaton int); // mihin funktioon pf1 viittaa?
void (*pf1)(signed int) =

Koska ff()-funktio on ylikuormitettu, pelkkä &ff-alustaja ei riitä oikean vaihtoehdon valitsemiseen. Ymmärtääkseen, mikä funktio alustaa osoittimen, kääntäjä etsii ylikuormitettujen funktioiden joukosta sellaisen, jolla on sama palautustyyppi ja parametriluettelo kuin funktiolla, johon osoitin viittaa. Tässä tapauksessa ff(unsigned int) -funktio valitaan.
Mutta entä jos osoittimen tyyppiä täsmälleen vastaavaa toimintoa ei ole? Sitten kääntäjä antaa virheilmoituksen:

ulkoinen void ff(vektori ); extern void ff(allekirjoittamaton int); // virhe: hakua ei löydy: virheellinen parametriluettelo void (*pf2)(int) = // virhe: vastaavuutta ei löydy: väärä paluutyyppi double (*pf3)(vektori ) = &ff;

Tehtävä toimii samalla tavalla. Jos osoittimen arvo on ylikuormitetun funktion osoite, niin funktion osoittimen tyyppiä käytetään operandin valintaan osoitusoperaattorin oikealla puolella. Ja jos kääntäjä ei löydä funktiota, joka täsmälleen vastaa haluttua tyyppiä, se antaa virheilmoituksen. Siten toimintoosoittimien välistä tyyppimuunnosa ei koskaan suoriteta.

Matriisi calc(const matriisi &); intcalc(int, int); int (*pc1)(int, int) = 0;
int (*pc2)(int, double) = 0; //...
// oikein: funktio calc(int, int) on valittu
pc1 = // virhe: ei vastaavuutta: virheellinen toinen parametrityyppi
PC2=

9.1.7. Suojattu sidonta A

Ylikuormitusta käytettäessä saa vaikutelman, että ohjelmassa voi olla useita samannimistä funktiota eri parametrilistoilla. Tämä sananmukaisuus on kuitenkin olemassa vain lähdetekstitasolla. Useimmissa käännösjärjestelmissä ohjelmat, jotka käsittelevät tätä tekstiä suoritettavan koodin tuottamiseksi, edellyttävät, että kaikki nimet ovat erillisiä. Linkkien editorit ratkaisevat yleensä ulkoiset linkit sanallisesti. Jos tällainen editori kohtaa nimen print kaksi tai useammin, se ei voi erottaa niitä tyyppianalyysin avulla (tyyppitiedot yleensä menetetään tässä vaiheessa). Joten se vain tulostaa viestin uudelleen määritellystä merkkitulostuksesta ja poistuu.
Tämän ongelman ratkaisemiseksi funktion nimi ja sen parametriluettelo koristellaan antamaan yksilöllinen sisäinen nimi. Kääntäjän jälkeen kutsutut ohjelmat näkevät vain tämän sisäisen nimen. Se, kuinka tämä nimenratkaisu tarkalleen tehdään, riippuu toteutuksesta. Yleisenä ideana on esittää parametrien lukumäärä ja tyypit merkkijonona ja liittää se funktion nimeen.
Kuten kohdassa 8.2 mainittiin, tällainen koodaus takaa erityisesti sen, että kaksi samannimistä funktion ilmoitusta eri tiedostoissa eri parametriluetteloilla ei tule linkkijälle saman funktion ilmoituksiksi. Koska tämä menetelmä auttaa erottamaan ylikuormitetut toiminnot linkin muokkausvaiheessa, puhumme turvallisesta linkityksestä.
Nimen koristelu ei koske funktioita, jotka on ilmoitettu ulkoisella "C"-direktiivillä, koska vain yksi monista ylikuormitetuista funktioista voidaan kirjoittaa puhtaalla C:llä. Linkkeri käsittelee kahta funktiota, joilla on eri parametrilistat, jotka on ilmoitettu ulkoisiksi "C"-teksteiksi. ja sama hahmo.

Harjoitus 9.1

Miksi ylikuormitetut toiminnot pitäisi ilmoittaa?

Harjoitus 9.2

Kuinka ilmoittaa virhe()-funktion ylikuormitetut versiot, jotta seuraavat kutsut ovat oikein:

Int-indeksi; int yläraja; char selectVal; // ... error("Matriisi rajojen ulkopuolella: ", indeksi, yläraja); error("Jako nollalla"); error("Virheellinen valinta", selectVal);

Harjoitus 9.3

Selitä toisen ilmoituksen vaikutus jokaisessa seuraavista esimerkeistä:

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

Harjoitus 9.4

Mikä seuraavista alustuksista aiheuttaa virheen? Miksi?

(a) void reset(int *); void (*pf)(void *) = nollaa; (b) intcalc(int, int); int (*pf1)(int, int) = laskenta; (c) ulkoinen "C" int compute(int *, int); int (*pf3)(int*, int) = laske; (d) void (*pf4)(vakiomatriisi &) = 0;

9.2. Kolme vaihetta ylikuormitusresoluutiolla

Toimintojen ylikuormituksen resoluutio kutsutaan prosessiksi valita funktio joukosta ylikuormitettuja, joka pitäisi kutsua. Tämä prosessi perustuu kutsuttaessa määritettyihin argumentteihin. Harkitse esimerkkiä:

T t1, t2; void f(int, int); void f(kelluke, kellua); int main() (
f(t1, t2);
paluu 0;
}

Tässä ylikuormituksen ratkaisuprosessin aikana määritetään T:n tyypistä riippuen, kutsutaanko funktiota f(int,int) vai f(float, float) käsiteltäessä lauseketta f(t1,t2) vai virhe tallennetaan.
Toimintojen ylikuormitusresoluutio on yksi C++-kielen monimutkaisimmista osista. Yrittäessään ymmärtää kaikki yksityiskohdat aloittelevat ohjelmoijat kohtaavat vakavia vaikeuksia. Siksi tässä osiossa annamme vain lyhyen yleiskatsauksen ylikuormitusratkaisun toiminnasta, jotta saat ainakin jonkinlaisen kuvan tästä prosessista. Niille, jotka haluavat tietää enemmän, seuraavat kaksi osiota tarjoavat yksityiskohtaisemman kuvauksen.
Toimintojen ylikuormituksen selvitysprosessi koostuu kolmesta vaiheesta, jotka näytämme seuraavassa esimerkissä:

void f(); void f(int); void f(double, double = 3,4); void f(merkki *, merkki *); void main() (
f(5,6);
paluu 0;
}

Kun toimintojen ylikuormitus ratkaistaan, suoritetaan seuraavat vaiheet:

  1. Tietyn kutsun ylikuormitettujen funktioiden joukko sekä funktiolle välitettyjen argumenttien luettelon ominaisuudet korostetaan.
  2. Ylikuormitetuista funktioista valitaan ne, joita voidaan kutsua annetuilla argumenteilla, ottaen huomioon niiden lukumäärä ja tyypit.
  3. Puhelua parhaiten vastaava toiminto löydetään.

Tarkastellaan jokaista kohdetta vuorotellen.
Ensimmäinen vaihe on tunnistaa ylikuormitettuja toimintoja, jotka otetaan huomioon tässä kutsussa. Tähän joukkoon sisältyviä toimintoja kutsutaan ehdokkaiksi. Ehdokasfunktio on funktio, jolla on sama nimi kuin kutsutulla, ja sen ilmoitus näkyy kutsun kohdassa. Esimerkissämme tällaisia ​​ehdokkaita on neljä: f(), f(int), f(double, double) ja f(char*, char*).
Tämän jälkeen tunnistetaan hyväksyttyjen argumenttien luettelon ominaisuudet, eli. niiden lukumäärä ja tyypit. Esimerkissämme luettelo koostuu kahdesta kaksoisargumentista.
Toisessa vaiheessa ehdokkaiden joukosta valitaan käyttökelpoiset - ne, jotka voidaan kutsua annetuilla argumenteilla Pysyvällä funktiolla on joko yhtä monta muodollista parametria kuin kutsutulle funktiolle välitetyt varsinaiset argumentit, tai enemmän, mutta sitten jokaiselle lisäparametrille oletusarvo. Jotta funktiota pidettäisiin pysyvänä, kaikilla kutsussa välitetyillä argumenteilla on oltava muunnos ilmoituksessa määritetyn muodollisen parametrin tyypiksi.

Esimerkissämme on kaksi vakiintunutta funktiota, jotka voidaan kutsua annetuilla argumenteilla:

  • funktio f(int) säilyi, koska sillä on vain yksi parametri ja varsinainen kaksoisargumentti muunnetaan muodolliseksi int-parametriksi;
  • funktio f(double,double) säilyi, koska toisella argumentilla on oletusarvo ja ensimmäinen muodollinen parametri on tyyppiä double, joka on täsmälleen todellisen argumentin tyyppi.

Jos toisen vaiheen jälkeen ei löytynyt vakaita toimintoja, puhelu katsotaan virheelliseksi. Tällaisissa tapauksissa sanomme, että kirjeenvaihto on puutteellista.
Kolmas vaihe on valita toiminto, joka sopii parhaiten puhelun kontekstiin. Tällaista toimintoa kutsutaan parhaiten seisovaksi (tai parhaaksi istuvuudeksi). Tässä vaiheessa muunnokset, joilla varsinaisten argumenttien tyypit välitetään muodostetun funktion muodollisten parametrien tyypeiksi, asetetaan paremmuusjärjestykseen. Sopivimmaksi katsotaan toiminto, jolle seuraavat ehdot täyttyvät:
varsinaisiin argumentteihin sovelletut muunnokset eivät ole huonompia kuin muunnokset, jotka vaaditaan kutsumaan muita vakiintuneita funktioita;
Joillekin argumenteille käytetyt muunnokset ovat parempia kuin muunnokset, jotka vaaditaan samojen argumenttien lähettämiseen muiden vakiintuneiden funktioiden kutsuissa.
Tyyppimuunnoksia ja niiden järjestystä käsitellään tarkemmin kohdassa 9.3. Tässä tarkastellaan vain lyhyesti esimerkkimme sijoitusmuunnoksia. Perustetulle funktiolle f(int) on käytettävä tyypin double to int todellisen argumentin vakiomuotoilua. Perustetulle funktiolle f(double,double) todellisen argumentin double tyyppi vastaa täsmälleen muodollisen parametrin tyyppiä. Koska tarkka vastaavuus on parempi kuin tavallinen tulos (ei konversiota on aina parempi kuin sellaisen saaminen), f(double,double) pidetään sopivimpana funktiona tälle kutsulle.
Jos kolmannessa vaiheessa ei ole mahdollista löytää ainoaa parasta vakiintuneista funktioista, eli ei ole olemassa sellaista vakiintunutta funktiota, joka sopisi enemmän kuin kaikki muut, niin kutsu katsotaan moniselitteiseksi, ts. virheellinen.
(Osa 9.4 käsittelee kaikki ylikuormituksen ratkaisuvaiheet yksityiskohtaisemmin. Ratkaisuprosessia käytetään myös kutsuttaessa ylikuormitettua luokan jäsenfunktiota ja ylikuormitettua operaattoria. Kohdassa 15.10 käsitellään ylikuormituksen ratkaisusääntöjä, jotka koskevat luokan jäsenfunktioita, ja jaksossa 15.11 käsitellään säännöt (ylikuormitetut operaattorit. Ylikuormitusresoluutio tulee ottaa huomioon myös malleista luodut toiminnot. Kohdassa 10.8 käsitellään mallien vaikutusta tähän resoluutioon.)

Harjoitus 9.5

Mitä tapahtuu funktion ylikuormituksen ratkaisuprosessin viimeisessä (kolmannessa) vaiheessa?

9.3. Argumenttityypin muunnokset A

Funktion ylikuormituksen ratkaisuprosessin toisessa vaiheessa kääntäjä tunnistaa ja luokittelee muunnokset, joita tulisi käyttää kutsutun funktion kuhunkin varsinaiseen argumenttiin, jotta se voidaan lähettää minkä tahansa vakiintuneen funktion vastaavan muodollisen parametrin tyyppiin. toimintoja. Ranking voi antaa yhden kolmesta mahdollisesta tuloksesta:

  • tarkka vastaavuus. Varsinaisen argumentin tyyppi vastaa täsmälleen muodollisen parametrin tyyppiä. Jos esimerkiksi ylikuormitettujen print()-funktioiden joukossa on nämä: void print(unsigned int); void print(const char*); voidprint(char);
  • sitten kukin seuraavista kolmesta kutsusta antaa tarkan vastaavuuden:
    allekirjoittamaton int a;
tulosta("a"); // vastaa print(char); tulosta("a"); // vastaa print(const char*); tulosta(a); // vastaa print(unsigned int);
  • vastaavuus tyyppimuunnoksen kanssa. Varsinaisen argumentin tyyppi ei vastaa muodollisen parametrin tyyppiä, mutta se voidaan muuntaa sellaiseksi: void ff(char); ff(0); // tyypin int argumentti heitetään tyyppiin char
  • noudattamatta jättäminen. Varsinaisen argumentin tyyppiä ei voida heittää funktion määrittelyssä olevan muodollisen parametrin tyyppiin, koska vaadittua muunnosa ei ole olemassa. Kumpaakaan seuraavista kahdesta print()-funktion kutsusta ei löydy:
  • // print()-funktiot on ilmoitettu kuten edellä int *ip; luokka SmallInt ( /* ... */ ); SmallInt si; tulosta(ip); // virhe: ei vastaa
    tulostaa(si); // virhe: ei vastaa
  • Tarkan vastaavuuden määrittämiseksi todellisen argumentin tyypin ei tarvitse vastata muodollisen parametrin tyyppiä. Joitakin triviaaleja muunnoksia voidaan soveltaa argumenttiin, nimittäin:

    • muunnetaan l-arvo r-arvoksi;
    • taulukon muuntaminen osoittimeksi;
    • funktion muuntaminen osoittimeksi;
    • täsmennyksiä.

    (Niitä käsitellään tarkemmin alla.) Tyyppimuunnoksen mukaisuuden luokka on monimutkaisin. On olemassa useita tällaisia ​​näytteitä, joita kannattaa harkita: tyyppilaajennukset (promootiot), vakiomuunnokset ja käyttäjän määrittämät konversiot. (Tässä luvussa käsitellään tyyppilaajennuksia ja vakiomuunnoksia. Käyttäjän määrittämät muunnokset esitellään myöhemmin, kun luokista on keskusteltu yksityiskohtaisesti; ne suorittaa muuntaja, jäsentoiminto, jonka avulla voit määrittää oman "standardisarjasi" ” -muunnoksia luokassa. Luvussa 15 tarkastellaan tällaisia ​​muuntimia ja kuinka ne vaikuttavat toimintojen ylikuormitusresoluutioon.)
    Valitessaan parasta vakiintunutta funktiota tietylle kutsulle kääntäjä etsii funktiota, jonka varsinaisiin argumentteihin sovelletut muunnokset ovat "parhaat". Tyyppikonversiot luokitellaan seuraavasti: tarkka vastaavuus on parempi kuin tyyppilaajennus, tyyppilaajennus on parempi kuin tavallinen tulos, ja tämä puolestaan ​​on parempi kuin käyttäjän määrittämä tulos. Palaamme sijoitukseen osiossa 9.4, mutta toistaiseksi käytämme yksinkertaisia ​​esimerkkejä näyttääksemme, kuinka se auttaa sinua valitsemaan sopivimman ominaisuuden.

    9.3.1. Lisätietoja tarkasta vastaavuudesta

    Yksinkertaisin tapaus tapahtuu, kun todellisten argumenttien tyypit ovat samat kuin muodollisten parametrien tyypit. Esimerkiksi alla on kaksi ylikuormitettua max()-funktiota. Sitten jokainen max()-kutsu vastaa täsmälleen yhtä ilmoitusta:

    int max(int, int); double max(double, double); int i1; void calc(double d1) (
    max(56, i1); // vastaa täsmälleen max(int, int);
    max(d1, 66,9); // vastaa täsmälleen max(double, double);
    }

    Luettelotyyppi vastaa täsmälleen vain siinä määritettyjä luettelointielementtejä sekä objekteja, jotka on ilmoitettu tämän tyyppisiksi:

    Enum Tokens (INLINE = 128; VIRTUAALINEN = 129; ); Tokens curTok = INLINE; enum Stat(epäonnistunut, hyväksytty); extern void ff(Tokens);
    extern void ff(Stat);
    ulkoinen void ff(int); int main() (
    ff(hyväksytty); // vastaa täsmälleen ff(Stat)
    ff(0); // vastaa täsmälleen ff(int)
    ff(curTok); // vastaa täsmälleen ff(Tokens)
    // ...
    }

    Edellä mainittiin, että varsinainen argumentti voi täsmälleen vastata muodollista parametria, vaikka niiden tyyppien heittämiseen tarvitaankin jokin triviaali muunnos, joista ensimmäinen on l-arvon muuntaminen r-arvoksi. L-arvo on objekti, joka täyttää seuraavat ehdot:

    • saat kohteen osoitteen;
    • voit saada kohteen arvon;
    • tätä arvoa on helppo muokata (ellei objektin määrittelyssä ole const-määritettä).

    Sitä vastoin r-arvo on lauseke, jonka arvo on evaluoitu, tai lauseke, joka ilmaisee väliaikaista objektia, jolle ei voida saada osoitetta ja jonka arvoa ei voi muuttaa. Tässä on yksinkertainen esimerkki:

    intcalc(int); int main() ( int lval, res; lval = 5; // arvo: lval; rvalue: 5
    res = calc(lval);
    // lvalue:res
    // rvalue: väliaikainen objekti arvon tallentamiseen,
    // calc()-funktion palauttama
    paluu 0;
    }

    Ensimmäisessä tehtävälauseessa muuttuja lval on l-arvo ja literaali 5 on r-arvo. Toisessa osoituskäskyssä res on l-arvo, ja väliaikainen objekti, joka tallentaa calc()-funktion palauttaman tuloksen, on r-arvo.
    Joissakin tilanteissa tilanteessa, jossa arvoa odotetaan, voidaan käyttää lauseketta, joka on l-arvo:

    intobj1; intobj2; int main() (
    // ...
    sisäinen = obj1 + obj2;
    paluu 0;
    }

    Tässä obj1 ja obj2 ovat l-arvoja. Kuitenkin summauksen suorittamiseksi main()-funktiossa niiden arvot poimitaan muuttujista obj1 ja obj2. L-arvolausekkeen edustaman objektin arvon erottamista kutsutaan l-arvon muuntamiseksi r-arvoksi.
    Kun funktio odottaa arvon välittämää argumenttia, jos argumentti on l-arvo, se muunnetaan r-arvoksi:

    #sisältää merkkijonoväri("violetti"); voidprint(merkkijono); int main() (
    tulostaa (väri); // tarkka vastaavuus: arvomuunnos
    // rvaluessa
    paluu 0;
    }

    Koska print(väri)-kutsun argumentti välitetään arvon mukaan, l-arvo muunnetaan r-arvoksi värin arvon erottamiseksi ja sen välittämiseksi print(string)-prototyyppifunktiolle. Vaikka tällainen heitto on tapahtunut, todellisen väriargumentin oletetaan täsmälleen vastaavan tulostus(merkkijono)-ilmoitusta.
    Funktioita kutsuttaessa ei aina ole tarpeen soveltaa tällaista muuntamista argumenteille. Viite on l-arvo; jos funktiolla on viiteparametri, niin kun funktiota kutsutaan, se saa l-arvon. Siksi kuvattua muunnosa ei sovelleta varsinaiseen argumenttiin, jota muodollinen viiteparametri vastaa. Oletetaan esimerkiksi, että seuraava funktio on ilmoitettu:

    #sisältää void print (list &);

    Alla olevassa kutsussa li on listaobjektia edustava l-arvo , välitetty print()-funktiolle:

    Lista li(20); int main() (
    // ...
    tulostaa(li); // tarkka vastaavuus: ei muunnoksia arvosta arvoksi
    // rvalue
    paluu 0;
    }

    Li:n yhdistäminen viiteparametriin katsotaan tarkaksi vastaavuudeksi.
    Toinen muunnos, joka edelleen korjaa tarkan vastaavuuden, on taulukon muuntaminen osoittimeksi. Kuten osiossa 7.3 todettiin, funktioparametri ei ole koskaan taulukkotyyppi, vaan se muuttuu osoittimeksi ensimmäiseen elementtiin. Vastaavasti varsinainen taulukkotyyppinen argumentti NT:stä (jossa N on taulukon elementtien lukumäärä ja T on kunkin elementin tyyppi) heitetään aina osoittimeen T:ksi. Tätä varsinaisen argumentin tyyppimuunnosa kutsutaan taulukosta taulukoksi. -osoittimen muunnos. Tästä huolimatta varsinaisen argumentin katsotaan täsmälleen vastaavan muodollista parametria, jonka tyyppi on "osoitin T". Esimerkiksi:

    int ai; void putArt(int *); int main() (
    // ...
    putArt(ai); // tarkka vastaavuus: muunna array muotoon
    // osoitin
    paluu 0;
    }

    Ennen putValues()-funktion kutsumista matriisi muunnetaan osoittimeksi, jolloin varsinainen argumentti ai (kolmen kokonaisluvun taulukko) välitetään int:n osoittimeksi. Vaikka putValues()-funktion muodollinen parametri on osoitin ja todellinen argumentti muunnetaan kutsuttaessa, niiden välille muodostuu tarkka vastaavuus.
    Tarkkaa vastaavuutta määritettäessä on myös mahdollista muuntaa funktio osoittimeksi. (Se mainittiin kohdassa 7.9.) Kuten taulukkoparametrista, funktioparametrista tulee funktioosoitin. Varsinainen "funktio"-tyypin argumentti heitetään myös automaattisesti funktioosoittimen tyyppiin. Tällaista varsinaisen argumentin muuntamista kutsutaan funktiosta osoitinmuunnokseksi. Kun muunnos suoritetaan, todellisen argumentin katsotaan täsmälleen vastaavan muodollista parametria. Esimerkiksi:

    Int lexicoCompare(const string &, const string &); typedef int (*PFI)(const string &, const string &);
    void sort(merkkijono *, merkkijono *, PFI); merkkijono; int main()
    {
    // ...
    lajitella (kuten,
    as + koko(as)/koko(kuten -1),
    lexicoCompare // tarkka haku
    // muuntaa funktion osoittimeksi
    ); paluu 0;
    }

    Ennen sort()-kutsua käytetään funktiosta osoittimeen muunnos, joka heittää lexicoCompare-argumentin tyypistä "function" tyyppiin "osoitin toimintoon". Vaikka funktion muodollinen argumentti on osoitin ja varsinainen argumentti on funktion nimi, ja siksi funktio on muunnettu osoittimeksi, todellisen argumentin oletetaan olevan täsmälleen sort()-funktion kolmas muodollinen parametri. .
    Viimeinen edellä mainituista on määrittäjien muunnos. Se koskee vain osoittimia ja koostuu const- tai volatile-määritteiden (tai molempien) lisäämisestä tyyppiin, joka osoittaa annettua osoitinta:

    Int a = (4454, 7864, 92, 421, 938); int*pi = a; bool is_equal(const int * , const int *); void func(int *parm) ( // tarkka vastaavuus pi:n ja parmin välillä: määritteiden muunnos
    if (on_equal(pi, parm))
    // ... return 0;
    }

    Ennen is_equal()-funktion kutsumista varsinaiset argumentit pi ja parm muunnetaan tyypistä "pointer to int" tyypiksi "pointer to const int". Tämä muunnos koostuu const-määritteen lisäämisestä osoitettuun tyyppiin ja kuuluu siksi määritysmuunnosten luokkaan. Vaikka funktio odottaa saavansa kaksi osoitinta const int:iin ja varsinaiset argumentit ovat osoittimia int:iin, oletetaan, että is_equal()-funktion muodollisten ja todellisten parametrien välillä on tarkka vastaavuus.
    Määritteen muunnos koskee vain tyyppiä, jota osoitin osoittaa. Sitä ei käytetä, kun muodollisella parametrilla on const- tai volatile-määrittäjä, mutta varsinaisella argumentilla ei ole.

    extern void takeCI(const int); int main() (
    int ii = ...;
    ota CI(ii); // määritteen muuntamista ei käytetä
    paluu 0;
    }

    Vaikka takeCI()-funktion muodollinen parametri on tyyppiä const int ja sitä kutsutaan argumentilla ii, jonka tyyppi on int, määritteen muuntamista ei ole: varsinaisen argumentin ja muodollisen parametrin välillä on tarkka vastaavuus.
    Kaikki yllä oleva pätee myös tapaukseen, jossa argumentti on osoitin ja const tai volatile-määritykset viittaavat tähän osoittimeen:

    extern void init(int *const); extern int *pi; int main() (
    // ...
    init(pi); // määritteen muuntamista ei käytetä
    paluu 0;
    }

    Init()-funktion muodollisen parametrin const-määrite viittaa itse osoittimeen, ei tyyppiin, jota se osoittaa. Siksi kääntäjä ei ota tätä määritettä huomioon jäsentäessään varsinaiseen argumenttiin sovellettavia muunnoksia. Pi-argumentille ei käytetä määritteen muuntamista: tämän argumentin ja muodollisen parametrin katsotaan vastaavan tarkasti.
    Kolmea ensimmäistä näistä muunnoksista (l-arvosta r-arvoon, taulukosta osoittimeen ja funktiosta osoittimeen) kutsutaan usein l-arvon muunnoksiksi. (Katsotaan luvusta 9.4, että vaikka sekä l-arvon muunnokset että täsmennysmuunnokset kuuluvat niiden muunnosten kategoriaan, jotka eivät riko tarkkaa vastaavuutta, sen astetta pidetään korkeampana siinä tapauksessa, että tarvitaan vain ensimmäinen muunnos. Seuraavassa osiossa , puhumme tästä hieman yksityiskohtaisemmin. .)
    Tarkka vastaavuus voidaan pakottaa käyttämällä eksplisiittistä tyyppiä. Jos esimerkiksi kaksi toimintoa on ylikuormitettu:

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

    Ff(0xffbc); // soita ff(int)

    vastaa täsmälleen ff(int), vaikka literaali 0xffbc on kirjoitettu heksadesimaalivakioksi. Ohjelmoija voi pakottaa kääntäjän kutsumaan ff(void *) -funktiota suorittamalla eksplisiittisesti heittotoiminnon:

    Ff(reinterpret_cast (0xffbc)); // soita ff(void*)

    Jos tällaista heittoa sovelletaan varsinaiseen argumenttiin, se saa tyypin, johon se muunnetaan. Eksplisiittiset tyyppimuunnokset auttavat hallitsemaan ylikuormituksen ratkaisuprosessia. Esimerkiksi, jos ylikuormitusresoluutio tuottaa epäselvän tuloksen (todelliset argumentit vastaavat yhtä hyvin kahta tai useampaa vakiintunutta funktiota), voidaan epäselvyyden ratkaisemiseksi käyttää eksplisiittistä castia, jolloin kääntäjä valitsee tietyn funktion.

    9.3.2. Lisätietoja tyyppilaajennuksesta

    Tyyppilaajennus on yksi seuraavista muunnoksista:

    • varsinainen argumentti tyyppi char, unsigned char tai short laajennetaan tyyppiin int. Varsinainen argumentti tyypin unsigned short laajennetaan kirjoittamalla int, jos int:n konekoko on suurempi kuin shortin koko, ja muussa tapauksessa kirjoittamalla unsigned int;
    • tyypin float argumentti laajennetaan tyypiksi double;
    • numerated type -argumentti laajenee ensimmäiseksi seuraavista tyypeistä, joka pystyy edustamaan kaikkia enum-jäsenarvoja: int, unsigned int, long, unsigned long;
    • bool-argumentti on laajennettu int-tyypiksi.

    Samanlaista laajennusta käytetään, kun todellisen argumentin tyyppi on jokin juuri luetelluista tyypeistä ja muodollinen parametri on vastaavaa laajennettua tyyppiä:

    extern void manip(int); int main() (
    manip("a"); // tyyppi char laajenee int
    paluu 0;
    }

    Merkiliteraali on tyyppiä char. Se laajenee int. Koska laajennettu tyyppi vastaa manip()-funktion muodollisen parametrin tyyppiä, sanomme, että sen kutsuminen vaatii argumenttityypin laajentamista.
    Harkitse seuraavaa esimerkkiä:

    extern void print (unsigned int); extern void print(int); extern void print(char); allekirjoittamaton char uc;
    print(uc); // tulosta(int); uc tarvitsee vain tyyppilaajennuksen

    Laitteistoalustalla, jossa etumerkitön char vie yhden tavun muistia ja int neljä tavua, laajennus muuntaa etumerkittömän charin int:ksi, koska se voi edustaa kaikkia etumerkittömiä merkkiarvoja. Tällaisessa konearkkitehtuurissa esimerkissä esitetyistä monista ylikuormitetuista funktioista print(int) tarjoaa parhaan vastineen etumerkittömälle char-argumentille. Kahdessa muussa toiminnossa yhteensopivuus vaatii vakiomuotoilun.
    Seuraava esimerkki havainnollistaa todellisen numeroidun tyypin argumentin laajentamista:

    Enum Stat (Fail, Pass); ulkoinen void ff(int);
    ulkoinen void ff(char); int main() (
    // oikein: enum-jäsen Pass laajenee kirjoittamaan int
    ff(hyväksytty); // ff(int)
    ff(0); // ff(int)
    }

    Joskus enumeiden laajentaminen tuo yllätyksiä. Kääntäjät valitsevat usein enumin esityksen sen elementtien arvojen perusteella. Oletetaan, että yllä oleva arkkitehtuuri (yksi tavu charille ja neljä tavua int:lle) määrittelee seuraavanlaisen enum:n:

    Enum e1 (a1, b1, c1);

    Koska elementtejä a1, b1 ja c1 on vain kolme, joiden arvot ovat 0, 1 ja 2 - ja koska kaikki nämä arvot voidaan esittää char-tyypillä, kääntäjä valitsee yleensä char edustamaan e1-tyyppiä. Tarkastellaan kuitenkin luetteloa e2 seuraavilla elementeillä:

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

    Koska yhden vakion arvo on 0x80000000, kääntäjän on valittava edustamaan e2 tyypillä, joka riittää tallentamaan arvon 0x80000000, eli unsigned int.
    Joten vaikka sekä e1 että e2 ovat enumeja, niiden esitykset ovat erilaisia. Tämän vuoksi e1 ja e2 laajenevat eri tyypeiksi:

    #sisältää merkkijonomuoto(int);
    merkkijonomuoto (unsigned int); int main() (
    muoto(a1); // puhelumuoto(int)
    muoto(a2); // puhelumuoto (allekirjoittamaton int)
    paluu 0;
    }

    Ensimmäistä kertaa format():ta kutsutaan, varsinainen argumentti laajennetaan tyyppiin int, koska charia käytetään edustamaan e1, ja siksi kutsutaan ylikuormitettua format(int)-funktiota. Toisessa kutsussa varsinaisen argumentin e2 tyyppi on unsigned int ja argumentti laajennetaan unsigned int:ksi, mikä saa aikaan ylikuormitetun formaatin (unsigned int) -funktion kutsumisen. Siksi muista, että kahden enumin käyttäytyminen ylikuormituksen ratkaisuprosessin suhteen voi olla erilainen ja riippua elementtien arvoista, jotka määrittävät tyypin laajennuksen.

    9.3.3. Lisätietoja tavallisesta muuntamisesta

    Vakiomuunnoksia on viisi tyyppiä, nimittäin:

    1. kokonaislukutyypin muunnokset: suoratoisto kokonaislukutyypistä tai luettelosta mihin tahansa muuhun kokonaislukutyyppiin (lukuun ottamatta muunnoksia, jotka luokiteltiin edellä tyyppilaajennuksiksi);
    2. liukulukutyypin muunnokset: valu mistä tahansa liukulukutyypistä mihin tahansa muuhun liukulukutyyppiin (lukuun ottamatta muunnoksia, jotka luokiteltiin edellä tyyppilaajennuksiksi);
    3. muunnokset kokonaislukutyypin ja liukulukutyypin välillä: casting mistä tahansa liukulukutyypistä mihin tahansa kokonaislukutyyppiin tai päinvastoin;
    4. osoittimen muunnokset: kokonaislukuarvon 0 syöttäminen osoitintyyppiin tai minkä tahansa tyyppisen osoittimen muuntaminen void*-tyypiksi;
    5. muunnokset bool-tyyppiin: suoratoisto mistä tahansa kokonaislukutyypistä, liukulukutyypistä, lueteltu tyypistä tai osoitintyypistä bool-tyyppiin.

    Tässä on joitain esimerkkejä:

    extern void print(tyhjä*); extern void print (double); int main() (
    int i;
    tulosta(i); // vastaa print(double);
    // i käy läpi vakiomuunnos int:stä ​​double
    tulosta(&i); // vastaa print(void*);
    // &i käy läpi vakiomuunnos
    // int*:sta tyhjäksi*
    paluu 0;
    }

    Ryhmiin 1, 2 ja 3 kuuluvat muunnokset ovat mahdollisesti vaarallisia, koska kohdetyyppi ei välttämättä edusta kaikkia lähdetyypin arvoja. Esimerkiksi kellukkeet eivät voi edustaa riittävästi kaikkia int-arvoja. Tästä syystä näihin ryhmiin sisältyvät muunnokset luokitellaan vakiomuunnoksiksi eikä tyyppilaajennuksiksi.

    int i; voidcalc(kelluke); int main() ( calc(i); // standardimuunnos kokonaislukutyypin ja liukulukutyypin välillä // on mahdollisesti vaarallinen riippuen // i return 0:n arvosta; )

    Kun calc()-funktiota kutsutaan, käytetään standardimuunnosa kokonaislukutyypistä int liukulukutyyppiin. Muuttujan i arvosta riippuen sitä ei ehkä ole mahdollista tallentaa kellukkeena ilman tarkkuuden menetystä.
    Oletetaan, että kaikki standardimuutokset vaativat yhtä paljon työtä. Esimerkiksi muunnolla merkistä etumerkittömään merkkiin ei ole korkeampaa prioriteettia kuin muuntamisella merkistä kaksinkertaiseksi. Tyyppien läheisyyttä ei oteta huomioon. Jos kaksi muodostettua funktiota vaativat todellisen argumentin standardimuunnoksen vastaamaan, kutsu katsotaan epäselväksi ja kääntäjä merkitsee sen virheeksi. Esimerkiksi kaksi ylikuormitettua toimintoa:

    extern void manip(pitkä); extern void manip(kelluke);

    niin seuraava kutsu on epäselvä:

    int main() ( manip(3.14); // virhe: ambiguity // manip(float) ei ole parempi kuin manip(int) return 0; )

    Vakio 3.14 on tyyppiä double. Yhden tai toisen vakiomuunnoksen avulla voidaan luoda vastaavuus minkä tahansa ylikuormitetun toiminnon kanssa. Koska tavoitteeseen johtaa kaksi muutosta, kutsua pidetään moniselitteisenä. Kumpikaan muunnos ei ole toisen edelle. Ohjelmoija voi ratkaista epäselvyyden joko eksplisiittisellä tyyppivalolla:

    Manip(staattinen_cast (3,14)); // manip(pitkä)

    tai käyttämällä päätettä, joka osoittaa, että vakio on float-tyyppiä:

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

    Tässä on lisää esimerkkejä epäselvistä kutsuista, jotka on merkitty virheiksi, koska ne vastaavat useita ylikuormitettuja toimintoja:

    extern void farith(signed int); ulkoinen void farith(kelluke); int main() (
    // jokainen seuraavista kutsuista on epäselvä
    farith("a"); // argumentti on tyyppiä char
    farith(0); // argumentti on tyyppiä int
    farith(2ul); // argumentti on tyyppiä unsigned long
    farith(3,14159); // argumentti on tyyppiä double
    farith(true); // argumentti on tyyppiä bool
    }

    Tavalliset osoittimen muunnokset ovat joskus intuitiivisia. Erityisesti arvo 0 heitetään osoittimeen mihin tahansa tyyppiin; näin saatua osoitinta kutsutaan nollaksi. Arvo 0 voidaan esittää kokonaislukutyypin vakiolausekkeena:

    void set(int*); int main() (
    // osoittimen muunnos 0:sta int*:ksi, jota käytetään argumenteissa
    // molemmissa puheluissa
    set(0L);
    set(0x00);
    paluu 0;
    }

    Vakiolauseke 0L (arvo 0 tyypin long int) ja vakiolauseke 0x00 (heksadesimaalinen kokonaisluku 0) ovat kokonaislukutyyppiä ja siksi ne voidaan muuntaa nollaosoittimeksi, jonka tyyppi on int*.
    Mutta koska enumit eivät ole kokonaislukutyyppejä, elementtiä, joka on yhtä suuri kuin 0, ei lähetetä osoitintyyppiin:

    Enum EN (zr = 0); set(zr); // virhe: zr ei voi muuntaa tyypiksi int*

    Set()-funktion kutsu on virhe, koska numeraatioelementin arvon zr ja int*-tyypin muodollisen parametrin välillä ei tapahdu muunnosa, vaikka zr on 0.
    Huomaa, että vakiolauseke 0 on tyyppiä int. Sen suoratoisto osoitintyyppiin edellyttää vakiomuunnos. Jos ylikuormitettujen funktioiden joukossa on funktio, jolla on muodollinen parametri tyyppiä int, ylikuormitus sallitaan sen eduksi, jos varsinainen argumentti on 0:

    Void print(int); voidprint(tyhjä*); void set(const char*);
    void set(char *); int main()(
    tulosta(0); // kutsutaan print(int);
    set(0); // epäselvyys
    paluu 0;
    }

    Kutsu print(int) on tarkka vastaavuus, kun taas kutsu print(void*) edellyttää arvon 0 lähettämistä osoitintyyppiin. Koska vastaavuus on parempi kuin muunnos, tämän kutsun ratkaisemiseksi valitaan print(int)-funktio. Set()-kutsu on moniselitteinen, koska 0 vastaa molempien ylikuormitettujen funktioiden muodollisia parametreja käyttämällä standardimuunnosta. Koska molemmat toiminnot ovat yhtä hyviä, epäselvyys on korjattu.
    Viimeinen mahdollinen osoittimen muunnos antaa sinun heittää minkä tahansa tyyppisen osoittimen tyyppiin void*, koska void* on yleinen osoitin mihin tahansa tietotyyppiin. Tässä on joitain esimerkkejä:

    #sisältää extern void reset(void *); void func(int *pi, merkkijono *ps) (
    // ...
    reset(pi); // osoittimen muunnos: int* void*
    /// ...
    reset(ps); // osoittimen muunnos: merkkijono* tyhjäksi*
    }

    Vain tietotyyppejä osoittavat osoittimet voidaan heittää tyhjäksi* vakiomuunnolla, osoittimia funktioihin ei voi tehdä tällä tavalla:

    Typedef int(*PFV)(); ulkoinen PFV-testi; // joukko osoittimia funktioihin extern void reset(void *); int main() (
    // ...
    nollaa (tekstikoko); // virhe: ei standardimuunnos
    // int(*)() ja void* välissä
    paluu 0;
    }

    9.3.4. Linkit

    Toiminnon varsinainen argumentti tai muotoparametri voi olla viittauksia. Miten tämä vaikuttaa tyyppimuunnossääntöihin?
    Mieti, mitä tapahtuu, kun viittaus on todellinen argumentti. Sen tyyppi ei ole koskaan viitetyyppi. Viiteargumenttia käsitellään l-arvona, jonka tyyppi on sama kuin vastaavan objektin tyyppi:

    int i; int& ri = i; voidprint(int); int main() (
    tulosta(i); // argumentti on int-tyypin arvo
    tulosta(ri); // sama
    paluu 0;
    }

    Varsinainen argumentti molemmissa kutsuissa on tyyppiä int. Viittauksen käyttäminen sen välittämiseen toisessa kutsussa ei vaikuta itse argumentin tyyppiin.
    Kääntäjän tarkastelemat vakiotyyppimuunnokset ja -laajennukset ovat samat, kun varsinainen argumentti on viittaus tyyppiin T ja kun se itse on sitä tyyppiä. Esimerkiksi:

    int i; int& ri = i; voidcalc(kaksois); int main() (
    calc(i); // vakiomuunnos kokonaislukutyypin välillä
    // ja liukulukutyyppi
    calc(ri); // sama
    paluu 0;
    }

    Ja miten muodollinen viiteparametri vaikuttaa varsinaiseen argumenttiin sovellettuihin muunnoksiin? Vertailu antaa seuraavat tulokset:

    • varsinainen argumentti sopii referenssiparametrin alustajaksi. Tässä tapauksessa sanomme, että niiden välillä on tarkka vastaavuus: void swap(int &, int &); void manip(int i1, int i2) (
      // ...
      swap(i1, i2); // oikein: call swap(int &, int &)
      // ...
      paluu 0;
      }
    • varsinainen argumentti ei voi alustaa viiteparametria. Tällaisessa tilanteessa tarkkaa vastaavuutta ei ole, eikä argumenttia voida käyttää funktion kutsumiseen. Esimerkiksi:
    • int obj; void fred(double &); int main() ( frd(obj); // virhe: parametrin on oltava tyyppiä const double & return 0; )
    • Frd()-funktion kutsuminen on virhe. Varsinainen argumentti on tyyppiä int ja se on muutettava tyypiksi double, jotta se vastaa muodollista viiteparametria. Tämän muunnoksen tulos on väliaikainen muuttuja. Koska viitteellä ei ole const-määritteitä, tällaisia ​​muuttujia ei voida käyttää sen alustamiseen.
      Tässä on toinen esimerkki, jossa muodollisen viiteparametrin ja todellisen argumentin välillä ei ole vastaavuutta:
    • luokka B; void takeB(B&); BannaB(); int main() (
      otaB(annaB()); // virhe: parametrin on oltava tyyppiä const B &
      paluu 0;
      }
    • TakeB()-funktion kutsuminen on virhe. Varsinainen argumentti on palautusarvo, ts. tilapäinen muuttuja, jota ei voida käyttää viitteen alustamiseen ilman const-määritteitä.
      Molemmissa tapauksissa näemme, että jos muodollisella viiteparametrilla on const-spesifier, sen ja todellisen argumentin välille voidaan määrittää tarkka vastaavuus.

    Huomaa, että sekä l-arvon muuntaminen r-arvoksi että viittauksen alustus katsotaan täsmälleen vastaavuudeksi. Tässä esimerkissä ensimmäinen funktiokutsu johtaa virheeseen:

    Void print(int); voidprint(int&); intiobj;
    int &ri = iobj; int main() (
    tulosta(iobj); // virhe: epäselvyys
    tulosta(ri); // virhe: epäselvyys
    tulostaa (86); // oikein: soita print(int)
    paluu 0;
    }

    iobj-objekti on argumentti, joka voidaan yhdistää molempiin print()-funktioihin, eli kutsu on moniselitteinen. Sama koskee seuraavaa riviä, jossa viite ri tarkoittaa objektia, joka vastaa molempia print()-funktioita. Kolmannella kutsulla kaikki on kuitenkin kunnossa. Hänelle print(int&) ei ole vakiintunut. Kokonaislukuvakio on r-arvo, joten se ei voi alustaa viiteparametria. Ainoa vakiintunut toiminto kutsua print(86) on print(int), minkä vuoksi se valitaan ylikuormitusresoluutiolla.
    Lyhyesti sanottuna, jos muodollinen argumentti on viittaus, varsinainen argumentti vastaa täsmällisesti, jos se voi alustaa viitteen, eikä muuten.

    Harjoitus 9.6

    Nimeä kaksi triviaalia muunnosa, jotka ovat sallittuja määritettäessä tarkkaa vastaavuutta.

    Harjoitus 9.7

    Mikä on kunkin argumentin muunnoksen sijoitus seuraavissa funktiokutsuissa:

    (a) void print (int *, int); int arr; tulosta (arr, 6); // funktiokutsu (b) void manip(int, int); manip("a", "z"); // funktiokutsu (c) int calc(int, int); kaksinkertainen dobj; double = calc(55.4, dobj) // funktiokutsu (d) void set(const int *); int*pi; set(pi); // funktiokutsu

    Harjoitus 9.8

    Mitkä näistä kutsuista ovat virheellisiä, koska varsinaisen argumentin tyypin ja muodollisen parametrin välillä ei ole muunnoksia:

    (a) enum Stat(hylätty, hyväksytty); mitätön testi (Stat); teksti(0); // funktiokutsu (b) void reset(void *); nollaa(0); // funktiokutsu (c) void set(void *); int*pi; set(pi); // funktiokutsu (d) #include lista ooppera(); void print(oper()); // funktiokutsu (e) void print(const int); intiobj; tulosta(iobj); // funktiokutsu

    9.4 Toimintojen ylikuormitusresoluutiotiedot

    Mainitsimme jo kohdassa 9.2, että toimintojen ylikuormituksen ratkaisuprosessi koostuu kolmesta vaiheesta:

    1. Aseta joukko ehdokasfunktioita tietyn kutsun ratkaisemiseksi sekä varsinaisen argumenttiluettelon ominaisuudet.
    2. Valitse ehdokkaiden joukosta vakiintuneet funktiot - ne, jotka voidaan kutsua tietyllä todellisten argumenttien luettelolla, ottaen huomioon niiden lukumäärä ja tyypit.
    3. Valitse kutsuun parhaiten sopiva funktio asettamalla todellisiin argumentteihin sovellettavat muunnokset järjestykseen, jotta ne vastaavat muodostetun funktion muodollisia parametreja.

    Olemme nyt valmiita tutkimaan näitä vaiheita tarkemmin.

    9.4.1. Ehdokastehtävät

    Ehdokasfunktio on funktio, jolla on sama nimi kuin kutsutulla. Ehdokkaat valitaan kahdella tavalla:

    • funktion ilmoitus näkyy kutsun kohdassa. Seuraavassa esimerkissä
      void f(); void f(int); void f(double, double = 3,4); void f(char*, char*); int main() (
      f(5,6); // tämän kutsun ratkaisemiseksi on neljä ehdokasta
      paluu 0;
      }
    • kaikki neljä f()-funktiota täyttävät tämän ehdon. Siksi ehdokasjoukko sisältää neljä elementtiä;
    • jos varsinaisen argumentin tyyppi on ilmoitettu jossain nimiavaruudessa, niin tämän avaruuden jäsenfunktiot, joilla on sama nimi kuin kutsutulla funktiolla, lisätään ehdokasjoukkoon: nimiavaruus NS ( luokka C ( /* ... */ ); void takeC( C&); ) // tyyppi cobj on luokka C, joka on ilmoitettu NS-nimiavaruudessa
      NS::Cobj; int main() (
      // mikään takeC()-funktioista ei ole näkyvissä kutsupisteessä
      otaC(cobj); // oikein: NS::takeC(C&) kutsutaan,
      // koska argumentti on tyyppiä NS::C, joten
      // TakeC()-funktio otetaan huomioon,
      // ilmoitettu NS-nimiavaruudessa
      paluu 0;
      }

    Siten ehdokasjoukko on kutsupisteessä näkyvän funktiojoukon ja todellisten argumenttityyppien kanssa samassa nimiavaruudessa ilmoitettujen funktioiden joukon liitto.
    Tunnistaessa ylikuormitettujen toimintojen joukkoa, jotka näkyvät puhelun kohdalla, pätevät jo aiemmin käsitellyt säännöt.
    Sisäkkäisessä laajuudessa ilmoitettu funktio piilottaa ulkoisen laajuuden samannimisen funktion sen sijaan, että se ylikuormittaisi. Tällaisessa tilanteessa vain sisäkkäisen laajuuden funktiot ovat ehdokkaita, ts. joita ei piiloteta kutsuttaessa. Seuraavassa esimerkissä kutsupisteessä näkyvät ehdokasfunktiot ovat format(double) ja format(char*):

    Char* muoto(int); void g() ( char *muoto(double); char* muoto(merkki*); muoto(3); // kutsuva muoto(double)
    }

    Koska globaalissa laajuudessa ilmoitettu muoto(int) on piilotettu, se ei sisälly ehdokasfunktioiden joukkoon.
    Ehdokkaat voidaan esitellä käyttämällä kutsumiskohdassa näkyviä ilmoituksia:

    Nimiavaruus libs_R_us ( int max(int, int); double max(double, double); ) char max(char, char); void func()
    {
    // nimiavaruuden funktiot ovat näkymättömiä
    // kaikki kolme kutsua ratkaistaan ​​globaalin funktion max(char, char) hyväksi
    max(87, 65);
    max(35,5; 76,6);
    max("J", "L");
    }

    Nimiavaruudessa libs_R_us määritellyt max()-funktiot ovat näkymättömiä kutsupisteessä. Ainoa näkyvä on max()-funktio globaalista laajuudesta; vain se sisältyy ehdokasfunktioiden joukkoon ja sitä kutsutaan jokaisessa kolmesta func-kutsusta (). Voimme käyttää use-lausetta paljastamaan max()-funktiot nimiavaruudesta libs_R_us. Mihin käyttöilmoitus laitetaan? Jos sisällytät sen maailmanlaajuiseen soveltamisalaan:

    Char max(merkki, merkki); käyttäen libs_R_us::max; // käyttämällä-ilmoitusta

    sitten max()-funktiot tiedostosta libs_R_us lisätään ylikuormitettujen funktioiden joukkoon, joka sisältää jo globaalissa laajuudessa ilmoitettua max()-funktiota. Nyt kaikki kolme funktiota näkyvät func():n sisällä ja niistä tulee ehdokkaita. Tässä tilanteessa func()-kutsut ratkaistaan ​​seuraavasti:

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

    Mutta mitä tapahtuu, jos lisäämme käyttöilmoituksen func()-funktion paikalliseen alueeseen, kuten tässä esimerkissä näkyy?

    void func() ( // käyttäen-ilmoitusta komennolla libs_R_us::max; // samat funktiokutsut kuin yllä
    }

    Mitkä max()-funktioista sisällytetään ehdokasjoukkoon? Muista, että käyttämällä ilmoitukset ovat sisäkkäisiä toistensa sisällä. Kun tällainen määrittely paikallisessa laajuudessa, globaali funktio max(char, char) on piilotettu, joten vain

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

    He ovat ehdokkaita. Nyt func()-kutsut ratkaistaan ​​seuraavasti:

    Void func() ( // using-declaration // yleinen funktio max(char, char) piilotetaan käyttämällä libs_R_us::max; max(87, 65); // libs_R_us::max(int, int) kutsutaan
    max(35,5; 76,6); // libs_R_us::max(double, double) kutsutaan
    max("J", "L"); // kutsu libs_R_us::max(int, int)
    }

    Käyttöohjeet vaikuttavat myös ehdokasfunktioiden joukon kokoonpanoon. Oletetaan, että päätämme käyttää niitä nimitilan libs_R_us max()-funktioiden näyttämiseen func()-funktiossa. Jos sijoitamme seuraavan käyttödirektiivin globaaliin laajuuteen, ehdokasfunktioiden joukko koostuu globaalista funktiosta max(char, char) ja funktioista max(int, int) ja max(double, double), jotka on ilmoitettu libs_R_us:ssa:

    Nimiavaruus libs_R_us ( int max(int, int); double max(double, double); ) char max(char, char);
    käyttäen nimiavaruutta libs_R_us; // käyttämällä käskyä void func()
    {
    max(87, 65); // kutsu libs_R_us::max(int, int)
    max(35,5; 76,6); // libs_R_us::max(double, double) kutsutaan
    }

    Mitä tapahtuu, jos laitat use-direktiivin paikalliseen soveltamisalaan, kuten seuraavassa esimerkissä?

    Void func() ( // käyttäen-directivea käyttäen nimiavaruutta libs_R_us; // samat funktiokutsut kuin yllä
    }

    Mikä max()-funktioista on ehdokkaiden joukossa? Muista, että käyttävä direktiivi tekee nimitilan jäsenet näkyväksi ikään kuin ne olisi ilmoitettu tämän tilan ulkopuolella, kohdassa, johon tällainen käsky sijoitetaan. Esimerkissämme libs_R_us:n jäsenet näkyvät func()-funktion paikallisessa laajuudessa, ikään kuin ne olisi julistettu avaruuden ulkopuolelle - globaalissa laajuudessa. Tästä seuraa, että func():n sisällä näkyvä ylikuormitettujen funktioiden joukko on sama kuin ennen, ts. sisältää

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

    Use-direktiivi näkyy paikallisessa tai globaalissa laajuudessa, func()-funktion kutsujen resoluutio ei vaikuta:

    Void func() ( käyttäen nimiavaruutta libs_R_us; max(87, 65); // kutsu libs_R_us::max(int, int)
    max(35,5; 76,6); // libs_R_us::max(double, double) kutsutaan
    max("J", "L"); // kutsutaan::max(int, int)
    }

    Ehdokasjoukko koostuu siis kutsupisteessä näkyvistä funktioista, mukaan lukien ne, jotka on otettu käyttöön use-declarations- ja use-direktiiveillä, sekä funktiot, jotka on ilmoitettu varsinaisiin argumenttityyppeihin liittyvissä nimiavaruuksissa. Esimerkiksi:

    Nimiavaruus basicLib ( int print(int); double print (double); ) nimitila matrixLib ( luokkamatriisi ( /* ... */ ); void print(const maxtrix &); ) void display() ( käyttäen basicLib::print matrixLib::matrix mObj;
    tulosta(mObj); // kutsu maxtrixLib::print(const maxtrix &) print(87); // basicLib::print(const maxtrix &) kutsutaan
    }

    Print(mObj)-ehdokkaita ovat basicLib::print(int)- ja basicLib::print(double)-funktioiden display() sisällä olevat use-ilmoitukset, koska ne näkyvät kutsupisteessä. Koska varsinainen funktion argumentti on tyyppiä matrixLib::matrix, matrixLib-nimiavaruudessa ilmoitettu print()-funktio olisi myös ehdokas. Mitkä ovat print(87) ehdokasfunktiot? Vain basicLib::print(int) ja basicLib::print(double) näkyvät kutsupisteessä. Koska argumentti on tyyppiä int, ylimääräistä nimiavaruutta ei huomioida muiden ehdokkaiden haussa.

    9.4.2. Vakiintuneet toiminnot

    Vakiintunut tehtävä on yksi ehdokkaista. Sen muodollisten parametrien luettelossa on joko sama määrä elementtejä kuin kutsutun funktion todellisten argumenttien luettelossa tai enemmän. Jälkimmäisessä tapauksessa lisäparametreille asetetaan oletusarvot, muuten funktiota ei voida kutsua annetulla argumenteilla. Jotta funktiota pidettäisiin vakaana, jokaisesta todellisesta argumentista on suoritettava muunnos vastaavan muodollisen parametrin tyypiksi. (Tällaisia ​​muunnoksia käsiteltiin kohdassa 9.3.)
    Seuraavassa esimerkissä f(5.6):n kutsulla on kaksi vakiintunutta funktiota: f(int) ja f(double).

    void f(); void f(int); void f(double); void f(char*, char*); int main() (
    f(5,6); // 2 perusfunktiota: f(int) ja f(double)
    paluu 0;
    }

    F(int)-funktio säilyi, koska sillä on vain yksi muodollinen parametri, joka vastaa kutsun todellisten argumenttien määrää. Lisäksi on olemassa vakiomuunnos double-tyyppisestä argumentista int:ksi. Myös f(double)-funktio säilyi; sillä on myös yksi parametri, jonka tyyppi on double, ja se vastaa täsmälleen todellista argumenttia. Ehdokasfunktiot f() ja f(char*, char*) on jätetty pois säilyneiden funktioiden luettelosta, koska niitä ei voida kutsua yhdellä argumentilla.
    Seuraavassa esimerkissä ainoa muodostettu funktio kutsumaan muotoa(3) on format(double). Vaikka muoto(char*)-ehdokas voidaan kutsua yhdellä argumentilla, varsinaisen int-argumentin tyypistä ei muunneta muodollisen parametrin char* tyyppiä, joten funktiota ei voida pitää vakiintuneena.

    Char* muoto(int); void g() ( // yleinen funktiomuoto(int) on piilotettu char* format(double); char* muoto(char*); muoto(3); // on vain yksi perusfunktio: format(double) )

    Seuraavassa esimerkissä kaikki kolme ehdokasfunktiota voivat lopulta kutsua max()-funktiota func(:n) sisällä. Niitä kaikkia voidaan kutsua kahdella argumentilla. Koska varsinaiset argumentit ovat tyyppiä int, ne vastaavat täsmälleen funktion libs_R_us::max(int, int) muodollisia parametreja ja ne voidaan heittää funktion libs_R_us::max(double, double) parametrien tyyppeihin. muuntamalla kokonaisluvut kelluiksi sekä tyypeiksi libs_R_us::max(char, char) funktioparametreiksi kokonaislukutyyppimuunnoksen avulla.


    käyttäen libs_R_us::max; char max(merkki, merkki);
    void func()
    {
    // kaikki kolme max()-funktiota ovat vakiintuneita
    max(87, 65); // kutsutaan komennolla libs_R_us::max(int, int)
    }

    Huomaa, että ehdokasfunktio, jolla on useita parametreja, poistetaan käytöstä heti, kun havaitaan, että yhtä varsinaisista argumenteista ei voida heittää vastaavan muodollisen parametrin tyyppiin, vaikka tällainen muunnos on olemassa kaikille muille argumenteille. Seuraavassa esimerkissä funktio min(char *, int) on jätetty pois säilyneiden joukosta, koska ensimmäisen int-argumentin tyyppiä ei ole mahdollista muuntaa vastaavan char * -parametrin tyypiksi. Ja tämä tapahtuu huolimatta siitä, että toinen argumentti vastaa täsmälleen toista parametria.

    extern double min(double, double); extern double min(char*, int); void func()
    {
    // yksi ehdokasfunktio min(double, double)
    min(87, 65); // soita min(double, double)
    }

    Jos ehdokasjoukosta poissulkemisen jälkeen kaikki funktiot, joilla on sopimaton määrä parametreja ja ne, joiden parametreille ei ollut sopivaa muunnosa, ei ole pysyviä, funktiokutsun käsittely päättyy käännösvirheeseen. Tässä tapauksessa sanotaan, että vastaavuutta ei löytynyt.

    Void print (unsigned int); voidprint(char*); voidprint(char); int*ip;
    luokka SmallInt ( /* ... */ );
    SmallInt si; int main() (
    tulosta(ip); // virhe: ei perustettuja toimintoja: hakua ei löytynyt
    tulostaa(si); // virhe: ei perustettuja toimintoja: hakua ei löytynyt
    paluu 0;
    }

    9.4.3. Paras vakiintunut ominaisuus

    Parhaaksi katsotaan muodostetut funktiot, joiden muodolliset parametrit vastaavat parhaiten varsinaisten argumenttien tyyppejä. Jokaisessa tällaisessa funktiossa kullekin argumentille käytetyt tyyppimuunnokset luokitellaan sen määrittämiseksi, kuinka hyvin se vastaa parametria. (Osassa 6.2 kuvataan tuetut tyyppimuunnokset.) Paras vakiintunut funktio on funktio, joka täyttää samanaikaisesti kaksi ehtoa:

    • argumenteihin sovelletut muunnokset eivät ole huonompia kuin muunnokset, jotka vaaditaan minkä tahansa muun vakiintuneen funktion kutsumiseen;
    • ainakin yhdelle argumentille käytetty muunnos on parempi kuin samalle argumentille missään muussa vakiintuneessa funktiossa.

    Saattaa käydä niin, että varsinaisen argumentin heittäminen vastaavan muodollisen parametrin tyyppiin edellyttää useita muunnoksia. Eli seuraavassa esimerkissä

    Int arr; void putArt(const int *); int main() (
    putArt(arr); // 2 muuntamista tarvitaan
    // taulukon osoittimeksi + täsmennysmuunnos
    paluu 0;
    }

    heittääksesi argumentin arr tyypistä “array of three int” tyyppiin “pointer to const int”, käytetään muunnossarjaa:

    1. Array to pointer -muunnos, joka muuntaa kolmen int:n taulukon osoittimeksi int:ksi.
    2. Specifier-muunnos, joka muuttaa osoittimen int:ksi osoittimeksi const int.

    Siksi olisi oikeampaa sanoa, että varsinaisen argumentin heittämiseksi muodostetun funktion muodollisen parametrin tyyppiin tarvitaan muunnossarja. Koska ei käytetä yhtä, vaan useita muunnoksia, funktion ylikuormituksen ratkaisuprosessin kolmas vaihe itse asiassa luokittelee muunnossekvenssit.
    Tällaisen sekvenssin järjestyksen katsotaan olevan huonoimman siihen sisältyvien muunnosten arvo. Kuten osiossa 9.2 selitetään, tyyppimuunnokset sijoittuvat seuraavasti: tarkka vastaavuus on parempi kuin tyyppilaajennus ja tyyppilaajennus parempi kuin tavallinen tulos. Edellisessä esimerkissä molemmilla muutoksilla on tarkka vastaavuussijoitus. Siksi koko sekvenssillä on sama arvo.
    Tällainen kokoelma koostuu useista muunnoksista, jotka on sovellettu esitetyssä järjestyksessä:

    l-arvon muunnos -> tyyppilaajennus tai vakiomuunnos -> spesifikaatiomuunnos

    Termi l-arvon muunnos viittaa kolmeen ensimmäiseen tarkan vastaavuuden muunnokseen, joita on käsitelty osiossa 9.2: l-arvon muunnos r-arvoksi, taulukon muunnos osoittimeksi ja funktiosta osoittimeksi muunnos. Muunnossarja koostuu nollan tai yhden l-arvon muunnoksesta, jota seuraa nolla tai yksi tyyppilaajennus tai standardimuunnos ja lopuksi nolla tai yksi spesifier-muunnos. Vain yhtä kutakin muunnostyyppiä voidaan käyttää varsinaisen argumentin syöttämiseksi muodollisen parametrin tyyppiin.

    Kuvattua sekvenssiä kutsutaan standardimuunnosten sekvenssiksi. On myös joukko käyttäjän määrittämiä muunnoksia, jotka liittyvät jäsenmuunnintoimintoon. (Muuntimia ja käyttäjän määrittämiä muunnossarjoja käsitellään luvussa 15.)

    Mitkä ovat sekvenssit, joissa todelliset argumentit muuttuvat seuraavassa esimerkissä?

    Nimiavaruus libs_R_us ( int max(int, int); double max(double, double); ) // using-declaration
    käyttäen libs_R_us::max; void func()
    {
    char c1, c2;
    max(c1, c2); // kutsu libs_R_us::max(int, int)
    }

    Max()-funktion kutsun argumentit ovat tyyppiä char. Argumenttimuunnosten järjestys kutsuttaessa libs_R_us::max(int,int) -funktiota on seuraava:

    1a. Koska argumentit välitetään arvon mukaan, l-arvon muuntaminen r-arvoksi poimii argumenttien c1 ja c2 arvot.

    2a. Argumentit muunnetaan charista int-muotoon käyttämällä tyyppilaajennusta.
    Argumenttimuunnosten järjestys libs_R_us::max(double,double)-funktiota kutsuttaessa on seuraava:
    1b. Muuttamalla l-arvon r-arvoksi saadaan argumenttien c1 ja c2 arvot.

    2b. Normaali muunnos kokonaisluku- ja floattyyppien välillä heittää argumentit tyypistä char tyyppiin double.

    Ensimmäisen sekvenssin sijoitus on tyyppilaajennus (huonoin käytetty muutos), kun taas toisen sekvenssi on vakiomuunnos. Koska tyyppilaajennus on parempi kuin tyyppimuunnos, libs_R_us::max(int,int)-funktio valitaan parhaiten sopivaksi tähän kutsuun.
    Jos argumenttimuunnosten järjestyssekvenssit eivät voi paljastaa yhtä vakiintunutta funktiota, kutsua pidetään moniselitteisenä. Tässä esimerkissä molemmat calc()-kutsut vaativat seuraavan järjestyksen:

    1. Muunna l-arvo r-arvoksi poimimaan i- ja j-argumenttiarvot.
    2. Vakiomuunnos todellisten argumenttien heittämiseksi vastaaviksi muodollisiksi parametreiksi.

    Koska ei voida sanoa, mikä näistä sarjoista on parempi kuin toinen, kutsu on epäselvä:

    Int i, j; ulkoinen pitkä kalkki (pitkä, pitkä); extern double calc(double, double); void jj() ( // virhe: epäselvyys, ei parasta vastaavuutta
    calc(i, j);
    }

    Specifier-muunnolla (const- tai volatile-määritteen lisääminen osoitinta osoittavaan tyyppiin) on tarkan vastaavuuden arvo. Kuitenkin, jos kaksi muunnossekvenssiä eroavat toisistaan ​​vain siinä, että toisen lopussa on ylimääräinen täsmennysmuunnos, niin sekvenssiä ilman sitä pidetään parempana. Esimerkiksi:

    void reset(int *); void reset(const int *); int*pi; int main() (
    reset(pi); // ilman määritteiden muuntamista on parempi:
    // valitse reset(int *)
    paluu 0;
    }

    Ensimmäisen ehdokasfunktion reset(int*) varsinaiseen argumenttiin sovellettu vakiomuunnossarja on tarkka vastaavuus, argumentin arvon erottaminen vaatii vain siirtymisen l-arvosta r-arvoon. Toiselle ehdokasfunktiolle reset(const int *) käytetään myös muunnosa l-arvosta r-arvoksi, mutta sitä seuraa myös specifier-muunnos tuloksena olevan arvon heittämiseksi osoittimesta int:iin osoittimeen const int. Molemmat sekvenssit vastaavat tarkasti, mutta siinä ei ole epäselvyyttä. Koska toinen sekvenssi eroaa ensimmäisestä siinä, että lopussa on spesifioiva muunnos, sekvenssiä ilman tällaista muunnosta pidetään parhaana. Siksi reset(int*) on paras pysyvä toiminto.
    Tässä on toinen esimerkki, jossa suoratoistomääritykset vaikuttavat valittuun sekvenssiin:

    int-ote(tyhjä*);
    int ekstrakti(const void *);

    int main() (
    ote(pi); // valitse ote (void *)
    paluu 0;
    }

    Tässä on kutsuttava kaksi perusfunktiota: extract(void*) ja extract(const void*). Poimi(void*)-funktion muunnossarja koostuu l-arvon muuntamisesta r-arvoksi argumentin arvon erottamiseksi, mitä seuraa tavallinen osoittimen muunnos: osoittimesta int osoittimeen tyhjäksi. Extract(const void*)-funktiolle tämä sekvenssi eroaa ensimmäisestä määritteiden lisämuunnolla, joka muuttaa tulostyypin osoittimesta voidiksi osoittimeksi const voidiksi. Koska sekvenssit eroavat toisistaan ​​vain tämän muunnoksen perusteella, ensimmäinen valitaan sopivammaksi ja siksi funktio extrakt(const void*) on paras pysyvä.
    Const- ja volatile-määritykset vaikuttavat myös viiteparametrien alustuksen järjestykseen. Jos kaksi tällaista alustusta eroavat toisistaan ​​vain const- ja volatile-määritteen lisäämisessä, niin alustusta ilman ylimääräistä määritystä pidetään parempana ylikuormitusresoluutiossa:

    #sisältää void manip(vektori &); void manip(const vector &); vektori f();
    ulkoinen vektori vec; int main() (
    manip(vec); // valitse manip(vektori &)
    manip(f()); // valitse manip(const vector &)
    paluu 0;
    }

    Ensimmäisessä kutsussa minkä tahansa funktiokutsun viittauksen alustus on täsmällinen vastaavuus. Mutta tämä haaste ei silti ole yksiselitteinen. Koska molemmat alustukset ovat samat, paitsi että toisessa tapauksessa on ylimääräinen const-määritelmä, alustusta ilman tällaista määritystä pidetään parempana, joten ylikuormitus ratkaistaan ​​vakiintuneen manip(vektorin) hyväksi &).
    Toista kutsua varten on vain yksi vakiintunut funktiomanip(const-vektori &). Koska varsinainen argumentti on väliaikainen muuttuja, joka sisältää f(:n palauttaman tuloksen), tällainen argumentti on r-arvo jota ei voida käyttää ei-vakiomuotoisen manip(vektorin) referenssiparametrin alustamiseen &). Siksi ainoa vakiintunut manip(const-vektori &).
    Tietenkin funktioilla voi olla useita todellisia argumentteja. Pysyvistä parhaiden valinta tulee tehdä ottaen huomioon kaikkien argumenttien muunnossekvenssien järjestys. Harkitse esimerkkiä:

    extern int ff(char*, int); extern int ff(int, int); int main() ( ff(0, "a"); // ff(int, int)
    paluu 0;
    }

    Funktio ff(), jossa on kaksi int-argumenttia, on valittu parhaaksi pysyväksi funktioksi seuraavista syistä:

    1. hänen ensimmäinen argumenttinsa on parempi. 0 vastaa täsmälleen muodollista int-parametria, kun taas tavallinen osoittimen muunnos vaaditaan vastaamaan char * -tyyppistä parametria;
    2. sen toisella argumentilla on sama arvo. Argumentille "a", jonka tyyppi on char, on käytettävä muunnossarjaa, jolla on tyyppilaajennus, jotta voidaan luoda vastaavuus kumman tahansa funktion toisen muodollisen parametrin kanssa.

    Tässä on toinen esimerkki:

    int compute(const int&, lyhyt); int compute(int&, double); ulkoinen intiobj;
    int main() (
    laske(iobj, "c"); // laske(int&, double)
    paluu 0;
    }

    Molemmat funktiot compute(const int&, short) ja compute(int&, double) säilyivät. Toinen on valittu parhaaksi seuraavista syistä:

    1. hänen ensimmäinen argumenttinsa on parempi. Ensimmäisen muodostetun funktion viitealustus on huonompi, koska se vaatii const-määritteen lisäämisen, jota ei tarvita toiselle funktiolle;
    2. sen toisella argumentilla on sama arvo. Char-tyyppiseen argumenttiin "c", jotta saataisiin aikaan vastaavuus minkä tahansa kahden funktion toisen muodollisen parametrin kanssa, on käytettävä muunnossarjaa, jolla on standardimuunnos.

    9.4.4. Argumentit oletusarvoilla

    Argumenttien käyttäminen oletusarvoilla voi laajentaa monia vakiintuneita toimintoja. Jäännös ovat toimintoja, joita kutsutaan tietyllä luettelolla todellisista argumenteista. Mutta sellaisella funktiolla voi olla enemmän muodollisia parametreja kuin todelliset argumentit annetaan, jos jokaiselle parametrille on jokin oletusarvo, jota ei ole määritetty:

    ulkoinen void ff(int); extern void ff(pitkä, int = 0); int main() (
    ff(2L); // vastaa ff(pitkä, 0); ff(0, 0); // vastaa ff(long, int);
    ff(0); // vastaa ff(int);
    ff(3,14); // virhe: epäselvyys
    }

    Ensimmäisessä ja kolmannessa kutsussa ff()-funktio ratkaistaan, vaikka vain yksi varsinainen argumentti välitettiin. Tämä johtuu seuraavista syistä:

    1. toiselle muodolliselle parametrille on oletusarvo;
    2. ensimmäinen parametri, jonka tyyppi on long, vastaa täsmälleen ensimmäisen kutsun varsinaista argumenttia, ja se voidaan heittää argumentin tyyppiin kolmannessa kutsussa sekvenssillä, jolla on vakiomuunnos.

    Viimeinen kutsu on epäselvä, koska molemmat muodostetut funktiot voidaan valita soveltamalla standardimuunnos ensimmäiseen argumenttiin. Funktiota ff(int) ei suositella vain siksi, että sillä on yksi parametri.

    Harjoitus 9.9

    Selitä, mitä tapahtuu, kun ratkaiset ylikuormituksen kutsumalla compute()-funktion main() sisällä. Mitä ominaisuuksia ehdokkaat ovat? Kumpi heistä kestää ensimmäisen askeleen jälkeen? Mitä muunnossarjaa tulee soveltaa varsinaiseen argumenttiin, jotta se vastaa kunkin muodostetun funktion muodollista parametria? Mikä toiminto on paras asema?

    Nimiavaruus primerLib ( void compute(); void compute(const void *); ) käyttäen primerLib::compute;
    void compute(int);
    void compute(double, double = 3,4);
    void compute(char*, char* = 0); int main() (
    laske(0);
    paluu 0;
    }

    Mitä tapahtuu, jos use-ilmoitus sijoitetaan main():n sisään ennen compute()-kutsua? Vastaa samoihin kysymyksiin.

    Toiminnan ylikuormitus

    Toimintojen ylikuormitus (operaattorit, toiminnot, menettelyt)- ohjelmoinnissa - yksi tavoista toteuttaa polymorfismi, joka koostuu mahdollisuudesta esiintyä samanaikaisesti yhdessä piirissä useita eri toiminnan muunnelmia (operaattori, toiminto tai proseduuri), joilla on sama nimi, mutta jotka eroavat parametrien tyypeistä joihin niitä sovelletaan.

    Terminologia

    Termi "overloading" on englanninkielisen "overloading" -kutsupaperi, joka ilmestyi venäjänkielisissä käännöksissä ohjelmointikieliä koskevista kirjoista 1990-luvun ensimmäisellä puoliskolla. Ehkä tämä ei ole paras käännösvaihtoehto, koska venäjän sanalla "ylikuormitus" on oma vakiintunut merkitys, joka eroaa radikaalisti äskettäin ehdotetusta, mutta se on kuitenkin juurtunut ja on melko laajalle levinnyt. Neuvostoajan julkaisuissa samanlaisia ​​mekanismeja kutsuttiin venäjäksi "uudelleenmäärittelyksi" tai "uudelleenmäärittelyksi", mutta tämä vaihtoehto ei ole kiistanalainen: englanninkielisten "override", "overload" ja "redefinition" käännöksissä syntyy ristiriitoja ja hämmennystä. uudelleenmääritellä".

    Syitä esiintymiseen

    Useimmissa varhaisissa ohjelmointikielissä oli rajoitus, jonka mukaan ohjelmassa ei voinut olla käytettävissä enempää kuin yksi operaatio samalla nimellä. Vastaavasti kaikilla ohjelman tietyssä kohdassa näkyvillä funktioilla ja proseduureilla on oltava eri nimet. Ohjelmoija ei voi käyttää ohjelmointikieleen kuuluvien funktioiden, proseduurien ja operaattoreiden nimiä ja merkintöjä omien toimintojensa, proseduurien ja operaattoreiden nimeämiseen. Joissakin tapauksissa ohjelmoija voi luoda oman ohjelmaobjektin toisen jo olemassa olevan objektin nimellä, mutta silloin uusi luotu objekti "päällekyy" edellisen kanssa, jolloin molempien vaihtoehtojen käyttäminen samanaikaisesti tulee mahdottomaksi.

    Tämä tilanne on epämukava joissakin melko yleisissä tapauksissa.

    • Joskus on tarve kuvata ja soveltaa ohjelmoijan luomiin tietotyyppeihin operaatioita, jotka vastaavat merkitykseltään kielellä jo saatavilla olevia. Klassinen esimerkki on kirjasto kompleksilukujen käsittelyä varten. Ne, kuten tavalliset numeeriset tyypit, tukevat aritmeettisia operaatioita, ja tälle operaatiolle olisi luonnollista luoda "plus", "miinus", "kerto", "jakaa" merkitsemällä ne samoilla operaatiomerkeillä kuin muille numeerisille operaatioille. tyypit. Kielellä määriteltyjen elementtien käyttökielto pakottaa luomaan monia funktioita nimillä, kuten ComplexPlusComplex, IntegerPlusComplex, ComplexMinusFloat ja niin edelleen.
    • Kun eri tyyppisiin operandeihin sovelletaan samaa merkitystä omaavia operaatioita, ne pakotetaan nimeämään eri tavalla. Kyvyttömyys käyttää samannimisiä funktioita erityyppisille funktioille johtaa siihen, että samalle asialle on keksittävä eri nimiä, mikä aiheuttaa sekaannusta ja voi jopa johtaa virheisiin. Esimerkiksi klassisessa C-kielessä on kaksi versiota vakiokirjastofunktiosta luvun moduulin löytämiseksi: abs() ja fabs() - ensimmäinen on tarkoitettu kokonaislukuargumentille, toinen todelliselle argumentille. Tämä tilanne yhdistettynä heikon C-tyypin tarkistukseen voi johtaa vaikeasti löydettäviin virheisiin: jos ohjelmoija kirjoittaa abs(x):n laskelmaan, jossa x on todellinen muuttuja, jotkin kääntäjät generoivat koodia ilman varoitusta, joka muunna x kokonaisluvuksi hylkäämällä murto-osat ja laske moduuli tuloksena olevasta kokonaisluvusta!

    Osittain ongelma ratkeaa olioohjelmoinnin avulla - kun uudet tietotyypit määritellään luokiksi, niille tehtävät toiminnot voidaan formalisoida luokkametodeina, mukaan lukien samannimiset luokkamenetelmät (koska eri luokkien menetelmillä ei tarvitse olla eri nimet), mutta ensinnäkin tällainen suunnittelutapa toimia erityyppisillä arvoilla on hankalaa, ja toiseksi se ei ratkaise uusien operaattoreiden luomisen ongelmaa.

    Operaattorin ylikuormitus itsessään on vain syntaktista sokeria, vaikka sellaisenaankin siitä voi olla hyötyä, koska sen avulla kehittäjä voi ohjelmoida luonnollisemmalla tavalla ja saa mukautetut tyypit käyttäytymään enemmän kuin sisäänrakennetut. Jos lähestymme asiaa yleisemmästä näkökulmasta, niin voimme nähdä, että työkalut, joiden avulla voit laajentaa kieltä, täydentää sitä uusilla operaatioilla ja syntaksisilla rakenteilla (ja operaatioiden ylikuormitus on yksi tällaisista työkaluista, objektien, makrojen ohella , funktionaaliset, sulkemiset) kääntävät sen jo metakielessä - keinona kuvata kieliä, jotka on suunnattu tiettyihin tehtäviin. Sen avulla on mahdollista rakentaa kullekin tietylle tehtävälle sille sopivin kielilaajennus, jonka avulla sen ratkaisua voidaan kuvata luonnollisimmassa, ymmärrettävämmässä ja yksinkertaisimmassa muodossa. Esimerkiksi ylikuormitusoperaatioiden sovelluksessa: monimutkaisten matemaattisten tyyppien (vektorit, matriisit) kirjaston luominen ja operaatioiden kuvaaminen niillä luonnollisessa, "matemaattisessa" muodossa luo "vektorioperaatioiden kielen", jossa monimutkaisuus laskelmat on piilotettu, ja ongelmien ratkaisua on mahdollista kuvata vektori- ja matriisioperaatioilla keskittyen ongelman olemukseen, ei tekniikkaan. Näistä syistä sellaiset keinot sisällytettiin kerran Algol-68-kieleen.

    Ylikuormitusmekanismi

    Toteutus

    Operaattorin ylikuormitukseen liittyy kahden toisiinsa liittyvän ominaisuuden tuominen kieleen: kyky ilmoittaa useita samalla nimellä olevia proseduureja tai funktioita samassa laajuudessa ja kyky kuvata omia toimintojen toteutuksia (eli toimintojen merkkejä, yleensä kirjoitettu infix-merkinnällä, operandien väliin). Periaatteessa niiden toteutus on melko yksinkertaista:

    • Useiden samannimien operaatioiden olemassaolon mahdollistamiseksi riittää, että kieleen viedään sääntö, jonka mukaan kääntäjä tunnistaa toiminnon (proseduurin, funktion tai operaattorin) paitsi nimen (notaatioiden) perusteella. myös parametrien tyypeillä. Joten abs(i), jossa i on ilmoitettu kokonaislukuna, ja abs(x), jossa x ilmoitetaan reaaliksi, ovat kaksi eri operaatiota. Pohjimmiltaan tällaisen tulkinnan tarjoaminen ei ole vaikeuksia.
    • Operaatioiden määrittelyn ja uudelleenmäärittelyn mahdollistamiseksi on tarpeen tuoda kieleen sopivat syntaktiset rakenteet. Vaihtoehtoja voi olla melko paljon, mutta itse asiassa ne eivät eroa toisistaan, riittää, että muistat, että lomakkeen syöttö "<операнд1> <знакОперации> <операнд2>» on pohjimmiltaan samanlainen kuin funktion kutsuminen «<знакОперации>(<операнд1>,<операнд2>)". Riittää, kun annat ohjelmoijan kuvata operaattoreiden käyttäytymistä funktioiden muodossa - ja kuvausongelma on ratkaistu.

    Vaihtoehdot ja ongelmat

    Proseduurien ja toimintojen ylikuormittaminen yleisidean tasolla ei pääsääntöisesti ole vaikea toteuttaa tai ymmärtää. Kuitenkin myös siinä on joitain "sudenkuoppia", jotka on otettava huomioon. Operaattorin ylikuormituksen salliminen aiheuttaa paljon enemmän ongelmia sekä kielen toteuttajalle että tällä kielellä työskentelevälle ohjelmoijalle.

    Tunnistautumisongelma

    Ensimmäinen kysymys, jonka prosessien ja toimintojen ylikuormituksen sallivan kielenkääntäjän kehittäjä joutuu kohtaamaan, on: kuinka valita samannimisten menettelyjen joukosta se, jota tässä tapauksessa tulisi soveltaa? Kaikki on kunnossa, jos proseduurista on variantti, jonka muodollisten parametrien tyypit vastaavat täsmälleen tässä kutsussa käytettyjen todellisten parametrien tyyppejä. Kuitenkin lähes kaikilla kielillä tyyppien käytössä on jonkin verran vapautta olettaen, että kääntäjä suorittaa tietyissä tilanteissa automaattisesti tyyppiturvallisia muunnoksia. Esimerkiksi reaali- ja kokonaislukuargumenttien aritmeettisissa operaatioissa kokonaisluku muunnetaan yleensä reaalityypiksi automaattisesti, ja tulos on todellinen. Oletetaan, että lisäysfunktiosta on kaksi muunnelmaa:

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

    Miten kääntäjän tulee käsitellä lauseketta y = add(x, i), missä x on float ja i on int? Ilmeisesti ei ole tarkkaa vastaavuutta. Vaihtoehtoja on kaksi: joko y=add_int((int)x,i) tai y=add_flt(x, (float)i) (tässä nimet add_int ja add_float tarkoittavat funktion ensimmäistä ja toista versiota, vastaavasti) .

    Herää kysymys: pitäisikö kääntäjän sallia tällainen ylikuormitettujen funktioiden käyttö, ja jos, niin millä perusteella se valitsee käytetyn muunnelman? Pitäisikö kääntäjän erityisesti yllä olevassa esimerkissä ottaa huomioon muuttujan y tyyppi valinnassa? On huomattava, että yllä oleva tilanne on yksinkertaisin, paljon monimutkaisemmat tapaukset ovat mahdollisia, mitä pahentaa se, että paitsi sisäänrakennettuja tyyppejä voidaan muuntaa kielen sääntöjen mukaan, myös ohjelmoijan ilmoittamia luokkia , jos heillä on sukulaisuussuhteita, voidaan heittää toisiinsa. Tähän ongelmaan on kaksi ratkaisua:

    • Estä epätarkka tunnistaminen ollenkaan. Vaadi, että jollekin tietylle tyyppiparille on olemassa täsmälleen oikea variantti ylikuormitetusta menettelystä tai operaatiosta. Jos tällaista vaihtoehtoa ei ole, kääntäjän pitäisi antaa virheilmoitus. Ohjelmoijan on tässä tapauksessa käytettävä eksplisiittinen muunnos todellisten parametrien lähettämiseksi haluttuun tyyppisarjaan. Tämä lähestymistapa on hankala kielillä, kuten C++, jotka sallivat melkoisen vapauden käsitellä tyyppejä, koska se johtaa merkittävään eroon sisäänrakennettujen ja ylikuormitettujen operaattoreiden käyttäytymisessä (aritmeettisia operaatioita voidaan soveltaa tavallisiin numeroihin ajattelematta, vaan muihin tyyppeihin - vain nimenomaisella muuntamalla) tai valtavan määrän vaihtoehtoja operaatioille.
    • Määritä tietyt säännöt "lähimmän istuvuuden" valitsemiseksi. Yleensä tässä versiossa kääntäjä valitsee varianteista ne, joiden kutsut saadaan lähteestä vain turvallisilla (häviöttömän tiedon) tyyppimuunnoksilla, ja jos niitä on useita, voi valita sen perusteella, kumpi muunnelma vaatii vähemmän. tällaisia ​​muunnoksia. Jos tulos jättää useamman kuin yhden mahdollisuuden, kääntäjä antaa virheen ja vaatii ohjelmoijaa määrittämään muunnelman.

    Toiminnan ylikuormitusta koskevat erityiset huomiot

    Toisin kuin menettelyt ja toiminnot, ohjelmointikielten infix-operaatioilla on kaksi lisäominaisuutta, jotka vaikuttavat merkittävästi niiden toimivuuteen: prioriteetti ja assosiatiivisuus, joiden olemassaolo johtuu mahdollisuudesta kirjoittaa "ketju"-operaattoreita (miten ymmärtää a + b * c : kuten (a + b)*c tai kuten a+(b*c)? Ilmaisu a-b+c on (ab)+c tai a-(b+c) ?).

    Kieleen sisäänrakennetuilla operaatioilla on aina ennalta määritelty perinteinen etusija ja assosiatiivisuus. Herää kysymys: mitä prioriteetteja ja assosiatiivisuutta on näiden operaatioiden uudelleenmääritellyillä versioilla tai lisäksi ohjelmoijan luomilla uusilla operaatioilla? On muitakin yksityiskohtia, jotka saattavat vaatia selvennystä. Esimerkiksi C:ssä on kahdenlaisia ​​lisäys- ja vähennysoperaattoreita ++ ja -- - etuliite ja jälkiliite, jotka toimivat eri tavalla. Miten tällaisten operaattoreiden ylikuormitettujen versioiden pitäisi käyttäytyä?

    Eri kielet käsittelevät näitä asioita eri tavoin. Siten C++:ssa operaattoreiden ylikuormitettujen versioiden ensisijaisuus ja assosiaatio säilyy samoina kuin kielessä määritellyt; on mahdollista ylikuormittaa erikseen lisäys- ja vähennysoperaattoreiden etu- ja jälkiliitemuodot erityisillä allekirjoituksilla:

    Joten int:tä käytetään allekirjoitusten muuttamiseen

    Ilmoitus uusista toiminnoista

    Tilanne uusien toimintojen julkistamisen myötä on vielä monimutkaisempi. Tällaisen julistuksen mahdollisuuden sisällyttäminen kieleen ei ole vaikeaa, mutta sen täytäntöönpano on täynnä merkittäviä vaikeuksia. Uuden operaation ilmoittaminen on itse asiassa uuden ohjelmointikielen avainsanan luomista, ja sitä monimutkaistaa se, että tekstissä olevat toiminnot voivat yleensä seurata muita tokeneita ilman erottimia. Kun ne ilmestyvät, leksikaalisen analysaattorin järjestämisessä syntyy lisävaikeuksia. Esimerkiksi, jos kielessä on jo operaatiot "+" ja unaari "-" (merkkimuutos), lauseke a+-b voidaan tulkita tarkasti + (-b) , mutta jos uusi operaatio +- on Ohjelmassa ilmoitettuna syntyy heti epäselvyyttä, koska sama lauseke voidaan jo jäsentää muodossa a (+-) b . Kielen kehittäjän ja toteuttajan tulee käsitellä tällaisia ​​ongelmia jollain tavalla. Vaihtoehdot voivat taas olla erilaisia: vaatia, että kaikki uudet toiminnot ovat yksimerkkisiä, oletetaan, että mahdollisille eroavaisuuksille valitaan operaation "pisin" versio (eli kunnes kääntäjän lukema seuraava merkkisarja täsmää mikä tahansa toiminto, sen lukeminen jatkuu), yritä havaita törmäykset käännöksen aikana ja luoda virheitä kiistanalaisissa tapauksissa ... Tavalla tai toisella kielet, jotka mahdollistavat uusien toimintojen ilmoittamisen, ratkaisevat nämä ongelmat.

    Ei pidä unohtaa, että uusien toimintojen yhteydessä on myös kysymys assosiatiivisuuden ja prioriteetin määrittämisestä. Vakiokielioperaation muodossa ei ole enää valmiita ratkaisuja, ja yleensä sinun on vain asetettava nämä parametrit kielen säännöillä. Tee esimerkiksi kaikista uusista operaatioista vasen-assosiatiivisia ja anna niille sama, kiinteä, prioriteetti tai ota käyttöön kieleen keino molempien määrittämiseen.

    Ylikuormitus ja polymorfiset muuttujat

    Kun ylikuormitettuja operaattoreita, toimintoja ja proseduureja käytetään vahvasti kirjoitetuissa kielissä, joissa jokaisella muuttujalla on ennalta määritetty tyyppi, on kääntäjän tehtävä valita, mitä ylikuormitetun operaattorin muunnelmaa käyttää kussakin tapauksessa, olipa se kuinka monimutkainen tahansa. . Tämä tarkoittaa, että käännetyillä kielillä operaattorin ylikuormituksen käyttö ei johda suorituskyvyn heikkenemiseen - joka tapauksessa ohjelman objektikoodissa on hyvin määritelty operaatio- tai funktiokutsu. Tilanne on toinen, kun kielessä on mahdollista käyttää polymorfisia muuttujia, eli muuttujia, jotka voivat sisältää eri tyyppisiä arvoja eri aikoina.

    Koska arvon tyyppi, johon ylikuormitettua toimintoa sovelletaan, ei ole tiedossa koodin käännöshetkellä, kääntäjä ei voi valita oikea vaihtoehto etukäteen. Tässä tapauksessa objektikoodiin on pakko upottaa fragmentti, joka välittömästi ennen tämän toiminnon suorittamista määrittää argumenttien arvojen tyypit ja valitsee dynaamisesti tätä tyyppijoukkoa vastaavan muunnelman. Lisäksi tällainen määritelmä on tehtävä jokaisen toiminnon suorituksen yhteydessä, koska jopa sama koodi, kun sitä kutsutaan toisen kerran, voidaan suorittaa eri tavalla.

    Siten operaattorin ylikuormituksen käyttö yhdessä polymorfisten muuttujien kanssa tekee väistämättömäksi dynaamisesti määrittää, mitä koodia kutsutaan.

    Kritiikkiä

    Kaikki asiantuntijat eivät pidä ylikuormituksen käyttöä siunauksena. Jos toimintojen ja proseduurien ylikuormitus ei yleensä ole moitittavaa (osittain koska se ei johda joihinkin tyypillisiin "operaattorin" ongelmiin, osittain siksi, että on vähemmän houkuttelevaa käyttää sitä väärin), niin operaattorin ylikuormitus on periaatteessa , ja tietyissä kielitoteutuksissa, on joutunut melko ankaran kritiikin kohteeksi monien ohjelmointiteoreetikojen ja -harjoittajien taholta.

    Kriitikot huomauttavat, että yllä kuvatut identifiointi-, tärkeysjärjestys- ja assosiatiivisuusongelmat tekevät usein ylikuormitettujen operaattoreiden käsittelystä joko tarpeettoman vaikeaa tai luonnotonta:

    • Henkilöllisyystodistus. Jos kielellä on tiukat tunnistussäännöt, ohjelmoijan on pakko muistaa, mille tyyppiyhdistelmille on ylikuormitettuja operaatioita ja lähettää niihin manuaalisesti operandeja. Jos kieli sallii "likimääräisen" tunnistamisen, ei koskaan voi olla varma, että jossain melko monimutkaisessa tilanteessa suoritetaan täsmälleen se versio operaatiosta, jonka ohjelmoija oli mielessään.
    • Prioriteetti ja assosiaatio. Jos ne on määritelty tiukasti, tämä voi olla hankalaa eikä aihepiiriin liittyvää (esimerkiksi joukkooperaatioissa prioriteetit poikkeavat aritmeettisista). Jos ohjelmoija voi asettaa ne, tästä tulee ylimääräinen virhelähde (jos vain siksi, että yhden toiminnon eri muunnelmilla osoittautuu olevan erilaiset prioriteetit tai jopa assosiatiivisuus).

    Kuinka paljon omien toimintojen käyttömukavuus voi painaa ohjelman hallittavuuden huononemisesta aiheutuvan haitan, on kysymys, johon ei ole selkeää vastausta.

    Kielitoteutuksen näkökulmasta samat ongelmat johtavat kääntäjien monimutkaisuuteen ja niiden tehokkuuden ja luotettavuuden heikkenemiseen. Ja ylikuormituksen käyttö polymorfisten muuttujien yhteydessä on myös selvästi hitaampaa kuin kovakoodatun toiminnon kutsuminen kääntämisen aikana ja tarjoaa vähemmän mahdollisuuksia objektikoodin optimointiin. Ylikuormituksen toteutuksen erityispiirteet eri kielillä ovat erillisen kritiikin kohteena. Joten C++:ssa kritiikin kohteena voi olla ylikuormitettujen funktioiden nimien sisäisen esityksen puuttuminen, mikä aiheuttaa yhteensopimattomuutta eri C++-kääntäjien kääntämien kirjastojen tasolla.

    Jotkut kriitikot vastustavat ylikuormitusta ohjelmistotekniikan teorian yleisten periaatteiden ja todellisen teollisen käytännön pohjalta.

    • "Puritaanisen" lähestymistavan kannattajat kielen rakentamiseen, kuten Wirth tai Hoare, vastustavat operaattorin ylikuormitusta yksinkertaisesti siksi, että siitä voidaan helposti luopua. Heidän mielestään tällaiset työkalut vain vaikeuttavat kieltä ja kääntäjää tarjoamatta tätä monimutkaista lisäominaisuuksia. Heidän mielestään ajatus tehtäväkeskeisen kielen laajennuksen luomisesta näyttää vain houkuttelevalta. Todellisuudessa kielenlaajennustyökalujen käyttö tekee ohjelman ymmärrettävän vain sen tekijälle - tämän laajennuksen kehittäjälle. Ohjelmasta tulee paljon vaikeampi muiden ohjelmoijien ymmärtää ja analysoida, mikä tekee ylläpidosta, muokkaamisesta ja tiimin kehittämisestä vaikeampaa.
    • On huomattava, että itse ylikuormituksen mahdollisuus on usein provosoiva rooli: ohjelmoijat alkavat käyttää sitä aina kun mahdollista, minkä seurauksena työkalusta, joka on suunniteltu yksinkertaistamaan ja virtaviivaistamaan ohjelmaa, tulee sen monimutkaisuuden ja hämmennyksen syy.
    • Ylikuormitetut operaattorit eivät välttämättä tee juuri sitä, mitä heiltä odotetaan lajinsa perusteella. Esimerkiksi a + b tarkoittaa yleensä (mutta ei aina) samaa kuin b + a, mutta "yksi" + "kaksi" eroaa sanasta "kaksi" + "yksi" kielissä, joissa +-operaattori on ylikuormitettu. merkkijonojen ketjutus.
    • Operaattorin ylikuormitus tekee ohjelmafragmenteista kontekstiherkempiä. Ilman tuntemaan lausekkeen operandityyppejä on mahdotonta ymmärtää, mitä lauseke tekee, jos se käyttää ylikuormitettuja operaattoreita. Esimerkiksi C++-ohjelmassa lauseke<< может означать и побитовый сдвиг, и вывод в поток. Выражение a << 1 возвращает результат побитового сдвига значения a на один бит влево, если a - целая переменная, но если a является выходным потоком , то же выражение выведет в этот поток строку «1» .

    Luokittelu

    Seuraavassa on joidenkin ohjelmointikielten luokitus sen mukaan, sallivatko ne operaattorin ylikuormituksen ja ovatko operaattorit rajoitettu ennalta määritettyyn joukkoon:

    Toiminnot Ei ylikuormitusta On ylikuormitus
    Rajoitettu määrä toimintoja
    • Tavoite-C
    • Python
    On mahdollista määritellä uusia toimintoja
    • PostgreSQL
    • Katso myös

      Wikimedia Foundation. 2010 .

      Katso, mitä "Function Overloading" on muissa sanakirjoissa:

        - (operaattorit, funktiot, menettelyt) ohjelmoinnissa yksi tavoista toteuttaa polymorfismi, joka koostuu useiden operaatioiden eri muunnelmien (operaattori, funktio tai ... ... Wikipedia) mahdollisuudesta olla samanaikaisesti samassa piirissä.

    C++ antaa sinun määrittää useamman kuin yhden määritelmän toimintoja nimi tai operaattori samalla alueella kuin ylikuormitustoiminto Ja operaattorin ylikuormitukset vastaavasti.

    Ylikuormitettu ilmoitus on ilmoitus, joka on ilmoitettu samalla nimellä kuin aiemmin ilmoitettu ilmoitus samassa laajuudessa, paitsi että molemmilla ilmoituksilla on eri argumentit ja ilmeisesti eri määritelmä (toteutus).

    Kun soitat ylikuormitetulle toiminto tai operaattori, kääntäjä määrittää sopivimman käytettävän määritelmän vertaamalla argumenttityyppejä, joita käytit kutsumaan funktiota tai operaattoria määritelmissä määritettyihin parametrityyppeihin. Kutsutaan prosessia, jossa valitaan sopivin ylikuormitettu toiminto tai operaattori ylikuormituksen resoluutio .

    Toimintojen ylikuormitus C++:ssa

    Sinulla voi olla useita määritelmiä samalle funktion nimelle samassa laajuudessa. Funktiomääritelmien tulee erota toisistaan ​​argumenttiluettelon argumenttien tyypin ja/tai lukumäärän suhteen. Et voi ylikuormittaa funktiomäärittelyjä, jotka eroavat vain palautustyypeillään.

    Alla on esimerkki, jossa sama toiminto Tulosta() käytetään erityyppisten tietojen tulostamiseen -

    #sisältää käyttäen nimiavaruutta std; luokka printData ( julkinen: 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; }

    Tulostussisä: 5 Kelluva tulostus: 500.263 Tulostusmerkki: Hello C++

    Operaattorin ylikuormitus C++:ssa

    Voit ohittaa tai ylikuormittaa useimmat C++:n sisäänrakennetut operaattorit. Siten ohjelmoija voi käyttää myös käyttäjän määrittelemiä operaattoreita.

    Ylikuormitetut operaattorit ovat toimintoja, joilla on erityiset nimet: avainsana "operaattori", jota seuraa määritettävän operaattorin symboli. Kuten kaikilla muillakin funktioilla, ylikuormitetulla operaattorilla on palautustyyppi ja parametriluettelo.

    Laatikkooperaattori+(const Box&);

    ilmoittaa append-operaattorin, johon voidaan käyttää lisäyksiä kaksi Box-objektia ja palauttaa lopullisen Box-objektin. Useimmat ylikuormitetut operaattorit voidaan määritellä tavallisiksi ei-jäsenfunktioiksi tai luokan jäsenfunktioiksi. Jos määritämme yllä olevan funktion ei-luokan jäsenfunktioksi, meidän on välitettävä kullekin operandille kaksi argumenttia seuraavasti:

    Laatikkooperaattori+(const Box&, const Box&);

    Alla on esimerkki operaattorin käsitteestä, kun se ladataan jäsenfunktiolla. Tässä objekti välitetään argumenttina, jonka ominaisuuksiin päästään tällä objektilla, objektia, joka kutsuu tätä operaattoria, voidaan käyttää käyttämällä Tämä operaattori alla kuvatulla tavalla -

    #sisältää käyttäen nimiavaruutta std; class Box ( julkinen: double getVolume(void) ( paluu pituus * leveys * korkeus; ) void setPituus(double len) ( pituus = len; ) void setBreadth(double bre) ( leveys = bre; ) void setHeight(double hei) ( korkeus = hei; ) // Ylikuormitus + operaattori kahden laatikkoobjektin lisäämiseksi. Laatikkooperaattori+(jatkuva Box& b) ( Box box; box.length = this->length + b.length; box.breadth = tämä->leveys + b .breadth; box.height = tämä->korkeus + b.korkeus; paluulaatikko; ) yksityinen: kaksinkertainen pituus; // Laatikon pituus kaksinkertainen leveys; // Laatikon leveys kaksinkertainen korkeus; // Laatikon korkeus ) ; // Ohjelman päätoiminto int main() ( Box Box1; // Ilmoita Box1 tyypin Box Box2; // Ilmoita Box2 tyypin Box Box3; // Ilmoi Box3 tyypin Box double volume = 0.0; // Store laatikon tilavuus tässä // laatikon 1 määrittely Box1.setLength(6.0); Box1.setBreadth(7.0); Box1.setHeight(5.0); // laatikon 2 määrittely Box2.setLength(12.0); ;Box2.setHeight(10.0) ); // laatikon 1 tilavuus tilavuus = Box1.getVolume(); cout<< "Volume of Box1: " << volume <

    Kun yllä oleva koodi käännetään ja suoritetaan, se tuottaa seuraavan tulosteen:

    Laatikon 1 tilavuus: 210 Laatikon 2 tilavuus: 1560 Laatikon 3 tilavuus: 5400

    Ylikuormitettavat / Ei-ylikuormitettavatOperaattorit

    Alla on luettelo operaattoreista, jotka voivat olla ylikuormitettuja.

    Funktioiden ylikuormitus tarkoittaa useiden funktioiden (kahden tai useamman) määrittelyä samalla nimellä, mutta eri parametreilla. Ylikuormitettujen toimintojen parametrijoukot voivat vaihdella järjestyksen, lukumäärän ja tyypin mukaan. Siten toimintojen ylikuormitusta tarvitaan, jotta vältetään samankaltaisia ​​toimintoja, mutta eri ohjelmalogiikkaa suorittavien funktioiden nimien päällekkäisyys. Harkitse esimerkiksi areaRectangle()-funktiota, joka laskee suorakulmion alueen.

    Float areaRectangle(float, float) //funktio, joka laskee suorakulmion alueen kahdella parametrilla a(cm) ja b(cm) ( palauttaa a * b; // kerro suorakulmion sivujen pituudet ja palauttaa tuloksena oleva tuote)

    Tämä on siis funktio, jossa on kaksi float-tyyppistä parametria, ja funktiolle välitettävien argumenttien tulee olla senttimetreinä, float-tyypin palautusarvo on myös senttimetreinä.

    Oletetaan, että lähtötietomme (suorakulmion sivut) on annettu metreinä ja senttimetreinä, esimerkiksi: a = 2m 35 cm; b = 1m 86 cm Tässä tapauksessa olisi kätevää käyttää neljän parametrin funktiota. Toisin sanoen jokainen suorakulmion sivujen pituus välitetään funktiolle kahdessa parametrissa: metreinä ja senttimetreinä.

    Float areaRectangle(float a_m, float a_sm, float b_m, float b_sm) // toiminto, joka laskee suorakulmion alueen 4 parametrilla a(m) a(cm); b(m) b(cm) ( paluu (a_m * 100 + a_sm) * (b_m * 100 + b_sm); )

    Funktion rungossa metreinä välitetyt arvot (a_m ja b_m) muunnetaan senttimetreiksi ja lisätään arvoihin a_sm b_sm, minkä jälkeen kerrotaan summat ja saadaan suorakulmion pinta-ala. cm. Tietenkin oli mahdollista muuntaa alkuperäiset tiedot senttimetreiksi ja käyttää ensimmäistä funktiota, mutta nyt ei ole kyse siitä.

    Nyt tärkeintä on, että meillä on kaksi funktiota, joilla on eri allekirjoitukset, mutta samat nimet (ylikuormitetut funktiot). Allekirjoitus on funktion nimen ja sen parametrien yhdistelmä. Miten näitä toimintoja kutsutaan? Ja ylikuormitettujen funktioiden kutsuminen ei eroa tavallisten funktioiden kutsumisesta, esimerkiksi:

    AreaRectangle(32, 43); // kutsutaan funktiota, joka laskee suorakulmion alueen kahdella parametrilla a(cm) ja b(cm) areaRectangle(4, 43, 2, 12); // kutsutaan funktiota, joka laskee suorakulmion alueen 4 parametrilla a(m) a(cm); b(m) b(cm)

    Kuten näet, kääntäjä valitsee itsenäisesti halutun toiminnon analysoimalla vain ylikuormitettujen funktioiden allekirjoituksia. Ohitamalla funktion ylikuormitus, voisi yksinkertaisesti ilmoittaa funktion eri nimellä, ja se tekisi tehtävänsä hyvin. Mutta kuvittele mitä tapahtuu, jos tarvitset enemmän kuin kaksi tällaista toimintoa, esimerkiksi 10. Ja jokaiselle sinun on keksittävä merkityksellinen nimi, ja vaikein asia muistaa ne. Juuri siksi toimintoja on helpompi ja parempi ylikuormittaa, ellei siihen tietenkään ole tarvetta. Ohjelman lähdekoodi näkyy alla.

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

    // koodi Koodi::Blocks

    // Dev-C++-koodi

    #sisältää käyttäen nimiavaruutta std; // ylikuormitettujen funktioiden prototyypit float areaRectangle(float a, float b); kellua alueSuorakulmio(kelluke a_m, kellua a_sm, kellua b_m, kellua b_sm); int main() ( cout<< "S1 = " << areaRectangle(32,43) << endl; // вызов перегруженной функции 1 cout << "S2 = " << areaRectangle(4, 43, 2, 12) << endl; // вызов перегруженной функции 2 return 0; } // перегруженная функция 1 float areaRectangle(float a, float b) //функция, вычисляющая площадь прямоугольника с двумя параметрами a(см) и b(см) { return a * b; // умножаем длинны сторон прямоугольника и возвращаем полученное произведение } // перегруженная функция 2 float areaRectangle(float a_m, float a_sm, float b_m, float b_sm) // функция, вычисляющая площадь прямоугольника с 4-мя параметрами a(м) a(см); b(м) b(cм) { return (a_m * 100 + a_sm) * (b_m * 100 + b_sm); }

    Ohjelman tulos näkyy kuvassa 1.