C++에서 새로 도입된 개념인 class를 정의하고 구현하는 방법까지 알아보았다.
이번엔 객체지향 그 자체인 다형성과 상속에 대해 공부해 보자.
자동차를 만든다고 가정해보자. 각 자동차자마다 세부 스펙은 다르겠지만, 아래와 같이 모든 자동차가 가지는 속성이 있다.
1.속도
2.색상
이를 class로 표현할 때 매번 모든 차량에 이 공통적인 특성을 반복해서 구현하기 보다는.. 하나를 구현해놓고 사용할 수 있으면 관리도 편하고 코드라인도 짧아질 것 이다.
최종적으로 아래 왼쪽에 있는 class 구조가 오른쪽 처럼 변경되게 해보자.
상속 대상이 되는 클래스를 우리는 "기본 클래스 라고한다.
그리고 기본 클래스의 상속을 받는 클래스를 "파생 클래스"라고 한다.
상속도 깊이 배울려면 한참 배워야하지만.. 이번엔 가장 기본적인 형태만 보자.
일단 부모 클래스인 Vehicle 클래스를 구현했다.
공통 속성인 속도, 색상 관련 함수가 구현되어 있다.
주목해야 할 점은 멤머변수가 private가 아닌 protected로 되어 있다는 것인데, 이렇게 해야 상속받은 class는 멤버변수를 사용할 수 있고 외부에선 접근할 수 없다.
#include <iostream>
#include <string>
using namespace std;
class Vehicle {
protected:
string color;
int speed;
public:
Vehicle(string c, int s) : color(c), speed(s) {}
void move() {
cout << "The vehicle is moving at " << speed << " km/h." << endl;
}
void setColor(string c) {
color = c;
}
string getColor() {
return color;
}
};
자식 클래스인 Bicycle class와 Truck class를 구현했다.
주목할 문법은 2개이다.
•class정의 시, 오른쪽에 public 부모 클래스명이 추가되었다는 것.
•자식 클래스의 생성자에서 부모클래스의 생성자가 호출되었다는 것.
// 파생 클래스 1: 자전거
class Bicycle : public Vehicle {
private:
bool hasBasket;
public:
Bicycle(string c, int s, bool basket) : Vehicle(c, s), hasBasket(basket) {}
void ringBell() {
cout << "Bicycle bell: Ring Ring!" << endl;
}
};
// 파생 클래스 2: 트럭
class Truck : public Vehicle {
private:
int cargoCapacity;
public:
Truck(string c, int s, int capacity)
: Vehicle(c, s), cargoCapacity(capacity) {
}
void loadCargo() {
cout << "Truck loading cargo. Capacity: " << cargoCapacity << " tons."
<< endl;
}
};
메인 함수이다. 자식 객체에서 부모 클래스에서 정의된 멤버함수를 사용하고 있는걸 볼 수 있다.
int main()
{
Bicycle b("Yellow", 30, true);
Truck t("Blue", 40, 95);
b.ringBell();
t.loadCargo();
return 0;
}

다형성이란 프로그램 언어 각 요소들(상수,변수,식,객체 메소드 등)이 다양한 자료형(type)에 속하는 것이 허가되는 성질을 가리킨다.
다형성
이렇게 말로만 들어선 잘 모르겠으니 한번 다형성이 적용되지 않은코드와 적용된 코드를 비교해보고 다형성에 대해 알아보자.
wolf class와 lion클래스도 정상적으로 만들어졌꼬 이 class들의 울음소리를 출력할 함수도 제작했다.
#include <iostream>
#include <string>
using namespace std;
class Lion
{
public:
Lion(string word):m_word(word){}
void bark() { cout << "Lion" << " " << m_word << endl; }
private:
string m_word;
};
class Wolf
{
public:
Wolf(string word) :m_word(word) {}
void bark() { cout << "Wolf" << " " << m_word << endl; }
private:
string m_word;
};
void print(Lion lion)
{
lion.bark();
}
void print(Wolf wolf)
{
wolf.bark();
}
int main()
{
Lion lion("ahaaaaaa!");
Wolf wolf("ohhhhh");
print(lion);
print(wolf);
return 0;
}

