Programozás alapjai II. (3. ea) C++

OO paradigmák, osztály, operátorok átdefiniálása

Szeberényi Imre, Somogyi Péter BME IIT szebi@iit.bme.hu

Programfejlesztés

Néhány programozási módszer

Korai szoftverkészítés jellemzői

Gépi nyelv?

// Kiírunk egy stringet void print(String str)
00401350   push        ebp
00401351   mov         ebp,esp
00401353   sub         esp,40h
00401356   push        ebx
00401357   push        esi
00401358   push        edi
00401359   lea         edi,[ebp-40h]
0040135C   mov         ecx,10h
00401361   mov         eax,0CCCCCCCCh
00401366   rep stos    dword ptr [edi]
00401368   mov         eax,dword ptr [ebp+8]
0040136B   push        eax
0040136C   push        offset string "%s" (0042201c)
00401371   call        printf (004037d0)
00401376   add         esp,8
00401379   pop         edi
0040137A   pop         esi
0040137B   pop         ebx
0040137C   add         esp,40h
0040137F   cmp         ebp,esp
00401381   call        __chkesp (00403620)
00401386   mov         esp,ebp
00401388   pop         ebp
00401389   ret
00401350  55 8B EC 83 EC 40 53 56  U‹ě.ě@SV
00401358  57 8D 7D C0 B9 10 00 00  WŤ}Ŕą...
00401360  00 B8 CC CC CC CC F3 AB  .¸ĚĚĚĚó«
00401368  8B 45 08 50 68 1C 20 42  ‹E.Ph. B
00401370  00 E8 5A 24 00 00 83 C4  .čZ$...Ä
00401378  08 5F 5E 5B 83 C4 40 3B  ._^[.Ä@;
00401380  EC E8 9A 22 00 00 8B E5  ěčš"..‹ĺ
00401388  5D C3 CC CC CC CC CC CC  ]ĂĚĚĚĚĚĚ

Strukturált tervezés

Moduláris tervezés

Dekompozíció

Funkcionális dekompozíció

Feladat: komplex számok

Olvassunk be 10 komplex számot és írjuk ki a számokat és abszolút értéküket fordított sorrendben! - Funkcionális dekompozíciónál az adatokon végzett tevékenységekre koncentrálunk:

Tevékenység Adat
beolvasas() és tárolás Komplex, KomplexTomb
kiiras() Komplex, KomplexTomb
abs() Komplex

Funkcionális dekompozícióval

struct Komplex {
  double re, im;
};

int main() {
  Komplex t[10]; // adatok
  beolvasas(t);  // művelet
  kiiras(t);     // művelet
  return 0;
}
#include <cmath>
#include <iostream>
using namespace std;
double abs(Komplex k) { // adatot ismerni kell
  return sqrt(k.re * k.re + k.im * k.im);
}
void beolvasas(Komplex t[]) { // ismerni kell
  for (int i = 0; i < 10; i++) {
    cin >> t[i].re >> t[i].im;
  }
}
void kiiras(Komplex t[]) { // ismerni kell
  for (int i = 9; i >= 0; i--) {
    cout << t[i].re << '+' << t[i].im << 'j' << abs(t[i]) << endl;
  }
}

Kőbe vésett adatszerkezet

Absztrakt adattípus (ADT)

Objektum

Objektum orientált dekompozíció

A feladat OO dekompozícióval

Olvassunk be 10 komplex számot és írjuk ki a számokat és abszolút értéküket fordított sorrendben!

Komplex k; // beolvas, kiir, abs
Komplex t; // tarol, elovesz
for (int i = 0; i < 10; i++) {
  k.beolvas();   // a k objektum beolvas
  t.tarol(i, k); // és a t obj. tárol
}                // műveletét aktivizáljuk
for (int i = 9; i >= 0; i--) {
  k = t.elovesz(i);
  k.kiir();
  cout << ' ' << k.abs() << endl;
}

Objektum orientált modell

Komplex obj. megvalósítása C-ben

struct Komplex {
  double re, im;
};
// összetartozásra csak a név utal
void beolvasKomplex(Komplex *kp);
double absKomplex(Komplex *kp);
void setKomplex(Komplex *kp, double r, double i);
int main() {
  double f;
  struct Komplex k1, k2;   // deklaráció és definíció
  setKomplex(&k1, 1.2, 0); // inicializálás
  f = absKomplex(&k1);     // Névtér hiánya
  f = absKomplex(&k2);
}

Interfész függvények paraméterei

setKomplex(Komplex *kp, double r, double i);
// ^ funckió + obj. típusa
//                  *kp: melyik konkrét adat
//                  r, i: művelet operandusai
void beolvasKomplex(Komplex *kp);
double absKomplex(Komplex *kp);

Ilyen paraméterezést használtunk a gyakorlaton a String esetében is.

C++ OO paradigmák

