
상속
자동차를 만든다고 생각해보자.
자동차는 다양한 종류가 있지만 그래도 같은 속성을 가지고 있는 것이 있다.
이를 class에서 만든다고 생각하면 자동차 여러 개를 만들 때
속도, 색상, 그 자동차만의 특성 이렇게 만들기보다
속도와 색상 같이 어느정도 겹칠 수 있는 부분은 특성 하나로 만들어 놓고
그걸 적용되게 하면 편할 것이다. 그 공통점을 하나로 만들고
클래스에서 객체를 생성할 때 그 공통점이 적용되도록 하는 것이 상속이다.
말 그대로 부모한테 유전을 받는 것처럼 공통적인 부분은 상속받을 수 있다!
Vehicle -> Bycle/Truck Class 구현
우선, 상속할 때는 멤버변수를 주로 protected로 사용한다. 이렇게..

string color; 위에 protected:가 보이는데 저렇게 되면 color와 speed 변수 모두 protected 접근제어를 받는 것이다.
이니셜라이저 리스트
Vehicle(string c, int s) : color(c), speed(s) {}은 대체 뭘까?
- 이니셜라이저 리스트(Initializer List)다.
C++에서 클래스의 멤버 변수를 초기화하는 특별한 방식이다. 그래도 이해가 안된다.
Vehicle(string c, int s) {
color = c; // 초기화
speed = s; // 초기화
}
이 코드를 생성자 본문 전에 :을 사용해서
: 멤버변수(매개변수), 멤버변수(매개변수)의 형태를 {}여기 앞에 추가하는 방식이다.
결국 멤버 변수 초기화를 더 직관적으로 보여주고 깔끔하니까 쓰는 것이다.
상속하는 방법

저기서는 class Bicycle : public Vehicle을 써서 public으로 상속을 했지만 다른 걸로도 할 수 있다. 하지만 많이 쓰는 게 public이기 때문에 일단 public 상속을 기본으로 알아두자.
: public (부모클래스명)을 자식클래스 뒤에 붙이면 상속이 된다.
부모 클래스 멤버 초기화의 원칙
- 부모 클래스의 멤버 변수는 상속 때문에 자식 클래스에 포함되지만,
자식 클래스의 영역에 속하지 않는다. - 따라서, 부모 클래스의 멤버 변수는 부모 클래스에 정의된 멤버로 간주된다.
- 자식 클래스의 생성자에서는 부모 클래스의 멤버를 직접 초기화할 수 없으며,
부모 클래스의 생성자를 호출하여 초기화해야 한다.
이니셜 라이저 리스트 - 응용
Bicycle(string c, int s, bool basket) : Vehicle(c, s), hasBasket(basket) {}
위 같은 원칙 때문에 Vehicle(c, s)로 부모 클래스의 생성자를 호출해 초기화하는 것이다.
그래서 : 부모클래스명(부모클래스의 매개변수)의 형태를 쓴다.
class Vehicle {
protected:
string color;
int speed;
public:
Vehicle() : color(""), speed(0) {} // 기본 생성자 추가
Vehicle(string c, int s) : color(c), speed(s) {
cout << "Vehicle initialized: Color = " << color << ", Speed = " << speed << endl;
}
};
class Bicycle : public Vehicle {
private:
bool hasBasket;
public:
Bicycle(string c, int s, bool basket) {
// 부모 클래스의 기본 생성자가 호출됨
color = c; // 직접 할당
speed = s; // 직접 할당
hasBasket = basket;
cout << "Bicycle initialized: HasBasket = " << (hasBasket ? "Yes" : "No") << endl;
}
};
만약 이니셜 라이저를 안쓰면 이렇게 부모클래스인 Vehicle에 기본 생성자를 추가하고
그 안의 값도 직접 할당해주어야 하니 코드가 복잡해진다.
부모클래스의 기본 생성자 호출 이유
자식 클래스를 초기화할 때 부모클래스의 생성자를 호출하지 않으면 기본 생성자가 호출된다. 부모클래스 먼저 초기화하는게 규칙이기 때문이다.
Why?
- 부모 클래스의 멤버가 제대로 초기화되지 않으면,
자식 클래스에서 해당 멤버를 사용할 때 예기치 않은 동작이 발생 - 그래서, 자식 클래스를 초기화할 때 부모클래스의 생성자를 호출하지 않으면
기본 생성자를 호출
상속 개념을 설명할 때 각 클래스에 대해 부모, 자식이란 표현을 썼는데
공식적인 문서에서는 상속의 대상이 되는 클래스가 기본 클래스,
상속을 받는 클래스를 파생 클래스라 부른다.
생성자가 호출되면 객체를 생성한다. 아래처럼 b, t라는 이름으로 생성자가 호출되었다면
int main()
{
Bicycle b("Yellow", 30, true);
Truck t("Blue", 40, 95);
b.ringBell();
t.loadCargo();
return 0;
}
b.ringBell();, t.loadCargo();처럼 객체 안의 동작들을
멤버 변수와 함수를 통해 사용할 수 있다.
출력 결과

