안녕하세요!
오늘도 계속해서 C++ 객체지향 프로그래밍에 대해서 학습할 예정입니다.
앞으로의 진행 순서는 다형성에 대해 학습한 후, 지금까지 학습한 내용으로 과제를 하나 진행할 겁니다.
그리고 자원 관리, 템플릿, STL 기초, 객체지향 설계, ... 순서로 진행이 되겠습니다.
오늘의 목표는 과제를 끝내고 제출하는 것까지로 설정하였습니다!
바로 학습 시작 해보도록 하겠습니다.
다형성(Polymorphism)
동일한 인터페이스에서 함수나 메소드가 다양한 동작으로 작동할 수 있는 객체 지향 프로그래밍의 특성입니다.
지난 글과 마찬가지로 실제로 코드를 작성해보고 그 코드를 설명하면서 학습을 진행하겠습니다.
먼저 animal.h 헤더파일을 만들고 그 곳에 Lion, Wolf, Dog 클래스를 선언하였습니다.
각 클래스에는 멤버 변수 barkSound가 있고 해당 변수에는 울음소리를 string으로 저장합니다.
멤버 함수로는 bark가 있으며, 해당 함수는 울음소리를 출력하는 함수입니다.
#ifndef _ANIMAL_H_
#define _ANIMAL_H_
#include <iostream>
#include <string>
using namespace std;
class Lion
{
public:
Lion(string defaultBarkSound);
void bark(void);
private:
string barkSound;
};
class Wolf
{
public:
Wolf(string defaultBarkSound);
void bark(void);
private:
string barkSound;
};
class Dog
{
public:
Dog(string defaultBarkSound);
void bark(void);
private:
string barkSound;
};
#endif
다음으로 main.cpp 소스코드파일에서 각 클래스의 생성자와 멤버 함수를 기능에 맞게 정의 하였습니다.
추가적으로 printBark() 함수를 만들어 인자에 객체를 넣으면 객체에 맞는 울음소리가 나오도록 함수를 정의 하였습니다.
그리고 main() 함수에서 Lion, Wolf, Dog 를 객체로 인스턴스화 한 후, printBark를 통해 울음소리를 출력하였습니다.
//#include "student.h"
//#include "vehicle.h"
#include "animal.h"
using namespace std;
// 다형성(Polymorphism)
Lion::Lion(string defaultBarkSound = "The Lion's bark") : barkSound(defaultBarkSound) {}
void Lion::bark(void)
{
cout << "Lion => " << barkSound << endl;
}
Wolf::Wolf(string defaultBarkSound = "The Wolf's bark") : barkSound(defaultBarkSound) {}
void Wolf::bark(void)
{
cout << "Wolf => " << barkSound << endl;
}
Dog::Dog(string defaultBarkSound = "The Dog's bark") : barkSound(defaultBarkSound) {}
void Dog::bark(void)
{
cout << "Dog => " << barkSound << endl;
}
void printBark(Lion lion)
{
lion.bark();
}
void printBark(Wolf wolf)
{
wolf.bark();
}
void printBark(Dog dog)
{
dog.bark();
}
int main(void)
{
Lion lion("Roar");
Wolf wolf("Awoo");
Dog dog("woof");
printBark(lion);
printBark(wolf);
printBark(dog);
return 0;
}
다만 이렇게 구현하면 각 클래스 마다 bark() 멤버 함수를 정의 해야하고 새로운 동물로 클래스를 만들 때마다 printBark() 함수가 클래스에 호환되도록 오버로딩(Overloading) 해주어야 합니다.
**오버로딩(Overloading)이란? 마이크로소프트에서는 "C++를 사용하면 동일한 범위에서 동일한 이름의 함수를 둘 이상 지정할 수 있습니다. 이러한 함수를 오버로드된 함수 또는 오버로드라고 합니다." 이렇게 설명하고 있습니다.
저의 경우에는 "하나의 함수 이름으로 다양한 자료형의 반환 값 혹은 인자 값을 받을 수 있고 입력 받은 인자의 자료형에 맞게 함수가 작동할 수 있도록 하는 것"이라고 이해하였습니다.**
객체지향언어인 C++의 다형성을 이용하면 기본이 되는 기본 클래스에 함수의 인터페이스를 정의하고 실제 구현만 파생 클래스에 하여 같은 작업을 덜할 수 있습니다. 이러한 동작을 동적 바인딩이라고 합니다.
동적 바인딩을 하는 방법은 기본 클래스의 인터페이스로 사용할 함수 앞에 virtual 키워드를 붙여줍니다.
virtual 키워드를 붙이면 가상 함수 테이블이 생성되고 기본 클래스를 상속 받은 파생 클래스에서 해당 함수를 재정의 하면 그 위치가 저장됩니다. 그리고 함수를 호출하면 호출한 파생 클래스에 맞게 함수가 실행됩니다.
위의 코드를 virtual 키워드를 사용해 구현 해보겠습니다.
animal.h 헤더파일에는 Animal 클래스를 선언하고 virtual 키워드를 붙여 bark() 함수를 정의 하였습니다.
그리고 Lion, Wolf, Dog 클래스를 Animal 클래스의 파생 클래스로 선언 하였습니다.
#ifndef _ANIMAL_H_
#define _ANIMAL_H_
#include <iostream>
#include <string>
using namespace std;
class Animal
{
public:
Animal(void) {};
virtual void bark(void) {};
};
class Lion : public Animal
{
public:
Lion(string defaultBarkSound);
void bark(void);
private:
string barkSound;
};
class Wolf : public Animal
{
public:
Wolf(string defaultBarkSound);
void bark(void);
private:
string barkSound;
};
class Dog : public Animal
{
public:
Dog(string defaultBarkSound);
void bark(void);
private:
string barkSound;
};
#endif
animal.h 헤더파일에서 Animal 클래스를 선언하고 멤버 함수들을 정의했기 때문에 main.cpp 소스코드파일에서는 printBark() 함수가 인자를 제대로 받을 수 있도록 수정하였습니다.
printBark() 함수를 보면 Animal 클래스의 포인터 타입으로 값을 받는 것을 확인 할 수 있습니다.
그 이유는 bark()함수를 virtual 키워드를 통해 동적 바인딩 하여 가상 함수 테이블이 생성되었고 파생 클래스에서 함수를 재정의 할 때, 재정의 된 함수의 위치 즉, 주소 값이 가상 함수 테이블에 저장되기 때문에 각각의 파생 클래스의 재정의 된 bark() 함수 주소 값을 가져오기 위해 인자를 Animal형 포인터로 받습니다.
//#include "student.h"
//#include "vehicle.h"
#include "animal.h"
using namespace std;
// 다형성(Polymorphism)
Lion::Lion(string defaultBarkSound = "The Lion's bark") : barkSound(defaultBarkSound) {}
void Lion::bark(void)
{
cout << "Lion => " << barkSound << endl;
}
Wolf::Wolf(string defaultBarkSound = "The Wolf's bark") : barkSound(defaultBarkSound) {}
void Wolf::bark(void)
{
cout << "Wolf => " << barkSound << endl;
}
Dog::Dog(string defaultBarkSound = "The Dog's bark") : barkSound(defaultBarkSound) {}
void Dog::bark(void)
{
cout << "Dog => " << barkSound << endl;
}
void printBark(Animal* animal)
{
animal->bark();
}
int main(void)
{
Lion lion("Roar");
Wolf wolf("Awoo");
Dog dog("woof");
printBark(&lion);
printBark(&wolf);
printBark(&dog);
return 0;
}
위 코드에서 printBark()에서 인자로 받은 Animal* animal 포인터 객체의 멤버 함수 bark()를 호출할 때, animal->bark(); 로 호출하였습니다. animal -> bark() 에서 "->"는 클래스 포인터의 멤버를 가져올 때 사용하는 방법으로 (*animal).bark() 와 같습니다.
이제 순수 가상 함수로 bark() 함수를 구현 해보겠습니다.
위 코드에 Animal 클래스를 보면 bark() 함수가 virtual void bark(void) {} 로 구현되어 있는 것을 볼 수 있습니다.
즉, {} 로 함수 내부가 비어있었지만 함수 내부를 정의했다는 의미이고 Animal 클래스와 같이 함수 내부 정의가 필요 없는 경우에 순수 가상 함수로 구현합니다.
그리고 클래스 내부에 순수 가상 함수가 하나라도 존재하는 경우, 그 클래스를 추상 클래스라고 합니다.
추상 클래스는 객체로 인스턴스화 될 수 없습니다. 오직 포인터 혹은 함수로 가리킬 수만 있으며, 파생 클래스는 순수 가상 함수를 반드시 재정의 해야합니다.
**파생 클래스는 기본 클래스의 순수 가상 함수를 반드시 재정의 해야하는 이유는 순수 가상 함수를 재정의 하지 않은 파생 클래스는 기본 클래스와 마찬가지로 추상 클래스가 됩니다.
그리고 추상 클래스는 객체로 인스턴스화 할 수 없기 때문에 구문 오류가 발생합니다.**
#ifndef _ANIMAL_H_
#define _ANIMAL_H_
#include <iostream>
#include <string>
using namespace std;
class Animal
{
public:
virtual void bark(void) = 0;
};
class Lion : public Animal
{
public:
Lion(string defaultBarkSound);
void bark(void);
private:
string barkSound;
};
class Wolf : public Animal
{
public:
Wolf(string defaultBarkSound);
void bark(void);
private:
string barkSound;
};
class Dog : public Animal
{
public:
Dog(string defaultBarkSound);
void bark(void);
private:
string barkSound;
};
#endif
Animal 클래스의 bark() 함수를 순수 가상 함수로 바꾸었습니다.
따라서 Animal 클래스는 추상 클래스가 되었습니다.
이렇게 다형성까지의 진도가 끝났습니다.
이제 과제를 진행해야 하는데요. 그 전에 과제 내용에 소멸자가 포함되어 있어서 소멸자까지 학습한 후, 과제를 진행하도록 하겠습니다.
소멸자(Destructor)
객체가 생성될 때 자동으로 한 번만 실행되는 멤버 함수, 생성자(Constructor)를 지난 글에서 학습하였습니다.
소멸자(Destructor)는 반대로 객체가 소멸할 때 자동으로 한 번만 실행되는 멤버 함수 입니다.
생성자(Constructor)가 멤버 변수의 초기화 혹은 객체가 동작할 준비를 하기 위해 사용된다면 소멸자(Destructor)는 리소스 해제 혹은 마무리 작업을 하기 위해 사용됩니다.
기본 형태는 ~클래스명( ) { 내용 } 의 형태를 갖춥니다.
소멸자(Destructor)의 경우에도 생성자(Constructor)와 마찬가지로 반환형을 아예 가지지 않습니다. (void와 다릅니다.)
소멸자(Destructor)는 생성자(Constructor)와 달리 매개변수를 가지지 않습니다.
new 키워드와 delete 키워드
컴퓨터의 메모리는 한정된 자원입니다. 따라서 프로그램이 원활하게 돌아가기 위해서는 메모리를 효율적으로 활용해야 합니다.
그리고 프로그래밍에서 컴퓨터의 메모리를 할당하고 해제하는 것이 new 키워드와 delete 키워드 입니다.
new 키워드와 delete 키워드에 대해 자세하게 배워보기 전에 메모리에 대해 이해하고 넘어가겠습니다.
컴퓨터는 프로그램이 실행되는 동안 필요한 정보를 메모리 공간에 저장합니다. 그리고 메모리의 종류는 크게 스택 메모리와 힙 메모리로 나누어집니다.
스택 메모리에는 일반적인 변수나 인자 등이 저장됩니다. 스택 메모리에 저장된 값들은 생존 주기가 끝나면 자동으로 할당이 해제된다는 특징이 있습니다.
생존 주기란, 변수나 함수, 프로그램이 생성되고 소멸하는 과정을 이르는 말입니다.
즉, 스택 메모리의 생존 주기는 함수의 지역 변수이면 함수가 끝남과 동시에 할당이 해제되고 프로그래머가 임의로 만들어준 지역(구역)의 지역 변수이면 해당 지역이 끝나는 시점 "}"을 만나는 즉시 할당이 해제됩니다.
힙 메모리는 동적 할당 시, new 키워드를 통해 메모리 공간을 할당해줄 수 있고 delete 키워드를 통해 할당된 공간을 해제할 수 있습니다.
스택 메모리 공간의 값들과 달리 힙 메모리 공간에 할당할 때는 자동으로 헤제되지 않으므로 메모리 누수가 있을 수 있어 신경을 기울여 관리해야 합니다. 따라서 힙 메모리에 저장된 값들은 생존 주기가 new 키워드로 할당된 순간 부터 delete 키워드로 해제하기 전까지 입니다.
** "동적으로 메모리를 할당한다." = "힙 메모리 공간을 할당한다." 와 같은 의미 입니다.**
//#include "student.h"
//#include "vehicle.h"
//#include "animal.h"
#include <iostream>
#include <string>
#include <vector>
using namespace std;
// 소멸자
void funcVariable(void) // 동적으로 변수 할당
{
int* exPtr = new int(10);
cout << "called funcVariable => int* exPtr = new int(10);\nresult: " << *exPtr << endl;
delete exPtr;
}
void funcArray(void) // 동적으로 배열 할당
{
int* exPtr = new int[3];
for (int i = 0; i < 3; i++)
{
exPtr[i] = (i + 1) * 10;
cout << "called funcArray => int* exPtr = new int[10];\nresult: " << exPtr[i] << endl;
}
delete[] exPtr;
}
int main(void)
{
funcVariable();
funcArray();
return 0;
}
동적 할당 된 변수의 경우, 자료형 *변수명 = new 자료형(값); 으로 할당하고 delete 변수명; 으로 해제합니다.
동적 할당 된 배열의 경우, 자료형 *배열명 = new 자료형[배열의 크기]; 로 할당하고 delete[] 배열명; 으로 해제합니다.
**delete(할당 해제)를 안할 경우, 메모리 누수가 생기므로 꼭 할당을 해제하여야 합니다.**
지금까지의 내용으로 과제를 진행할 수 있을 것 같아 과제를 진행했습니다.
제가 작성한 소스코드를 설명하면서 학습을 진행하겠습니다.
// 1주차 과제
#include <iostream>
#include <string>
using namespace std;
// TODO: Phone 클래스 구현
// - displayBrand()와 showFeature() 순수 가상 함수를 포함하도록 구현하세요
// - 소멸자를 반드시 virtual로 선언하세요
class Phone
{
public:
Phone(void) {
cout << "Constructed Object : Phone" << endl;
}
virtual void displayBrand(void) = 0;
virtual void showFeature(void) = 0;
virtual ~Phone(void) {
cout << "Destructed Object : Phone" << endl;
}
};
// Samsung 클래스 작성 (Phone 클래스를 상속받음)
class Samsung : public Phone {
public:
Samsung(void) {
cout << "Constructed Object : Samsung" << endl;
}
void displayBrand(void) {
cout << "Samsung" << endl;
}
void showFeature(void) {
cout << "Galaxy S Series" << endl;
}
~Samsung(void) {
cout << "Destructed Object : Samsung" << endl;
}
};
// pple 클래스 작성 (Phone 클래스를 상속받음)
class Apple : public Phone {
public:
Apple(void) {
cout << "Constructed Object : Apple" << endl;
}
void displayBrand(void) {
cout << "Apple" << endl;
}
void showFeature(void) {
cout << "iPhone Pro Series" << endl;
}
~Apple(void) {
cout << "Destructed Object : Apple" << endl;
}
};
// Xiaomi 클래스 작성 (Phone 클래스를 상속받음)
class Xiaomi : public Phone {
public:
Xiaomi(void) {
cout << "Constructed Object : Xiaomi" << endl;
}
void displayBrand(void) {
cout << "Xiaomi" << endl;
}
void showFeature(void) {
cout << "Redmi Note Series" << endl;
}
~Xiaomi(void) {
cout << "Destructed Object : Xiaomi" << endl;
}
};
int main() {
// TODO: main 함수 구현
// - Phone* 타입의 배열을 생성하여 Samsung, Apple, Xiaomi 객체를 저장
// - 반복문을 사용하여 각 객체의 displayBrand()와 showFeature()를 호출
// - 반복문을 사용하여 메모리 해제를 위해 delete 호출
Phone* phone[3];
phone[0] = new Samsung();
phone[1] = new Apple();
phone[2] = new Xiaomi();
for (int i = 0; i < 3; i++) {
phone[i]->displayBrand();
phone[i]->showFeature();
delete phone[i];
}
return 0;
}
먼저 기본 클래스가 되는 Phone 클래스를 작성해야 하고 Phone 클래스에는 멤버 함수로 displayBrand() 함수와 showFeature() 함수가 순수 가상 함수로 선언되어 있어야 합니다.
추가로 소멸자 ~Phone() 은 virtual 키워드를 사용하여 가상 함수로 만들어야 한다고 조건이 주어져 있습니다.
해당 부분을 아래와 같이 작성하였습니다.
class Phone
{
public:
Phone(void) {
cout << "Constructed Object : Phone" << endl;
}
virtual void displayBrand(void) = 0;
virtual void showFeature(void) = 0;
virtual ~Phone(void) {
cout << "Destructed Object : Phone" << endl;
}
};
일단 생성자를 통해 파생 클래스가 생성될 때, "Constructed Object : Phone" 문자가 출력되게 하였습니다.
다음으로는 조건에 있던 displayBrand() 함수와 showFeature() 함수를 순수 가상 함수로 작성하였습니다.
마지막 조건 소멸자 ~Phone()를 virtual을 "반드시" 사용하여 선언하라고 하여 virtual ~Phone() 으로 선언하였고 파생 클래스의 메모리 공간이 해제될 때, 같이 "Destructed Object : Phone"가 출력되도록 작성하였습니다.
**사실 Phone 클래스는 추상 클래스이기 때문에 인스턴스화 될 수 없습니다. 따라서 메모리 공간을 할당하여 객체화 될 수 없기 때문에 생성자와 소멸자의 문자열은 크게 의미있는 문장이 아닙니다..**
// Samsung 클래스 작성 (Phone 클래스를 상속받음)
class Samsung : public Phone {
public:
Samsung(void) {
cout << "Constructed Object : Samsung" << endl;
}
void displayBrand(void) {
cout << "Samsung" << endl;
}
void showFeature(void) {
cout << "Galaxy S Series" << endl;
}
~Samsung(void) {
cout << "Destructed Object : Samsung" << endl;
}
};
// pple 클래스 작성 (Phone 클래스를 상속받음)
class Apple : public Phone {
public:
Apple(void) {
cout << "Constructed Object : Apple" << endl;
}
void displayBrand(void) {
cout << "Apple" << endl;
}
void showFeature(void) {
cout << "iPhone Pro Series" << endl;
}
~Apple(void) {
cout << "Destructed Object : Apple" << endl;
}
};
// Xiaomi 클래스 작성 (Phone 클래스를 상속받음)
class Xiaomi : public Phone {
public:
Xiaomi(void) {
cout << "Constructed Object : Xiaomi" << endl;
}
void displayBrand(void) {
cout << "Xiaomi" << endl;
}
void showFeature(void) {
cout << "Redmi Note Series" << endl;
}
~Xiaomi(void) {
cout << "Destructed Object : Xiaomi" << endl;
}
};
다음은 파생 클래스를 선언하는 과정입니다.
파생 클래스 Samsung, Apple, Xioami 역시 기본 클래스 Phone과 마찬가지로 생성자와 소멸자를 통해 콘솔 창에서 인스턴스화와 할당 해제 될 때, 문자열이 출력되도록 구현하였습니다.
displayBrand() 함수와 showFeature() 함수는 재정의 하여 해당 클래스에 맞는 문구가 출력되게 작성하였습니다.
int main() {
// TODO: main 함수 구현
// - Phone* 타입의 배열을 생성하여 Samsung, Apple, Xiaomi 객체를 저장
// - 반복문을 사용하여 각 객체의 displayBrand()와 showFeature()를 호출
// - 반복문을 사용하여 메모리 해제를 위해 delete 호출
Phone* phone[3];
phone[0] = new Samsung();
phone[1] = new Apple();
phone[2] = new Xiaomi();
for (int i = 0; i < 3; i++) {
phone[i]->displayBrand();
phone[i]->showFeature();
delete phone[i];
}
return 0;
}
마지막으로 main() 함수는 조건에 Phone* 타입의 배열을 생성 즉, 포인터 배열로 각 클래스를 저장하여야 합니다.
따라서 Phone* phone[3]; 로 포인터 배열을 생성하고 배열의 크기를 3으로 선언하였습니다.
이후 직접 new 키워드를 통해 각각의 배열의 공간에 Samsung 클래스, Apple 클래스, Xiaomi 클래스를 인스턴스화 하였습니다.
다음으로 반복문으로 배열을 순회하면서 각 객체의 displayBrand() 함수와 showFeature() 함수 호출이 조건으로 주어져 for문을 통해 phone 배열을 순회하였고 각각의 클래스의 displayBrand() 함수와 showFeature() 함수를 호출하였습니다.
마지막 조건으로 반복문을 사용해 메모리 해제를 위해 delete 키워드 호출이 주어져, for문을 새로 작성하는 대신 함수 호출이 끝나면 바로 delete를 통해 할당이 해제될 수 있도록 작성하였습니다.
오늘은 여기까지 학습하였습니다.
하루종일 비가 내려 선선한 하루여서 기분 좋은 하루 였습니다!
내일도 언제나와 같이 힘내서 열심히 공부하도록 하겠습니다!
'[내일배움캠프] 본 과정 학습내용' 카테고리의 다른 글
| [내배캠] 11일차 C++ 학습 내용 정리 (0) | 2026.07.10 |
|---|---|
| [내배캠] 10일차 C++ 학습 내용 정리 (0) | 2026.07.09 |
| [내배캠] 8일차 C++ 학습 내용 정리 (0) | 2026.07.07 |
| [내배캠] 5일차 Unreal Blueprint 내용 정리(2) 및 7일차 C++ 학습 내용 정리 (0) | 2026.07.06 |
| [내배캠] 5일차 Unreal Blueprint 내용 정리(1) (0) | 2026.07.03 |