728x90
황기태 저자의 명품 C++ Programming 개정판을 읽고 학습한 내용을 정리한 포스트입니다!
https://search.shopping.naver.com/book/catalog/32436115747
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
'Programming Language > C++' 카테고리의 다른 글
[C++] 추상 클래스 (1) | 2023.12.19 |
---|---|
[C++] 가상 함수와 오버라이딩의 활용 사례 (1) | 2023.12.18 |
[C++] 가상 함수와 오버라이딩 (1) | 2023.12.18 |
[C++] 다중 상속과 가상 상속 (0) | 2023.12.15 |
[C++] public, protected, private 상속 (0) | 2023.12.14 |