출력 결과가 왜 이렇게 나왔는지 알아보자..

여기서 Bicycle b("Yellow", 30, true);으로 Bicycle의 객체는 b가 되었다.
그리고 b.ringBell();로 b의 ringBell 함수까지 실행되었다.

Truck t("Blue", 40, 95);로 Truck의 객체는 t가 되었다. 이게 생성자를 호출하면서 객체를 만든건데 그러면
color에는 "Blue"
speed에는 40
cargoCapcity에는 95란 값이 초기화되어 들어갈 것이다.
초기화는 선언과 대입을 동시에 처리.
대입은 아래 코드처럼 생성자의 중괄호 안에 =을 사용해 값을 대입하는 것이다.
class Truck {
private:
int speed;
public:
Truck(int s) { speed = s; } // 선언 후 대입
};
Truck t(50); // speed는 먼저 기본값으로 생성되고 이후 50으로 대입

loadCargo()를 보면 cargoCapacity의 값이 95로 초기화되었다는 걸 상기하고 위에서 다시 결과를 확인해보자. 그래서 출력이 그렇게 나온거구나~ 하면서 이해가 되었다.
다형성
동물들 울음소리 출력하는 프로그램 만들기
다형성을 배우기 위해 동물들의 울음소리를 출력하는 프로그램을 만들어야 한다.
다형성이 적용되지 않은 Lion과 Wolf 클래스

여기서는 두 클래스의 코드 형태가 비슷하다는 것만 알고 넘어갔다.

만든 동물 클래스를 인자로 받고 울음소리를 출력하는 함수 제작
이렇게 하면 두 클래스의 bark()함수가 비슷해보이지만
Lion은 lion의 울음소리 / Wolf는 wolf의 울음소리를 출력하게 된다!
Lion lion("ahaaaaaa!"); 여기서 생성자를 호출했는데 그러면

인자로 넣은 ahaaaaa! 값이 word에 전달되고, 초기화 리스트 : m_word(word)를 통해
바로 m_word에 초기화된다.
bark() 함수에서 출력할 때, m_word에 담긴 값 ahaaaaa!가 출력된다!
그래서 print(lion);으로 객체 안의 함수를 호출하면
아래처럼 Lion ahaaaaaa!가 첫번째 줄에 나오게 된다!

만약 다형성이 적용되지 않은 코드에 기능을 추가하게 된다면?



동작이 잘 되긴한다. 하지만 기능이 하나 늘어날 때마다
새로운 클래스와 print 함수를 만들어줘야만 했다.
print 함수는 bark()앞에 오는 동물들이 추가할 때마다 달라져서
동물 클래스가 늘어날 때마다 똑같이 추가해야 되는 구조다.
분명 더 좋은 방법이 있을 것이다.
다형성 적용해보기

사실 여기서 인자로 받는 값인 동물 클래스만 다르다.
그 안에서 bark()함수는 변하지 않는다.
함수 안에서 하는 일은 똑같다는 것이다. 그럼 이런 함수의 대표를 만든다면?

변경점
Animal 클래스가 추가되었다. 동물 클래스들이 Animal 클래스를 상속받고 있다.
virtual void bark()함수가 추가되었다. 그 함수를 동물 클래스 내에서 구현하고 있다.
다형성이란?
"하나의 이름(타입)으로 여러 형태를 표현할 수 있는 성질" 때문에 다형성이라 한다.
예를 들어, "개(Dog)는 동물(Animal)이다", "사자(Lion)는 동물(Animal)이다"처럼
부모 클래스 타입(Animal)으로 여러 자식 클래스 객체(Dog, Lion)를 참조할 수 있다.
즉, 하나의 이름이 여러 형태로 동작할 수 있다!

