함수재정의란? 자식 클래스에서 부모클래스와 동인한 이름의 함수를 사용할 수 있도록 하는 것이다.
기본적으로 자식클래스의 객체를 호출하지만, 업캐스팅을 하면, 부모의 객체를 호출한다.
이는 컴파일 시에 정적으로 결정된다.
calss A{
public:
void f(){cout << "부모A함수를 출력"<<endl;}
};
class B:public A{
public:
void f() {cout << "자식B함수를 출력"<<endl;}
};
void main(){
B case1, *pcase1;
pcase1 = &case1;
case1-> f(); // 자식의 f()함수를 출력한다.
A case2,;
pcase2 = &case1; //업캐스팅
pcase2->f(); // 부모의 f()함수를 출력한다.
}
오버라이딩이란? 부모클래스에 작성된 virtual 함수를 자식클래스에서 함수재작성하여, 기본클래스의 virtual함수를 무력화 시키는 것을 말한다.
calss A{
public:
virtual void f(){cout << "부모 A가상함수를 출력"<<endl;}
};
class B:public A{
public:
virtual void f() {cout << "자식 B가상함수를 출력"<<endl;}
};
void main(){
B case1, *pcase1;
pcase1 = &case1;
case1-> f(); // 자식의 f()함수를 출력한다.
A *pcase2;
pcase2 = pacse1; //업캐스팅
pcase2->f(); // 자식의 f()함수를 출력한다.,동적 바인딩 자동 발생
}
업캐스팅 명령을 했음에도 불구하고, 포인터가 자식을 가리킨다. 그 이유가 뭘까?
이는 virtual이라는 명령이 부모클래스의 맴버를 자식 메모리스택에 넣을 때, 부모맴버 공간은 1번만 할당하도록 지시하는 명령어이기 때문이다. 따라서 이미 f()함수를 자식에서 정의하였기 때문에 업캐스팅한 부모인 f()를 한번 찾아가지만, 자식의 f()를 동적(runtime)으로 실행하는 것이다. 이를 오버라이딩이라고 부른다.
class 도형{
protected:
virtual void drwa(){};
}
class 사각형: public 도형{
virtual void drwa(){};
}
class 원형: public 도형{
virtual void drwa(){};
}
class 삼각형: public 도형{
virtual void drwa(){};
}
void paint(도형 *p){
p->draw();
}
int main(){
paint(new 원형()); // 동적 호출
paint(new 사각형());
paint(new 삼각형());
}
위처럼 virtual로 실행시, 인자를 도형 타입으로 받는 업캐스팅에도 불구하고 자식들의 draw가 실행된다.
virtual함수에 대하여 컴파일러는 이를 runtime에 동적으로 처리하도록 미룬다. 이를 동적 바인딩이라고 한다. 동적바인딩은 구체적으로, 자식클래스의 객체에 대하여 부모클래스의 포인터로 가상함수가 호출 될 때 일어난다.
calss A{
public:
void paint(){draw()};
virtual void draw{
cout << "부모 draw를 출력" << endl;
};
class B:public A{
public:
virtual void draw(){
cout << "자식 draw를 출력" << endl;
};
void main(){
A *pA = new B(); //업캐스팅
pA->paint(); // 부모 paint를 갔다가 부모draw()를 무효화후,
// 자식의 draw()를 runtime에 출력(동적으로 접근)
delete pA;
}
동적 바인딩은 runtime에 실행되기 때문에 draw()를 drow()로 오타를 내는 등의 코드를 작성하면, 컴파일시에 이를 알아챌 수 없다.
이를 컴파일 단계에서 체크하기 위한 코드가 override와 final코드이다.
override 사용
class shape{
public:
virtual void draw();
};
class Rect : public Shape{
public:
void drow() override; //오타 방지- 컴파일 에러 발생
};
참고로 여기서 자식에게 virtual을 사용하지 않았는데, 이는 부모가 virtual이면 자식이 자동으로 virtual처리되기 때문이다. 이는 N번째 함수 재정의에도 똑같이 작동한다.
final 사용
class shape{
public:
virtual void draw() final;
};
class Rect : public Shape{
public:
void draw() override;
};
final은 함수재정의를 중단하라는 의미이다. 부모클래스의 draw를 출력한뒤 override하기 위해 자식을 찾아가지 않는다.
final 사용2
class shape final{
public:
virtual void draw();
};
class Rect : public Shape{ // 컴파일 오류
public:
void draw() ;
};
또한, 위처럼 상속자체를 막을 수 있는 방법도 있으나 자주 쓰진않는다.
오버라이딩을 final등을 통해 막아 부모를 호출할 수도 있지만, 범위 지정연산자로도 부모를 호출할 수 있다.
class Shape{
public:
virtual void draw(){
cout << "부모 draw출력 << endl;
};
};
class Circle : public Shape{
public:
int x;
virtual void draw(){
Shape::draw();
cout << "부모draw를 자식에서 호출" << endl;
};
};
int main(){
Circle circle;
Shape *pS = &circle; //업캐스팅
pS -> drwa(); //동적바인딩으로 자식draw 후에 정적 바인딩으로 부모draw호출,
pS -> Shape::draw(); // 정적바인딩으로 부모draw호출
동일한 이름(identifier)의 변수나 함수가 여러 곳에 선언되어 있을 때, 가장 가까운 범위에 선언된 이름을 사용하는 규칙이다. 클래스나 블록 내에 선언된 이름과 동일한 이름이 전역 범위(global area)에 선언되면, 전역 범위에 선언된 이름은 클래스나 블록으로부터 숨겨지게(hidden) 된다. 이 때 다음과 같이 범위 지정 연산자(::)를 사용하면 전역 범위의 이름에 접근할 수 있다.
void sendMessage(const char* msg) { cout << msg << endl; } //전역함수
class Window
{
public:
void sendMessage(const char* msg) { cout << "windowmsg:" << msg << endl; }
// const:변경방지Lock 역할
void run()
{
::sendMessage("Global Hello"); // 전역 함수 호출
sendMessage("Local Hello"); // 멤버 함수 호출
}
}
int main()
{
Window window;
window.run();
}
부모클래스의 소멸자는 대체로 virtual로 만들것을 추천한다. 이유는 자식클래스의 객체가 부모클래스의 포인터로 delete되는 상황에서도 정상적인 소멸을 하기 위해서이다.
A *p = new B(); //업캐스팅
delete p; // p가 부모클래스이기 때문에, A객체를 삭제하려고 시도한다.
따라서 정작 동적할당된 B객체의 소멸이 실행되지 않고 메모리공간에 남는다.
class A{
public:
virtual ~A();
};
class B : public A{
public:
virtual ~B();
};
int main(){
A *p = new B();
delete *p;
먼저 A를 삭제시도한다. 이 때 자식이 있는 것을 확인후 동적 바인딩을 통해 B를 접근해 삭제한 뒤 A를 삭제한다.
동일한 함수이름을 매개변수, 타입 등으로 여러개 만들 수 있는기능
오버라이딩과 착각하지 말자.
virtual 함수는 자식 클래스에서 오버라이딩할 함수를 알려주는 인터페이스의 역할을 한다.

부모클래스에서 virtual을 명시함으로써 draw()가 오버라이딩할 함수임을 알려준다.
// 부모클래스
class Shape {
Shape* next;
protected:
virtual void draw();
public:
Shape () {next = NULL;}
virtual ~Shape(){}
void paint();
Shape* add(Shape *p);
Shape *getNext() {return next;} // next에 저장된 자기 위치를 반환
};
void Shape::paint(){
draw();
}
void Shape::draw() {
cout << "--Shape--" << endl;
}
Shape* Shape::add(Shape *p){
this -> next = p; // 현재객체의 next 변수를 새로 입력받은 객체의 주소로 입력한다.
return p; // 추가된 객체 주소인 p를 반환한다.
}
//자식 클래스1. 원
class Circle : public Shape {
protected:
virtual void draw();
};
void Circle::draw() {
cout << "Circle" << endl;
}
//자식 클래스2. 사각형
class Rect : public Shape {
protected:
virtual void draw();
};
void Rect::draw() {
cout << "Rectangle" << endl;
}
//자식 클래스3. 선
class Line : public Shape {
protected:
virtual void draw();
};
void Line::draw() {
cout << "Line" << endl;
}
int main (){
Shape *pStart = null;
Shape *pLast;
pStart = new Circle(); // pstart위치에 Circle 객체 생성
pLast = pStart;
pLast = pLast->add(new Rect()) // Shape타입 plast에게 사각 객체생성후, 위치반환
// Plast에는, '
pLast = pLast->add(new Circle()); // 반한된 위치에 원 객체 생성후, 위치반환
pLast = pLast->add(new Line()); // 같은 내용;;
pLast = pLast->add(new Rect()); // 같은 내용;;
Shape *p = pStart; //처음 생성한 원(시작위치)를 포인터 p에 넣는다.(얕은 복사)
while(p != null) { //포인터가 null이 아닐 때 가동
p-> paint(); // 자식 draw를 오버라이딩 하여 출력
p = p-> getNext(); //
}
p = pStart;
while(p != Null) {
Shape *q = p-> getNext(); // 다음 도형의 next값을 기억한다.
delete p; //현재 도형을 삭제한다.
p=q; // 다음 도형의 주소를 포인터 p에 저장한다.
}
};
class shape{
public:
virtual void draw() = 0; // 순수가상함수 선언
오버라이딩 사용을 위해 아무런 코드가 없는 선언만 있는 함수
순수가상함수가 1개이상 있는 클래스를 추상클래스라고 한다.
추상클래스는 실행코드가 없기 때문에 추상클래스 객체를 만들 수 없다. 단, 포인터는 만들 수 있다. 순수가상함수는 가상함수 뒤에 =0; 을 붙이는 것으로 선언가능하다.
class Shape{
public:
void paint(){ draw();}
virtual void draw() = 0; // 순수가상함수 선언
}
int main(){
Shape shape; // 컴파일 오류
Shape *p; // 포인터 선언은 가능
}
추상클래스의 사용 목적은 목차과 같다.어떤 기능을 가지도록할지 추상클래스에서 정하는 것이다.
설계와 구현을 분리하고 계층적 상속관계를 가질 수 있다.
그러나 추상 클래스의 최대 장점은 부모클래스 구현부에서 오버라이딩 할 함수의 구현부를 적지 않아도 된다는 점이다.