This is an old revision of the document!


Laborator 02 - Noțiuni de C++

Obiective

În urma parcurgerii acestui laborator studentul va:

  • înțelege conceptul de template
  • înțelege conceptul de referințe din C++
  • înțelege conceptul de read-only introdus prin identificatorul const

Templates

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.

Function Template

Î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);

Class Template

Concret, să presupunem că avem o clasă numită KeyStorage care are:

  • o cheie (de tip int)
  • un membru de date generic (al cărui tip de date nu îl știm la momentul scrierii clasei).

Vrem să putem folosi codul clasei indiferent de tipul de date al membrului.

Iată cum putem face acest lucru:

KeyStorage.h
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.

main.cpp
#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.

Where's the magic happening?

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).

Guideline-uri implementare

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

  • Trebuie să scrieți întreaga implementare în header! sau
  • Scrieți descrierea clasei generice în header, în fișierul de implementare fiecare metodă declarată este de fapt o funcție cu template și la sfârșitul implementării adăugat template class numeclasa<numetip>;

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).

Clasa KeyStorage

Iată mai jos o structură mai dezvoltată pentru clasa KeyStorage, în care cheia este setată în constructor. .

KeyStorage.h
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ă:

  • în header (în cazul template-urilor, acest mod este cel mai indicat).
  • în fișierul de implementare .cc / .cpp (al cărui schelet parțial îl găsiți mai jos).
KeyStorage.cpp
#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>;

Referințe

In C++ există două modalități de a lucra cu adrese de memorie:

  • pointeri (la fel ca cei din C)
  • referințe.

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 sunt iniţializate la creare (pointerii se pot iniţializa oricând)
  • referinţa este legată de un singur obiect şi această legătură nu poate fi modificată pentru un alt obiect
  • referințele nu au operații speciale, toți operatorii aplicați asupra referințelor sunt de fapt aplicați asupra variabilei referite(de exemplu extragerea adresei unei referințe va returna adresa variabilei referite)
  • nu există referinţe nule – ele sunt întotdeauna legate de locaţii de memorie

Referinţele se folosesc:

  • în listele de parametri ale funcţiilor
  • ca valori de întoarcere ale funcţiilor

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:

  • specifică un obiect a cărui valoare nu poate fi modificată
  • specifică metodele unui obiect read-only care pot fi apelate

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 modificat
  • const 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:

  • funcțiile const pot fi apelate pe toate obiectele
  • funcțiile non-const pot fi apelate doar pe obiectele non-const.

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.

Exerciții

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.

  • Care este diferența între struct și class în C++?
  • Ce înseamnă specializarea unui template? Cum se realizează aceasta?
  • Ce este o clasă abstractă?
  • Când este indicat să folosim template-uri versus clase abstracte?
  • Ce face keyword-ul static în fața metodei unei clase în C++?
  • Ce este diferit între o metodă statică și o metodă normală a unei clase? (Hint: explicați cum e cu pointer-ul this)

Și multe altele…

Bibliografie

sd-ca/laboratoare/laborator-02.1425975268.txt.gz · Last modified: 2015/03/10 10:14 by silviu_emil.popescu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0