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.

1. Függvény rajzolása, C

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 egy void*-ot passzolgat tovább a plot() az f()-nek; amely void* 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!

2. Függvény rajzolása, C++

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!

3. Megszentségteleníthetetlenségeskedéseitekért

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

leghosszabb_1.c

Í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

leghosszabb_2.c

Í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(&current, "");
    copy(&longest, "");
    while ((c=getchar()) != EOF) {
        if (isalpha(c))
            append(&current, c);
        else {
            if (strlen(current) > strlen(longest))
                copy(&longest, current);
            copy(&current, "");
        }
    }
    free(current);
    printf("leghosszabb: %s\n", longest);
    free(longest);
}

C++-ban, saját osztállyal

leghosszabb_3.cpp

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?