Az OOP szemlélet
Czirkos Zoltán · 2024.08.20.
OOP tervezés, OOP szemlélet erősítése.
Egy C-s program C++-os újraírása nem csak abból áll, hogy minden malloc()
-ot new
-ra cserélünk.
C-ben is programozhatunk objektumorientáltan, és C++-ban is programozhatunk procedurális szemlélettel.
A procedurális és az objektumorientált szemlélet alapvetően, gyökeresen más felépítésű programhoz vezet ugyanannál a feladatnál. A mai labor erre igyekszik rámutatni. A feladatokban kisebb-nagyobb programokat kell megírni procedurális = „mit kell csinálni most, itt helyben” és objektumorientált = „bízzuk a részfeladatokat egy osztályra” szemlélettel. A két szemlélet között nehéz meghúzni a határvonalat, de az elkészült programokat összehasonlítva jól látszik majd a különbség.
A laborokhoz
A laborok mellé minden héten lesz kiírva egy beadandó az admin portálon. Ide óra végén töltsd fel a forráskódokat (*.cpp, *.h)! A feladatokat ezért külön projektben oldd majd meg, ne írd felül a megoldásokat.
Labor otthoni munkában
A labor teljesítéséhez legalább az első két feladatot meg kell oldani.
Adott az alábbi kis program, amely egy egyváltozós függvényt rajzol ki a képernyőre.
#include <stdio.h>
#include <math.h>
enum Consts {
W = 79,
H = 20,
};
void clear(char page[H][W]) {
for (int y = 0; y < H; ++y)
for (int x = 0; x < W; ++x)
page[y][x] = ' ';
}
void plot(char page[H][W], char c, double (*f)(double)) {
for (int x = 0; x < W; ++x) {
double fx = (x - W/2)/4.0;
double fy = f(fx);
int y = (fy * 4.0) * -1 + H/2;
if (y >= 0 && y < H)
page[y][x] = c;
}
}
void print(char page[H][W]) {
for (int y = 0; y < H; ++y) {
for (int x = 0; x < W; ++x)
putchar(page[y][x]);
putchar('\n');
}
}
int main() {
char page[H][W];
clear(page);
plot(page, '.', sin);
plot(page, '+', cos);
print(page);
}
++++++ ...... +++++ ...... ++++++...... + .++ . ++ ++ . ++ .+ . . + . + . + . + . + . + . . + . + . + . + . + . + . . + . + . + .. + . + . + . . + . + . + . + . + . + . .+ . + .+ . + .+ .. + +++++...... ++++++...... ++++++...... +++++
Nézd át jól a kódot – ezt a programot kaptad, hogy tovább kellene fejleszteni. Másold be a fejlesztőkörnyezetbe!
sin(x), sin(2x), sin(3x)
Az első továbbfejlesztési kérés a következő. Úgy szeretnék a programot használni,
hogy a felhasználó parametrizálhassa a sin(d*x)
alakú függvényét:
printf("sin(d * x), d = ?\n");
scanf("%lf", &d);
plot(page, '.', ... valahogyan sin d*x ...);
Hogyan kell ehhez C-ben (nem C++) módosítani a rajzoló plot()
függvényt?
Milyen okból korlátozza a double (*f)(double)
paraméter a megvalósítást?
Találj ki egy módszert, hogyan kerülöd meg a problémát, és valósítsd is meg C-ben! Gondolj
arra, hogy igazából a sin(d*x)
csak egy példa... Lehetne d*sin(x)
,
d*sin(c+x)
, esetleg a*sin(b*x+c)+sqrt(d*x*x)
is a függvény, amit
ábrázolni kell.
Többféle megoldás is lehetséges, azonban mindegyik úgy tűnik, hogy valamiért nem túl szerencsés. A választott megoldásodnak van valamilyen hiányossága?
Megoldás
Lehetőségek pl.:
- Adni kell az ábrázolt függvénynek egy plusz paramétert, amelyiken keresztül ez a parametrizálás elvégezhető. Ezt a
paramétert az ábrázolás közben a meghívott függvénynek mindig át kell adni. Ez azért nem túl szerencsés, mert elrugaszkodik
a matematikától: a sin(2x), sin(3x) stb. egyváltozós matematikai függvények, míg a program új ábrázolandó függvénye
„kétváltozós” lett.
void plot(char page[H][W], char c, double (*f)(double, double), double param) { ... double fy = f(fx, param); ... } double sin_d_times_x(double x, double d) { return sin(d * x); }
- Az előző megoldásnak az is baja, hogy csak egy paramétere lehet. C-ben ezen csak úgy tudunk segíteni, ha a
double param
helyett egyvoid*
-ot passzolgat tovább aplot()
azf()
-nek; amelyvoid*
tetszőlegesen nagy struktúrára mutat, akármennyi adattal. - Használhatunk globális változót is. Ez egyszerűbb programokban talán még ez a legjobb megoldás. Azonban nagyobb
programokban ez nem szerencsés: globális változó esetén nehéz követni, mi függ össze mivel.
double d; void plot(char page[H][W], char c, double (*f)(double)) { ... double fy = f(fx); ... } double sin_d_times_x(double x) { return sin(d * x); }
Tetszőleges lapméret
Újabb igény érkezik: a felhasználó adhassa meg a lap méretét (amelyet eddig a Consts
enum
adott meg). Írd át a programot C-ben, figyelve arra, hogy a rajz origója továbbra is
a lap közepe maradjon! Figyeld meg, mely helyeken kell módosítani a programot!
Használd az előadáson bemutatott, változóval megadott méretű tömböket!
Az előző feladat kódját kell C++-ba átírni.
sin(x), sin(2x), sin(3x), C++
Hogyan lehet megoldani a parametrizálható függvényt C++-ban? Az igazán OOP-s megoldás nem a sablon (template), hanem az, ha
függvényre mutató pointer helyett a plot()
egy függvényobjektumot kap, amelynek egy virtuális függvényét hívva
kapja meg az eredményt. Valahogy így:
Sin_d_times_x f(0.5); // f(x) = sin(0.5*x)
plot(page, '*', f);
Írd meg így a programot! Miért jobb ez a megoldás, mint bármi, amit procedurális kódban írni tudunk?
Megoldás
A globális változókat is elkerüli, és a hívott „függvény” is egy paraméterű marad, mint a matematikai párja. A változtatás lényege, hogy a függvény helyett függvényobjektumot használunk, és így adatot tudunk társítani mellé. Egy leszármazáson keresztül bármennyit!
A virtuális függvény lehet akár a függvényhívó operátor is, de ez nem szükségszerű.
class Func {
public:
virtual double operator()(double x) const = 0;
};
void plot(char page[H][W], char c, Func const& f) {
...
double fy = f(fx);
...
}
class Sin_d_times_x : public Func {
public:
Sin_d_times_x(double d) {
d_ = d;
}
virtual double operator()(double x) const {
return sin(d_ * x);
}
private:
double d_;
};
Ha a plot()
függvényt sablonná alakítjuk, átvehet sima függvényre mutató pointert is.
Ilyenkor a fenti öröklés akár el is hagyható, mert a sablon úgyis átvehet bármit, nem kell az osztályok
közötti kapcsolatot leírni az örökléssel.
Tetszőleges lapméret, C++
Vedd most újra az eredeti programot, a dinamikus lapméret előtti változatot. Írd át úgy, hogy a rajz kezelése
objektumorientáltan történjen: a paraméterként átadott tömb helyett mindig egy Page
objektumot adj át, amely
tárolja a méretét (és az lekérdezhető), amelynek van setchar()
függvénye, amely figyel a túlindexelésre, esetleg
print()
és clear()
függvénye is! Ha ezzel elkészültél, utána végezd el az előző módosítást:
tedd dinamikussá a lap méretét! (A Page
osztály másoló konstruktorával most ne töltsd az időt, nem lényeges.)
A főprogram valami ilyesmi lehet:
Sin_d_times_x f(0.5);
Page p(40, 10);
plot(p, '*', f);
p.print();
Megoldás
#include <cstdio>
#include <cmath>
class Page {
public:
Page(int w, int h) {
w_ = w;
h_ = h;
page_ = new char*[h_];
for (int y = 0; y < h_; ++y)
page_[y] = new char[w_];
clear();
}
/* destruktor, másoló konstruktor, op= helye */
void clear() {
for (int y = 0; y < h_; ++y)
for (int x = 0; x < w_; ++x)
page_[y][x] = ' ';
}
void print() const {
for (int y = 0; y < h_; ++y) {
for (int x = 0; x < w_; ++x)
putchar(page_[y][x]);
putchar('\n');
}
}
void setchar(int x, int y, char c) {
if (x >= 0 && x < w_ && y >= 0 && y < h_)
page_[y][x] = c;
}
int get_width() const {
return w_;
}
int get_height() const {
return h_;
}
private:
int w_, h_;
char **page_;
};
void plot(Page &page, char c, double (*f)(double)) {
for (int x = 0; x < page.get_width(); ++x) {
double fx = (x - page.get_width()/2)/4.0;
double fy = f(fx);
int y = (fy * 4.0) * -1 + page.get_height()/2;
page.setchar(x, y, c);
}
}
int main() {
Page p(40, 10);
plot(p, '.', sin); /* ebben most nincs benne a fv. obj */
p.print();
}
Hány helyen, hol kell ehhez most módosítanod a programot? Mennyire érzed úgy, hogy a módosítások közben a feladathoz illő, vagy a feladathoz nem illő implementációs részletekkel kell foglalkoznod?
Megoldás
Bár a kód hosszabb, a felelősségek sokkal jobban ki vannak osztva benne: minden, ami a lap kezeléséhez tartozik, az a
Page
osztályba kerül. Ideértve a memóriakezelést, a túlindexelés kezelését, a méret nyilvántartását.
A plot()
függvény interfésze nem függ attól, hogy a Page
osztály belül hogyan
tárolja az adatokat, nem kell azt is módosítani, amikor a Page
osztály memóriakezelése megváltozik.
Ez az egységbe zárás lényege!
2012-es Prog1 vizsgafeladat volt az alábbi.
Írj programot, mely a szabványos bemenetről fájl végéig olvas egy szöveget, a benne található leghosszabb szót megtalálja, és a futás végén a standard kimenetre írja! Egy szó alatt a bemeneti karakterfolyam olyan szakaszát értjük, melyben csak angol betűk vannak. A szavak bármilyen hosszúak lehetnek!
Ehhez dinamikus memóriakezelést kell használni. Karakterenként olvasva a bemenetet, valami ilyet képzelhetünk el:
while (c = új karakter) { if (c egy betű) { szó += c; } else { if (szó > leghosszabb) leghosszabb = szó; szó = ""; } }
C-ben, favágó módszerrel
Írd meg a fenti programot tisztán C-ben, úgy hogy közben nem használhatsz saját függvényeket! A beépített malloc()
,
getchar()
, isalpha()
függvényeket természetesen igen. A megvalósításkor mindig csak arra figyelj, mi a
következő lépés a számítógép számára: azt kell írnod a kódba.
C-ben, függvényekkel
Írd meg a programot C-ben úgy, hogy a sztringek nyújtása +=
és értékadása =
művelethez függvényt
írsz!
Megoldás
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
void append(char **str, char what) {
char *newstr = malloc(strlen(*str) + 2);
sprintf(newstr, "%s%c", *str, what);
free(*str);
*str = newstr;
}
void copy(char **str, char *what) {
free(*str); /* a szabvany szerint szabad null ptr-t */
*str = malloc(strlen(what) + 1);
strcpy(*str, what);
}
int main() {
char *current = NULL, *longest = NULL;
int c;
copy(¤t, "");
copy(&longest, "");
while ((c=getchar()) != EOF) {
if (isalpha(c))
append(¤t, c);
else {
if (strlen(current) > strlen(longest))
copy(&longest, current);
copy(¤t, "");
}
}
free(current);
printf("leghosszabb: %s\n", longest);
free(longest);
}
C++-ban, saját osztállyal
Az előző, függvényes változatban már a dinamikus memóriakezelést végző programrészeket (a sok malloc()
-ot,
free()
-t) külön függvényekbe költöztetted. Csinálj ezekből osztályt, és használd őket a kódban! Írd meg egy saját sztring
típussal a programot!
Megoldás
#include <iostream>
#include <cctype>
#include <cstring>
#include <cstdio>
using namespace std;
class String {
char *str;
public:
String(char const *s = "") { str = new char[strlen(s)+1]; strcpy(str,s); }
String(String const &other) {
str = new char[strlen(other.str)+1];
strcpy(str,other.str);
}
String &operator=(String rhs) {
swap(this->str,rhs.str);
return *this;
}
String &operator+= (int c) {
char *uj = new char[strlen(this->str)+2];
sprintf(uj, "%s%c", this->str, c);
delete [] str;
str = uj;
return *this;
}
size_t size() { return strlen(str); }
~String() {
delete [] str;
}
friend ostream &operator<<(ostream&os, String const &s);
};
ostream &operator<<(ostream &os, String const &s) {
os << s.str;
return os;
}
int main() {
String szo;
String leghosszabb;
szo = "";
leghosszabb = "";
char c;
while (cin.get(c)) {
if (isalpha(c))
szo += (char) c;
else {
if (szo.size() > leghosszabb.size())
leghosszabb = szo;
szo = "";
}
}
cout << leghosszabb << endl;
return 0;
}
Összehasonlítás
Hasonlítsd össze a kapott programokat, különösen az elsőt (C), a harmadikat (C++), és a pszeudokódot! Melyiknél érzed úgy, hogy „azt kell írni, amire gondolsz”? A két forráskód közül melyik hasonlít a pszeudokódra?