728x90

황기태 저자의 명품 C++ Programming 개정판을 읽고 학습한 내용을 정리한 포스트입니다!

https://search.shopping.naver.com/book/catalog/32436115747

 

C++ Programming : 네이버 도서

네이버 도서 상세정보를 제공합니다.

search.shopping.naver.com

 

C++ 오버라이딩 특징

성공 조건과 실패
  • 가상 함수의 이름과 매개 변수 타입, 개수뿐 아니라 리턴 타입도 일치해야 오버라이딩이 성공
  • 아래는 리턴 타입이 달라 가상 함수 fail()의 오버라이딩이 실패한 사례
#include <iostream>
using namespace std;

class Base {
public:
	virtual void success() { cout << "Base::success() called" << endl; }
	virtual void fail() { cout << "Base::fail() called" << endl; }
	virtual void g(int x) { cout << "Base::g(int) called" << endl; }
};

class Derived : public Base {
public:
	virtual void success() { cout << "Derived::f() called" << endl; } // 오버라이딩 성공
	virtual int fail() { cout << "Base::fail() called" << endl; } // 오버라이딩 실패. 리턴 타입이 다름
	virtual void g(int x, double d) { cout << "Derived::g(int, double) called" << endl; } // 오버로딩 사례. 정상 컴파일
};
  • virtual int fail()의 경우 리턴 타입이 달라 Derived::fail : Base::fail과 재정의 가상 함수의 반환 형식이 다르거나 공변이 아닙니다.라는 에러가 발생한다.

 

오버라이딩 시 virtual 지시어 생략 가능
  • 가상 함수의 virtual  속성은 상속되는 성질이 있다.
  • 따라서, 파생 클래스에서 virtaul 키워드를 생략해도 자동으로 가상 함수가 된다.
class Base {
public:
	virtual void f(); // 가상 함수
};

class Derived : public Base {
public:
	// virtual void f(); 가상 함수. virtual 생략가능
	void f();
};

class GrandDerived : public Derived {
public:
	void f();
};

 

가상 함수의 접근 지정
  • 가상 함수도 보통 함수와 마찬가지로 private, protected, public 중 자유롭게 지정할 수 있다.

 

아래와 같이 상속이 반복되는 경우 가상 함수를 호출한 결과를 살펴보자.

#include <iostream>
using namespace std;

class Base {
public: virtual void f() { cout << "Base::f() called" << endl; }
};

class Derived : public Base {
public: void f() { cout << "Derived::f() called" << endl; }
};

class GrandDerived  : public Derived {
public: void f()  { cout << "GrandDerived::f() called" << endl; }
};

int main() {
	GrandDerived g;
	Base *bp;
	Derived *dp;
	GrandDerived *gp;

	bp = dp = gp = &g; // 모든 포인터가 객체 g를 가리킴

	bp->f(); // Base의 멤버 f() 호출 --> 결과는?
	dp->f(); // Derived의 멤버 f() 호출 --> 결과는?
	gp->f(); // GrandDerived의 멤버 f() 호출 --> 결과는?
}
  • 결과는 동적 바인딩에 의해 모두 GrandDerived의 함수 f()를 호출한다.

 

오버라이딩과 범위 지정 연산자(::)

  • 존재감을 상실한 기본 클래스의 가상 함수를 실행하기 위해서는 범위 지정 연산자(::)를 이용하여 호출하면 된다.
  • 아래 예제는 범위 지정 연산자를 이용하여 기본 클래스 Shape의 가상 함수를 정적 바인딩으로 호출하는 예시이다.
#include <iostream>
using namespace std;

class Shape {
public:
	virtual void draw() { 
		cout << "--Shape--"; 
	}
};

class Circle : public Shape {
public:
	int x;
	virtual void draw() { 
		Shape::draw(); // 기본 클래스의 draw() 호출. 정적 바인딩
		cout << "Circle" << endl; // 필요한 기능 추가
	}
};

int main() {
	Circle circle;
	Shape * pShape = &circle;

	pShape->draw();  // 동적 바인딩 발생. draw()는 virtual이므로
	pShape->Shape::draw(); // 정적 바인딩 발생. 범위지정연산자로 인해
}
  • 범위 지정 연산자를 이용하여 정적 바인딩을 발생시키는 2가지 예시를 보았다.
  • main 함수에서 호출 뿐만 아니라 클래스의 멤버 함수에서도 범위 지정 연산자를 활용하여 기본 클래스의 가상 함수를 호출할 수 있다.
  • 2번째 경우는 실제 응용에서도 많이 활용되는 부분이므로 제대로 이해하고 넘어가자!!
    • 상속은 기본 클래스에 새로운 기능을 추가하는 것 => 일반적으로 파생 클래스의 가상 함수(Circle::draw())를 작성하는 개발자는 기본 클래스의 가상 함수 (Shape::draw())를 그대로 활용하고 기능을 추가하여 개발한다.
    • 따라서, 기본 클래스의 가상 함수에 중요한 기능이 들어 있어 파생 클래스에서 재작성하는 것이 무리이므로, 기본 클래스의 가상 함수를 호출하여 이용한 후 추가적으로 코딩한다.

 

Tip!! 범위 지정 연산자

  • 컴퓨터 언어 이론에서 범위 규칙(scope rule)이란 동일한 이름(identifier)의 변수나 함수가 여러 곳에서 선언되었을 때, 가장 가까운 범위에 선언된 이름을 사용하는 규칙
  • 클래스나 블록 내에 선언된 이름과 동일한 이름이 전역 범위(global area)에 선언되면, 전역 범위에 선언된 이름은 클래스나 블록으로부터 숨겨지게(hidden) 된다.
  • 이 때, 다음과 같이 범위 지정 연산자(::)를 사용하면 숨겨진 전역 범위의 이름(함수나 변수 등)에 접근할 수 있다.
