M.A. Jankowska, G. Sypniewska-Kamińska
LABORATORIUM NR 03
TEMAT : WYBRANE ZAAWANSOWANE ZAGADNIENIA ZWIĄZANE Z FUNKCJAMI W C++
I. Przeciążanie funkcji
Terminami przeciążanie, przeładowanie albo polimorfizm funkcji określamy rozwiązanie pozwalające na stosowanie w jednym projekcie tej samej nazwy dla wielu funkcji. Listę parametrów formalnych funkcji nazywamy sygnaturą funkcji. Jeżeli dwie funkcje mają identyczne listy parametrów formalnych, co do liczby parametrów, ich typów i uporządkowania na liście, to mówimy, że funkcje mają takie same sygnatury. Nazwy parametrów są nieistotne, mogą się różnić. W języku C++ można nadawać identyczne nazwy funkcjom, które różnią się sygnaturami, na przykład liczbą parametrów lub ich typami.
Poniżej wypisano prototypy kilku przeciążonych funkcji, które w pewnym projekcie służą do obliczania sumy dwóch albo trzech liczb.
double add(double a, double b);
double add(double a, double b, double c);
double add(double* a, double b);
double add(const double* a, double b);
Sygnatury tych funkcji są różne, więc można stosować przeciążanie. Kompilator na podstawie liczby i typów argumentów aktualnych pojawiających się przy wywołaniu funkcji add wybierze odpowiednią jej wersję.
Zwróćmy uwagę, że tylko różne sygnatury są wymagane do zastosowania mechanizmu przeciążania funkcji, można więc przy spełnieniu tego warunku nadawać jednakową nazwę wielu funkcjom o rozmaitym przeznaczeniu. Zazwyczaj jednak z przeciążania korzysta się w celu objęcia wspólną nazwą kilku funkcji służących do tego samego celu, ale mających różne co do liczby albo najczęściej tylko co typu parametry. Poniżej kod programu, w którym zastosowano polimorfizm funkcji dla kilku funkcji służących do dodawania dwóch bądź trzech argumentów różnych typów.
#include "stdafx.h"
#include <iostream>
using std::cout; using std::cin; using std::endl;
double add(double a, double b);
double add(double a, double b, double c);
double add(double* a, double b);
double add(const double* a, double b);
int _tmain(int argc, _TCHAR* argv[])
{
double x, y, z;
cout <<"x = ";cin>>x;
cout <<"y = ";cin>>y;
cout <<"x + y = "<< add(x,y)<<endl; cout <<"z = ";cin>>z;
cout <<"x + y + z = "<< add(x,y,z)<<endl; cout <<"x + z = "<< add(&x, z)<<endl; cout <<"y + z = "<< add(&y, z)<<endl; return 0;
}
double add(double a, double b)
{
return a+b;
}
double add(double a, double b, double c)
{
return a+b+c;
}
double add(double* a, double b)
{
return *a+b;
}
double add(const double* a, double b)
{
return *a+b;
}
Należy pamiętać, że w trakcie kompilacji typ i referencja do tego typu są nierozróżnialne. Zatem pomimo formalnej różnicy w obu postaciach list parametrów funkcji
Laboratorium 3
1
M.A. Jankowska, G. Sypniewska-Kamińska
double add(double a, double b);
double add(double& a, double& b);
kompilator nie jest w stanie wybrać właściwej funkcji. W tym przypadku przeciążanie nie jest możliwe, co kompilator sygnalizuje komunikatem
: error C2668: 'add' : ambiguous call to overloaded function Zazanczmy jeszcze, że do zastosowania mechanizmu przeciążania nie wystarczy sama różnica w typach funkcji. Przy próbie nadania jednakowych nazw funkcjom o podanych niżej prototypach int add(float a, float b);
float add(float a, float b);
po komplilacji pojawi sie komunikat o błędzie
: error C2556: 'int add(double,double)' : overloaded function differs only by return type from 'double add(double,double)'
Konieczne jest, aby funkcje różniły się nie tylko typem ale sygnaturami, na przykład int add(int a, float b);
float add(float a, float b);
ZADANIA
Korzystając z polimorfizmu funkcji napisz program wywołujący funkcje o jednakowej nazwie służące do dodawania dwóch n-
elementowych wektorów, których elementami są wartości typu int, float i double.
#include "stdafx.h"
#include <iostream>
#include <ctime>
using std::cout;
using std::cin;
using std::endl;
void add_vectors(int n, int* a, int* b, int* c);
void add_vectors(int n, float* a, float* b, float* c);
void add_vectors(int n, double* a, double* b, double* c); int _tmain(int argc, _TCHAR* argv[])
{
int p[20], q[20], wynik[20];
float fp[20], fq[20], fwynik[20];
double d1[20], d2[20], d3[20];
srand(static_cast<unsigned>(time(0)));
int n;
cout <<" n = "; cin>>n;
for (int i=0; i<n; i++)
{
p[i] = rand();
q[i] = rand();
fp[i] = 2.0f/static_cast<float>(rand());
fq[i] = 4.0f/static_cast<float>(rand());
d1[i] = static_cast<double>(rand())/100.0;
d2[i] = static_cast<double>(rand())/100.0;
}
add_vectors(n, p, q, wynik);
for (int i=0; i<n; i++)
{
cout <<p[i]<<" + "<<q[i]<<" = "<<wynik[i]<<endl;
}
cout << endl;
add_vectors(n, fp, fq, fwynik);
for (int i=0; i<n; i++)
{
cout <<fp[i]<<" + "<<fq[i]<<" = "<<fwynik[i]<<endl;
}
cout << endl;
add_vectors(n, d1, d2, d3);
for (int i=0; i<n; i++)
Laboratorium 3
2
M.A. Jankowska, G. Sypniewska-Kamińska
{
cout <<d1[i]<<" + "<<d2[i]<<" = "<<d3[i]<<endl;
}
return 0;
}
void add_vectors(int n, int* a, int* b, int* c)
{
for (int i=0; i<n; i++)
{
c[i] = a[i] + b[i];
}
}
void add_vectors(int n, float* a, float* b, float* c)
{
for (int i=0; i<n; i++)
{
c[i] = a[i] + b[i];
}
}
void add_vectors(int n, double* a, double* b, double* c)
{
for (int i=0; i<n; i++)
{
c[i] = a[i] + b[i];
}
}
II. Szablony funkcji
Szablon funkcji jest narzędziem służącym do zdefiniowania wielu funkcji, które wykonują takie same operacje na argumentach różnych typów I ewentualnie mogą generować wynik różniący się typem. Szablon można więc traktować jako definicję funkcji przetwarzającej dane hipotetycznego typu (albo kilku hipotetycznych typów). Typ w definicji szablonu jest parametrem reprezentowanym przez dowolnie wybrany identyfikator.
Poniżej zamieszczono definicję szablonu funkcji przeznaczonej do dodawania dwóch n-elementowych wektorów o elementach dowolnego typu.
template <typename typ> void add_vectors( int n, typ* a, typ* b, typ* c)
{
for (int i=0; i<n; i++)
{
c[i] = a[i] + b[i];
}
}
W powyższej definicji typ jest identyfikatorem wybranym przez programistę.
Jeżeli w programie z poprzedniego zadania zastosujemy szblon funkcji, to kod programu będzie nastepujący:
#include "stdafx.h"
#include <iostream>
#include <ctime>
using std::cout; using std::cin; using std::endl;
template <typename typ> void add_vectors( int n, typ* a, typ* b, typ* c); int _tmain(int argc, _TCHAR* argv[])
{
int p[20], q[20], wynik[20];
float fp[20], fq[20], fwynik[20];
double d1[20], d2[20], d3[20];
srand(static_cast<unsigned>(time(0)));
int n;
cout <<" n = "; cin>>n;
for (int i=0; i<n; i++)
{
p[i] = rand();
q[i] = rand();
fp[i] = 2.0f/static_cast<float>(rand());
fq[i] = 4.0f/static_cast<float>(rand());
d1[i] = static_cast<double>(rand())/100.0;
d2[i] = static_cast<double>(rand())/100.0;
}
Laboratorium 3
3
M.A. Jankowska, G. Sypniewska-Kamińska
add_vectors(n, p, q, wynik);
for (int i=0; i<n; i++)
{
cout <<p[i]<<" + "<<q[i]<<" = "<<wynik[i]<<endl;
}
cout << endl;
add_vectors(n, fp, fq, fwynik);
for (int i=0; i<n; i++)
{
cout <<fp[i]<<" + "<<fq[i]<<" = "<<fwynik[i]<<endl;
}
cout << endl;
add_vectors(n, d1, d2, d3);
for (int i=0; i<n; i++)
{
cout <<d1[i]<<" + "<<d2[i]<<" = "<<d3[i]<<endl;
}
return 0;
}
template <typename typ> void add_vectors( int n, typ* a, typ* b, typ* c)
{
for (int i=0; i<n; i++)
{
c[i] = a[i] + b[i];
}
}
Definicja szablonu zapisana za funkcją main albo w odrębnym pliku wymaga umieszczenia w kodzie prototypu szablonu, czyli instukcji
template <typename typ> void add_vectors( int n, typ* a, typ* b, typ* c); Programiści stosują dwie konwencje zapisywania nagłówka szablonu:
–
w jednej linii, jak w powyższym przykładzie,
–
w dwóch liniach; w pierwszej zapisują template <typename typ>, w drugiej nagłówek funkcji, na przykład template <typename typ>
void add_vectors( int n, typ* a, typ* b, typ* c)
Jeżeli kompitator napotka wywołanie funkcji o nazwie zdefiniowanej w szablonie, to na podstawie typów argumentów aktualnych wygeneruje potrzebną wersję tej funkcji dla konkretnego typu/typów. Zastosowanie szablonu zwalnia więc programistę z konieczności żmudnego tworzenia wielu wersji analogicznych funkcji realizujących taki sam algorytm dla danych różnych typów.
W przypadku szablonu funkcji, która generuje watość, nagłówek szblonu musi jeszcze zawierać identyfikator typu wartości funkcji. Może to być ustalony typ, ale niekoniecznie - można zdefiniować typ wartości funkcji w postaci parametru. Na przykład szablon funcji przeznaczonej do obliczania iloczynu skalarnego dwóch n-elementowych wektorów o elementach dowolnego typu i zwracającej wynik jako wartość zgodną z typem elementów wektorów może mieć postać template <typename typ> typ scalar_product( int n, typ* a, typ* b)
{
typ s = 0;
for (int i = 0; i<n; i++)
{
s = s + a[i]*b[i];
}
return s;
}
Jeżeli natomiast funkcja ma zwracać jako wynik wartość typu double, niezależnie od typu argumentów, to szablon można napisać następująco:
template <typename typ> double scalar_product( int n, typ* a, typ* b)
{
typ s = 0;
for (int i = 0; i<n; i++)
{
s = s + a[i]*b[i];
}
return s;
}
Laboratorium 3
4
M.A. Jankowska, G. Sypniewska-Kamińska
W pewnych przypadkach komplilator na podstawie wywołania funkcji nie jest w stanie jednoznacznie ustalić typu zwracanego przez funkcję. Pojawia się wówczas błąd kompilacji z opisem
: could not deduce template argument for 'typ'
Wówczas programista musi wskazać w jawny sposób typ wyniku funkcji przy jej wywołaniu. Nazwę typu funkcji należy umieścić w nawiasach kątowych, pomiędzy nazwą funkcji a listą argumentów, na przykład funkcja_1<double>(n);
ZADANIA
1. Napisz i uruchom program zawierający szablon funkcji, która przydziela pamięć jednowymiarowej tablicy dynamicznej o elementach dowolnego typu.
#include "stdafx.h"
#include <iostream>
using namespace std;
template <typename typ> typ* rozlokuj_wektor( int& n)
{
cout << "okresl liczbe elementow tablicy n = "; cin >>n; typ* pvec = new typ[n];
return pvec;
}
int _tmain(int argc, _TCHAR* argv[])
{
int n;
double* w;
w = rozlokuj_wektor<double>(n);
for (int i = 0; i<n; i++)
{
w[i] = pow(static_cast<double>(i), 1.0/3.0);
cout <<i<<" : "<< w[i] <<endl;
}
delete [] w;
int* y = rozlokuj_wektor<int>(n);
return 0;
}
2. Napisz i uruchom program zawierający szablon funkcji, która przydziela pamięć dwuwymiarowej tablicy dynamicznej o elementach dowolnego typu.
Laboratorium 3
5