프로그래밍 지식/C++

C++문법/ 가상, Virtual (+ Override)

게임이 더 좋아 2021. 11. 24. 16:50
반응형
728x170

 

Virtual이라는 키워드가 C++에 존재한다.

무슨 용도로 쓰일까??

알아보자

 

참고로 여기서 가상이라는 것 실존하지 않는다는 뜻이 절대 아니다.


 

우선 답부터 말해주자면 동적 바인딩, Dynamic Binding을 위해서 쓰인다.

즉, 컴파일 시에 어떤 함수가 실행될 지 정해지지 않고 런타임 시에 정해지는 것이다.

여기서 동적 바인딩이란 컴파일 시에 정적 타입으로 참조할 주소가 정해지는 것과 달리 객체를 체크해서 참조할 주소를 정하는 것이라 생각하면 된다.

우선 머리에만 넣었다가 이따가 다시 예로 알아보도록 하자.

 


 

우선 업캐스팅을 알아보자 (다운 캐스팅도 있지만 생략)

업캐스팅이란 부모 클래스의 타입으로 자식객체를 선언하는 것

다운 캐스팅이란 자식 클래스 타입으로 부모객체를 선언하는 것

 

질문을 해보겠다.

 

사람은 동물인가?  -> O

동물은 사람인가? -> X라고 보는게 일반적

 

Animal* anm = new Person(); // -> O 업캐스팅
Person* p = new Person(); // -> 타입일치

///////////////////////
Animal* anm = new Animal(); // -> 타입일치
Person* p = new Animal(); // -> 다운 캐스팅(지양 및 오류)

 

만약 동물이 이동한다고 생각해보자.

 

그냥 그냥 예를 들어서

일반적으로 사람은 두 발로 걸어다니지만

동물은 네 발로 걸어다닌다.

 

예를 들어서

Person 객체에 Move()를 호출하면 두 발로 걷기가 호출되어야 한다.

하지만 Animal 타입으로 선언된 Person 객체에 Move()를 호출하면

Animal의 Move()가 나온다.

 

객체는 Person인데 호출은 Animal의 Move를 하지??? 뭐가 잘못된건가??

그것이 아니다.

 

Animal 타입으로 선언된 포인터는 해당 클래스만 가르키는 것이지

그 밑의 파생 클래스를 가리키지 않는다는 말이다.

 

즉, 가리키지 않으니 정보가 없으므로 해당 타입을 가리키고 해당 타입의 함수를 호출할 수 밖에 없는 것이다.

 


 

오 그렇구나..? 그게 왜...?

 

우선 가상 함수는 3가지 조건을 만족할 때 쓰이게 된다.

1. 상속 계층 구성

2. 자식 클래스가 멤버함수 재정의(override)

-> 다만 항상 오버라이딩을 해야하는 것은 아니다.

virtual로 선언하고 재정의하고 싶은 클래스만 재정의해도 된다.

3. 포인터로부터 멤버 함수를 호출할 때

 

**4. 가상 소멸자(아래 글 확인) -> 항상 소멸자는 virtual로 선언해야함

[C || C++ 문법] - C++문법/ 생성자와 소멸자 , Constructor & Destructor

 

만약 이런 상황을 생각해보자.

우선 어떤 객체가 쓰일 지 모르는 상황이다.

-> 즉, 어떤 파생 클래스의 객체인지를 모른다.

 

C++에서 업캐스팅은 허락이 되니까 최상위 기반클래스로 선언하고 객체를 받으려고 한다.

하지만 어떤 객체가 쓰이든 말든 포인터는 기반 클래스만을 가리킬테니

항상 기반 클래스의 함수가 실행될 것이니 객체에 따른 동작이 되지를 않는다.

 

즉, 우리는 어떤 것이 쓰일지 모름에도 쓰려면 해당 타입을 선언시켜줘야 하는 어려움이 생겼다.

(정확한 사용을 위해선 정확하 타입을 알아야 하는 것이다.)

-> 하지만 모르는데??

 

아니 모르는데 어떻게 가요

 

그래서 우리는 Virtual을 써서 동적으로 바인딩을 시켜주겠다는 것이다.

Virtual 로 함수를 선언하고 이 함수는 가상 함수, Virtual Function이 된다.

Virtual은 해당 객체를 확인해서 해당 객체의 함수를 호출해주는 것이다.

