상속의 성질과 포인터
상속은 다양한 성질을 지니고 있다.
그 다양한 성질과 포인터가 만나게 되면 더욱 다양한 성질을 띄게 되는데,
이는 C++에서의 객체를 사용함에 있어서 다양한 방식을 제공하게 해준다.
객체 상속과 포인터
class Test { private: int Testnum; public: Test(int num) { this->Testnum = num; } void test() { std::cout << "test in Test" << std::endl;} }; class Test2 : public Test { private: int Test2num; public: Test2(int num) : Test(num) { this->Test2num = num; } void test() { std::cout << "test in Test2" << std::endl;} }; int main(){ Test *test = new Test2(10); test->test(); return 0; }
위 코드의 main 함수에 Test *test = new Test2(10);
을 보자 분명히 Test 형 포인터 인데 Test2를 넣고 있다.
이말인 즉슨, 부모클래스형 포인터는 자식 클래스의 포인터를 담을 수 있다는 뜻이다.
근데 이게 동작이 굉장히 신기한데, main의 test->test();
함수를 실행 시키면, Test의 test()함수 결과인 “test in Test”가 출력된다.
즉 같은 함수가 있을 땐 자료형의 함수를 불러온다는 뜻이다.
참고로, 만약
int main(){
Test2 *test2 = new Test2(10);
Test *test = test2;
test2->test();
test->test();
return 0;
}
해당 코드를 실행하면
“test in Test2”
“test in Test”가 출력된다. 즉, 포인터의 자료형에 따라 불러올 함수가 결정 된다는 거다.
하지만 이는 자식 클래스에서 오버라이딩한 이유가 없어지게 되며, 문제가 있다고 볼 수 있다.
이를 해결하기 위해 도입하는 개념이 바로 Virtual Function, 가상함수이다.
가상함수 Virtual Function
가상함수의 선언은 virtual
키워드 선언을 통해서 이뤄진다.
이후 오버라이딩을 하게 되면 자료형의 관계없이 오버라이딩 된 함수가 사용된다.
class Test { private: int Testnum; public: Test(int num) { this->Testnum = num; } virtual void test() { std::cout << "test in Test" << std::endl; } }; class Test2 : public Test { private: int Test2num; public: Test2(int num) : Test(num) { this->Test2num = num; } void test() { std::cout << "test in Test2" << std::endl; } }; int main(){ Test2 *test2 = new Test2(10); Test *test = test2; test2->test(); test->test(); return 0; }
Test 클래스에 test() 함수 앞에 virtual 선언이 되어 있다.
즉, 이후 상속 받은 Test2에서 해당 함수를 오버라이트할 경우 자료형의 관계없이 Test2의 test()를 불러오란 뜻이다.
가상 함수와 소멸자
class Test { private: int Testnum; public: Test(int num) { this->Testnum = num; } virtual void test() { std::cout << "test in Test" << std::endl; } ~Test() { std::cout << "delete Test" << std::endl; } }; class Test2 : public Test { private: int Test2num; public: Test2(int num) : Test(num){ this->Test2num = num; } virtual void test() { std::cout << "test in Test2" << std::endl; } ~Test2(){ std::cout << "delete Test2" << std::endl; } }; int main(){ Test *test = new Test2(10); delete test; return 0; }
위 코드를 실행하면 “delete Test” 만 뜨게 되며 ~Test() 소멸자만 뜨게 된다.
즉, 클래스에서 소멸자의 호출 또한 “자료형”의 소멸자만이 호출된다.
이는 메모리 leak이 일어 날 수 있기에 이 또한 virtual 키워드를 통해 해결 할 수 있다.
virtual ~Test() { ... }
와 같이 바꿔주면,
“delete Test2
delete Test” 라는 결과가 뜨며, ~Test2()와 ~Test()가 순서대로 소멸 하는 것을 볼 수 있다.
++ 순수 가상함수
클래스 중에서 객체 생성을 목적으로 정의되지 않는 클래스도 있다고한다.
이때 virtual void test() = 0;
이렇게 정의하게 되면,
Test test = new Test(1)
와 같이 객체를 생성하려고 하면 컴파일 에러가 발생한다.
class Test { private: int Testnum; public: Test(int num) { this->Testnum = num; } virtual void test() = 0; }; class Test2 : public Test { private: int Test2num; public: Test2(int num) : Test(num) { this->Test2num = num; } void test() { std::cout << "test in Test2" << std::endl; } }; int main(){ Test2 *test2 = new Test2(10); Test *test = test2; test2->test(); test->test(); return 0; }
Test 클래스의 virtual void test() = 0;
은 new Test(1)
과 같은 Test 객체를 생성할시 컴파일 에러를 출력한다.
즉, Test 클래스는 상속을 위한 클래스이며,
객체로 생성되어 사용되어선 안되고,
test()함수는 상속 받은 클래스에서 오버라이트 해야 된다는 의미다.
+++ 참조(Reference)
포인터 뿐만이 아니라 참조자 또한 같은 동작을 한다.
int main(){ Test2 test2(10); Test &test = test2; test.test(); return 0; }
단순히 “->”가 “.”으로 선언 방식에서 살짝의 차이를 제외하곤 포인터와 동일한 동작을 한다.