virtual(가상)

-> 존재하지 않는 것을 존재하는 것처럼 느끼게 하는 것

 

프로그래밍에서 virtual(가상)

-> 존재하는 것을 존재하지 않는 것처럼 만드는 것.

-> 함수의 반환 타입 앞에 virtual 키워드를 붙이면 가상 함수가 된다.

(존재하는 함수를 존재하지 않는 것처럼 만든다.)

 

가상 함수를 통해 함수를 호출하면 함수를 없는 것처럼 만드는 것이기 때문에 호출해야할 함수가 사라진다.

이 경우, 같은 함수를 다시 만들어서 호출한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class CObj
{
public:
    virtual void Func()
    {
        cout << "CObj Func" << endl;
    }
};
 
void main()
{
    CObj    obj;
    obj.Func();
}
cs

위와 같이 Func() 함수가 가상 함수일 경우

Func()함수가 없는 것처럼 만들어져서 호출이 불가능하게 된다.

이 때 컴파일 과정에서 Func() 함수를 다시 만들어 호출을 해준다.

 

부모 클래스의 가상 함수를 자식 클래스가 오버라이딩할 경우

자식 클래스의 함수 또한 가상 함수가 된다.

(virtual 키워드 또한 상속이 된다.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class CObj
{
public:
    virtual void Func()
    {
        cout << "CObj Func" << endl;
    }
};
class CPlayer : public CObj
{
public:
    void Func()
    {
        cout << "CPlayer Func" << endl;
    }
public:
    void CPlayer_Func()
    {
 
    }
};
cs

 

부모의 가상 함수를 자식 클래스가 오버라이딩할 경우

virtual 키워드로 인한 혼란스러움을 방지하기 위해

자식 클래스에서 오버라이딩한 함수에도 virtual 키워드를 명시해주는 것이 좋다.

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
class CObj
{
public:
    virtual void Func()
    {
        cout << "CObj Func" << endl;
    }
};
class CPlayer : public CObj
{
public:
    virtual void Func()
    {
        cout << "CPlayer Func" << endl;
    }
public:
    void CPlayer_Func()
    {
 
    }
};
class CSub_Player : public CPlayer
{
public:
    virtual void Func()
    {
        cout << "CSub_Player Func" << endl;
    }
};
cs

가상 함수 판단

#1. 가상 함수가 아닐 경우 객체 타입 기준으로 함수를 호출한다.

 

#2. 가상 함수일 경우 객체 타입이 아닌 실 객체의 함수를 호출한다.

 

가상 함수 테이블

-> virtual 키워드가 단 하나 이상이라도 존재한다면

-> 컴파일러는 가상 함수 테이블을 만든다.

-> 가상 함수 테이블에는 virtual 함수들의 주소가 저장 된다.

-> 이후, 함수를 호출할 때 해당 함수가 virtual 함수일 경우

-> 객체 타입 기준으로 호출하지 않고 가상 함수 테이블을 확인하여 호출한다.

 

가상 함수 포인터

-> virtual 키워드가 단 하나 이상이라도 존재한다면

-> 컴파일러가 가상 함수 포인터를 멤버로 추가한다.

-> virtual 함수를 호출할 때 가상 함수 포인터가 참조하고 있는 테이블의 함수를 호출한다.

 

가상 소멸자

 

상속 관계에서의 생성자, 소멀자 호출 순서

객체 생성 -> 메모리 할당 -> 부모 생성자 호출 -> 자식 생성자 호출 -> 자식 소멸자 호출 -> 부모 소멸자 호출

-> 메모리 반환 -> 객체 소멸

 

다음과 같이 부모와 자식 클래스에 생성자와 소멸자를 추가하고 디버깅을 하게되면.

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
class CObj
{
public:
    CObj() { cout << "CObj 생성자 호출" << endl; }
    ~CObj() { cout << "CObj 소멸자 호출" << endl; }
public:
    virtual void Func() {}
};
 
class CPlayer : public CObj
{
public:
    CPlayer() { cout << "CPlayer 생성자 호출" << endl; }
    ~CPlayer() { cout << "CPlayer 소멸자 호출" << endl; }
public:
    virtual void Func() {}
};
 
void main()
{
    CObj*        pObj = new CPlayer;
 
    cout << "=========================" << endl;
 
    delete pObj;
}
cs

 

위와같이 CPlayer의 소멸자는 호출이 되지 않은 것을 볼 수 있다.

-> 이유는 소멸 시키는 대상이 pObj이기 때문이다.

-> pObj를 통해 알 수 있는 것은 Heap 영역의 시작 주소만 알 수 있다.

-> 시작 주소부터 몇 바이트를 소멸할 것인지는 타입을 통해 확인한다.

-> 타입이 CObj*이기 때문에 CObj의 소멸자만 호출하고 끝이난다.

-> CPlayer의 소멸자를 호출하기 위해서는 가상 함수 포인터를 사용하면된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CObj
{
public:
    CObj() { cout << "CObj 생성자 호출" << endl; }
    virtual ~CObj() { cout << "CObj 소멸자 호출" << endl; }
public:
    virtual void Func() {}
};
 
class CPlayer : public CObj
{
public:
    CPlayer() { cout << "CPlayer 생성자 호출" << endl; }
    virtual ~CPlayer() { cout << "CPlayer 소멸자 호출" << endl; }
public:
    virtual void Func() {}
};
cs

 

위와 같이 소멸자 앞에 virtual 키워드를 추가하면 CPlayer의 소멸자까지 호출 되는 것을 확인해 볼 수 있다.

 

한 가지 팁아닌 팁이 있다면

1
2
3
4
5
6
7
8
class CObj
{
public:
    explicit CObj() { cout << "CObj 생성자 호출" << endl; }
    virtual ~CObj() { cout << "CObj 소멸자 호출" << endl; }
public:
    virtual void Func() {}
};
cs

위와 같이 explicit 키워드와 가상 소멸자를 함께 사용한다면 코드를 이전보다 훨씬 보기 좋게 바꿀 수 있다.

 

순수 가상 함수

-> 함수의 정의부가 없는 함수.

-> ' = 0 '으로 마무리 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class CObj
{
public:
    virtual void Func() = 0;
};
 
class CPlayer : public CObj
{
public:
    virtual void Func()
    {
 
    }
};
cs

-> 단, 순수 가상 함수는 객체를 만들 수 없다.

-> 객체 포인터로는 사용이 가능한다.

상속 관계에서 다형성을 만들 경우 순수 가상 함수를 사용한다.

-> 자식 클래스에서 부모의 순수 가상 함수를 오버라이딩(재정의) 하지 않을 경우

-> 자식 또한 추상 클래스가 되기 때문에 실수를 방지할 수 있다.

 

추상 클래스란?

-> 순수 가상 함수를 단 하나라도 가지고 있는 클래스를 추상 클래스라 한다.

-> 추상 클래스는 객체로 만들 수 없다.

-> 단, 객체 포인터로는 사용이 가능하다.

'Programming > C++ Basic' 카테고리의 다른 글

C++ 바인딩  (0) 2020.09.21
C++ 캐스팅(static_cast, dynamic_cast, const_cast, reinterpret_cast)  (0) 2020.09.18
C++ 오버라이딩  (0) 2020.09.18
C++ 객체 포인터  (0) 2020.09.17
C++ 상속성  (0) 2020.09.17

오버라이딩

-> 상속 관계에 있어서 부모 클래스가 가지고 있는 멤버 함수를 자식 클래스가 그대로 재정의하는 문법

-> 반환 타입, 함수 이름, 매개 변수 개수, 매개 변수 타입이 모두 같다.

 

부모와 자식이 동일한 함수를 가지고 있고, 이를 호출할 경우 모호성이 발생해야 한다.

즉, 부모와 자식 둘 다 같은 이름의 함수를 가지고 있어야 함.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class CObj
{
public:
    void Func()
    {
        cout << "CObj Func" << endl;
    }
};
 
class CPlayer : public CObj
{
public:
    void Func()
    {
        cout << "CPlayer Func" << endl;
    }
};
 
cs

단, 오버라이딩은 모호성이 발생하지 않는다.

 

오버라이딩 된 함수를 호출할 경우 실 객체 기준으로 판단하지 않고

객체 타입 기준으로 판단하여 함수를 호출한다.

-> 객체 타입 기준의 함수가 앞으로 나오고, 실 객체의 함수가 뒤로 숨는다.

 

1
2
3
4
5
6
7
8
void main()
{
   객체 타입               실 객체
    CObj*        pObj = new CPlayer;
    pObj->Func();
 
    delete pObj;
}
cs

CPlayer는 CObj를 상속 받고 있다.

-> CObj의 CPlayer 클래스 내부에는 Func()함수가 있다.

-> 당연히 모호성이 발생해야 하지만, 오버라이딩 문법이 적용되어 모호성이 발생하지 않는다.

 

 

객체 포인터

-> 객체 포인터 변수에 본인 또는 본인을 상속 받는 객체의 주소를 저장할 수 있다.

-> 반대의 경우는 불가능하다.

class CObj
{
public:
	void CObj_Func()
	{

	}
};
class CPlayer : public CObj
{
public:
	void CPlayer_Func()
	{

	}
};
CPlayer* pObj1 = new CObj; // 불가능

 

단, 객체의 멤버에 접근할 때는 객체 타입 기본으로 판단한다.

객체 타입         
 CObj*		pObj = new CObj;
 CObj*		pPlayer = new CPlayer;

 

pPlayer의 실 객체는 CPlayer 이지만
객체 타입이 CObj이기 때문에 CObj가 가지고 있는 함수만 호출이 가능하다.

pPlayer->CObj_Func();
pPlayer->CPlayer_Func();		// 불가능

 

하지만 객체 타입이 CPlayer 이면 자식 클래스이기 때문에 부모 클래스에 있는 함수까지 호출이 가능하다.

CPlayer	  player;
player.CObj_Func();
player.CPlayer_Func();

 

객체 타입을 자식클래스로 하면 부모 클래스로 동적 할당이 불가능하다.

자식 -> 부모는 참조가 가능하지만 부모 -> 자식은 불가능.

하지만 부모 클래스가 자식 클래스를 참조하는 방법이 있다.

 

가상 함수를 이용하는 것이다.(virtual)

'Programming > C++ Basic' 카테고리의 다른 글

C++ 가상 함수(virtual), 가상 소멸자, 순수 가상 함수  (0) 2020.09.18
C++ 오버라이딩  (0) 2020.09.18
C++ 상속성  (0) 2020.09.17
C++ friend  (0) 2020.09.17
C++ this 포인터, extern 키워드  (0) 2020.09.16

상속

-> 부모 클래스가 가진 것을 자식 클래스에게 물려주는 것.

-> 프로그래밍에서 물려주는 것은 복사 후 붙여넣기 이다.

 

 

상속을 구현할 때 자식 클래스에서 어떤 부모클래스를 상속 받을지 명시한다.

class 자식클래스명 : 부모클래스명

 

 

상속의 조건

-> 부모 클래스를 상속 받을 때 어떤 조건으로 상속 받을지 설정할 수 있다.

-> 조건(기본 접근 지정자)을 설정하지 않을 경우 기본 private 형태로 상속을 받는다.

class A(부모)
{
public:
  void A_Func()
  {
  
  }
};

class B(자식) : public A(부모)
{
public:
  void B_Func()
  {
  
  }
}

 

 

상속 관계에서의 생성자와 소멸자 호출 순서.


생성 과정
객체 생성 -> 메모리 할당 -> 부모 생성자 호출 -> 자식 생성자 호출

소멸 과정
자식 소멸자 호출 -> 부모 소멸자 호출 -> 메모리 반환 -> 객체 소멸

 

상속의 관계

-> 상속 관계를 구성할 때 조건에 맞도록 구성해야한다.

 

#1. is-a 관계 

Object is a Player
-> 오브젝트는 플레이어다.
모든 오브젝트가 플레이어가 아니다! -> 문장이 성립하지 않는다.

Player is a Object
-> 플레이어는 오브젝트이다.
문장이 성립한다.

 

#2. has-a 관계
Player is a Sword
-> 플레이어는 검이다!
문장이 성립하지 않는다.

Sword is a Player
-> 검은 플레이어이다!
문장이 성립하지 않는다.

Player has a Sword
-> 플레이어는 검을 가진다.
문장이 성립된다.

 

다중 상속이란?
-> 다중 상속을 이용하면 여러 클래스들을 상속 받을 수 있다.

class CAxe
{
public:
	void Swing()
	{
		cout << "CAxe Swing" << endl;
	}
};

class CSword
{
public:
	void Swing()
	{
		cout << "CSword Swing" << endl;
	}
};

class CPlayer : public CSword, public CAxe
{
public:
	CSword	m_Sword;
	CAxe		m_Axe;
};

-> 단, 다중 상속은 모호성이 가장 큰 문제이다.

CPlayer	 player;
player.Swing();

플레이어는 검과 도끼를 상속 받은 상태이다.
이후 Swing을 호출할 때 검의 Swing인지 도끼의 Swing인지 알 수 없는 상황이 발생한다.

 

이를 방지하기위해서 has-a 관계가 성립될 경우에는 상속을 받지 않고, 포함 관계로 구성하는 것이 좋다.

 

포함 관계란?
-> 상속을 받는 것이 아니라, 멤버로 객체를 포함시키는 것이다.

class CPlayer
{
public:
	CSword	m_Sword;
	CAxe		m_Axe;
};

 

'Programming > C++ Basic' 카테고리의 다른 글

C++ 오버라이딩  (0) 2020.09.18
C++ 객체 포인터  (0) 2020.09.17
C++ friend  (0) 2020.09.17
C++ this 포인터, extern 키워드  (0) 2020.09.16
C++ 복사 생성자(얕은 복사, 깊은 복사)  (0) 2020.09.16

+ Recent posts