📢 공부내용에 앞서서
본 게시글은 면접준비 및 자기계발을 목적으로 작성된 게시글입니다
공부한 내용을 토대로 남들에게 설명할 수 있도록 이해하는 과정에 작성한 게시글이니 참고바랍니다
📢 포스트에 앞서서
본 게시글은 러프하게 작성된 게시글로
당장 몇일뒤의 코딩 테스트를 위해 작성된 게시글입니다
추후에 내용을 보충하여 재업로드 할 예정입니다
📖 추상 클래스(abstract)
- 추상 클래스는 보다 구체적인 클래스가 파생될 수 있는 일반 개념식 역할을 합니다
- C++에선 순수 가상함수가 하나라도 포함된 클래스는 추상 클래스로 변환됩니다
- 추상 클래스는 인스턴스화 할 수 없습니다
- 순수 가상함수는 내용이 없이 형식만 정의된 함수입니다
- '이 형식대로 만들어서 사용하라'는 의미로, 자식 클래스는 이를 반드시 오버라이드 해야 합니다
📌 virtual 키워드
-
virtual 키워드는 메서드, 속성, 인덱서 또는 이벤트 선언을 수정하고 파생 클래스에서 재정의하도록 허용하는 데 사용됩니다
-
예를 들어 이 메서드는 이를 상속하는 모든 클래스에서 재정의할 수 있습니다
-
가상 멤버의 구현은 파생 클래스의 재정의 멤버로 변경할 수 있습니다
-
상속 관계에서 자식이 해당 함수를 오버라이딩 하기 위해 사용한다, 즉 부모 함수에 virtual이 없으면 자식이 override 할 수 없다
-
또한 virtual 키워드를 사용했을 시 런타임에 가상함수 테이블이 생성되어 올바른 함수를 찾아갈 수 있게 만든다
-
상속 관계에서 부모의 소멸자엔 반드시 virtual을 붙여야 하는데, 이것은 소멸 시에 virtual이 없으면 올바른 자식 객체까지 찾아가지 못 하여 부모의 부분만 소멸되고 자식의 부분은 남는 현상이 발생하기 때문이다 (이는 곧 메모리 누수, 오염으로 이어진다)
-
객체 생성 및 소멸 과정에서는 절대 생성자/소멸자에서 가상 함수를 호출하면 안되는데, 자식 객체 생성 시에 가상함수를 호출하게 되면 부모가 먼저 생성될 때, 자식 객체는 아직 초기화 되지 않은 상태이므로 자식 객체는 자신이 부모 클래스인 것처럼 동작한다, 즉 부모 클래스의 virtual이 호출된다
-
그래서 의도하지 않은 동작을 하게 된다, 또한 미정의 동작을 수행할 수 있다(Undefined Behavior)
📌 가상 함수 테이블
- 가상 함수 테이블은 클래스마다 존재한다 (인스턴스 마다가 아님)
- 각각의 인스턴스들은 해당 클래스의 가상함수 테이블을 가리키는 포인터 변수를 하나씩 가진다
- 즉 가상함수가 있는 클래스의 인스턴스 메모리 크기를 구할 땐 이 포인터 변수의 크기를 더해주는 것을 잊지 말아야 한다
(32bit 환경이면 4bytes, 64bit 환경이면 8bytes)
- 상속 관계의 클래스들은 가상함수 테이블을 사용해 오버라이드 된 함수를 찾아간다
- 가상함수테이블은 해당 클래스의 모든 가상함수의 주소값을 제공한다
- vtable에는 해당 가상함수들의 주소값이 들어있다.
- 상속 관계가 A->B이면 클래스 A의 vtable에는 A의 가상함수들 A::aaa(void)
- 클래스 B가 만약 A의 aaa함수를 오버라이딩 했다면 B의 vtable에는 B::aaa(void) 식으로 가상함수 테이블의 내용이 작성된다
- 실제 코드를 보면 자식 클래스의 가상함수 테이블에서는 자식 클래스에서 오버라이드 된 함수를 가리키는 것을 볼 수 있다
📖 C++ 스타일 캐스팅
단순히 (int*)와 같이 캐스팅하는 C스타일과 달리, C++은 4개의 캐스팅 방법으로 나뉘어진다
📌 const_cast
- 해당 포인터 객체의 상수성을 제거한다
- 상수성은 항상 유지되어야 하지만, 외부의 다른 라이브러리를 사용할 때 인자값으로 비상수성 객체를 요구한다던지 하는 불가피한 상황에서 한시적으로 사용한다
- 단, const_cast를 사용하는 것은 해당 객체를 인자로 받는 함수 내에서 해당 상수변수를 수정하지 않는다는 것을 알고 있을때만 사용해야 한다. 만약 함수 내에서 객체의 내용이 바뀐다면, 상수 제한을 잠깐 푸는것에 그치지 않고 상수성 자체를 잃어버리는 것이기 때문에 문제가 생기게 된다. 이럴 땐 유도리 있게 사용할 것이 아니라 프로그램의 전체 구조를 다시 생각해 보아야 한다
📌 static_cast
- 일반적인 캐스팅 방식
- static cast의 단점은, 런타임 타임 검사를 하지 않는다는 것이다(compile 타임에 정적으로 수행함)
- static_cast는 업캐스팅과 다운캐스팅을 둘 다 할 수 있는데, 계층 구조가 실제로 유효한지는 검사하지 않는다
- 즉 자식 클래스를 부모 클래스로 업캐스팅하여 사용하다가(다형성을 위해), 다시 자식 클래스로 다운캐스팅하는 것은 유효하며 정상적인 사용법이지만, 순수한 부모 클래스인 객체를 자식으로 다운캐스팅하여 사용하는 것도 막지 않는다는 것이다.
이런식으로 캐스팅하게되면 메모리 침범 등 심각한 오류가 발생한다
- 계층 구조 간 안전한 캐스팅을 하려면 dynamic_cast를 사용한다
- 대신 dynamic cast는 vtable에 RTTI(RunTime Type Information)가 저장되기 때문에 virtual 함수가 하나라도 있어야함
📌 dynamic_cast
- 런타임 타입 검사를 수행하는 안전한 다운캐스팅에 사용하는 캐스팅 방법이다
- 다이나믹 캐스트는 타입이 유효한지 체크하기 위해 런타임에 타입 검사를 수행하기 때문에, 런타임 비용이 높아서 잘 사용되지 않는다
📌 static_cast VS dynamic_cast
<static_cast> 정적으로 형변환을 해도 아무런 문제가 없다는 것은 이미 어떤 녀석인지 알고 있을 경우에 속할 것이고
<dynamic_cast> 동적으로 형변환을 시도해 본다는 것은 이 녀석의 타입을 반드시 질의해 봐야 된다는 것을 의미합니다
RTTI를 해야 하는 경우엔 dynamic_cast를 이용해 런타임의 해당 타입을 명확히 질의해야 하고,
그렇지 않은 경우엔 static_cast를 사용하여 변환 비용을 줄이는 것이 좋습니다
📌 reinterpret_cast
- 타입을 강제 변환한다
- double을 byte로 변환하는 등의 임의 변환이 가능하다
- 타입을 비트 단위로 1:1 변환시킨다. 그렇기에 원본 데이터가 소실되거나, 원하지 않는 결과가 나올 수 있으므로 주의해야 한다
📚 참고출처
얌얌코딩 (게임 개발) : https://youtu.be/D-STWJ24Kcs?si=SchohSwV6LRH3nZv
tera_geniel.log : https://ence2.github.io/2020/11/c-캐스팅-총정리스마트포인터-캐스팅-포함/