티스토리 뷰
해당 글은 '윤성우의 열혈 C++ 프로그래밍'을 참고하여 작성함.
C++에서 상속 파트를 공부하다 보면 기존에 자바에 익숙한 사람이라면 뭐지...? 싶은 부분이 있다.
자식 클래스 객체를 부모 클래스 객체에 할당하고 해당 객체의 메소드를 호출하는 경우, 자바라면 자식 클래스에 정의한 메소드가 호출된다. 반면 C++은 부모 클래스(기초 클래스)에 정의한 멤버 함수가 호출된다. 일단 해당 상황을 코드로 정리해보자.
class Parent {
void print() {
System.out.println("Parent");
}
}
class Child extends Parent {
@Override
void print() {
System.out.println("Child");
}
void print2() {
System.out.println("Child2");
}
}
class Main {
public static void main(String args[]) {
Parent obj = new Child();
obj.print(); //"Child" 출력
//obj.print2(); //컴파일 오류!
}
}
이 경우 obj
객체는 Parent
자료형이기 때문에 obj
객체로 print2
메소드 호출시 'print2 심볼을 찾을 수 없다'는 컴파일 오류가 발생한다. 반면 Parent
클래스에 정의되어 있는 print
메소드는 호출 가능하나 Parent
클래스에 정의되어있는 print
메소드가 아닌 실제 객체 자료형 Child
클래스에 정의되어있는 print
메소드가 호출되어 "Child"가 출력된다.
비슷한 코드를 C++로 구현하면 다음과 같다.
#include <iostream>
using namespace std;
class Base {
public:
void print() {
cout << "Base" << endl;
}
};
class Derived : public Base {
public:
void print() {
cout << "Derived" << endl;
}
void print2() {
cout << "Derived2" << endl;
}
};
int main() {
Base* obj = new Derived();
obj->print(); //"Base" 출력
//obj->print2();
return 0;
}
유도 클래스 객체를 가리키는 기초 클래스 자료형의 포인터 obj
또한 print2
멤버함수 호출 시 'print2 멤버이름을 찾을 수 없다'는 메시지로 컴파일 오류가 발생한다. 반면 print
멤버함수 호출 시에는 자바와 달리 Derived
클래스에 정의된 함수가 아닌 Base
클래스에 정의된 함수가 호출된다. 즉 기초 클래스(부모 클래스)에 정의된 함수가 호출되는 것이다. 이는 바로 obj
포인터의 자료형이 Base
클래스이기 때문이다.
하지만 객체지향의 원리 중 하나인 다형성을 만족하기 위해서 실제 객체 자료형에 따라 호출되는 멤버함수가 다르길 원하는 상황이 있다. 이때 바로 virtual
키워드로 가상함수를 선언하면 된다. 가상함수를 통해 자바에서와 같이 실제 객체 생성을 위해 호출된 생성자의 클래스에 정의된 멤버함수가 호출될 수 있다.
위 예제에서 Base
클래스 내 print
멤버함수를 다음과 같이 정의하면 obj->print();
호출 시 "Derived"가 출력될 것이다. (기초 클래스에 선언된 가상함수는 유도 클래스에 그대로 상속, 따라서 유도 클래스에서 가상함수 오버라이딩 시 virtual
키워드 생략 가능)
class Base {
public:
virtual void print() {
cout << "Base" << endl;
}
};
번외
int main() {
Derived obj2;
Base obj = obj2;
obj.print(); //항상 "Base" 출력
return 0;
}
위와 같이 Derived
객체를 Base
객체에 대입하여 초기하하면 print
함수가 아무리 가상함수로 선언되어있다고 하더라도 항상 Base
클래스에 정의된 print
멤버함수가 호출된다. 그 이유는 조금만 생각하면 알 수 있는데 Base
객체 초기화 시 Base
클래스의 복사 생성자가 호출되어 obj
의 실제 객체는 Derived
클래스형이 아닌 Base
클래스형이기 때문이다.
여기서 복사 생성자의 인자는 Base
클래스로 선언되어있기 때문에 매개변수로 'Base 클래스 또는 Base 클래스를 직접 또는 간접적으로 상속하는 클래스'를 전달받을 수 있다. 즉 인자로 Derived
객체가 전달된 Base
의 복사 생성자가 호출되어 Base
객체가 생성된 것이다.
순수 가상함수, 가상 소멸자
#include <iostream>
#include <cstring>
using namespace std;
class Bag {
private:
char* model;
int price;
public:
Bag(char* model, int price) :price(price) {
this->model = new char[strlen(model) + 1];
strcpy(this->model, model);
}
~Bag() {
cout << "~Bag()" << endl;
delete []model;
}
virtual void showSecret() const {}
};
class Carrier : public Bag {
private:
char* pwd;
public:
Carrier(char* model, int price, char* pwd) : Bag(model, price) {
this->pwd = new char[strlen(pwd) + 1];
strcpy(this->pwd, pwd);
}
~Carrier() {
cout << "~Carrier()" << endl;
delete []pwd;
}
void showSecret() const {
cout << pwd << endl;
}
};
class LuxuryBag : public Bag {
private:
char* serialNum;
public:
LuxuryBag(char* model, int price, char* serialNum) : Bag(model, price) {
this->serialNum = new char[strlen(serialNum) + 1];
strcpy(this->serialNum, serialNum);
}
~LuxuryBag() {
cout << "~LuxuryBag()" << endl;
delete []serialNum;
}
void showSecret() const {
cout << serialNum << endl;
}
};
int main() {
Bag* bag1 = new Carrier("model1", 10000, "1234");
Bag* bag2 = new LuxuryBag("model2", 1000000, "abcd1234");
bag1->showSecret();
bag2->showSecret();
delete bag1;
delete bag2;
return 0;
}
실행결과
1234
abcd1234
~Bag()
~Bag()
위 코드에는 2가지 문제가 있다. 먼저 Bag
클래스는 유도 클래스 Carrier
, LuxuryBag
에게 공통 속성을 상속하기 위한 역할만을 할 뿐 실제 객체를 생성하지 않는다. 즉 오버라이딩된 함수 showSecret
는 Bag
객체를 통해 호출되지 않아 빈 몸체의 함수로만 정의되어 있다. 이 경우 Bag* bag = new Bag();
는 잘못된 코드여야 하나 컴파일 오류 없이 잘 실행된다. 문제 해결을 위한 방법으로는 가상함수 showSecret
을 순수 가상함수로 선언하여 Bag
를 추상 클래스로 만드는 것이다. 아래 코드와 같이 수정하면 Bag
객체 생성 시 컴파일 오류가 발생한다. (순수 가상함수가 하나라도 포함된 클래스는 불완전하므로 추상 클래스이다.)
virtual void showSecret() = 0; //순수 가상함수
//......
Bag* bag = new Bag(); //컴파일 오류!
두번째 문제로는 Bag
의 소멸자만이 호출되어 Carrier
, LuxuryBag
의 생성자에서 할당한 메모리 공간을 해제하지 못해 메모리 누수 문제가 발생한다는 것이다. 해당 문제 해결을 위해서는 실제 객체 클래스의 소멸자를 호출할 수 있도록 소멸자에도 virtual
키워드를 붙여주면 된다. 이 경우 클래스 상속계층의 최하위 클래스의 소멸자 → 최상위 클래스의 소멸자가 순서대로 호출된다. (기초 클래스 소멸자의 virtual
키워드는 유도 클래스로 상속)
virtual ~Bag() {
delete []name;
}
실행결과
1234
abcd1234
~Carrier()
~Bag()
~LuxuryBag()
~Bag()
가상 소멸자를 통해 Carrier
, LuxuryBag
에서 할당한 메모리 공간 pwd
, serialNum
또한 안전하게 해제되는 것을 볼 수 있다.
자바에서의 모든 메소드는 기본적으로 C++의 가상함수라고 생각해도 무방할 것 같다. 다만 C++에서는 C의 특성을 남기고자 virtual
키워드로 가상함수를 선언할 수 있는 선택권을 남겨놓은 것이 아닌가 싶다. 아무튼 객체지향에서의 상속의 다형성을 코드에 구현하기 위해 virtual
키워드를 적절히 사용하면 좋을 것 같다.
끝.
'C++' 카테고리의 다른 글
[환경설정] VSCode에서 C/C++ 컴파일 및 실행하기 (0) | 2023.02.03 |
---|---|
[개념] C++의 형변환 연산자 (0) | 2022.07.02 |
[개념] C++의 상속 3계명? (0) | 2022.06.24 |
[개념] C++의 복사 생성자 (0) | 2022.06.22 |
[개념] C++의 멤버 이니셜라이저 (0) | 2022.06.20 |
- Total
- Today
- Yesterday
- spring
- facade 패턴
- Transaction
- servlet filter
- Git
- 디자인 패턴
- 전략 패턴
- rest api
- C++
- 템플릿 콜백 패턴
- Java
- FrontController
- vscode
- 모두의 리눅스
- 서블릿 컨테이너
- Gitflow
- spring aop
- ParameterizedTest
- junit5
- JPA
- Front Controller
- spring boot
- Linux
- github
- Spring Security
- Assertions
- SSE
- mockito
- 단위 테스트
- QueryDSL
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 |