::전역범위이름

 

  • 전역 변수와 지역 변수의 이름이 같은 경우 ::을 이용하여 전역 변수에 접근하는 예시이다.
#include <iostream>
using namespace std;

int n=11; // 전역 변수

int main() {
	int n=3; // 지역 변수
	cout << ::n << endl; // 전역 변수 n(11)을 출력
	cout << n << endl;  // 지역 변수 n(3)을 출력
}

 

  • 클래스의 멤버 함수와 외부 함수의 이름이 같은 경우 외부 함수를 호출하는 예시이다.
#include <iostream>
using namespace std;

void sendMessage(const char* msg) { cout << msg << endl; }

class Window {
public:
	void sendMessage(const char* msg) { cout << "window msg : " << msg << endl; }
	void run() {
		::sendMessage("Global Hello"); // 전역 함수 호출
		sendMessage("Local Hello"); // 멤버 함수 호출
	}
};

int main() {
	Window window;
	window.run();
}

/*
Global Hello
window msg : Local Hello
*/

 

가상 소멸자

  • 기본 클래스의 소멸자를 만들 때 가상 함수로 작성할 것을 권하는데 그 이유는 파생 클래스의 객체가 기본 클래스에 대한 포인터로 delete 되는 상황에서도 정상적인 소멸이 되도록 하기 위함

 

소멸자를 가상 함수로 선언하지 않은 경우
  • p가 Base 타입이므로 컴팡리러는 ~Base() 소멸자를 호출하도록 컴파일
  • 따라서, ~Base()만 실행되고 ~Derived()가 실행되지 않는다.
Base *p = new Derived();
delete p;

 

소멸자를 가상 함수로 선언한 경우
  • 소멸자를 가상 함수로 선언하면, ~Base()에 대한 호출은 실행 중 동적 바인딩에 의해 ~Derived()에 대한 호출로 변하게 되어 ~Derived()가 실행된다.
  • 또한, 파생 클래스의 소면자는 자신이 실행된 후 기본 클래스의 소멸자를 호출하도록 컴파일되기 때문에, ~Derived() 코드 실행 후 ~Base() 코드가 실행되어 기본 클래스와 파생 클래스의 소멸자가 모두 순서대로 실행된다.
  • 만약, virtual 키워드를 삭제한다면, delete bp;의 경우 ~Base()만 실행될 것이다.
#include <iostream>
using namespace std;

class Base {
public:
	virtual ~Base() { cout << "~Base()" << endl; } // 동적바인딩
};

class Derived: public Base {
public:
	virtual ~Derived() { cout << "~Derived()" << endl; } // 파생 클래스의 소멸자는 자신의 코드 실행 후, 기본 클래스의 소멸자를 호출하도록 컴파일
};

int main() {
	Derived *dp  = new Derived();
	Base *bp = new Derived();

	delete dp; // Derived의 포인터로 소멸
	delete bp; // Base의 포인터로 소멸
}

/*
~Derived()
~Base()
~Derived()
~Base()
*/

 

  • 다시 말해, 소멸자를 가상 함수로 선언하면 객체를 기본 클래스의 포인터로 소멸하든, 파생 클래스의 포인터로 소멸하든 파생 클래스와 기본 클래스의 소멸자를 모두 실행하는 정상적인 소멸의 과정이 진행된다.
  • 또한, 생성자는 가상 함수가 될 수 없으며, 생성자에서 가상 함수를 호출해도 동적 바인딩이 일어나지 않는다. 

 

오버로딩, 함수 재정의, 오버라이딩 비교

오버로딩
  • 매개 변수의 타입이나 개수가 다른 함수들을 여러 개 중복 작성하는 것
  • 함수 중복은 상속 관계에서도 존재할 수 있는데, 기본 클래스의 멤버 함수와 이름은 같지만 매개 변수 타입이나 개수가 다른 함수를 파생 클래스에서 멤버 함수로 작성 가능 
함수 재정의와 오버라이딩
  • 기본 클래스의 가상 함수를 파생 클래스에서 재작성하여 동적 바인딩을 유발시키는 것이 오버라이딩
  • 가상 함수가 아닌 멤버 함수를 재작성하여 정적 바인딩으로 처리되는 것을 함수 재정의라고 한다.
비교 요소 오버로딩 함수 재정의(가상 함수 x) 오버라이딩
정의 매개 변수 타입이나 개수가 다르지만, 이름이 같은 함수들이 중복 작성되는 것 기본 클래스의 멤버 함수를 파생 클래스에서 이름, 매개 변수 타입과 개수, 리턴 타입까지 완벽히 같은 원형으로 재작성 하는 것 기본 클래스의 가상 함수를 파생 클래스에서 이름, 매개 변수 타입과 개수, 리턴 타입까지 완벽히 같은 원형으로 재작성하는 것
존재 1. 클래스의 멤버들 사이.
2. 외부 함수들 사이.
3. 기본 클래스와 파생 클래스 사이에 존재 가능
상속 관계 상속 관계
목적 이름이 같은 여러 개의 함수를 중복 작성하여 사용의 편의성 향상 기본 클래스의 멤버 함수와 별도로 파생 클래스에서 필요하여 재작성 기본 클래스에 구현된 가상 함수를 무시하고, 파생 클래스에서 새로운 기능으로 재작성하고자 함
바인딩 정적 바인딩. 컴파일 시에 함수들의 호출 구분 정적 바인딩. 컴파일 시에 함수의 호출 구분 동적 바인딩. 실행 시간에 오버라이딩된 함수를 찾아 실행
객체 지향 특성 컴파일 시간 다형성 컴파일 시간 다형성 실행 시간 다형성
728x90