여기서 dog class가 하나더 만들어졌다고 해보자. 여기서 주목할 점은 사용자가 알아야 할 'class'가 하나 더 늘어났으며, 외부 함수도 하나 더 필요해졌다는 것 이다.
#include <iostream>
#include <string>
using namespace std;
class Lion
{
public:
Lion(string word):m_word(word){}
void bark() { cout << "Lion" << " " << m_word << endl; }
private:
string m_word;
};
class Wolf
{
public:
Wolf(string word) :m_word(word) {}
void bark() { cout << "Wolf" << " " << m_word << endl; }
private:
string m_word;
};
class Dog
{
public:
Dog(string word) :m_word(word) {}
void bark() { cout << "Dog" << " " << m_word << endl; }
private:
string m_word;
};
void print(Lion lion)
{
lion.bark();
}
void print(Wolf wolf)
{
wolf.bark();
}
void print(Dog dog)
{
dog.bark();
}
int main()
{
Lion lion("ahaaaaaa!");
Wolf wolf("ohhhhh");
Dog dog("oooooooooooooops");
print(lion);
print(wolf);
print(dog);
return 0;
}

새로운 동물이 생길때마다 관리해야 할 class가 많이 생기는 이유는 아래와 같다.
Lion,Wolf,Dog모두 타입이 다르기 때문에 전부 따로 관리 해야 한다.
여기서 필요한 개념이 다형성이다.
다형성은 대표 class를 만들어서 정의만 하고, 실제 구현은 파생 클래스에서 하는 기법이다.
이 때 실제 호출시 파생 클래스를 학인해라 라는 의미로 함수앞에 virtual을 붙인다.
이러한 모양의 함수를 가상함수라고 한다.

#include <iostream>
#include <string>
using namespace std;
class Animal
{
public:
Animal() {}
virtual void bark() {};
};
class Lion : public Animal
{
public:
Lion(string word) :m_word(word) {}
void bark() { cout << "Lion" << " " << m_word << endl; }
private:
string m_word;
};
class Wolf : public Animal
{
public:
Wolf(string word) :m_word(word) {}
void bark() { cout << "Wolf" << " " << m_word << endl; }
private:
string m_word;
};
class Dog : public Animal
{
public:
Dog(string word) :m_word(word) {}
void bark() { cout << "Dog" << " " << m_word << endl; }
private:
string m_word;
};
void print(Animal* animal)
{
animal->bark();
}
int main()
{
Lion lion("ahaaaaaa!");
Wolf wolf("ohhhhh");
Dog dog("oooooooooooooops");
print(&lion);
print(&wolf);
print(&dog);
return 0;
}

기본 클래스에 일반 가상함수가 적용된 코드이다. 일반 가상함수의 정의는 필수가 아니다.
Animal타입 앞에 이 붙었다. 이걸 포인터 타입이라고 한다. 일반 변수가 값을 담는다면, 포인터변수는 말 그대로 변수를 가르킬 수 있다고 보면 된다. 대상이 되는 변수는 앞에 &를 붙이면 된다.
부가적으로 와 &에대한 내용은 포인터와 레퍼런스를 참고하자.
#include <iostream>
using namespace std;
// 기본 클래스: Animal
class Animal {
public:
// 가상 함수: 자식 클래스에서 재정의 가능
virtual void makeSound() {
cout << "Animal makes a sound." << endl;
}
};
// 파생 클래스: Dog
class Dog : public Animal {
public:
void makeSound() {
cout << "Dog barks: Woof! Woof!" << endl;
}
};
// 파생 클래스: Cat
class Cat : public Animal {
public:
void makeSound() {
cout << "Cat meows: Meow! Meow!" << endl;
}
};
int main() {
// Animal 타입 포인터로 다양한 객체를 가리킴
Animal* myAnimal;
Dog myDog;
Cat myCat;
// Dog 객체 가리키기
myAnimal = &myDog;
myAnimal->makeSound(); // Dog의 makeSound() 호출
// Cat 객체 가리키기
myAnimal = &myCat;
myAnimal->makeSound(); // Cat의 makeSound() 호출
return 0;
}

순수 가상함수가 적용된 코드이다.
• 가상함수에 0을 대입하는 것 같은 문법이다. 순수가상함수는 해당 함수를 파생 클래스에서 반드시 구현해야 한다.
• 순수가상함수를 포함한 클래스는 그 자체로 변수가 될 수 없다.(인스턴스화 한다라고도한다.) 따라서 변수 선언시 에러가 발생함.
#include <iostream>
using namespace std;
// 기본 클래스: Animal
class Animal {
public:
// 가상 함수: 자식 클래스에서 재정의 가능
virtual void makeSound() = 0;
};
// 파생 클래스: Dog
class Dog : public Animal {
public:
void makeSound() {
cout << "Dog barks: Woof! Woof!" << endl;
}
};
// 파생 클래스: Cat
class Cat : public Animal {
public:
void makeSound() {
cout << "Cat meows: Meow! Meow!" << endl;
}
};
int main() {
// Animal 타입 포인터로 다양한 객체를 가리킴
Animal* myAnimal;
Dog myDog;
Cat myCat;
// Dog 객체 가리키기
myAnimal = &myDog;
myAnimal->makeSound(); // Dog의 makeSound() 호출
// Cat 객체 가리키기
myAnimal = &myCat;
myAnimal->makeSound(); // Cat의 makeSound() 호출
return 0;
}

