
override
override와 final은 상속과 관련된 몇가지 문제를 해결하기 위해 C++에서 도입된 지정자이다 (키워드 아님)
자식 클래스에서 부모 클래스의 가상 함수를 override 하려면 부모 클래스의 함수 시그니처와 반환 타입이 정확히 일치해야 한다 (const 유무도 동일해야 함)
class Base
{
public:
virtual void Foo1(int a) { std::cout << "Foo1(int)" << std::endl; }
};
class Derived : public Base
{
public:
virtual void Foo1(short a) { std::cout << "Foo1(short)" << std::endl; }
};
int main()
{
Derived d1{};
Base& rBase{ d1 };
rBase.Foo1(10); //Foo1(int)가 출력됨
return 0;
}
rBase는 Base타입 참조 변수이고 d1으로 초기화 했다, 이때 rBase.Foo1()을 호출 했을 때 정상적으로 Derived클래스에 Foo1()이 override되었다면 실제 참조하는 객체 클래스의 함수인 foo1(short)가 호출되었을 것이다, 하지만 Foo1은 Base클래스에서는 int 인자, Derived클래스에서는 short 인자를 가지는 서로 다른 함수이기 때문에 그냥 Base::Foo1(int)이 호출되게 된다
rBase 변수의 타입인 Base::Foo1(int)을 탐색하고 그 뒤에 Derived::Foo1(int)로 override되었는지 확인한다, 이때 override되지 않았기 때문에 Base::Foo1(int)가 호출된 것이다
이렇게 정상적인 override가 되지 않는다면 예상치 못한 결과가 나올 수 있다
이러한 문제를 해결하기 위해 override 지정자를 사용한다
override지정자는 해당 함수는 부모 클래스의 가상 함수를 override 하는것이 확실하다고 컴파일러에게 명시하는 지정자이다
override지정자는 멤버 함수의 끝에 붙인다, 만약 const함수라면 const를 먼저 쓰고 override를 쓴다
만약 override처리된 함수가 실제로 봤더니 아무런 함수도 override하지 않는다면 에러로 판단한다
class Base
{
public:
virtual void Foo1(int a) { std::cout << "Foo1(int)" << std::endl; }
};
class Derived : public Base
{
public:
virtual void Foo1(short a) override { std::cout << "Foo1(short)" << std::endl; }
};
//error
class Base
{
public:
virtual void Foo1(int a) { std::cout << "Foo1(int)" << std::endl; }
};
class Derived : public Base
{
public:
virtual void Foo1(int a) override { std::cout << "Foo1(short)" << std::endl; }
};
//ok
override 지정자를 사용하여 실제로 해당 함수가 부모 클래스의 가상함수를 override한 함수인지 컴파일 타임에 체크가 가능하다 (런타임에 디버깅 하는것보다 훨씬 편함)
override 지정자는 성능 저하가 전혀 없다, 따라서 모든 가상 함수 override에는 override 지정자를 붙히는걸 강력히 권장한다
final
그렇다면 final은 어떤 지정자일까?
final지정자는 가상 함수를 더 이상 다른 클래스에서 override하지 못하게 막거나 특정 클래스를 아예 상속시키지 않을때 사용한다
final로 지정된 함수를 override하거나 final로 지정된 클래스를 상속받으려 한다면 컴파일 타임에 에러가 발생된다
class Base
{
public:
virtual void Foo1(int a) { std::cout << "Foo1(int)" << std::endl; }
};
class Derived : public Base
{
public:
virtual void Foo1(int a) override final { std::cout << "Foo1(short)" << std::endl; }
};
class Derived2 : public Derived
{
public:
virtual void Foo1(int a) override { std::cout << "Foo1(short)" << std::endl; }
}
//X, Foo1(int)은 final로 지정되었기 때문에 더 이상 override할 수 없다
final은 보통 override 뒤에 작성한다
class 자체를 final로 지정하고 싶다면 class 이름 뒤에 final을 붙혀준다
class Base
{
public:
virtual void Foo1(int a) { std::cout << "Foo1(int)" << std::endl; }
};
class Derived final : public Base
{
public:
virtual void Foo1(int a) override { std::cout << "Foo1(short)" << std::endl; }
};
//error, Derived는 final클래스이기 때문에 상속 받을 수 없다
class Derived2 : public Derived
{
public:
virtual void Foo1(int a) override { std::cout << "Foo1(short)" << std::endl; }
}
파생 클래스가 부모 클래스의 가상 함수를 override 하려면 반환형뿐 아니라 모든 함수 시그니처가 동일해야 한다고 정리했다, 하지만 예외가 존재하는데 이를 Covariant return type이라고 한다 (공변 반환 타입)
가상 함수의 반환형이 특정 클래스에 대한 포인터나 참조일 경우 그 함수를 오버라이드 하는 함수는 해당 클래스의 파생 클래스에 대한 포인터나 참조를 반환할 수 있다
class Base
{
public:
virtual Base* GetBase(int a) { std::cout << "GetBase(int)" << std::endl; return this; }
};
class Derived : public Base
{
public:
virtual Derived* GetBase(int a) override { std::cout << "GetBase(short)" << std::endl; return this; }
};
int main()
{
Derived d1{};
Base& rBase{ d1 };
rBase.GetBase(10);
return 0;
}
원래였으면 타입이 달라 override가 불가능 했지만 특정 클래스의 포인터, 참조를 반환하는 함수를 override할 때 해당 클래스 타입의 파생 클래스에 대한 포인터나 참조로 return은 허용되기 때문에 위 코드는 정상적으로 동작하여 GetBase(short)가 출력된다
class Base
{
public:
virtual Base* GetBase(int a) { std::cout << "GetBase(int)" << std::endl; return this; }
void printType() { std::cout << "returned a Base\n"; }
};
class Derived : public Base
{
public:
virtual Derived* GetBase(int a) override { std::cout << "GetBase(short)" << std::endl; return this; }
void printType() { std::cout << "returned a Derived\n"; }
};
int main()
{
Derived d1{};
Base& rBase{ d1 };
d1.GetBase(10)->printType();
rBase.GetBase(10)->printType();
return 0;
}
이 경우에서 d1.GetBase(10)->printType()은 Derived::printType()이 호출되지만 rBase.GetBase(10)->printType()은 Base::printType()이 호출된다
왜냐하면 d1.GetBase()는 Derived이기 때문에 Derived::printType()이 호출된다 하지만 rBase.GetBase()도 Derived::GetBase()가 되어 Derived로 return되지만 rBase.GetBase()의 static type은 Base이기 때문에 Base*로 업캐스팅 된다, 이때 printType()은 가상함수가 아니기 때문에 Base::printType()이 호출되는 것이다
만약 printType()이 가상함수 였다면 Derived::printType()이 호출되었을 것이다
가상 소멸자
class Base {
public:
~Base()
{
std::cout << "Calling ~Base()\n";
}
};
class Derived : public Base {
private:
int* m_array{};
public:
Derived(int length)
: m_array{ new int[length] }
{
}
~Derived()
{
std::cout << "Calling ~Derived()\n";
delete[] m_array;
}
};
int main()
{
Derived* derived{ new Derived(10)};
Base* base{ derived };
delete base;
return 0;
}
위 코드는 ~Base()가 호출되게 된다
Derived 객체를 동적으로 생성하고 해당 주소를 Base*에 저장했다
이때 delete base가 되었을 때 소멸자가 가상 함수가 아니기 때문에 Base::~Base()를 호출하게 되는것이다
그렇게 되면 Derived(int)에서 동적할당한 m_array는 정상적으로 delete되지 않아 메모리 누수가 발생할 수 있다
class Base
{
public:
virtual ~Base()
{
std::cout << "Calling ~Base()\n";
}
};
class Derived : public Base
{
private:
int* m_array{};
public:
Derived(int length)
: m_array{ new int[length] }
{
}
virtual ~Derived() override
{
std::cout << "Calling ~Derived()\n";
delete[] m_array;
}
};
이렇게 소멸자를 virtual로 만들게 되면 정상적으로 Derived::~Derived()가 호출되고 자식 클래스 객체가 소멸되었으니 부모 클래스 객체도 소멸되어 그 뒤에 Base::~Base()도 호출된다
따라서 Derived(int)에서 동적할당된 m_array도 잘 delete된다
만약 부모 클래스 소멸자에 내용이 없다면 default로 지정하면 된다
~Base() = default; //virtual 기본 소멸자 생성 명시
가상 할당
operator=도 virtual로 사용이 가능하지만 권장하지 않는다
가상화 무시
함수의 가상화를 무시하고 싶을때는 범위 지정 연산자::를 사용해서 함수를 호출한다
class Base
{
public:
virtual ~Base() = default;
virtual void Foo1() { std::cout << "BaseFoo1()" << std::endl; }
};
class Derived : public Base
{
public:
virtual void Foo1() override { std::cout << "DerivedFoo1()" << std::endl; }
};
int main()
{
Derived d1{};
Base& refBase{ d1 };
refBase.Base::Foo1(); //Base::Foo1()을 명시적으로 호출
//refBase.Foo1()은 Derived::Foo1()호출
return 0;
}
이렇게 범위 지정 연산자::를 사용하여 호출하면 가상 함수 테이블을 보지 않고 직접 명시한 클래스의 함수를 호출하라는 의미가 된다
그렇다면 모든 소멸자를 전부 virtual로 만드는게 좋을까?
virtual을 사용하게 되면 가상 함수 테이블을 가리키는 v-ptr을 모든 객체들이 가지게 되어 오버헤드가 발생할 수 있다, 따라서 필요할 때만 virtual화 시키는걸 권장한다
binding이란?
C++ 프로그램이 실행되면 main()함수의 맨 위부터 차례대로 실행되게 된다, 이때 함수 호출을 만나게 되면 실행 지점은 호출된 함수의 시작 부분으로 이동하게 된다
컴파일러는 C++ 각 구문을 컴파일 과정에서 기계어로 변환한다, 이때 이러한 기계어에는 고유한 주소가 부여되게 된다, 함수도 마찬가지로 기계어로 변환되고 주소를 할당받는다
따라서 함수는 주소를 갖게된다
프로그래밍에서 binding이란 이름과 속성을 연관시키는 과정이라고 할 수 있다, 함수 바인딩은 어떤 함수를 호출할 때 해당 함수가 어떤 정의와 연관될 지 결정하는 과정이다
그리고 바인딩 된 함수를 실제로 호출하는 과정을 dispatching이라고 한다
Early binding
대부분의 컴파일러가 하는 함수 호출은 직접 함수 호출이다, 말 그대로 함수를 직접 호출하는 방식이다
struct Foo
{
void print(int val)
{
std::cout << val;
}
};
void print(int val)
{
std::cout << val;
}
int main()
{
print(10);
Foo f1;
f1.print(200);
return 0;
}
이러한 함수 호출들이 직접 함수 호출이다
컴파일러는 직접 함수 호출 시 call한 함수가 어떤 정의와 연관되어야 하는지 컴파일 타임에 결정할 수 있다
이를 early binding, static binding(정적 바인딩)이라고 한다
overload된 함수나 함수 템플릿도 컴파일 타임에 call한 함수가 어떤 정의와 연관되어야 하는지 결정될 수 있다
template <typename T>
void printValue(T value) {
std::cout << value << '\n';
}
void printValue(double value) {
std::cout << value << '\n';
}
void printValue(int value) {
std::cout << value << '\n';
}
int main() {
printValue(5); // printValue(int)에 대한 직접 함수 호출
printValue<>(5); // printValue<int>(int)에 대한 직접 함수 호출
return 0;
}
Late binding
위의 케이스와 다르게 call한 함수가 어떤 정의와 연괸되어야 하는지 결정이 런타임에 결정되는 경우가 있다
이를 Late binding, dynamic binding(동적 바인딩)이라고 한다
보통 static type만으로 호출할 함수가 결정되지 않고 dynamic type으로 호출할 함수를 결정하는걸 의미한다
이를 간단하게 말하면 함수 호출이 이루어지는 시점에 컴파일러나 링커가 실제로 어떤 함수가 호출될 지 모르는 경우를 의미한다
C++에서 Late binding은 함수 포인터를 사용하거나 가상함수 호출로 확인할 수 있다
struct Foo
{
void print(int val)
{
std::cout << val;
}
};
void print(int val)
{
std::cout << val;
}
int main()
{
void (*funcptr)(int) { print };
(*funcptr)(10);
return 0;
}
이렇게 함수 포인터를 통한 함수 호출은 간접 함수 호출이라고 한다
(*funcptr)(10)이 호출되는 시점에 컴파일러는 해당 함수 포인터로 인해 어떤 함수가 호출될 지 알 수 없다, 런타임에 함수 포인터가 가리키는 주소로 인해 간접 함수 호출이 발생한다
Late binding은 Early binding보다 한 단계의 간접 참조가 추가되기 때문에 약간은 더 비효율적이다, Early binding은 간접 참조가 없어 바로 함수의 주소로 jump가 가능하지만 Late binding은 간접 참조가 추가되어 포인터에 저장된 주소를 읽은 후 해당 함수 주소로 jump를 해야하기 때문이다 (물론 더 유연함)