Egységbezárás C++-ban

struct Komplex {
  double re, im;                // adatok
  void set(double r, double i); // tagfüggvények
  double abs();
};
int main() {
  Komplex k1, k2;
  // A fv. névben elég a funkciót jelölni.
  // A saját adatot sem kell átadni.
  k1.re = 1.2;
  k1.im = 0;
  k1.set(1.2, 0);
  double f = k1.abs();
  set(&k1, 1.2, 0); // vs. setKomplex(&k1, 1.2, 0);
}

Adattakarás C++-ban

struct Komplex {
private: // privát adatok
  double re, im;
public: // nyilvános tagfüggvények
  void set(double r, double i);
  double abs();
};
int main() {
  Komplex k1;
  // Közvetlen hozzáférés a priváthoz TILOS:
  // k1.re = 1.2; k1.im = 0;
  // helyette csak így:
  k1.set(1.2, 0);
  double f = k1.abs();
}

Osztály

Adatelérés megvalósítása

C++

class Komplex {
  double re, im;

public:
  void set(double r, double i) {
    re = r;
    im = i;
  }
};
int main() {
  Komplex k1;
  k1.set(1.2, 3.4);
}

C:

struct Komplex {
  double re, im;
};
void setKomplex(struct Komplex *this, double r, double i) {
  // a konkrét objektumra mutat ^
  this->re = r;
  this->im = i;
}
int main() {
  struct Komplex k1;
  setKomplex(&k1, 1.2, 3.4);
}

this ≡ példányra mutató ptr.

class Komplex {
  double re, im;
public:
  void set(double re, double im) {
    this->re = re; this->im = im;
  } // ^             ^ k2.set(2.1, -4);
  double abs();
};
#include <cmath>
double Komplex::abs() {
  return sqrt(this->re * this->re + this->im * this->im);
  // *this azt az objektumot jelenti, amelyre a tagfüggvényt
} //  meghívták. Itt a *this a k1 lesz:
int main() { Komplex k1, k2; k1.set(1, 2); double f = k1.abs();
}

Kívül és belül definiált tagfüggvény

class Komplex {
  double re, im; // adatok privátak
public:
  void set(double r, double i) { re = r; im = i; }
  //    ^ inline-nak megfelelő
  double abs();
};
#include <cmath>
#include <iostream>
double Komplex::abs() { return sqrt(re * re + im * im); }
int main() { //^ scope operátor
  Komplex k1; k1.set(1.2, 3.4);
  std::cout << k1.abs();
}

Tagfüggvények szerepe

Konstans tagfüggvények

class Komplex {
  double re, im;
public:
  void set(double r, double i) {
    re = r;
    im = i;
  }
  double getRe() const { return re; }
  double getIm() const { return im; }
  double abs() const;
}; // Nem változtat(hat)ja meg az állapotot (adatokat)
#include <cmath>
double Komplex::abs() const { return sqrt(re * re + im * im); }

Alapértelmezett tagfüggvények

Automatikusan keletkező (implicit deklarált):

Konstruktor

KONSTRUKTOR: Az objektum létrejöttekor hívódik. Feladata, hogy alapállapotba hozza az objektumot. Ha nem deklarálunk egyet sem, akkor implicit jön létre.

class Komplex {
  double re, im;
public:
  Komplex() {} // konstruktornak nincs visszatérési típusa
  // Ilyen üres programozott törzs keletkezik implicit módon
  Komplex(double r, double i) { re = r; im = i; }
};
int main() {
  Komplex k1;      // paraméter nélkül hívható (default)
  Komplex k2 = k1; // másoló (copy) ctr. (ez most implicit)
  Komplex k3 = Komplex(1.2, 3.4);
  //            ^ ideiglenes objektum
}

Destruktor

DESTRUKTOR: Az objektum megszüntetésekor hívódik. Alapvető feladata, hogy megszüntesse az obj. által din. mem. területen létrehozott objektumokat/adatokat.

class Komplex {
  double re, im;
public:
  ~Komplex() {} // destruktornak paramétere sincs
  // ^ Ez keletkezne implicit módon
};
int main() {
  Komplex k1;      // paraméter nélkül hívható (default)
  Komplex k2 = k1; // másoló (copy) konstruktor
} // destruktorok hívódnak

Komplex példa újból

#include <iostream>
class Komplex {
  double re, im;
public:
  Komplex(double r) { re = r; }
  Komplex(double r, double i) { re = r; im = i; }
  double getRe() const { return re; }
  double getIm() const { return im; }
  ~Komplex() { std::cout << "Nincs mit megszüntetni"; }
};
int main() {
  Komplex k1(1.3, 0); // definíció és inic.
  Komplex k2(3); //  Komplex k3; // Nincs ilyen ctr.
} // destruktorok meghívódnak

Default argumentummal