new연산자를 활용한 코드이다.
(new, delete는 추후에 자세히 다루도록 하겠다.)
• new 연산자를 하지 않고 클래스 배열을 만들게 되면, 생성과 동시에 기본생성자가 모두 호출된다. 원하는 시점에 객체가 생성되게 하기 위해서 new 연산자를 사용한다.
• new 연산자로 생성한 객체는 사용자가 직접 메모리 관리를 해줘야 한다. 출력값을 보고 어떤 함수가 어떤 순서로 호출되었는지 유심히 보자.
#include <iostream>
using namespace std;
// 기본 클래스: Employee
class Employee {
public:
Employee() {
cout << "Employee 기본 생성자 호출!" << endl;
}
virtual void work() {
cout << "Employee is working." << endl;
}
virtual ~Employee() {
cout << "Employee 소멸자 호출!" << endl;
}
};
// 파생 클래스: Developer
class Developer : public Employee {
public:
Developer() {
cout << "Developer 기본 생성자 호출!" << endl;
}
void work() {
cout << "Developer is coding." << endl;
}
~Developer() {
cout << "Developer 소멸자 호출!" << endl;
}
};
// 파생 클래스: Manager
class Manager : public Employee {
public:
Manager() {
cout << "Manager 기본 생성자 호출!" << endl;
}
void work() {
cout << "Manager is planning." << endl;
}
~Manager() {
cout << "Manager 소멸자 호출!" << endl;
}
};
int main() {
cout << "=== 정적 배열 사용 ===" << endl;
// Employee 배열 (기본 생성자 호출됨)
Employee team_static[2]; // 기본 생성자만 호출됨
team_static[0].work(); // Employee의 work() 호출
team_static[1].work(); // Employee의 work() 호출
cout << "=== 동적 배열 사용 ===" << endl;
// 동적 메모리 할당
Employee* team_dynamic2[2];
team_dynamic2[0] = new Developer(); // Developer 객체 생성
team_dynamic2[1] = new Manager(); // Manager 객체 생성
for (int i = 0; i < 2; i++) {
team_dynamic2[i]->work(); // 다형성 적용, 각각의 work() 호출
}
// 동적 메모리 해제
for (int i = 0; i < 2; i++) {
delete team_dynamic2[i];
}
return 0;
}

간단하게 다형성을 활용하여 게임 스킬 사용 프로그램을 만들어보자.
• 기본 클래스
Adventure라는 기본 클래스를 정의하세요.
useSkill()이라는 순수가상함수를 선언하세요.
• 파생 클래스
Warrior, Mage, Archer라는 세 가지 파생 클래스를 만드세요.
각 클래스의 useSkill 함수를 재정의 해서 아래와 같이 출력하세요.
Warrior: Warrior uses Slash!
Mage: Mage casts Fireball!
Archer: Archer shoots an Arrow!
• 다형성 구현
Adventure*타입의 포인터를 사용하여 여러 모험가 객체를 가르키고, 반복문을 통해 각 모험가의 스킬을 호출하는 동작을 구현하세요.
아래는 완성된 모습이다.

Warrior uses Slash!
Mage casts Fireball!
Archer shoots an Arrow!
#include <iostream>
#include <vector>
using namespace std;
class Adventurer {
public:
virtual void useSkill() = 0; // 순수 가상 함수
virtual ~Adventurer(){}
};
class Warrior : public Adventurer {
public:
void useSkill() override{
cout << "Warrior uses Slash!" << endl;
}
};
class Mage : public Adventurer {
public:
void useSkill() override {
cout << "Mage casts Fireball!" << endl;
}
};
class Archer : public Adventurer {
public:
void useSkill() override {
cout << "Archer shoots an Arrow" << endl;
}
};
int main() {
vector<Adventurer*> adventurers;
adventurers.push_back(new Warrior());
adventurers.push_back(new Mage());
adventurers.push_back(new Archer());
// 각 모험가의 스킬 사용
for (int i = 0; i < adventurers.size(); i++) {
adventurers[i]->useSkill();
}
// 메모리 해제
for (int i = 0; i < adventurers.size(); i++) {
delete adventurers[i];
}
return 0;
}

오늘은 객체지향특징에서의 중요한 상속과 다형성에 대해 공부해보았다. 오랜만에 다시봐서 그런가 헷갈리는 부분도많았고 포인터부분도 좀 헷갈렸다. 다시 공부해서 개념을 확실히 해야겠다.