변경점
print함수가 Animal 클래스를 인자를 받는 걸로 해서 print함수들이 저거 하나로 통일되었다. 그 다음 클래스 뒤에 원래 없던 *이 생겼고 bark()함수를 호출하는 방식도 바뀌었다.
예시로 dog.bark(); → animal->bark();로 .에서 ->으로 바뀌었다.
main함수에서 print함수를 호출할 때도 lion앞에 &가 붙은걸 볼 수 있다.
작동 원리
1. Lion lion("ahaaaaaa!");
lion 객체가 생성되고 메모리 공간에 저장된다.
2. print(&lion);
&lion은 lion 객체의 메모리 주소를 반환한다.
이 주소값은 Animal* animal이라는 포인터 변수에 전달된다.
3. animal->bark();
animal 포인터가 lion 객체를 가리키고 있으므로, 포인터를 통해 bark() 함수가 호출된다.
포인터라는 개념은 아래에서 정리했다.
포인터
1. 포인터는 객체의 주소값을 저장하고, 그 위치를 가리킨다..
이를 객체를 참조한다고 표현할 수 있다.
2. 포인터를 선언할 때의 *
변수 선언 시, *가 붙으면 그 변수는 포인터가 된다.
예시로 int* ptr; → ptr은 int 타입 데이터를 가리키는 포인터.
3. ->
포인터로 객체의 위치를 받을 때, 객체의 멤버에 접근하려면 . 대신 ->를 사용한다.
예: animal->bark는 포인터 animal이 가리키는 객체의 bark() 함수를 호출한다.
4. &
변수(예: Animal animal) 앞에 붙여서 해당 변수의 메모리 주소를 가져온다.
이 메모리 주소를 포인터에 저장하면, 포인터는 그 변수(객체)를 가리킬 수 있다.
↓예제코드로 이해를 더 잘해보자..!
Animal animal; // 객체 생성
Animal* ptr = &animal; // 포인터 ptr에 animal 객체의 주소값을 저장
ptr->bark(); // 포인터를 통해 객체의 멤버 함수 호출
↑위의 내용은 초보자의 관점에서 정리한 내용이다.
아래에서 더 전문적으로 정리된 내용을 볼 수 있다.↓
포인터 추가 정리
포인터는 C++에서 중요한 개념 중 하나로, 메모리 주소를 직접 다루는 데 사용된다.
그냥 배우면 어렵지만 메모리 구조와 함께 학습하면 이해하기가 훨씬 쉬워진다.
1. 메모리와 변수
- 메모리는 프로그램이 실행될 때 사용하는 RAM(Random Access Memory)의 한 부분
- 메모리는 여러 “칸”으로 이루어져 있고, 각 칸은 고유한 주소를 가진다.
- 변수는 메모리의 특정 칸에 데이터를 저장하는 “이름표” 역할을 한다.
<예제>
int a = 5;
- 변수 a는 메모리의 특정 주소를 차지하고, 값 5를 저장한다.
- a의 주소는 &a로 접근할 수 있다.
2. 포인터
- 포인터는 다른 변수의 메모리 주소를 저장하는 변수다.
- 즉, 포인터는 메모리의 “위치”를 가리키는 역할을 한다.
- 포인터 사용의 두 가지 핵심 연산이 있다.
- 참조(&): 변수의 메모리 주소를 얻음.
- 역참조(*): 포인터가 가리키는 주소에 저장된 값을 얻음.
<예제>
int* ptr; // 'int' 타입의 데이터를 가리킬 포인터
여기서 ptr이 포인터가 되는 것이다!
3. 메모리와 포인터
<예시>
이름주소값a | 0x100 | 10 |
ptr | 0x200 | 0x100 |
a는 변수로, 값 10이 메모리 주소 0x100에 저장되어 있다.
ptr는 포인터로, 메모리 주소 0x100을 값으로 저장하며,
그 포인터 자체는 0x200에 저장되어 있다.
<코드로 보는 예시>
#include <iostream>
using namespace std;
int main() {
int a = 10; // 변수 a 선언
int* ptr = &a; // ptr은 a의 주소를 저장
cout << "a의 값: " << a << endl; // 10
cout << "a의 주소: " << &a << endl; // 0x100 (a의 메모리 주소)
cout << "ptr의 값: " << ptr << endl; // 0x100 (a의 주소, ptr이 가리키는 주소)
cout << "ptr이 가리키는 값: " << *ptr << endl; // 10 (ptr이 가리키는 주소의 값)
}
4. 포인터 관련 헷갈릴 수 있는 점
- int* ptr에서 *는 “포인터 타입”을 선언하는 것을 의미.
- *ptr에서 *는 “역참조” 연산을 의미.
virtual 키워드의 의미