-> 다시 말하면 virtual 키워드가 선언되었다?? 

-> 해당 부분에 대해서는 객체의 확인이 필요한 부분이구나?

-> 객체를 확인해서 동작이 달라져야 하는 구나? 

-> override를 하나보구나? (사실 오버라이딩 안할 것이면 virtual 할 필요가 없음)

-> 동물도 사람도 네 발로 기면 굳이 virtual로 선언하여 객체를 구분할 필요가 없다는 말이다.

-> 다형성을 활용하는 방법 중 하나가 virtual 키워드를 활용하는 것이구나?

-> 물론 부모 클래스에서 virtual을 써주는 것이 일반적이고 맞는 경우다.

 

**그리고 파생 클래스의 함수가 기반 클래스의 함수를 오버라이드 하기 위해서는 두 함수의 꼴이 정확히 같아야 한다. -> 조금이라도 다르다면 override 키워드를 써주어야 한다.

 

여기서 꼴이 같다는 것은 이름만 같은 것이 아니라 시그니처가 같다는 뜻이다.

매개변수 종류와 타입이 같다는 말이다.

 


 

??? 객체를 확인한다고?? 무슨 뜻이지 ??

 

예를 들어 알아보자

#include <iostream>


//기반 클래스
class Base {

 public:
  Base() { std::cout << "기반 클래스" << std::endl; } // 생성자

  virtual void what() { std::cout << "기반 클래스의 what()" << std::endl; } // 멤버함수 (Virtual 키워드 쓰임)
};

//기반 클래스를 상속 받은 클래스
class Derived : public Base {

 public:
  Derived() : Base() { std::cout << "파생 클래스" << std::endl; } //생성자 (Base가 실행되고 Delivered의 생성자가 실행됨)

  void what() { std::cout << "파생 클래스의 what()" << std::endl; } // 멤버함수
};

int main() {

  //분명 다른 객체 2개를 생성함
  Base p;
  Derived c;
 
  //하지만 업캐스팅을 사용해서 다른 클래스의 객체를 하나의 기반 클래스의 포인터를 만듬
  Base* p_c = &c;
  Base* p_p = &p;

  std::cout << " == 실제 객체는 Base == " << std::endl;
  p_p->what();

  std::cout << " == 실제 객체는 Derived == " << std::endl;
  p_c->what();

  return 0;
}

 

바인딩

더보기

1. 

"흠, p_c 는 Base 포인터니까 Base 의 what() 을 실행해야지"
"어 근데 what 이 virtual 이네?"

"잠깐. 이거 실제 Base 객체 맞어? 아니네 Derived 객체네"
"그럼 Derived 의 what 을 실행해야지"

2.

"흠, p_c 는 Base 포인터니까 Base 의 what() 을 실행해야지"
"어 근데 what 이 virtual 이네?"

"잠깐. 이거 실제 Base 객체 맞어? 어 맞네."
"Base 의 what 을 실행하자"

 

 

 

결과는??

기반 클래스
기반 클래스
파생 클래스
 == 실제 객체는 Base == 
기반 클래스의 what()
 == 실제 객체는 Derived == 
파생 클래스의 what()

 

?? 최상위 기반 클래스의 타입으로 선언을 했는데도 객체를 구별해서 동작했다!!

우리는 이제서야 동적으로 어떠한 객체의 동작이 정해지는 것도 문제 없이 실행할 수 있게 되었다.

 


 

 

여기서는 override도 동시에 이루어졌다. (설명은 아래에 있다)

override도 키워드로 예약되어있는데

위에서는 명시적으로 override라고 선언하지 않아도 실제로 override된 함수가 실행이 되었다.

**(함수 모양이 같음)

override 키워드가 있는 이유는 실수로 오버라이드를 하지 않는 경우를 막기 위해서다.

-> 이름이 같아도 시그니처가 다르면 오버라이드가 되지 않는다.

 


 

?? 그게 뭔데요???

예를 들어보자

 

#include <iostream>
#include <string>

class Base {
  std::string s;

 public:
  Base() : s("기반") { std::cout << "기반 클래스" << std::endl; }

  virtual void incorrect() { std::cout << "기반 클래스 " << std::endl; }
};
class Derived : public Base {
  std::string s;

 public:
  Derived() : Base(), s("파생") {}