#include <iostream>
class Komplex {
  double re, im;
public:
  Komplex(double r = 0, double i = 0) { re = r; im = i; }
  double getRe() const { return re; }
  double getIm() const { return im; }
  void kiir(std::ostream &os = std::cout) const; // *
  ~Komplex() { std::cout << "Nincs mit megszüntetni"; }
};
void Komplex::kiir(std::ostream &os) const { // *
  os << re << '+' << im << 'j';
} // * Csak az egyik helyen, tipikusan a deklarációnál
//   jelöljük a default-ot!

Paraméter nélküli (default) konstr.

Objektum létrehozása alapállapottal.

Automatikusan hívódik minden olyan esetben, amikor az objektumnak alapállapotban kell létrejönnie. Nem keletkezik implicit, ha van legalább egy explicit. Pl:

Most itt tartunk

#include <iostream>
class Komplex {
  double re, im;
public:
  Komplex(double r = 0, double i = 0) { re = r; im = i; }
  double getRe() const { return re; } // getIm() idem.
  void kiir(std::ostream &os = std::cout) const;
  void setRe(double r) { re = r; } // setIm(double) idem.
  void beolvas(std::istream &is = std::cin);
};
int main() {
  Komplex k1, k2(1, 1), kt[10], k3; kt[2].kiir();
  Komplex *kp = new Komplex[100]; // mi történik itt?
  delete[] kp;
} // itt mit ír ki?

Mit tud az osztályunk?

Tud összeadni, ha megtanítjuk

k1 = k2 + k3

Műveletekkel bővített Komplex

class Komplex {
  double re, im;
public:
  Komplex(double r = 0.0, double i = 0.0) {
    re = r; im = i; }
  Komplex operator+(const Komplex &k) const {
    Komplex sum(k.re + re, k.im + im); return sum;
  }
  Komplex operator+(double r) const {
    return operator+(Komplex(r)); }
};
int main() { Komplex k1, k2, k3; k1 + k2; k1 + 3.14; k1 = k2; }
// 3.14 + k1; bal oldal nem obj.! Ezért globális fv. kell!

double + Komplex

class Komplex {
  double re, im;
public:
  Komplex(double r, double i) {
    re = r;
    im = i;
  }
};
// Globális fv., nem tagfüggvény:
Komplex operator+(double r, const Komplex &k) {
  return Komplex(k.re + r, k.im);
}
// Baj van! Nem férünk hozzá, mivel privát!

1. megoldás: privát adat elérése pub. fv. használatával:

class Komplex {
  double re, im;

public:
  Komplex(double r, double i = 0) {
    re = r; im = i;
  }
  double getRe() const { return re; } // publikus lekérdező
  double getIm() const { return im; } // függvények
};
// Globális fv., nem tagfüggvény:
Komplex operator+(const double r, const Komplex &k) {
  return Komplex(k.getRe() + r, k.getIm());
}

Kiírás: cout << k1

A bal oldal objektum ugyan, de nincs a kezünkben. Ezért csak egy operator<<(cout, k1) hívásra illeszthető globális függvénnyel lehet megoldani:

#include <iostream>
std::ostream &operator<<(std::ostream &os, const Komplex &k) {
  k.kiir(os);
  return os;
}
// std::ostream &-t ad vissza, így láncolható:
// cout << k1 << k2;

Beolvasás: cin >> k1

A bal oldal objektum ugyan, de nincs a kezünkben. Ezért csak egy operator>>(cin, k1) hívásra illeszthető globális függvénnyel lehet megoldani:

#include <iostream>
std::istream &operator>>(std::istream &is, Komplex &k) {
  k.beolvas(is);
  return is;
}

// A kiir() és a beolvas() tagfüggvény akár el is hagyható:
std::ostream &operator<<(std::ostream &os, const Komplex &k) {
  return os << k.getRe() << '+' << k.getIm() << 'j';
}

Op. túlterhelés szabályai

Op. túlterhelés előnye/hátránya

Egy furcsa példa

Komplex k1, k2;
double d = (double)k1; // mit jelent?  valós rész? abs?
// Jelentse a valós részt:
/* Komplex {
// …
  operator double() { return re; }
  // formálisan nincs típusa !!!
};
*/

Veszély! A típuskonverzió automatikus is lehet!

Pl: k1 + 3.14(double)k1 + 3.14 lesz, ha nincs operator+(Komplex, double)

Demo

#include <iostream>
using std::cout; using std::endl;
struct Valami {
  Valami() { cout << "HAHO!" << endl; }
  ~Valami() { cout << "Jaj!" << endl; }
};
int main() {
  cout << "1." << endl;
  Valami o1;
  cout << "2." << endl;
  Valami o2;
  Valami *o3 = new Valami;
  delete o3;
  return 0;
}

https://git.ik.bme.hu/Prog2/eloadas_peldak/ea_03