C++ 객체지향 핵심 실습: 다형성, 연산자 오버로딩, 클래스 템플릿

실습 1: 추상 인터페이스와 실행 시 다형성

미디어 콘텐츠 발행 시스템을 설계하며 순수 가상 함수와 가상 소멸자의 개념을 익혀봅니다.

media.hpp

#pragma once
#include <string>

// 발행물 공통 인터페이스
class Media {
public:
    explicit Media(const std::string &title_ = "");
    virtual ~Media() = default;

    virtual void release() const = 0;
    virtual void consume() const = 0;

protected:
    std::string title;
};

class Novel : public Media {
public:
    Novel(const std::string &title_ = "", const std::string &writer_ = "");
    void release() const override;
    void consume() const override;
private:
    std::string writer;
};

class Cinema : public Media {
public:
    Cinema(const std::string &title_ = "", const std::string &filmmaker_ = "");
    void release() const override;
    void consume() const override;
private:
    std::string filmmaker;
};

class Track : public Media {
public:
    Track(const std::string &title_ = "", const std::string &performer_ = "");
    void release() const override;
    void consume() const override;
private:
    std::string performer;
};

media.cpp

#include <iostream>
#include "media.hpp"

Media::Media(const std::string &title_) : title{title_} {}

Novel::Novel(const std::string &title_, const std::string &writer_)
    : Media{title_}, writer{writer_} {}

void Novel::release() const {
    std::cout << "출간: [" << title << "] 저자 " << writer << '\n';
}

void Novel::consume() const {
    std::cout << "독서 중: [" << title << "] 저자 " << writer << '\n';
}

Cinema::Cinema(const std::string &title_, const std::string &filmmaker_)
    : Media{title_}, filmmaker{filmmaker_} {}

void Cinema::release() const {
    std::cout << "개봉: [" << title << "] 연출 " << filmmaker << '\n';
}

void Cinema::consume() const {
    std::cout << "시청 중: [" << title << "] 연출 " << filmmaker << '\n';
}

Track::Track(const std::string &title_, const std::string &performer_)
    : Media{title_}, performer{performer_} {}

void Track::release() const {
    std::cout << "발매: [" << title << "] 아티스트 " << performer << '\n';
}

void Track::consume() const {
    std::cout << "청취 중: [" << title << "] 아티스트 " << performer << '\n';
}

demo_polymorphism.cpp

#include <memory>
#include <vector>
#include "media.hpp"

void rawPointerDemo() {
    std::vector<Media*> catalog;
    catalog.push_back(new Novel("1984", "George Orwell"));
    catalog.push_back(new Cinema("기생충", "봉준호"));
    catalog.push_back(new Track("Bohemian Rhapsody", "Queen"));

    for (Media *ptr : catalog) {
        ptr->release();
        ptr->consume();
        delete ptr;
    }
}

void smartPointerDemo() {
    std::vector<std::unique_ptr<Media>> catalog;
    catalog.push_back(std::make_unique<Novel>("삼국지", "나관중"));
    catalog.push_back(std::make_unique<Cinema("올드보이", "박찬욱")>);
    catalog.push_back(std::make_unique<Track>("봄날", "방탄소년단"));

    for (const auto &item : catalog) {
        item->release();
        item->consume();
    }
}

void staticBindingDemo() {
    Novel book("데미안", "Hermann Hesse");
    book.release();
    book.consume();
}

핵심 개념 정리

  • 추상 클래스: Media는 순수 가상 함수를 포함하므로 직접 인스턴스화할 수 없음
  • 가상 소멸자: virtual ~Media() = default를 생략하면 파생 클래스 자원 누수 발생
  • 오버라이드 검증: override 키워드로 기반 클래스 함수와 시그니처 불일치 컴파일 오류 방지

실습 2: 연산자 오버로딩과 커스텀 정렬

도서 판매 기록을 관리하며 출력 연산자와 비교 논리를 재정의합니다.

volume.hpp

#pragma once
#include <string>

class Volume {
public:
    Volume(const std::string &title_,
           const std::string &creator_,
           const std::string &locale_,
           const std::string &code_,
           double tag_);

    friend std::ostream& operator<<(std::ostream &out, const Volume &vol);

private:
    std::string title;
    std::string creator;
    std::string locale;
    std::string code;
    double tag;
};

volume.cpp

#include <iomanip>
#include <iostream>
#include "volume.hpp"

Volume::Volume(const std::string &title_,
               const std::string &creator_,
               const std::string &locale_,
               const std::string &code_,
               double tag_)
    : title{title_}, creator{creator_}, locale{locale_}, code{code_}, tag{tag_} {}

std::ostream& operator<<(std::ostream &out, const Volume &vol) {
    using std::left;
    using std::setw;

    out << left
        << setw(12) << "도서명:" << vol.title << '\n'
        << setw(12) << "저자:" << vol.creator << '\n'
        << setw(12) << "역자:" << vol.locale << '\n'
        << setw(12) << "식별코드:" << vol.code << '\n'
        << setw(12) << "정가:" << vol.tag;

    return out;
}

transaction.hpp / transaction.cpp

// transaction.hpp
#pragma once
#include "volume.hpp"

class Transaction {
public:
    Transaction(const Volume &goods, double sold_at, int qty);
    int quantity() const;
    double proceeds() const;

    friend std::ostream& operator<<(std::ostream &out, const Transaction &tx);

private:
    Volume goods;
    double sold_at;
    int qty;
};

// transaction.cpp
#include <iomanip>
#include <iostream>
#include "transaction.hpp"

Transaction::Transaction(const Volume &goods_, double sold_at_, int qty_)
    : goods{goods_}, sold_at{sold_at_}, qty{qty_} {}

int Transaction::quantity() const { return qty; }

