상속, 메모리 관리, 재사용, ... 이것들이 바로 virtual 키워드들과의 관련이 있는 말들이에요. 사실, 학과 커리큘럼을 이수하면서는 이 키워드를 이용하지 않아도 문제를 해결 하는대는 저어어언혀 지장이 없었어요. 하지만 요근래 혼자하는 프로그램도 덩치가 커지고, 클래스를 상속받거나 기능자체를 재사용해야 하는 일이 많은데다가, 궁극적으로 회사(구체적으로 면접)에서 "기본적인" 프로그래밍 능력으로 많이들 생각하는 거 같더라구요.
이것저것 많이 봤는데 생각보다 이 키워드가 해주는 일이 많아서 좀 헷갈리기도 하고 어려운데, 그렇다고 더 좋은, 더 높은 완성도를 이룰 수 있는데 어찌 패스해야 할까요~~
라는 생각에 포스팅을 시작 하겠지만, 마구마구 파고들면 너무 많은 내용을 언급해야 하므로 "최대한" 간단히 하도록 노력할게요:D
-
단일 상속
이 전에 UML 포스팅을 하면서 상속에 대해서 언급을 한적이 있었어요. 설계를 위한 이론적인 내용이지만, 구현에 있어서 그 본질이 바뀌지는 않겠죠.
한번만 보고 와보세요 :D
구현적 측면에서 얘기하자면 "클래스를 재사용 한다"에 초점이 맞춰져 있는 거에요. 다이어그램으로 보자면 아래와 같은 그림으로 볼 수 있죠.
동물은 먹거나 걷는데, 이는 육식동물과 초식동물이 동시에 가지고 있는 행동이죠. 둘 다 사용하지만 몇 가지 다른 점 때문에 공통된 부분까지 중첩된 부분이 많이 포함된 클래스들을 만들 필요없이 "재사용"하기 위해서 위와 같은 설계가 이루어 지게 되죠.
코드는 아래와 같아요.
// 동물 클래스 class Animal { public: Animal(){ printf("동물 생성자\n"); } ~Animal(){ printf("동물 소멸자\n"); } public: void Eat(){ printf("동물::먹다()"); } void Walk(){ printf("동물::걷다()"); } }; // 육식동물 클래스 class Carnivore : public Animal { public: Carnivore(){ printf("육식동물 생성자\n"); } ~Carnivore(){ printf("육식동물 소멸자\n"); } }; // 초식동물 클래스 class Herbivore : public Animal { public: Herbivore(){ printf("초식동물 생성자\n"); } ~Herbivore(){ printf("초식동물 소멸자\n"); } };
보통 학과 커리큘럼의 과제에서 상속을 사용하지 않아도 문제가 풀리긴 하지만, 혹여 상속을 사용 한다면 아래와 같이 사용 하겠죠.
int main() { // 육식동물 호랭이 printf("main에서 육식동물 클래스로 객체를 생성\n"); Carnivore tiger; // 초식동물 토깽이 printf("main에서 초식동물 클래스로 객체를 생성\n"); Herbivore rabbit; tiger.Eat(); rabbit.Eat(); system("pause"); return 0; }
어떤 결과가 나올지 예상 되시나요?
결과의 내용은 아래와 같아요.- "동물"클래스를 상속받은 육식, 초식동물 클래스는 모두 동물 클래스를 생성하고, 육식동물 클래스를 생성했다.
- 먹는다는 함수를 사용하게 되면, 부모 클래스인 "동물" 클래스의 먹다 를 호출한다.
- 모두 생성자는 호출 했지만, 소멸자는 호출하지 않았다.
일단 별다르게 신경쓰지 않고도 할 수 있는 것은 이 정도 에요. 과제만 하는대는 이렇게만 써도 되는데, 가장 중요한 포인트는 "포인터"에요. 포인터를 사용하지 않는 다는 것은 지역변수로 사용 한다는 이야기이고, 그것은 곧 메모리의 스택영역에 변수가 쌓인다는 거죠.
지역변수가 나쁘다는 얘기는 아니에요. 메모리에 대해서 전혀 신경 쓸 필요가 없으니까요. 다만, 스택이라는 영역자체가 공간할당이 적기 때문에 클래스가 커지면 변수 생성시 차지하는 영역의 빈 곳이 없어진다는 거죠. 또한 특정한 지역(코드상에서 {} 안에서)내에서만 사용하고 사라지기 때문에 전역 변수로 설정하지 않는 이상 지역변수는 지역에서만 쓰다가 사라지는게 되구요.
그래서 "포인터*"의 사용이 불가피 하다는 거죠.
포인터의 개념을 알고 계신다는 전제하에 진행되는 포스팅 이기 때문에, 모르시는 분들은 일단 구글링 하시고 와주시면 감사하겠어요 :)
다시 코드를 작성해 볼게요.
int main() { // 육식동물 호랭이 printf("main에서 육식동물 클래스로 객체를 생성\n"); Carnivore *tiger = new Carnivore(); // 초식동물 토깽이 printf("main에서 초식동물 클래스로 객체를 생성\n"); Herbivore *rabbit = new Herbivore(); tiger->Eat(); rabbit->Eat(); // 포인터 이기에 메모리반환을 반드시 // 해줘야 하는 // 귀차니즘과 불편함은 존재해요-_-ㅋ delete tiger; delete rabbit; system("pause"); return 0; }
결과를 볼까요?
이제 학과 과제 해결 수준 보다 "약간" 수준 높게 만들어 진거 같네요:D 하지만, 재사용을 한다는게 원래 있는거만 사용한다는 의미는 아니에요. 육식동물과 초식동물은 걷는 건 둘 다 네발로 다니니까, "걷다"에 대해서는 별로 수정이 필요 없다고 가정 해보죠. 먹는다는 내용만 바뀌는 거에요.
그럼 육식, 초식 동물 클래스에 먹는것과 관련된 함수를 추가 해 보죠.
// 육식동물 클래스 class Carnivore : public Animal { public: Carnivore(){ printf("육식동물 생성자\n"); } ~Carnivore(){ printf("육식동물 소멸자\n"); } public: void Eat(){ printf("육식동물이 고기를 우적우적\n"); } }; // 초식동물 클래스 class Herbivore : public Animal { public: Herbivore(){ printf("초식동물 생성자\n"); } ~Herbivore(){ printf("초식동물 소멸자\n"); } public: void Eat(){ printf("초식동물이 풀때기를 아삭아삭\n"); } };
메인코드에는 변함이 없어요. 실행 결과는 아래와 같이 나오네요.
각각 자식 클래스에서의 해당 함수를 호출 하게 되죠. 부모 클래스에 같은 이름을 가진 함수가 있더라도요. 여기 까지는 변수가 별로 없는 학과 과제 인거 같아요. 조금만 레벨을 높여 볼까요?
-
그룹화된 객체들의 동일한 행동
여태까지 동물로 예를 들었으니까, 그 예시대로 빗대어 볼게요. 위의 예시들은 집에서 호랑이 한마리, 토끼 한마리를 키우는 거에요.(호랑이를 집에서??!!! <- 따지지 마세요☞☜) 그럼 앞으로는 동물원을 운영하는 거죠. 호랑이, 사자, 곰, 토끼, 염소, 기린, ......
아이들이 밥 먹을 시간이 되었어요. 모든 객체들을 가져와서 Eat(); 해야겠죠?^-^
... 농담 이에요.
일단, 아래와 같은 코드가 작성 될 거에요.
예시만을 위해서 동적배열은 사용하지 않고 그냥 정적배열만을 사용 한 것이므로, 실제로 변수가 정해지지 않고 많아 질 때는 동적 배열을 사용 하세요.
int main() { Carnivore *carnivore[3]; carnivore[0] = new Carnivore(); carnivore[1] = new Carnivore(); carnivore[2] = new Carnivore(); Herbivore *herbivore[3]; herbivore[0] = new Herbivore(); herbivore[1] = new Herbivore(); herbivore[2] = new Herbivore(); for(int i = 0 ; i < 3 ; ++i) { carnivore[i]->Eat(); herbivore[i]->Eat(); } // 포인터 이기에 메모리반환을 반드시 // 해줘야 하는 // 귀차니즘과 불편함은 존재해요-_-ㅋ for(int i = 0 ; i < 3 ; ++i) { delete carnivore[i]; delete herbivore[i]; } system("pause"); return 0; }
결과는 아래와 같아요.
그런대, 새로운 클래스가 만들어졌다고 생각해 볼께요. 파충류도 있고, 조류도 있고, 어류도 있고, 양서류도 있고, ... 그때마다 해당 클래스의 모임을 불러올 수는 없죠.
그래서 클래스를 캐스팅 할 때 그룹화 시킬 수 있는 부모클래스로 만들어 버리는 거죠.
예시를 위해서 개체수는 1개씩으로 하고 그냥 육식동물과 초식동물만으로 이루어진 앞선 예제를 사용 할게요.
int main() { Animal *animals[2]; animals[0] = new Carnivore(); animals[1] = new Herbivore(); for(int i = 0 ; i < 2 ; ++i) animals[i]->Eat(); // 포인터 이기에 메모리반환을 반드시 // 해줘야 하는 // 귀차니즘과 불편함은 존재해요-_-ㅋ delete animals[0]; delete animals[1]; system("pause"); return 0; }
하지만 문제점이 있어요.
찾으셨나요???
바로 육식동물과 초식동물의 소멸자를 부르지 않는다는 것과 각각의 Eat()를 호출하지 않는 다는 점이에요. 학과레벨에서는 클래스 자체를 자식 클래스만 사용하는데다가 대부분 포인터를 사용하지 않기 때문에 메모리 누수의 문제가 없지만, 클래스 캐스팅으로 객체를 생성하게 되면 이러한 문제점이 발생하죠.
물론 당장에 코드가 돌아가지 않는 Error는 아니에요. 장기적으로 봤을 때 메모리 누수의 결정적 원인이라는 거죠.
문제를 해결해 볼까요? 각각의 함수 앞에 virtual 키워드를 붙여주면 끝나요.
// 동물 클래스 class Animal { public: Animal(){ printf("동물 생성자\n"); } virtual ~Animal(){ printf("동물 소멸자\n"); } public: virtual void Eat(){ printf("동물::먹다()\n"); } virtual void Walk(){ printf("동물::걷다()\n"); } };
놀랍게도 그럼 문제는 모두 해결 되요!
virtual 키워드가 붙게되면,- 함수의 경우, 마지막에 바인딩된 클래스(예제에서는 육식, 초식 동물 클래스)의 함수만 호출
- 소멸자의 경우, 마지막에 바인딩된 클래스 부터 그로부터 파생된 부모 클래스 모두 호출
잠시간의 예시를 위해서, 한번 더 상속시킨 클래스를 넣어 볼까요?
// 육식동물 클래스 class Carnivore : public Animal { public: Carnivore(){ printf("육식동물 생성자\n"); } virtual ~Carnivore(){ printf("육식동물 소멸자\n"); } public: virtual void Eat(){ printf("육식동물이 고기를 우적우적\n"); } }; // 임시적으로 호랑이 클래스 class Tiger : public Carnivore { public: Tiger(){ printf("호랑이 생성자\n"); } ~Tiger(){ printf("호랑이 소멸자\n"); } public: void Eat(){ printf("호랑이가 고기를 으적으적\n"); } }; // 생략 int main() { Animal *tiger = new Tiger(); tiger->Eat(); delete tiger; }
그럼, 그에 상응하는 모든 부모 클래스의 소멸자를 모두 호출하면서 끝나게 되요.
어찌된 일인지 생성자는 virtual을 하지 않아도 부모 생성자가 호출되는데, 해도해도 아직 모르겠네요. 이 부분은 추후에 알아낸다면, 추가 할게요.
추가적인 내용! 다형성 2012.08.06
이 부분에 대해서 어디서 들었나 싶었는데, 곰곰히 생각해보고 알아보니까, "다형성"에 관한 내용이더군요.
일단, 다형성이라함은 다양한 형태의 성질을 가진 것인데, 위의 예시처럼,
객체 자체는 동물이지만, 실제로 할당된 것은 육식 또는 초식의 형태잖아요.
그리고 그렇게 만들어진 동물은 그냥 Eat()이나 Walk()만 하면 각각에 맞는 행동을 하게 되는 거죠
저는 그런거 생각안하고 예시를 동물에 빗대었지만 맥락은 같은 "도형" 이라는 예시가 있죠.
도형은 그 밑으로 삼각형, 사각형, 등등 이 있는데 각각의 그리는 방법은 달라도 "그린다" 라는 내용은 모두 동일 하게 가지고 있죠.
그럼 도형이라는 객체를 만들어서 삼각형, 사각형 등의 객체로 캐스팅 하게 되면 만들어 놓은 객체들은 각각에 맞는 행동을 하게 되는 거에요.
구현적 측면에서 봤을때는 공통적인 행동들을 모두 "도형" 클래스에 만들어야 하지만, 우리는 항상 도형과 같은 클래스를 만드는건 아니니까 이건 설계에서 조심스럽게 생각할 필요가 있죠.
-
추상 클래스화에 의한 구현 실수로 인한 오류 억제
정리를 한번 더 해볼게요. 여태까지 만들었던 클래스들을 보게 되면, "먹는다"라는 함수는 육식동물이 다르고, 초식동물이 다르잖아요? 그런데 굳이 "동물"클래스에서 정의 하는게 필요 할까요?
여기서 제가 언급하는 것은 "정의 필요성" 이에요. 지금과 같은 예시에서 정의는 필요해요. 왜냐?! 동물이라는 클래스 자체는 "먹다"가 꼭 필요한 특징이기 때문이죠. 육식이든, 초식이든 동물은 먹어야 하잖아요. 그래서 정의를 생략 해 버리는 거에요.
// 동물 클래스 class Animal { public: Animal(){ printf("동물 생성자\n"); } virtual ~Animal(){ printf("동물 소멸자\n"); } public: virtual void Eat() = 0; virtual void Walk(){ printf("동물::걷다()\n"); } };
함수의 몸체가 구현이 되지 않은 상태. 즉, null을 가지게 되면 해당 클래스는 추상 클래스로 분류되고 이를 이용한 객체의 생성이 불가능해 집니다. 이를 그림으로 표현하면 아래와 같은 클래스 다이어그램으로 표현 할 수 있죠.
이전에 만들었던 내용은 "일반화" 이고, 지금은 "실체화" 에요. 두 개의 차이는 지금 예시로 보여주고 있는 부모 클래스인 "동물" 클래스가 그 스스로 객체 생성을 할 수 있는 가의 유무 에요.
이렇게 해두면, 동물 클래스를 상속 받는 육식과 초식은 각각 "먹다"에 대한 내용을 반드시 정의해야 사용이 가능하답니다. 만약 상속을 받았는데도 정의를 하지 않는다면 이는 컴파일 오류로 간주하고 빌드 자체가 되지 않아요.
이전에 우리는 이미 각기 별도로 구현 했지만, 내용을 빼서 오류를 유발 시켜 볼까요?
물론, 동물클래스에서 선언한 내용을 상속받는 클래스에서 사용한다고 하면 virtual 키워드만 붙이고 남겨놔도 상관은 없어요. 그렇지만 방금처럼 반드시 구현해야 하는 내용을 빼먹고 그냥 넘어가게 되는 오류 억제는 할 수 없겠죠? :)
-
기능의 분리와 구현 실수로 인한 오류 억제
기능을 분리해서 상속받아 사용 하는 방법도 있어요.
동물원(어느새 동물원으로 컨셉을 굳힌...)을 관리하는 로봇이 새로 들어 왔어요. 로봇의 여느 많은 기능들이 있겠지만 (일단 다른 기능들은 제하고) 살펴보니 기존에 만들었던 동물 클래스의 "걷다" 라는 행동은 로봇 역시 하는 거에요. 하지만 "먹다" 라는 행동은 하지 않으니 그 부분에 대해서는 기능 분할이 딱히 필요는 없죠.
그래서 걷는다라는 기능을 따로 분리 시킬 게요.
-
걷는다 기능 분리
struct WALK { virtual void Walk() = 0; };
-
동물은 "걷다"라는 특징이 있으니까 이를 상속
// 동물 클래스 class Animal : public WALK { public: Animal(){ printf("동물 생성자\n"); } virtual ~Animal(){ printf("동물 소멸자\n"); } public: virtual void Eat() = 0; virtual void Walk(){ printf("동물::걷다()\n"); } };
-
새로운 로봇 클래스를 만들면서, 이를 역시 상속
// 로봇 클래스 class Robot : public WALK { public: Robot(){ printf("로봇 생성자\n"); } ~Robot(){ printf("로봇 소멸자\n"); } public: void Walk(){ printf("로봇이 기잉기잉 걸어갑니다.\n"); } };
이전 부분에서 언급했던 거 처럼, 육식이든 초식이든 걷는 방법은 동일해서 별도의 구현이 필요 없을 때, 부모클래스인 동물 클래스에서 virtual만 붙인 상태에서 정의를 해둔 상태에요. 육식과 초식은 Walk()를 하더라도 동물 클래스가 가지고 있는 Walk()가 불려져서 사용되죠.
이러한 추상클래스를 상속 받는 것 역시, 사용되는 하위 클래스에서 정의하지 않으면 컴파일 에러가 나게 되므로 정의하지 않는 실수를 방지 할 수 있죠. 물론, 그 추상 클래스를 넣는거 자체를 까먹는다면 할말은 없지만, 나중에 하다보면 "어? 이 기능을 왜 수행할 수 없지?" 이러면서 넣을 수 있으니까 그 때 저런 방법으로 넣어두면 좋죠.
struct를 사용 했는데, C++에서의 구조체는 클래스와 동일하지만 단 한가지의 중요한 차이만 있어요.
class는 기본적으로 모든 변수나 함수를 별도의 언급이 없다면 private으로 설정하는 반면에,
struct는 기본적으로 별도의 언급이 없다면 public으로 설정 한답니다.
위의 예시는 모두 사용해야 하는 public 이므로 구태여 struct를 사용해 봤어요. 코딩 취향 따라서 작성하세요 :)
저는 여기서 추상 클래스라고 계속 언급을 했지만, 이러한 기능적 측면으로 분류시켜 별도로 빼서 사용하는 그 자체를 자바에서는 인터페이스(Interface)라고 하죠. 또한 용어 역시 상속이 아닌 확장이라는 개념으로 다중 상속을 하지 못하는 점을 보완 했다죠.(자바 유저가 아니라서 제 3자형 멘트ㅋㅋ) C++에서는 인터페이스 개념이 없기 때문에 추상 클래스라고만 언급 한다는 점을 짚고 넘어가려구요 :)
다이어그램으로 표현하면 아래처럼 할 수 있을 거에요.
-
걷는다 기능 분리
-
다중 상속 시 반복된 부분에 의한 메모리 절약
제목부터 의아 할 수 있습니다만, 단순히 다중 상속에 의한 메모리 중첩은 생기지 않아요.
클래스 D는 A, B, C를 모두 상속받지만, 내부의 변수명이 같아도 어디까지나 이건 중첩이 아니죠. 각기 다른 특징이 있는 클래스 들이니까, 변수의 특징 역시 다르겠죠. 변수를 사용 할 때는 "부모 클래스 이름.변수명"으로 사용하면 분류가 되니까요.
그런데 이들 클래스들이 모두 어떤 클래스로 부터 상속을 받는 다면?
클래스 A, B, C입장에서는 클래스 P에 있는 변수 p를 사용하는데 아무 부담이 없어요. 하지만, 클래스 D에서는 저 변수를 어떻게 처리 해야 할까요? 뭔지 알고...??
그런 의미에서 새로운 클래스와 함께 변수를 추가해 볼게요. 변수는 위의 그림 처럼 최상위 부모 클래스인 "동물" 클래스에 간단하게 int 형으로 추가를 시키고, 육식과 초식의 모든 특징을 부여받은 "잡식동물" 클래스를 만들면 위의 예시가 딱 되겠죠. 바로 아래 처럼요.
-
잡식동물 클래스
여기서는, 동물 클래스에 있는 변수에 접근하기 위한 임의의 함수를 만들어 봤어요.
// 잡식동물 클래스 class Omnivore : public Carnivore , public Herbivore { public: Omnivore(){ printf("잡식동물 생성자\n"); } ~Omnivore(){ printf("잡식동물 소멸자\n"); } public: void Eat(){ printf("잡식동물이 고기쌈을 꿀꺼억\n"); } void AnimalVar(){ printf("동물 클래스 수 : %d\n", Animal::a); } };
-
동물 클래스의 변수 추가
// 동물 클래스 class Animal : public WALK { protected: int a; public: Animal():a(3){ printf("동물 생성자\n"); } virtual ~Animal(){ printf("동물 소멸자\n"); } public: virtual void Eat() = 0; virtual void Walk(){ printf("동물::걷다()\n"); } };
이렇게만 만들면, 우리는 아름다운 오류를 볼 수 있어요.
해결방법으로는 중간에 있는 자식 클래스인 육식과, 초식이 동물 클래스를 상속받을 때 virtual 키워드를 붙여주면 되요.-
육식동물 클래스
// 육식동물 클래스 class Carnivore : virtual public Animal { public: Carnivore(){ printf("육식동물 생성자\n"); } ~Carnivore(){ printf("육식동물 소멸자\n"); } public: void Eat(){ printf("육식동물이 고기를 우적우적\n"); } };
-
초식동물 클래스
// 초식동물 클래스 class Herbivore : virtual public Animal { public: Herbivore(){ printf("초식동물 생성자\n"); } ~Herbivore(){ printf("초식동물 소멸자\n"); } public: void Eat(){ printf("초식동물이 풀때기를 아삭아삭\n"); } };
잡식 클래스에서 동물 클래스에 있는 멤버변수를 사용하는 임의의 함수를 테스트 하기 위해 아래와 같은 main 코드를 작성 했어요.
int main() { Animal *omnivore = new Omnivore(); omnivore->Eat(); Omnivore *tmpOmnivore = dynamic_cast<Omnivore*>(omnivore); tmpOmnivore->AnimalVal(); // 포인터 이기에 메모리반환을 반드시 // 해줘야 하는 // 귀차니즘과 불편함은 존재해요-_-ㅋ delete omnivore; system("pause"); return 0; }
캐스팅에 대해서, 추후에 따로 포스팅 할 예정이니까 뭔지 모르는데 궁금하신 분들은 구글링 하시거나 아니면, 아~ 저런것도 있구나~ 하면서 넘어가시면 될 거 같아요 :)
그럼 깔끔하고 아름답게, 출력! -
잡식동물 클래스
'Programming > C/C++' 카테고리의 다른 글
임의의 벡터간 각도 구하기 (0) | 2011.12.15 |
---|---|
3x3 행렬 코딩 (0) | 2011.12.14 |
C reference 정리 pdf (0) | 2011.09.03 |
포인터 2차 동적 할당 (0) | 2011.02.21 |
Random double 값 추출하기 (0) | 2011.02.10 |
결과는 아래처럼, 소멸자도 같이 불려진답니다.
생성시에는 부모->자식 순으로,
소멸시에는 자식->부모 순으로,
생성자와 소멸자는 각각 호출되는 거죠.
main에서만 위의 내용을 실행 했을 때 결과가 나오지 않은 것은 return 0; 후에 나오는데 너무 빨라서....-_-;;;;;;