Articol 01 - Introducere in C++

În cadrul acestui articol ne propunem să ilustrăm conceptele din C++ cu care veți lucra pe parcursul acestui semestru.

Într-un mod extrem de simplist spus C++ este un superset al limbajului C, iar tot ceea ce ați învățat în C la PC se poate compila cu un compilator pentru limbajul C++, funcționalitatea rămânând aceeași.

Obiective

Ne dorim să:

  • Realizăm tranziția de la C la C++
  • înțelege conceptul de referințe din C++
  • înțelege conceptul de read-only introdus prin identificatorul const
  • Înțelegem ce presupune definirea unei clase

De ce C++?

Pentru că C++ permite implementarea structurilor de date cu tipuri de date generice, prin intermediul template-urilor, într-un mod care nu presupune trecerea la programarea orientată pe obiecte. În cadrul acestui articol nu ne așteptăm să dobândiți cunoștințe (elementare sau avansate) legate de programarea obiectuală, întrucât în anul II există un curs dedicat acestui lucru.

Sintaxa C++

Definirea structurii

În cadrul laboratorului de Programarea Calculatoarelor am învățat să declarăm și să folosim tipuri de date complexe, structuri în limbajul C. Pentru a recapitula, iată mai jos un exemplu simplu de astfel de structură, pentru a reprezenta un număr complex.

complex.h
#ifndef __COMPLEX_H
#define __COMPLEX_H
 
struct complex {
    double re;
    double im;
};
 
#endif // __COMPLEX_H

Accesarea membrilor

Tipul de date definit mai sus, ca orice alt tip de date, poate fi folosit drept:

  • o variabilă locală ( automată), alocată pe stivă
    • accesarea membrilor se face cu operatorul . (referențierea structurii)
    • number.re (membrul re al obiectul number)
  • un pointer către o zonă alocată dinamic, pe heap.
    • accesarea membrilor se face cu operatorul (dereferențierea structurii)
    • pNumber→re (membrul re al obiectul indicat de pNumber)

Mai jos puteți urmări un exemplu în acest sens:

main.cc
#include <stdio.h>
#include <stdlib.h>
#include "complex.h"
 
int main() {
    // variabila locala de tip struct complex
    struct complex number;
    number.re = 0;
    number.im = 1;
 
    // variabila de tip pointer, catre o zona de memorie alocata dinamic
    struct complex *pNumber = (struct complex *)malloc(sizeof(struct complex));
    pNumber->re = 1;
    pNumber->im = 0;
 
    free(pNumber);
 
    return 0;
}

Funcții specifice structurii

Dându-se o variabilă de tip struct complex vom dori să efectuăm diferite operații asupra acesteia.

complex.h
#ifndef __COMPLEX_H
#define __COMPLEX_H
 
struct complex {
    double re;
    double im;
};
 
// Initializeaza campurile unei structuri date.
void complex_initialize(struct complex *number, double re, double im);
 
// Intoarce o structura ce contine numarul complex conjugat.
struct complex complex_conjugate(struct complex *number);
 
#endif // __COMPLEX_H

Ce observăm că au în comun cele două funcții? Hint: primul parametru, care reprezintă un pointer către o zonă de memorie care reține tipul struct complex

Metode

Acest pattern de a defini funcții specifice unui anumit tip de date este extrem de întâlnit. De asemenea, se observă că aceste funcții nu ar putea fi folosite în combinație cu alte structuri de date, ele fiind specifice struct complex.

Așadar, iată cum o structură poate să capete funcții specifice (codul de mai jos este specific C++):

complex.h
#ifndef __COMPLEX_H
#define __COMPLEX_H
 
struct complex {
    double re;
    double im;
 
    void complex_initialize(double re, double im);
    struct complex complex_conjugate();
};
#endif // __COMPLEX_H

Astfel, am definit funcţii care operează pe tipul nostru de date și care pot fi apelate întocmai cum se realizează accesarea membrilor de date. Observăm însă că a dispărut primul parametru! De ce?

Funcțiile definite mai sus pot fi apelate numai pe o variabilă de tip struct complex sau struct complex * în același mod în care se accesează și variabilele membru. Ne dorim, totuși, ca în corpul acestor funcții membru să putem modifica (sau cel puțin accesa) ceilalți membri ai variabilei pe care facem apelul. Cum putem ști, în corpul funcției, pe ce variabilă am făcut apelul ca să putem accesa informația necesară?

