본문 바로가기

[C++] 함수 오버라이딩의 특수 케이스

Kwonriver 2023. 1. 9.
728x90

 

함수 오버라이딩을 하다보면 의도하지 않은 동작을 할 때가 있다. 부모 클래스와 자식 클래스가 static 함수를 같은 이름으로 둘 다 들고 있다거나, 오버로딩 된 함수의 일부만 오버라이딩 한다거나 하는 등의 일들이 발생한다. 이러한 예외적인 상황에 대해 알아본다.

 

  1. 동일한 이름의 static 함수 처리
  2. 부모 클래스의 오버로딩한 함수 중 일부만 오버라이딩 하는 경우
  3. Public이 아닌 함수의 오버라이딩
  4. 부모 클래스 함수에 디폴트 매개변수가 있는 경우
  5. 오버라이딩 함수의 접근 지시자가 다른 경우

 


[동일한 이름의 static 함수 처리]

기본적으로 C++에서는 static 함수를 오버라이드 할 수 없다.  애초에 virtual 과 static 을 동시에 사용할 수가 없기 때문에 컴파일 자체가 불가능하다. 그러나 같은 이름을 가진 static 함수를 만드는 것은 가능한데 맨 위 사진이 바로 그것이다. Wash라는 이름의 static 함수를 부모클래스와 자식 클래스 모두 정의하였는데 컴파일 에러가 발생하지 않는다. 

 

    Car::Wash();
    ConvertibleCar::Wash();

위 코드의 결과값은 누구나 예상할 수 있을 것이다. 클래스 각각의 Wash함수가 호출된다. 이는 static 함수가 클래스에 속하기 때문에 상속과 관계없이 클래스를 따라간다. 그렇다면 pCar->Wash();는 어떤 함수가 호출될까. new 키워드를 사용해서 ConvertibleCar로 인스턴스를 생성하였지만 저장은 Car*에 하였다. 이런 경우엔 어떤 클래스를 호출할까.

 

 

결과는 포인터를 저장한 자료형인 Car의 Wash가 호출된다. 더 복잡한 상황도 만들 수 있는데 ConvertibleCar를 Car를 사용해 참조하게 되는 경우이다.

 

int main()
{
    ConvertibleCar myOpenCar;
    Car &ref = myOpenCar;
    
    myOpenCar.Wash();
    ref.Wash();
    
    return 0;
}

 

이 경우에도 ref.Wash()는 저장 타입인 Car의 static 함수를 호출한다. 이러한 이유는 C++에서 static 함수를 호출할 때 현재 속한 객체가 아닌 컴파일 시간에 지정된 타입으로 호출할 함수를 결정하기 때문이다. 즉, 위 코드가 컴파일 될 때 ref는 Car& 이기 때문에 저장하고 있는 객체와 관계없이 Car의 static 함수를 호출한다.

 


[부모 클래스의 오버로딩한 함수 중 일부만 오버라이딩 하는 경우]

부모 클래스에서 특정 함수를 오버로딩하여 많이 만들었을 수 있다. 또한 자식 클래스가 해당 함수의 일부만 오버라이딩하는 경우가 발생할 수 있다. 아래 코드를 보자.

class Car
{
public:
    virtual void Accel() {cout<<"car accel"<<endl;};
    virtual void Accel(float value){cout<<"car accel float"<<endl;};
};

class SUV : public Car
{
public:
    virtual void Accel() override {cout<<"suv accel"<<endl;};
};

 

SUV는 Car클래스의 Accel 함수 중 일부만 오버라이딩하였다. 이렇게 사용한다고 에러가 발생하지는 않는다. 다만 위처럼 코드를 작성할 시 SUV에서 오버라이딩 한 함수 이외의 다른 오버로딩 함수는 모두 가려진다.(사용 불가) 이는 컴파일러가 판단하기를 실수로 특정 함수만 오버라이딩 했다고 판단하기 때문이다. 즉, 개발자가 실수했다고 판단하여 나머지 함수를 모두 사용 불가능하게 가려버린다.  다만 이런 상황에서는 아래의 문제가 발생할 수 있다.

 

 

일반적인 예상대로라면 부모 클래스가 가지고 있는 Accel(float value) 함수가 호출될 것이지만 컴파일러가 다른 오버로딩 함수를 모두 가려버렸기 때문에 해당 함수를 찾을 수 없어 에러가 발생한다. 

 

위 같은 상황을 해결하는 방법이 몇 가지 있다.

  • 부모 클래스로 업캐스팅하여 호출
  • using 키워드를 사용하여 부모의 함수를 강제로 사용 가능하도록 세팅

업캐스팅하여 호출(좌), using 키워드 사용(우)

 

위 두가지 방법 모두 부모의 기능을 정상적으로 사용할 수 있다. 다만 업캐스팅 방법의 경우 매번 타입을 변경하고 호출해야하기 때문에 실질적으로 작업을 하다보면 매우 귀찮음이 따른다. using을 사용할 때는 오버라이딩 하지 않은 부모클래스의 함수를 모두 가져온다. 다만 사용자가 오버라이딩 해야하는 함수를 실수로 오버라이딩 하지 않았을 때 부모 클래스의 함수가 자동적으로 호출되기 때문에 컴파일 에러가 발생하지 않아 오류 해결에 시간이 걸릴 수 있다. 장단점이 있으니 상황에 맞게 잘 사용하여야 한다.

 


