실습 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 | 스트림 객체가 좌측 피연산자여야 함 |