우선 여기서 animal 포인터에 각각
print(&lion);
print(&wolf);
print(&dog);
로 lion, wolf, dog의 주소값을 받았다. 그러면 어떤일이 일어날까?

여기서 void bark()함수 앞에 'virtual'이 붙은걸 볼 수 있다.
실제 내가 가리키는 클래스에 bark()함수가 구현되어 있는지를 확인하고
해당 클래스에 bark()함수가 구현되어 있다면 자식 클래스의 bark()를 호출해준다. 그렇다면.. 어떤식으로 이 코드들이 작동할까?
작동 원리(lion을 기준으로 설명)
1. animal 포인터에 void bark()함수가 virtual로 되어있다.
2. Lion은 Animal의 상속을 받고 있다.

3. 그러므로 virtual 키워드가 있는 bark()함수는 실제 animal 포인터에 lion의 주소값을 전달받았을 것이다.
4. 그 animal 포인터가 가리키는 lion클래스의 bark()가 호출된다.
알게된 점(다형성)
위 내용에서 void print(Animal* animal)와 virtual void bark()로 자식 클래스의 함수들을 호출해서 사용할 수 있었다.
공통된 속성을 가진 여러 클래스에서, '대표적인 부모 클래스 1개'를 통해
자식 클래스의 함수를 호출할 수 있도록 하는 개념이다.
부모 클래스에 새로운 자식 클래스가 추가되더라도,
부모 클래스의 포인터나 참조를 통해 자식 클래스의 함수에 접근할 수 있다.
이를 통해 확장성 있고 유연한 코드 작성이 가능해지는 것이 다형성의 큰 장점이다!!
이것이 객체 지향의 대표적인 개념이다.
다형성 관련 문법 더 알아보기
가상 함수

앞에서 말했듯 virtual 키워드가 함수 앞에 붙으면 가상함수가 된다.

여기서 myAnimal 포인터에 다른 변수인 myDog의 메모리 주소를 저장하고
myAnimal->makeSound()을 써서 그 포인터가 가리키는 주소의 makeSound()를 호출했다.

myAnimal 포인터에 다른 변수의 메모리 주소를 저장하지 않았다면
Animal클래스의 makeSound()를 호출하겠지만
myAnimal = &myDog;에서 Dog객체의 메모리주소를 가리키도록 했기 때문에
Dog객체 안의 makeSound()를 호출하는 것이다.
여기서 가상함수를 만들었다고 무조건 자식 클래스에서
그 함수를 재정의할 필요는 없다는 사실을 알 수 있다!

그래서 결과가 이렇게 나타나는 것이다!
순수 가상함수

1. 가상함수에 0을 대입하는 문법이다.
2. 가상함수와는 다르게 해당 함수를 자식클래스에서 반드시 구현해야 한다.


순수 가상함수에는 선언만 있고 구현을 해주는 {}문이 없기 때문에 이런식으로 객체를 만들려 할 때 오류가 난다.
3. 순수 가상함수를 포함한 클래스는 그 자체로 변수가 될수 없다.
순수 가상함수가 포함된 클래스를 '추상 클래스'라고 한다.
객체를 생성하는 것을 "인스턴스화한다"라고도 한다.
추상 클래스를 변수로 선언하면 에러가 발생한다.
결론은 추상 클래스로 객체를 생성하려면 변수가 아닌 포인터를 써야 한다.

에러에 대한 생각
약간 몸이 아프면 "아파!" 하고 신호를 주거나
뜨거우면 "앗 뜨거!", 차가우면 "앗 차가!" << 이런식으로 신호를 주는데
그런 신호를 받고 우리가 어떤 행동을 취해야할 지 대처하는 것처럼
에러는 내 생각대로 구현되지 않은 코드를 알려줘서 최종 결과물의 퀄리티를 높일 수 있다.
그래서 순수 가상함수를 쓴다면 에러가 발생하기 때문에
자식 클래스에 가상함수를 구현하는 것을 까먹을 일이 없게 된다!
new, delete를 활용한 예시 코드
정적 배열을 쓸 때 생성자의 호출 방식


여기서 배열의 원소는 2개다. 그러면 각각 하나의 원소가 생성될 때 마다 생성자를 호출한다.
배열의 크기만큼 객체가 생성되는 것이다.
생성자가 객체가 생성되는 순간에 호출되듯 / 소멸자는 객체가 소멸되는 순간에 호출된다.


만약에 이런식의 인자를 받는 생성자를 호출하고 싶을 때 정적 배열을 쓰면 기본 생성자 밖에 호출되지 않는다.
그래서 기본 생성자를 다 호출하고 싶은게 아니라면 동적 배열을 사용해야 한다!
동적 배열을 쓸 때의 동작


