학습 목표
- 상속의 개념과 활용 이해
- 기본 클래스와 파생 클래스 정의 및 구현
- 접근 제어자(protected)의 역할과 사용법 학습
- 다형성의 원리와 실제 활용 방안 숙지
1. 상속
1.1 상속 없는 차량 관리 프로그램
기존 방식의 문제점
class Bicycle {
string color;
int speed;
bool hasBasket;
public:
Bicycle(string c, int s, bool basket) : color(c), speed(s), hasBasket(basket) {}
void move() { cout << "The bicycle is moving at " << speed << " km/h." << endl; }
void ringBell() { cout << "Bicycle bell: Ring Ring!" << endl; }
};
class Truck {
string color;
int speed;
int cargoCapacity;
public:
Truck(string c, int s, int capacity) : color(c), speed(s), cargoCapacity(capacity) {}
void move() { cout << "The truck is moving at " << speed << " km/h." << endl; }
void loadCargo() { cout << "Truck loading cargo. Capacity: " << cargoCapacity << " tons." << endl; }
};
문제점 요약
- 공통 속성(color, speed)과 동작(move)이 각 클래스에 중복되어 구현됨.
- 새로운 차량 클래스 추가 시 기존 코드 수정 필요.
1.2 상속을 활용한 클래스 정의
클래스 구조
- 기본 클래스(Vehicle): 공통 속성과 동작 정의
- 파생 클래스(Bicycle, Truck): 고유 특성 추가
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; }
};
Bicycle 클래스
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; }
};
Truck 클래스
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; }
};
1.3 접근 제어자(protected)의 사용
- protected: 파생 클래스에서 접근 가능하지만 외부에서는 접근 불가.
- 효과: 공통 속성을 외부에 노출하지 않으면서 파생 클래스에서 재사용 가능.
class Vehicle {
protected:
string color;
int speed;
};
전체 프로그램 구현
int main() {
Bicycle b("Yellow", 30, true);
Truck t("Blue", 40, 95);
b.move();
b.ringBell();
t.move();
t.loadCargo();
return 0;
}
결과: 공통 동작과 속성을 상속으로 관리하여 코드 중복을 제거하고 확장성을 확보.
2. 다형성 (Polymorphism)
2.1 다형성 정의
- 다형성: 대표 클래스(부모 클래스)를 정의하고, 세부 구현은 이를 상속받는 파생 클래스에서 담당하도록 하는 기법.
- 다양한 데이터 타입을 하나의 인터페이스로 처리할 수 있으며, 유지보수성과 확장성을 극대화함.
2.2 다형성 적용 전 코드
문제점
- 동물마다 별도의 클래스와 함수를 생성해야 함.
- 새로운 동물 클래스 추가 시, 기존 코드에 변경 사항이 생김.
- 코드 중복이 발생하고, 유지보수가 어려움.
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;
}
2.3 다형성 적용
개선 방법
- 공통 부모 클래스 생성: 모든 동물 클래스가 상속받는 Animal 클래스 정의.
- 가상 함수 사용: 부모 클래스에 virtual 키워드를 사용해 공통 메서드(bark) 정의.
- 동적 바인딩: 부모 클래스 포인터를 사용하여 적절한 자식 클래스의 메서드 호출.
class Animal {
public:
virtual void bark() = 0; // 순수 가상 함수
};
class Lion : public Animal {
private:
string m_word;
public:
Lion(string word) : m_word(word) {}
void bark() override { cout << "Lion: " << m_word << endl; }
};
class Wolf : public Animal {
private:
string m_word;
public:
Wolf(string word) : m_word(word) {}
void bark() override { cout << "Wolf: " << m_word << endl; }
};
class Dog : public Animal {
private:
string m_word;
public:
Dog(string word) : m_word(word) {}
void bark() override { cout << "Dog: " << m_word << endl; }
};
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;
}
3. 포인터 개념과 활용
3.1 포인터란?
- 포인터(pointer)는 변수의 메모리 주소를 저장하는 특별한 변수.
- C++에서 포인터는 강력하면서도 위험한 도구로, 잘못된 사용 시 프로그램 충돌이나 메모리 누수 등을 초래할 수 있음.
포인터 기본 문법
int main() {
int a = 10;
int* p = &a; // 'p'는 'a'의 메모리 주소를 저장
cout << "a의 값: " << a << endl; // 10 출력
cout << "a의 주소: " << &a << endl; // 메모리 주소 출력
cout << "p가 가리키는 값: " << *p << endl; // 'p'가 가리키는 값, 즉 10 출력
*p = 20; // 'p'를 통해 'a'의 값을 수정
cout << "a의 새로운 값: " << a << endl; // 20 출력
return 0;
}
3.2 포인터와 배열
- 배열은 포인터와 밀접한 연관이 있음.
- 배열 이름은 첫 번째 요소의 주소를 가리키는 포인터로 사용 가능.
int main() {
int arr[3] = {1, 2, 3};
int* p = arr; // 배열 이름은 첫 번째 요소의 주소를 나타냄
cout << *p << endl; // 1 출력
cout << *(p + 1) << endl; // 2 출력
cout << *(p + 2) << endl; // 3 출력
return 0;
}
3.3 포인터의 위험성
- 초기화되지 않은 포인터 사용
- 메모리 해제 후 포인터 사용
- 잘못된 주소에 접근
스마트 포인터
- C++11에서 도입된 스마트 포인터는 메모리 누수를 방지하는 데 유용.
- 주요 타입: std::unique_ptr, std::shared_ptr, std::weak_ptr.
#include <memory>
#include <iostream>
using namespace std;
int main() {
unique_ptr<int> uptr = make_unique<int>(42); // 동적 메모리 자동 관리
cout << *uptr << endl; // 42 출력
return 0;
}
4. 다형성을 활용한 게임 스킬 사용 프로그램(숙제)
4.1 숙제 개요
다형성을 활용하여 다양한 모험가들이 고유의 스킬을 사용하는 프로그램을 구현합니다. 이는 객체지향 프로그래밍(OOP)의 핵심 개념인 상속과 다형성을 실제로 적용하는 예제입니다.
4.2 프로그램 구현
기본 클래스
Adventure라는 기본 클래스를 정의하며, 모든 모험가 클래스의 부모로 사용
class Adventure {
public:
virtual void useSkill() = 0; // 순수 가상 함수
virtual ~Adventure() {} // 가상 소멸자
};
파생 클래스
각 직업(전사, 마법사, 궁수)의 스킬을 재정의
class Warrior : public Adventure {
public:
void useSkill() override {
cout << "Warrior uses Slash!" << endl;
}
};
class Mage : public Adventure {
public:
void useSkill() override {
cout << "Mage casts Fireball!" << endl;
}
};
class Archer : public Adventure {
public:
void useSkill() override {
cout << "Archer shoots an Arrow!" << endl;
}
};
다형성 구현
Adventure 포인터를 사용하여 다양한 모험가 객체를 가리키고, 반복문으로 각 객체의 스킬을 호출
int main() {
// 모험가 객체 생성
Warrior warrior;
Mage mage;
Archer archer;
// 모험가 리스트 생성
Adventure* adventurers[] = {&warrior, &mage, &archer};
// 모든 모험가의 스킬 호출
for (Adventure* adventurer : adventurers) {
adventurer->useSkill();
}
return 0;
}
4.3 실행 결과
프로그램 실행 시, 각 모험가의 스킬이 호출됨
Warrior uses Slash!
Mage casts Fireball!
Archer shoots an Arrow!
4.4 학습 포인트
- 다형성의 이점: 공통된 인터페이스(Adventure)를 통해 코드를 간결하고 확장 가능하게 유지.
- 가상 함수 활용: 각 파생 클래스의 고유 동작을 동적으로 호출.
- 순수 가상 함수: 추상 클래스를 통해 강력한 구조적 제약 제공.
최종 요약
위의 예제는 다형성을 활용한 객체지향 프로그래밍의 핵심 원리를 잘 보여줍니다. 다양한 객체를 동일한 방식으로 처리할 수 있으며, 코드의 재사용성과 유지보수성을 높입니다. 이를 통해 실무적인 응용 프로그램에서도 구조적 효율성을 높이는 방법을 배웠습니다.
과제 임시 저장
필수 과제:
#include <iostream>
using namespace std;
class Animal {
public:
virtual void makeSound() = 0; // 순수 가상 함수
virtual ~Animal() {} // 가상 소멸자
};
class Dog : public Animal {
public:
void makeSound() override { cout << "Bow Wow" << endl; } //overrisde로 makeSound 재정의
};
class Cat : public Animal {
public:
void makeSound() override { cout << "Meow Meow" << endl; }
};
class Cow : public Animal {
public:
void makeSound() override { cout << "Moo Moo" << endl; }
};
int main() {
Dog dog;
Cat cat;
Cow cow;
//다형성
Animal* animals[] = { &dog, &cat, &cow };
for (Animal* animal : animals) { // animals 배열 안에 있는 Animal 타입의 animal의 주소들
animal->makeSound();
}
}
도전 과제:
#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;
// Animal 기본 클래스
class Animal {
public:
virtual void makeSound() = 0; // 순수 가상 함수
virtual ~Animal() {} // 가상 소멸자
};
// Dog 클래스
class Dog : public Animal {
public:
void makeSound() override { cout << "Bow Wow" << endl; }
};
// Cat 클래스
class Cat : public Animal {
public:
void makeSound() override { cout << "Meow Meow" << endl; }
};
// Cow 클래스
class Cow : public Animal {
public:
void makeSound() override { cout << "Moo Moo" << endl; }
};
// Zoo 클래스
class Zoo {
private:
Animal* animals[10]; // 동물 객체를 저장하는 동적 배열
int animalCount = 0; // 현재 동물 개수
public:
// 동물을 동물원에 추가하는 함수
void addAnimal(Animal* animal) {
if (animalCount < 10) {
animals[animalCount++] = animal; // 동물 객체를 배열에 추가
} else {
cout << "동물원에 더 이상 동물을 추가할 수 없습니다!" << endl;
}
}
// 동물원에 있는 모든 동물의 행동을 수행하는 함수
void performActions() {
for (int i = 0; i < animalCount; ++i) { // 모든 동물 객체에 대해 순차적으로 소리 내기
animals[i]->makeSound();
}
}
// Zoo 소멸자
~Zoo() {
for (int i = 0; i < animalCount; ++i) {
delete animals[i]; // 동적 할당된 동물 객체 메모리 해제
}
}
};
// 랜덤 동물을 생성하는 함수
Animal* createRandomAnimal() {
int random = rand() % 3; // 랜덤 수를 3을 나눈 나머지. 0, 1, 2 중 하나가 생성됨
// 난수 값에 따라 Dog, Cat, Cow 객체를 동적으로 생성하고 그 주소값(포인터)을 반환
if (random == 0) {
return new Dog();
} else if (random == 1) {
return new Cat();
} else {
return new Cow();
}
}
int main() {
srand(static_cast<unsigned int>(time(0))); // 난수 생성, 랜덤 수를 양수로 형 변형
Zoo zoo;
// 동물 5마리를 랜덤으로 생성하여 동물원에 추가
for (int i = 0; i < 5; ++i) {
Animal* randomAnimal = createRandomAnimal();
zoo.addAnimal(randomAnimal); // 동물원에 동물 추가
}
// 동물들의 소리와 행동을 실행
zoo.performActions();
return 0;
}
더보기
Zoo 클래스
class Zoo {
private:
Animal* animals[10]; // 동물 객체를 저장하는 동적 배열
int animalCount = 0; // 현재 동물 개수
public:
void addAnimal(Animal* animal) {
if (animalCount < 10) {
animals[animalCount++] = animal; // 동물 객체를 배열에 추가
} else {
cout << "동물원에 더 이상 동물을 추가할 수 없습니다!" << endl;
}
}
void performActions() {
for (int i = 0; i < animalCount; ++i) {
animals[i]->makeSound(); // 모든 동물 객체에 대해 순차적으로 소리 내기
}
}
~Zoo() {
for (int i = 0; i < animalCount; ++i) {
delete animals[i]; // 동적 할당된 동물 객체 메모리 해제
}
}
};
- Zoo 클래스는 동물들을 관리하는 클래스입니다.
- animals[10]: 최대 10마리 동물을 저장할 수 있는 Animal* 타입의 고정 크기 배열입니다.
- animalCount: 현재 동물원에 저장된 동물의 개수를 추적하는 변수입니다. 이를 통해 동물의 추가와 삭제를 관리합니다.
addAnimal 함수
void addAnimal(Animal* animal) {
if (animalCount < 10) {
animals[animalCount++] = animal; // 동물 객체를 배열에 추가
} else {
cout << "동물원에 더 이상 동물을 추가할 수 없습니다!" << endl;
}
}
- addAnimal 함수는 동물 객체를 받아 animals 배열에 추가하는 함수입니다.
- animalCount가 10 미만일 때만 동물을 추가할 수 있도록 조건문을 사용하여 배열 크기를 초과하는 동물이 추가되지 않도록 방지합니다.
- 동물 객체가 추가될 때마다 animalCount를 증가시켜, 추가된 동물의 개수를 추적합니다.
~Zoo 소멸자
~Zoo() {
for (int i = 0; i < animalCount; ++i) {
delete animals[i]; // 동적 할당된 동물 객체 메모리 해제
}
}
- ~Zoo() 소멸자는 Zoo 객체가 소멸될 때 호출됩니다. 이때 동물원에 추가된 동물 객체들이 동적으로 생성되었기 때문에 delete를 통해 메모리를 해제합니다.
- animalCount만큼 반복하여, animals 배열에 저장된 각 동물 객체의 메모리를 해제합니다.
이 코드는 동물 객체를 생성하고, 동물원에 추가하며, 동물들의 소리와 행동을 출력하는 프로그램입니다. 각 동물 클래스는 Animal 클래스를 상속받아 다형성을 구현하며, Zoo 클래스는 동물 객체들을 관리하고 소멸 시 동적 메모리 해제를 통해 메모리 누수를 방지합니다.