C++ se ocupă (în spate, fără intervenția noastră) să paseze un parametru extra funcției apelate. El este de tip struct complex* și se numește this; nu face parte efectiv din semnătura funcției și este un cuvânt cheie (rezervat) în C++, deci aveți grijă cum vă numiți variabilele.

Aceste funcții membru cu proprietățile pe care tocmai le-am menționat se numesc metode. Este un termen pe care îl veți folosi de acum încolo în acest context și mai ales în programarea orientată pe obiecte începând cu semestrul următor.

Mai jos observați implementarea metodelor în antetul definit mai sus și folosirea lui this. Compilați și rulați codul de mai jos cu g++.

complex.h
#ifndef __COMPLEX_H
#define __COMPLEX_H
 
struct complex {
    double re;
    double im;
 
    void complex_initialize(double re, double im) {
        this->re = re;
        this->im = im;
    }
 
    struct complex complex_conjugate() {
        struct complex conjugate;
        conjugate.complex_initialize(this->re, -(this->im));
 
        return conjugate;
    }
};
#endif // __COMPLEX_H
main.cc
#include <stdio.h>
#include "complex.h"
 
int main() {
    struct complex number;
 
    number.complex_initialize(2, 3);
    printf("%.2lf %.2lf\n", number.re, number.im);
 
    number.complex_initialize(5, 6);
    printf("%.2lf %.2lf\n", number.re, number.im);
 
    return 0;
}

Alocarea / Dealocarea dinamică

C++ introduce perechea de keyword-uri new și delete, care se folosesc pentru a aloca dinamic instanțe ale claselor.

Complex *numar = new Complex(2, 3);
delete numar;

Keyword-ul new apelează constructorul clasei, iar keyword-ul delete apelează destructorul clasei.

Observație

  • Un obiect se alocă/dezalocă cu combinația new/delete
  • Un vector de obiecte se alocă/dezalocă cu combinația new[]/delete[]
Complex *numere = new Complex[10];
delete[] numere;

Dacă dorim să alocăm memorie dinamică în mai multe dimensiuni, vom folosi o procedură asemănătoare cu cea folosită în C. Presupunând că vrem să alocăm o matrice de dimensiune NxM atunci declarăm un dublu pointer, alocăm N pointeri pentru linii și apoi M elemente pentru fiecare linie în parte.

int **mat;
mat = new int*[N];
 
for (int i = 0; i < N; i++) {
  mat[i] = new int[M];
}
 
// ..
// Folosire matrice
// ..
 
// Dezalocare
for (int i = 0; i < N; i++) {
  delete[] mat[i];
}
delete[] mat;

În cazul de mai sus, N și M nu trebuie sa fie constante, pot fi (de exemplu) variabile citite de la tastatură.
Atenție Memoria alocată nu va fi toată într-o zonă continuă (doar elementele unei linii alocate cu new int[M] vor fi continue, dar între două linii consecutive pot exista “spații” în memorie).

Dacă toate dimensiunile (mai puțin prima) sunt constante (cunoscute la compile time) se poate aloca dinamic memoria în felul următor:

int (*mat)[100] = new int[N][100];
// mat va fi alocata în zona de heap (nu pe stivă), într-o zonă continuă de memorie
 
// ..
// Folosire matrice
// ..
 
// Dezalocare
delete[] mat;

Atenție Nu convertiți matricea de mai sus la un pointer dublu, deoarece cele două nu sunt același lucru (prima este un vector de N pointeri care pointează către liniile matricei, pe când a doua este o zonă continuă de memorie în care compilatorul accesează elementele la fel ca într-o matrice clasică).

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.

Compilare

Sursele C++ se compilează folosind compilatorul g++. Acesta permite exact aceleași opțiuni de bază ca și gcc, compilatorul utilizat pentru sursele de C.

  • Încercați să compilați și să rulați codul din cele 3 fișiere de mai sus.
g++ complex.cc main.cc -o exemplu

Ce observați?

Înlocuiți acum keyword-ul class cu keyword-ul struct și compilați din nou.

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 este o clasă abstractă?
  • 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 obligatorie

Bibliografie recomandată

sd-ca/laboratoare/laborator-01.txt · Last modified: 2016/02/21 19:49 by radu.stochitoiu
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