1. strat! 이전에 생성자가 호출되지 않은 이유는 Employee* team_dynamic2[2];에서 생성자가 호출된 것이 아니라, 단지 Employee 객체의 위치를 담을 수 있는
포인터 배열이 생성되었기 때문이다.
2. 동적 배열은 객체의 위치를 저장할 포인터 공간이 배열 크기만큼 할당된다.
따라서, 객체를 생성하려면 team_dynamic2[0] = new Developer();처럼
new 키워드를 사용해야 한다.
이것은 동적 배열이 선언과 동시에 생성자를 호출하지 않기 때문이다.
3. 그리고 Developer나 Manager의 기본 생성자 호출 전에 Employee의 기본 생성자가 먼저 호출되는 이유는 자식 클래스가 생성되는 순간, 자식 클래스의 생성자와 동일한 인자를 받는 부모 생성자가 먼저 실행되어야 하기 때문이다.
이것은 객체 생성 규칙이므로 그냥 외워야 한다.
앞으로도 디버깅했을 때 나오는 출력값을 보고 뭐가 어떻게 동작한건지
해석을 할 수 있어야한다!!
delete 키워드

delete는 동적배열에서 new로 객체를 생성해주는 생성자 역할을 했다면 delete는 객체를 메모리에서 해제해주는 소멸자 역할을 한다고 생각하면 된다.
왜 굳이 소멸자가 필요할까?
만약 소멸자를 사용하지 않고 객체를 메모리에서 해제하지 않는다면,
프로그램이 종료될 때까지 메모리가 점점 쌓이게 된다.
지금 생각하기에 메모리가 가득차면 딱봐도 오류가 생길 거 같다.
숙제
숙제 설명
다형성을 이용해 다양한 직업을 가진 모험가들이
각기 다른 스킬을 사용하는 프로그램을 구현해보자!
- 기본 클래스
- Adventure라는 기본 클래스를 정의하세요
- useSkill()이라는 순수가상함수를 선언하세요
- 파생 클래스
- Warror, Mage, Archer라는 세 가지 파생 클래스를 만드세요
- 각 클래스의 useSkill 함수를 재정의 해서 아래와 같이 출력하세요
- Warror : Warror uses Slash!
- Mage : Mage casts Fireball!
- Archer : Archer shoots an Arrow!
- 다형성 구현
- Adventure*타입의 포인터를 사용하여 여러 모함가 객체를 가르키고, 반복문을 통해 각 모함가의 스킬을 호출하는 동작을 구현하세요
완료!
#include <iostream>
#include <vector>
using namespace std;
class Adventure {
public:
virtual void useSkill() = 0;
virtual ~Adventure() {
cout << "객체가 소멸되었습니다!" << endl;
}
};
class Warrior : public Adventure {
public:
void useSkill() {
cout << "Warrior : Warrior uses Slash!!" << endl;
}
};
class Mage : public Adventure {
public:
void useSkill() {
cout << "Mage : Mage casts Fireball!!" << endl;
}
};
class Archer : public Adventure {
public:
void useSkill() {
cout << "Archer : Archer shoots an Arrow!!" << endl;
}
};
int main() {
vector<Adventure*> adventures;
adventures.push_back(new Warrior);
adventures.push_back(new Mage);
adventures.push_back(new Archer);
// 각 모험가들의 스킬 사용!!
for (int i = 0; i < adventures.size(); ++i) {
adventures[i]->useSkill();
}
// 메모리 해제
for (int i = 0; i < adventures.size(); ++i) {
delete adventures[i];
}
return 0;
}
출력 결과

이렇게 출력이 잘 나오는 것까지 확인했다!
공부 후 느낀 점
1. C++에서 배운 개념들을 정확하게 익히려면 머리와 손이 이해할 때까지 관련 예제를 풀어보거나 실험을 해야만 한다.
'C++ 프로그래밍' 카테고리의 다른 글
[2024-12-31 / Day 46] 오버로딩과 오버라이딩 (0) | 2025.04.22 |
---|---|
[2024-12-31 / Day 45] 템플릿이란 무엇일까요? (0) | 2025.04.21 |
[2024-12-30 / Day 44] C++에서 자원 관리하기 (1) | 2025.04.21 |
[2024-12-23 / Day 42] Class 개념 정리 (0) | 2025.04.21 |
[2024-12-23 / Day 41] C++ 프로그래밍 기초 (0) | 2025.04.21 |