Responsabili
În urma parcurgerii acestui laborator studentul va:
Motivul principal pentru care folosim C++ în cadrul SD este datorită funcționalității oferite de template-uri.
Acestea permit generalizarea tipurilor de date folosite în interiorul funcțiilor și claselor.
Sintaxa pentru acestea este:
template <class identifier> declaratie; template <typename identifier> declaratie;
declaratie poate fi fie o funcție, fie o clasă. Nu există nicio diferență între keyword-ul class și typename - important este că ceea ce urmează după ele este un placeholder pentru un tip de date.
În primul rând template-urile pot fi aplicate funcțiilor.
Un exemplu comun și simplu este următorul:
template<typename T> T getMax(T a, T b) { return a > b ? a : b; }
Funcția poate fi apelată astfel:
getMax<int>(2, 3); getMax<double>(3.2, 4.6);
Concret, să presupunem că avem o clasă numită KeyStorage care are:
Vrem să putem folosi codul clasei indiferent de tipul de date al membrului.
Iată cum putem face acest lucru:
template<typename T> class KeyStorage { public: int key; T member; };
În funcția main, să presupunem că vrem să folosim clasa cu membrul de tip long.
#include "KeyStorage.h" int main() { KeyStorage<long> keyElement; return 0; }
Practic, oriunde folosim tipul de date T în clasă, este înlocuit cu tipul pe care îl specificăm.
Sunt destul de multe lucruri de spus despre template-uri, dar ne vom concentra pe lucrurile care schimbă modul în care ați implementat până acum.
Template-urile sunt de fapt indicii pentru compilator pentru a genera cod la rândul lui! Practic, voi îi spuneți compilatorului un șablon generic pe care ați vrea să-l folosiți și el trebuie să fie pregătit să îl pună la dispoziția voastră când aveți nevoie.
Ce trebuie să rețineți din asta? Totul se întâmplă la compile time, nu la run time.
Compilatorul practic analizează modul în care voi folosiți clasa respectivă și generează pentru fiecare mod în care o folosiți șablonul corespunzător. Folosirea KeyStorage<int> și KeyStorage<float> determină compilatorul să genereze cod pentru ambele clase (înlocuind o dată T cu int și altă cu float).
Pentru că totul se întâmplă la compile time, înseamnă că în momentul în care compilatorul întâlnește secvența de cod ce folosește template-uri trebuie să știe toate modurile în care aceasta este folosita.
Asta înseamnă că:
Ultimul rând de fapt forțează folosirea template-ului cu un anumit tip de date și deci compilatorul generează cod corespunzător (trebuie să scrieți asta pentru toate tipurile).
Iată mai jos o structură mai dezvoltată pentru clasa KeyStorage, în care cheia este setată în constructor. .
template<typename T> class KeyStorage { public: KeyStorage(int k); ~KeyStorage(); T getMember(); T setMember(T element); private: T member; int key; };
Implementarea completa a ei poate fi realizată:
#include "KeyStorage.h" template<typename T> KeyStorage<T>::KeyStorage(int k) { //TODO } template<typename T> KeyStorage<T>::~KeyStorage() { } //TODO: restul metodelor. // La sfarsit, cu tipurile de date pe care le veti folosi. template class KeyStorage<int>; template class KeyStorage<long>;
In C++ există două modalități de a lucra cu adrese de memorie:
Referinţa poate fi privită ca un pointer constant inteligent, a cărui iniţializare este forţată de către compilator (la definire) şi care este dereferenţiat automat.
Semantic, referințele reprezintă aliasuri ale unor variabile existente. La crearea unei referinţe, aceasta trebuie iniţializată cu adresa unui obiect (nu cu o valoare constantă).
Sintaxa pentru declararea unei referințe este:
tip& referinta = valoare;
Exemplu:
int x=1, y=2; int& rx = x; //referinta rx = 4; //modificarea variabilei prin referinta rx = 15; //modificarea variabilei prin referinta rx =y; //atribuirea are ca efect copierea continutului //din y in x si nu modificarea adresei referintei
Spre deosebire de pointeri:
Referinţele se folosesc:
Motivul pentru aceste tipuri de utilizări este unul destul de simplu: când se transmit parametrii funcțiilor, se copiază conținutul variabilelor transmise pe stivă, lucru destul de costisitor. Prin transmiterea de referințe, nu se mai copiază nimic, așadar intrarea sau ieșirea dintr-o funcție sunt mult mai putin costisitoare.
În C++, există mai multe întrebuințări ale cuvântului cheie const:
Pentru a specifica, un obiect a cărui valoare nu poate fi modificată, const se poate folosi în următoarele feluri:
const tip variabila
⇒ specifică o variabilă constantătip const& referinta_ct = variabilă;
⇒ specifică o referință constantă la un obiect, obiectul neputând fi modificatconst int *p_int
⇒ specifică un pointer la int modificabil, dar conținutul locației de memorie către care p_int
arată nu se poate modifica.int * const p_int
⇒ specifică un pointer la int care nu poate fi modificat (Variabilei p_int
nu i se poate asigna nici o valoare, dar conținutul locației de memorie către care p_int
arată se poate modifica)Orice obiect constant poate apela doar funcții declarate constante. O funcție constantă se declară folosind sintaxa:
void fct_nu_modifica_obiect() const; //am utilizat cuvântul cheie const //dupa declarația funcției fct_nu_modifica_obiect
Această declaratie a functiei garantează faptul că obiectul pentru care va fi apelată nu se va modifica.
Regula de bază a apelării membrilor de tip funcție ai claselor este:
const
pot fi apelate pe toate obiecteleExemple:
//declarație class Complex { private: int re; int im; public: Complex(); int GetRe() const; int GetIm() const; void SetRe(int re); void SetIm(int im); }; //apelare Complex c1; const Complex c2; c1.GetRe(); //corect c1.SetRe(5); //corect c2.GetRe(); //corect c2.SetRe(5); //incorect
Pentru clasa Complex, definim funcţiile care asigură accesul la partea reală, respectiv imaginară a unui număr complex:
double getRe(){ return re; } double getIm(){ return im; }
Dacă am dori modificarea părţii reale a unui număr complex printr-o atribuire de forma:
z.getRe()=2.;
constatăm că funcţia astfel definită nu poate apărea în partea stângă a unei atribuiri.
Acest neajuns se remediază impunând funcţiei să returneze o referinţă la obiect, adică:
double& getRe(){ return re; }
Codul de mai sus returnează o referință către membrul re
al obiectului Complex z
, așadar orice atribuire efectuată asupra acestui câmp va fi vizibilă și în obiect.