Responsabili
În cadrul acestui laborator ne propunem să continuăm ilustrarea conceptelor din C++ cu care veți lucra pe parcursul acestui semestru.
Ne dorim să:
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.
Un mecanism specific C++ este supraîncarcarea operatorilor, prin care programatorul poate asocia noi semnificaţii operatorilor deja existenţi. De exemplu, dacă dorim ca două numere complexe să fie adunate, în C trebuie să scriem funcții specifice, nenaturale. În C++ putem scrie foarte ușor:
Complex a(2,3); Complex b(4,5); Complex c=a+b; //operatorul + a fost supraîncarcat pentru a aduna două numere complexe
Acest lucru este posibil, întrucât un operator este văzut ca o funcție, cu declarația:
tip_rezultat operator#(listă_argumente);
Așadar pentru a supraîncărca un operator pentru o anumită clasă, este necesar să declarăm funcția următoare în corpul acesteia:
tip_rezultat operator#(listă_argumente);
Există câteva restricții cu privire la supraîncarcare:
Funcţiilor membru li se transmite un argument implicit this (adresa obiectului curent), motiv pentru care un operator binar poate fi implementat printr-o funcţie membru nestatică cu un singur argument.
Operatorii sunt interpretați în modul următor:
#include <iostream> class Complex { public: double re; double im; Complex(double real, double imag): re(real), im(imag) {}; //operatori supraîncărcaţi ca funcţii membre Complex operator+(const Complex& d); Complex operator-(const Complex& d); Complex& operator+=(const Complex& d); };
#include "complex.h" Complex Complex::operator+(const Complex& d){ return Complex(re+d.re, im+d.im); } Complex Complex::operator-(const Complex& d){ return Complex(re-d.re, im-d.im); } Complex& Complex::operator+=(const Complex& d){ re+=d.re; im+=d.im; return *this; }
Așa cum am amintit mai sus, majoritatea operatorilor pot fi supraîncărcați. O atenție importantă trebuie acordată operatorului de atribuire, dacă nu este supraîncărcat, realizează o copiere membru cu membru.
Pentru obiectele care nu conţin date alocate dinamic la iniţializare, atribuirea prin copiere membru cu membru funcţionează corect, motiv pentru care nu se supraîncarcă operatorul de atribuire.
Operatorul de atribuire poate fi redefinit numai ca funcţie membră, el fiind legat de obiectul din stânga operatorului =, motiv pentru care va întoarce o referinţă la obiect.
class String{ char* s; int n; // lungimea sirului public: String(); String(const char* p); String(const String& r); ~String(); String& operator=(const String& d); String& operator=(const char* p); };
#include "String.h" #include <string.h> String& String::operator=(const String& d){ if(this != &d){ //evitare autoatribuire if(s) //curatire delete [] s; n=d.n; //copiere s=new char[n+1]; strcpy(s, d.s); } return *this; //intoarce referinta la obiectul modificat } String& String::operator=(const char* p){ if(s) delete [] s; n=strlen(p); s=new char[n+1]; strcpy(s, p); return *this; }
Reprezintă un tip de constructor special care se folosește când se dorește/este necesară o copie a unui obiect existent. Dacă nu este declarat, se va genera unul default de către compilator.
Poate avea unul din următoarele prototipuri
1) Apel explicit
MyClass m; MyClass x = MyClass(m); /* apel explicit al copy-constructor-ului */
2) Transfer prin valoare ca argument într-o funcție
void f(MyClass obj); ... MyClass o; f(o); /* se apelează copy-constructor */
3) Transfer prin valoare ca return al unei funcții
MyClass f() { MyClass a; return a; /* se apelează copy-constructor */ }
4) La inițializarea unei variabile declarate pe aceeași linie
MyClass m; MyClass x = m; /* se apelează copy-constructor */
Reprezintă un concept de must do pentru C++. Astfel:
Explicație: dacă funcționalitatea vreunuia dintre cei 3 se vrea mai specială decât cea oferită default, atunci mai mult ca sigur se dorește schimbarea funcționalității default și pentru ceilalți 2 rămași.
class Complex { private: int re; int im; public: Complex(const Complex& c) { re = c.re; im = c.im; printf("copy contructor\n"); } void operator=(const Complex& c) { re = c.re; im = c.im; printf("assignment operator\n"); } ~Complex() { printf("destructor\n"); } };
Această secțiune nu este punctată și încearcă să vă facă o oarecare idee a tipurilor de întrebări pe care le puteți întâlni la un job interview (internship, part-time, full-time, etc.) din materia prezentată în cadrul laboratorului.
Și multe altele…