double Transaction::proceeds() const { return qty * sold_at; }

std::ostream& operator<<(std::ostream &out, const Transaction &tx) {
    using std::left;
    using std::setw;

    out << tx.goods << '\n'
        << left << setw(12) << "판매가:" << tx.sold_at << '\n'
        << setw(12) << "수량:" << tx.qty << '\n'
        << setw(12) << "매출:" << tx.proceeds();

    return out;
}

sales_report.cpp

#include <algorithm>
#include <iomanip>
#include <iostream>
#include <vector>
#include "transaction.hpp"

int main() {
    std::vector<Transaction> ledger;

    // 데이터 입력 생략...

    // 함수 객체 대신 람다로 정렬 기준 명시
    std::sort(ledger.begin(), ledger.end(),
        [](const Transaction &a, const Transaction &b) {
            return a.quantity() > b.quantity();
        });

    for (const auto &entry : ledger) {
        std::cout << entry << "\n\n";
    }
}

실습 3: 클래스 템플릿으로 코드 일반화

동일한 논리를 여러 타입에 적용할 때 템플릿의 필요성을 확인합니다.

중복 구조 (템플릿 미사용)

class Point2D {
public:
    Point2D(int a, int b) : x{a}, y{b} {}
    void show() const { std::cout << x << ", " << y << '\n'; }
private:
    int x, y;
};

class Point2D_Double {
public:
    Point2D_Double(double a, double b) : x{a}, y{b} {}
    void show() const { std::cout << x << ", " << y << '\n'; }
private:
    double x, y;
};

템플릿으로 통합

template<typename T>
class Coordinate {
public:
    Coordinate(T a, T b) : x{a}, y{b} {}
    void show() const { std::cout << x << ", " << y << '\n'; }
private:
    T x, y;
};

// 사용 예
Coordinate<int> p1(3, 4);
Coordinate<double> p2(3.14, 2.71);
Coordinate<std::string> p3("hello", "template");

실습 4: 가상 함수와 객체 슬라이싱 방지

#pragma once
#include <string>

class RobotPet {
public:
    explicit RobotPet(const std::string &alias) : alias_{alias} {}
    virtual ~RobotPet() = default;

    std::string nickname() const { return alias_; }
    virtual std::string vocalize() const = 0;

private:
    std::string alias_;
};

class RobotFeline : public RobotPet {
public:
    explicit RobotFeline(const std::string &alias = "Kitty")
        : RobotPet{alias} {}
    std::string vocalize() const override { return "nya~"; }
};

class RobotCanine : public RobotPet {
public:
    explicit RobotCanine(const std::string &alias = "Puppy")
        : RobotPet{alias} {}
    std::string vocalize() const override { return "bow-wow"; }
};
// 다형성 활용 시 주의사항
void safeUsage() {
    std::vector<std::unique_ptr<RobotPet>> shelter;
    shelter.push_back(std::make_unique<RobotFeline>("momo"));
    shelter.push_back(std::make_unique<RobotCanine>("choco"));

    for (const auto &pet : shelter) {
        std::cout << pet->nickname() << ": " << pet->vocalize() << '\n';
    }
    // 소멸자가 virtual이면 각 파생 클래스 소멸자도 정상 호출
}

실습 5: 산술 연산자와 입출력 연산자 오버로딩

imaginary.hpp

#pragma once
#include <iostream>

template<typename Scalar>
class Imaginary {
public:
    Imaginary() : re{0}, im{0} {}
    Imaginary(Scalar r, Scalar i = 0) : re{r}, im{i} {}
    Imaginary(const Imaginary& src) : re{src.re}, im{src.im} {}

    Scalar realPart() const { return re; }
    Scalar imagPart() const { return im; }

    Imaginary& operator+=(const Imaginary& rhs) {
        re += rhs.re;
        im += rhs.im;
        return *this;
    }

    bool operator==(const Imaginary& rhs) const {
        return re == rhs.re && im == rhs.im;
    }

    friend Imaginary operator+(const Imaginary& lhs, const Imaginary& rhs) {
        return Imaginary{lhs.re + rhs.re, lhs.im + rhs.im};
    }

    friend std::ostream& operator<<(std::ostream& out, const Imaginary& c) {
        out << c.re;
        if (c.im >= 0) out << "+" << c.im << "i";
        else            out << "-" << -c.im << "i";
        return out;
    }

    friend std::istream& operator>>(std::istream& in, Imaginary& c) {
        in >> c.re >> c.im;
        return in;
    }

private:
    Scalar re, im;
};

demo_complex.cpp

#include <iostream>
#include "imaginary.hpp"

void arithmeticDemo() {
    using std::cout;
    using std::boolalpha;

    Imaginary<int> a(3, -4), b(a);

    cout << "a = " << a << '\n';
    cout << "b = " << b << '\n';
    cout << "a + b = " << a + b << '\n';

    a += b;
    cout << "after a += b: " << a << '\n';
    cout << "a == b ? " << boolalpha << (a == b) << '\n';
}

void ioDemo() {
    Imaginary<double> x, y;
    std::cout << "Enter two complex numbers: ";
    std::cin >> x >> y;

    std::cout << "x = " << x << '\n';
    std::cout << "y = " << y << '\n';

    const Imaginary<double> z(x);
    std::cout << "z.real = " << z.realPart()
              << ", z.imag = " << z.imagPart() << '\n';
}

설계 포인트

연산자구현 방식이유
operator+=멤버 함수좌측 객체가 암묵적으로 this로 바인딩
operator+비멤버 friend대칭적 호출(내장 타입 + 객체) 지원
operator<<, operator>>비멤버 friend스트림 객체가 좌측 피연산자여야 함

태그: C++ OOP 다형성 연산자 오버로딩 클래스 템플릿

7월 6일 05:44에 게시됨