6장. 상속, 그리고 객체 지향 설계🕶

안녕하세요! 두두코딩 입니다 ✋
오늘은 Effective C++ 항목 36에 대해 알아보겠습니다.

🖇 소스코드에 마우스를 올리고 copy 버튼을 누를 경우 더 쉽게 복사할 수 있습니다!

궁금한 점, 보안점 남겨주시면 성실히 답변하겠습니다. 😁
+ 감상평 댓글로 남겨주시면 힘이됩니다. 🙇

바인딩

바인딩이란? 컴퓨터 프로그래밍에서 각종 값들이 확정되어 더 이상 변경할 수 없는 구속(bind) 상태가 되는 것을 말한다.

프로그래밍에서는 정적바인딩 과 동적바인딩 2가지 바인딩을 사용한다.

정적바인딩 (Static Binding)

프로그램 실행 이전에 값이 확정되면 “정적바인딩” 이라고 한다. 보통 Compile 단계 혹은 Linking 단계에서 값이 결정되는 것을 의미한다.

Compile 단계에서 값이 결정되면 실행시간에 해석할 필요 혹은 찾을 필요가 없기 때문에 성능적 효율이 높아진다.

일반함수 혹은 멤버함수와 같은 비가상 함수는 “정적바인딩” 된다고 보면 된다.

동적바인딩 (Dynamic Binding)

프로그램이 실행된 이후 (런타임 시) 값이 결정되면 “동적바인딩” 이라고 한다. 보통 포인터가 가리키는 실제 객체를 연결할 때 사용된다.

런타임 시, 자유롭게 객체 혹은 함수가 변경가능하기 때문에 유연성이 올라간다.

C++에서는 virtual 과같은 가상함수는 “동적바인딩” 된다고 보면 된다.

먼저 바인딩의 개념을 정리한 이유는, 항목 36장에서는 해당 개념을 알고 있다는 전제하에 포스팅을 작성하기 때문이다.

비가상 함수는 재정의 절대 금지 🚫

보통 C++에서는 비가상 함수 같은 경우 재정의를 하면 안된다고 알려준다. 왜그럴까?

아래의 예시를 통해 알아보자.

class B {
	public:
		void mf();
		...
};

class D : public B { ... };

위 코드와 같이, Base를 담당하는 B 클래스와 Derived 를 담당하는 D 파생 클래스가 있다고 생각해보자.

이 때 아래의 코드에서 mf()를 부르게 될 경우 어떻게 동작될까?

int main() {
	D x;

	// B 클래스의 mf();
	B *pB = &x;
	pB->mf();

	// B 클래스의 mf();
	D *pD = &x;
	pD->mf();
}

생각해보면 너무 당연하다. 우리는 D x;라고 선언한 후 D타입의 x를 포인터로 가리켜 mf()를 호출하기 때문에 B 클래스에서 상속받은 mf()를 호출하는 것이 너무 당연하다.

이 당연한 사실에, 아래와 같이 구현을 살짝 변경하게 된다면 어떻게 될까?

// D 클래스에서 mf를 재정의해보자..
class D: public B{
	public:
		void mf();	// B::mf()를 가려버림..
		...
};

// 이 경우에 mf()를 다시 호출한다면?

int main() {

	// B 클래스의 mf();
	B *pB = &x;
	pB->mf();

	// D 클래스의 mf();
	D *pD = &x;
	pD->mf();
}

이게 무슨일인가?

동일한 객체를 가리키도록 했는데 호출되는 함수가 다르다.. 이 무슨 황당한 일인가🤔?

그 이유는 앞서 설명했던 “비가상함수” 같은 경우 정적바인딩 되기 때문이다. 구체적으로, pB는 B 에대한 포인터 타입으로 선언되었기 때문에 “pB에 입장에서 비가상함수는 B 클래스 내 존재하는 함수” 라고 인식할 것이다.

따라서, pB가 파생된 객체를 가리키고 있다고 해도 “비가상 함수는 내 클래스 영역에 있어” 라는 정적바인딩의 원리 때문에 B::mf()를 호출하게 된다.

가상함수는 동적바인딩?

위의 내용과 달리, 가상함수 virtualmf()를 구축할 경우 동적바인딩으로 묶이게 된다. 따라서, 프로그램 실행 시점에 어떤 객체와 연결되는지 확인하고, 해당 객체의 mf()를 부르게 해준다.

class B {
	public:
		virtual void mf();
		...
};

class D : public B {
	public:
		virtual void mf() override;
		...
};

int main() {
	D x;

	// D 클래스의 mf();
	B *pB = &x;
	pB->mf();

	// D 클래스의 mf();
	D *pD = &x;
	pD->mf();
}

위 코드와 같이 구성할 경우, 실제 가리키는 객체를 판단하는 동적바인딩 개념 덕분에 실제 객체 내 함수를 참조하도록 한다. 즉, D 객체를 포인터들이 가리키기 때문에 pBpDD::mf()를 호출한다.

어떻게 해야돼?

위의 이야기들을 통해 정적바인딩의 개념과 동적바인딩의 개념이 적용될 경우 내가 원하는 코드로 동작하지 않을 수 있다는 점을 알게되었다.

그럼 어떻게 해야되는거지 🤔???

우리는 앞서 public 상속은 “is-a (…는 …의 일종이다)” 라는 개념을 배웠으며, 비가상함수는 불변 동작을 위해 정해둔다라는 것을 배웠다.

이 2가지 개념을 적용해 정리해보자.

위 2가지 개념을 설계시 적용하면 된다.

현재 D 클래스는 비가상 멤버 함수를 재정의해서 사용하고 있는데, 2가지 명제를 다 어긋난다.

만약 꼭 mf를 재정의 해야된다면, 해당 명제에서 어긋나지 않도록 가상함수로 구현을 하든 public 상속을 받지 않도록 해야한다.

즉, 이번 항목의 제목 처럼 상속받은 비가상 함수를 파생 클래스에서 재정의 하는 것은 절대 금물!! 이라는 점을 꼭 기억하자.

Reference