[ Public이 아닌 함수의 오버라이딩 ]

자바나 C#과 다르게 C++은 protected 뿐만 아니라 private 로 지정된 함수도 오버라이딩 할 수 있다. 이는 접근지정자의 역할이 호출 대상만을 제한하며 구현에 관해서는 제한하지 않기 때문이다. 이러한 기능을 이용하여 부모 클래스에서 protected로 만들어진 함수를 자식 클래스가 public으로 오버라이딩 하여 외부에서 호출하는 경우도 존재한다. 

 

class Car
{
protected:
    virtual float GetFuelEfficiency() {return 10.0f;};
public:
    virtual float GetDriveabledDistance(int nOilQuantity) {return nOilQuantity * GetFuelEfficiency();};
};

class HybridCar : public Car
{
protected:
    virtual float GetFuelEfficiency() override {return 15.0f;};
public:
};

 

위 같은 클래스가 있을 때 HybridCar는 Car보다 연비가 좋아 더 많은 거리를 갈 수 있다.  이 때 GetFuelEfficiency함수는 현재 차량 클래스에 따라 값이 바뀌게 된다. 그러나 실제 사용 측면에서는 변경된 점이 없다. 여전히 GetFuelEfficiency 함수를 외부에서 호출할 수는 없지만 Car와 HybridCar의 주행 가능 거리는 다르게 나온다. 따라서 기존 클래스의 골격을 유지한 채 특정 기능만 변경이 필요할 때는 private 또는 protected 함수의 상속이 유용할 수 있다.

 

 


[ 부모 클래스 함수에 디폴트 매개변수가 있는 경우 ]

부모 클래스의 디폴트 매개변수가 있는 함수를 오버라이딩했는데 자식 클래스는 디폴트 매개변수의 값을 다른 값을 넣어야 할 때가 있을 수 있다. 아래의 코드를 보자.

 

class Car
{
public:
    virtual void Accel(float value = 5) { cout << "Car::value is " << value << endl;};
};

class SuperCar : public Car
{
public:
    virtual void Accel(float value = 15) { cout << "SuperCar::value is " << value << endl;};
};

 SuperCar는 부모 클래스인 Car보다 기름을 더 많이 먹는다. 이러한 설정을 Accel 함수의 디폴트 매개변수 값으로 표현하였다. 일반적으로 생성한 인스턴스와 저장한 포인터의 타입이 같은 경우에는 문제가 발생하지 않는다. 각각에 맞는 디폴트 값이 세팅된다. 문제는 레퍼런스로 처리할 때 발생하는데 함수 자체는 SuperCar의 함수가 호출되지만 디폴트 값은 부모 클래스의 값이 들어간다.

 

 

오른쪽 사진에서 함수의 디폴트 매개변수 값이 서로 다른 것을 볼 수 있다.

 

이러한 원인은 C++ 컴파일러가 디폴트 매개변수를 컴파일 시간에 결정하기 때문이다. 즉, 컴파일 할 때 pCar->Accel()의 디폴트 매개변수는 5로 고정되지만 실행한 뒤 런타임에서 함수를 호출할 때는 SuperCar::Accel()이 호출되기 때문에 이러한 현상이 발생한다. 즉, 디폴트 매개변수의 값이 상속되지 않고 새로운 함수(SuperCar::Accel(float value = 5)) 함수가 새롭게 오버로딩 된다.

 


[ 오버라이딩 함수의 접근 지시자가 다른 경우 ] 

부모 클래스에서 public으로 지정된 함수를 자식 클래스에서 protected 또는 private로 수정할 수 있다.

class Car
{
public:
    virtual int CarNumber(){ return 1;};
};

class SubCar : public Car
{
protected:
    virtual int CarNumber() override { return 100;};
public:
};

 

위 코드는 문제 없이 컴파일 되지만 SubCar에서 CarNumber를 호출하는 경우 에러가 발생한다. 그런데 업캐스팅하여 CarNumber 함수를 호출하면 에러가 발생하지 않는다. 즉, 오버라이딩 함수의 접근 지정자를 변경하는 것은 자유로운 편이다. 물론 부모 클래스에서 public으로 선언된 함수를 자식클래스에서 좁은 범위로 제한하는 일은 자주 일어나지 않는다.

 

 

그렇지만 반대의 경우는 일어날 수 있는 가능성은 높다. 부모 클래스에서 protected로 설정한 함수를 자식 클래스에서 public으로 접근 지시자를 변경하는 것인데 아래와 같다.

 

class Car
{
protected:
    virtual int CarNumber(){ return 1;};
};

class SubCar : public Car
{
public:
    virtual int CarNumber() override { return 100;};
};

위 코드에서 접근지시자만 변경한 코드이다.

이를 앞선 코드와 동일하게 실행하면 에러가 발생한다. 대신 SubCar에서 호출한 CarNumber는 에러가 발생하지 않고 정상적으로 호출된다. 이외에도 기능의 변화는 없지만 접근지시자만 변경하고 싶다면 using 키워드를 사용하면 된다.

 


 

728x90