Table of Contents

Laborator 02 - Noțiuni de C++

Responsabili

Obiective

În urma parcurgerii acestui laborator studentul va:

Referințe

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.

Keyword const

Î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:

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:

Exemple:

//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

Funcții care returnează referințe

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.

Clase/metode prietene

Așa cum am văzut în primul laborator, fiecare membru al clasei poate avea 3 specificatori de acces:

Alegerea specificatorilor se face în special în funcție de ce funcționalitate vrem să exportăm din clasa respectivă.

Dacă vrem să accesăm datele private/protejate din afara clasei, avem următoarele opțiuni:

O funcție prieten are următoarele proprietăți:

O clasă prieten are următoarele proprietăți:

De asemenea, dacă clasa A este considerată prieten cu clasa B, nu înseamnă că si clasa B este considerată prieten cu clasa A. Nici tranzitivitatea nu este valabilă în relaţia de prietenie dintre clase.

Exemplu:

class Complex{
 
private:
    int re;
    int im;
public:
    int GetRe();
    int GetIm();
    friend double ComplexModul(Complex c);   //am declarat fct ComplexModul ca prieten
    friend class Polinom;   //Acum clasa Polinom care acces deplin la membrii **re** și **im**
};
 
double ComplexModul(Complex c)
{
   return sqrt(c.re*c.re+c.im*c.im);  //are voie, intrucat e prietena
}

Supraîncarcarea operatorilor

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:

Operatori supraîncărcaţi ca funcţii prieten

Un operator binar va fi reprezentat printr-o funcţie nemembră cu două argumente, iar un operator unar, printr-o funcţie nemembră cu un singur argument.

Utilizarea unui operator binar sub forma a#b este interpretată ca operator#(a,b).

Argumentele sunt clase sau referinţe constante la clase.

Supraîncărcarea operatorilor << şi >>

În C++, orice dispozitiv de I/O este văzut drept un stream, așadar operațiile de I/O sunt operații cu stream-uri, care se definesc în felul următor:

Acești operatori pot fi supraîncărcați pentru o clasă pentru a defini operații de I/O direct pe obiectele clasei.

Supraîncărcarea se poate efectua folosind funcții friend utilizând următoarea sintaxă:

istream& operator>> (istream& f, clasa & ob);        //Acum pot scrie in >> ob
ostream& operator<< (ostream& f, const clasa & ob);  //Acum pot scrie out << ob

Operatorii » și « întorc fluxul original, pentru a scrie înlănțuiri de tipul f»ob1»ob2.

Funcţiile operator pentru supraîncărcarea operatorilor de I/O le vom declara ca funcţii prieten al clasei care interacţionează cu fluxul.

Complex.h
#include <iostream>
 
class Complex
{
public:
    double re;
    double im;
 
    Complex(double real=0, double imag=0): re(real), im(imag) {};
 
    //supraîncărcarea  operatorilor +, - ca functii de tip "friend" 
    friend Complex operator+(const Complex& s, const Complex& d);
    friend Complex operator-(const Complex& s, const Complex& d);
 
    //funcţii operator pentru supraîncărcarea operatorilor de intrare/ieşire  
    //declarate ca funcţii de tip "friend"   
    friend std::ostream& operator<< (std::ostream& out, const Complex& z);
    friend std::istream& operator>> (std::istream& is, Complex& z);
};
Complex.cpp
#include "complex.h"
 
Complex operator+(const Complex& s, const Complex& d){
  return Complex(s.re+d.re,s.im+d.im);
}
 
Complex operator-(const Complex& s, const Complex& d){
  return Complex(s.re+d.re,s.im+d.im);
}
 
std::ostream& operator<<(std::ostream& out, const Complex& z){
   out << "(" << z.re << "," << z.im << ")"<< std::endl;
   return out;
}
 
std::istream& operator>>(std::istream& is, Complex& z){
  is >> z.re >> z.im;
  return is;
}
main.cpp
#include "complex.h"
 
int main() {
 Complex a(1,1), b(-1,2);
 std::cout << "A: " << a << "B: " << b;
 std::cout << "A+B: " << (a+b);
 std::cin >> b;
 std::cout << "B: " << b;
 a=b;
 std::cout << "A: " << a << "B: " << b;
}

Operatori supraîncărcaţi ca funcţii membre

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:

Complex.h
#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);
 
    friend std::ostream& operator<< (std::ostream& out, const Complex& z);
    friend std::istream& operator>> (std::istream& is, Complex& z);
};
Complex.cpp
#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;
}
 
std::ostream& operator<<(std::ostream& out, const Complex& z){
   out << "(" << z.re << "," << z.im << ")"<< std::endl;
   return out;
}
 
std::istream& operator>>(std::istream& is, Complex& z){
  is >> z.re >> z.im;
  return is;
}

Supraîncărcarea operatorului de atribuire

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.

Pentru clasele ce conţin date alocate dinamic, copierea membru cu membru, executată în mod implicit la atribuire conduce la copierea pointerilor la datele alocate dinamic, în loc de a copia datele.

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.

String.h
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);
};
String.cpp
#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;
}

Exerciții

1) [3p] Implementați clasa Complex, cu următoarele particularități:

  1. [1p] Vor exista doi constructori:
    1. primul, vid, va inițializa coordonatele la 0.
    2. al doilea va primi ca argumente partea reală și imaginară.
  2. Se vor implementa funcţii membre pentru:
    1. [0.5p] determinarea părților reale și imaginare.
    2. [1p] supraîncărcarea operatoriilor de comparație <, >, == folosind ca și criteriu de comparație modulul numărului complex.
    3. [0.5p] supraîncărcarea operatorilor +, - pentru a permite operaţii cu două argumente numere complexe.

Luati acest exercitiu ca exemplu. In laborator veti avea de rezolvat exercitii la prima vedere.

1) [3p] Implementati clasa Fractie, cu următoarele particularități:

  1. [1p] Vor exista doi constructori:
    1. primul vid.
    2. al doilea va primi ca argumente numitorul și numărătorul.
  2. Se vor implementa funcţii membre pentru:
    1. [0.5p] determinarea numitorului și numărătorului.
    2. [1p] supraîncărcarea operatoriilor de comparație <, >, ==.
    3. [0.5p] supraîncărcarea operatorilor +, -.

2) [4p] Implementaţi clasa template Set care să permită lucrul cu mulțimi de obiecte, cu următoarele particularități:

  1. Constructorul va primi dimensiunea maximă de elemente care pot fi ținute în mulțime și va aloca spațiul necesar.
  2. [0.5p] Se va defini și un destructor, care va dezaloca memoria alocată dinamic.
  3. Se vor implementa funcţii membre pentru:
    1. [1p] supraîncărcarea operatorului += pentru adăugarea unui nou element în mulțime (dacă elementul există deja în mulțime atunci nu va mai fi adăugat).
    2. [0.5p] supraîncărcarea operatorului -= pentru eliminarea unui element din mulțime.
  4. Se vor implementa funcţii friend (nemembre) pentru:
    1. [1p] testul de egalitate a două mulțimi ( supraîncărcarea operatorului == ): două mulțimi sunt egale daca conțin aceleași elemente.
    2. [0.5p] supraîncărcarea operatorului « (pentru scriere).
    3. [0.5p] supraîncărcarea operatorului » (pentru citire).

3) [1p bonus] Verificați cu valgrind că nu aveți memory-leaks.

Interviu

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…

Bibliografie