  void incorrect() const { std::cout << "파생 클래스 " << std::endl; }
};
int main() {
  Base p;
  Derived c;

  Base* p_c = &c;
  Base* p_p = &p;

  std::cout << " == 실제 객체는 Base == " << std::endl;
  p_p->incorrect();

  std::cout << " == 실제 객체는 Derived == " << std::endl;
  p_c->incorrect();
  return 0;
}

위의 예와 거의 똑같지만

Delivered의 멤버함수인 incorrect( ) 가 기반 클래스랑 조금 다르게 생겼다. (const가 붙어있다.)

 

그래서 우리의 의도가 Derived 의 incorrect 함수가 기반 클래스를 오버라이드 하는 것이였다면

정상적으로 작동하지 않았고 게다가 오류로 검출되지 않는다.

버그는 컴파일 타임에 잡을 수 없게 되므로 추적이 힘들다는 것이다.

 

즉, 우리가 명시적으로 override라고 선언한다면

컴파일러에서 실제로 이 함수의 이름을 가진 함수를 override 하겠다고 정확하게 말하는 것이 좋다는 말이다.

 

override라고 선언하고 다시 실행해보면 오류가 난다.

test.cc:19:8: error: 'void Derived::incorrect() const' marked 'override', but does not override

함수의 모양이 똑같이 생기지 않은 것이다.

 

const를 지워주면 override가 문제없이 된다.

 


 

++ 내용

 

부모 클래스에 Virtual로 선언한 함수

자식 클래스에 있는 같은 꼴(시그니처)을 하고 있는 함수들에게 자동으로 Virtual이라고 선언이 된다.

 

자식 클래스에서 명시적으로 override를 할 경우에는(override 키워드를 쓰는 경우)

암묵적으로 virtual이라는 가정 하에 작성하기 때문에

virtual 키워드를 쓰지 않는다.

당연한게 Override를 쓴다는 것 자체가 객체 별로 다르게 작동하고 싶다는 의미이고

Virtual도 객체 별로 다르게 작동하고 싶다는 말이라서 그렇다.

-> virtual void Function( )  override { ..... } 에 같이 쓸 필요가 없다는 말이다.

 


// 21.12.14 업데이트

참고로 순수 가상 함수, Pure Virtual Function 이라는 것이 있는데

이는 부모 클래스에서 아무 정의도 하지 않았기 때문에 무조건 파생 클래스에서 재정의를 해줘야 하는 함수다.

인터페이스와 같다. 해당 멤버 함수를 상속은 받지만 구현은 자식클래스에서만 한다.

우리는 이러한 부모 클래스를 추상 클래스, Abstract class라고 부른다.

또한 이러한 구현을 하는 자식클래스를 구체 클래스, Concrete class라고 부른다.

 

이러한 추상 클래스는 구현이 불완전하여 자신 객체를 생성할 수는 없지만 구체 클래스의 조상이 되어 전체를 대표하는 느낌을 가진다.

즉, 자기 자신은 불완전한 구현을 하더라도 자식 클래스들에게 무조건 구현해야함을 알려줌으로써 다형성을 부여하는 역할을 한다.

추상 클래스를 상속받았다면 순수 가상 함수를 무조건 구현해야하는 의무를 가진다.이는 강제성이 있다.

 

부모클래스에서 순수 가상 함수를 만들 때는 함수의 본체 ({ })를 생략하고 = 0만 붙여서 선언한다.

virtual void draw() = 0; //부모클래스에서 순수 가상 함수 선언

 


 

Virtual 요약

  • 한 개 이상의 가상 함수를 포함하는 클래스에 대해서는 컴파일러가 가상 함수 테이블을 만든다.
  • 가상 함수 테이블은 실제 호출되어야 할 함수의 위치정보를 담고 있는 테이블이다.
  • 가상 함수 테이블은 객체의 생성과 상관없이 main 함수가 호출되기 이전에 메모리 공간에 할당된다.
  • 가상 함수 테이블은 멤버 함수의 호출에 사용되는 일종의 데이터이다.
  • 따라서 가상 함수를 포함하는 클래스는 가상 함수 테이블을 이용해 함수를 호출하므로 포인터가 가리키는 클래스의 멤버함수가 아닌 객체 선언시 자신의 클래스에 맞게 오버라이딩된 함수를 호출한다.

 


 

참고링크

https://modoocode.com/210

반응형
그리드형