동적할당
컴파일 시에 메모리를 할당하는 정적할당과 달리, 런타임 중에 메모리를 할당하는 것을 말합니다. C++에서는 new 연산자를 사용하여 동적할당하고, 사용이 끝난 이후엔 반드시 메모리를 해제해 줘야 합니다. 그렇지 않을 경우 메모리 누수가 발생하여 성능 저하나 예기치 못한 오류가 발생할 수 있습니다.
new/delete와 malloc/free 차이
new와 delete는 호출 시 생성자와 소멸자를 각각 호출합니다. 또한 new는 메모리를 할당할 때 자료형 단위로 할당합니다.
malloc과 free는 호출 시 생성자와 소멸자를 호출하지 않습니다. 또한 malloc은 메모리를 할당할 때 바이트 단위로 할당합니다.
바인딩
프로그램 실행 시 변수나 함수들은 그 내용을 저장할 메모리를 할당해야 하는데, 그 이전에 변수 및 함수의 타입과 값을 결정해야 합니다. 이때, 타입과 값을 결정하는 것을 바인딩이라고 합니다.
정적 바인딩, 동적 바인딩
바인딩을 언제 하는지에 따라 정적 바인딩과 동적 바인딩으로 나뉘는데, 정적 바인딩은 컴파일 타임에 바인딩되는 것을 말하고, 동적 바인딩은 런타임에 바인딩되는 것을 말합니다.
가상함수
가상함수는 다형성을 구현하기 위한 기능 중 하나로, 파생클래스에서 재정의할 것을 염두하고 만드는 멤버 함수입니다.
가상함수를 호출할 때 객체가 가지는 함수가 아닌, 가상 함수 테이블을 통해 실행될 함수의 주소를 찾습니다. 이러한 방식으로 기본 클래스의 포인터 또는 참조를 사용하여 파생클래스에서 재정의한 가상함수를 호출할 수 있고, 이로 인해 다형성이 구현됩니다.
> virtual 키워드
virtual 키워드를 꼭 사용해야 하나요?
부모 클래스에서 함수를 선언할 땐 virtual 키워드를 꼭 붙이고, 자식 클래스에서 재정의(override) 할 경우엔 virtual 키워드를 붙여도 되고 안붙여도 됩니다.
가상함수를 사용해야 하는 이유는?
컴파일러는 함수를 호출할 때 매우 복잡한 과정을 거치므로, 함수를 호출하기 위한 코드를 컴파일 타임에 고정된 메모리 주소로 변환시킵니다. 이것을 정적 바인딩이라고 하는데 일반 함수의 경우 모두 정적바인딩을 하게 됩니다.
따라서 Parent 부모클래스 객체와 Child 자식클래스 객체가 있을 때, 중간에 Parent* p = c (Child*객체); 처럼 자식클래스의 주소를 넣고 함수를 호출시켜도, Parent 클래스의 함수가 호출됩니다. 중간에 p 포인터의 주소를 바꿔줬음에도 불구하고 컴파일 당시에 이미 정적바인딩으로 호출되는 함수의 주소값이 결정되었기 때문입니다.
이를 해결하기 위해선, 부모클래스와 자식클래스의 함수를 가상함수로 만들어 동적바인딩 해야합니다. 가상함수로 일반함수들을 선언하면 포인터의 타입이 아닌, 포인터가 가리키는 객체의 타입에 따라 멤버 함수를 선택하게 됩니다.
가상함수 테이블
가상함수 테이블은 가상함수의 포인터들을 담고 있는 테이블로써, 프로그램이 실행되면 main 함수가 호출되기 전에 메모리 공간에 할당됩니다.
클래스에 가상함수가 있다면, 해당 클래스의 객체가 생성될 때 가상 함수 테이블의 포인터가 객체에 할당됩니다. 이 포인터는 가상함수 호출 시 실행될 실제 함수의 주소를 참조합니다. 이렇게 하면 기본 클래스의 포인터 또는 참조를 사용하여 파생 클래스에서 재정의된 가상 함수를 호출할 수 있습니다.
가상함수 테이블 구성

Parent* p 객체와 Parent를 상속받는 Child* c 객체를 각각 조사식에 찍어보면, 해당 객체 안에 __vfptr 이라는 가상함수 테이블이 보이게 됩니다.
Parent 클래스에서 2개의 가상함수를 선언하면 가상함수 테이블에 func1 과 func2 인 2개의 가상함수가 들어가 있고,
[0]번 인덱스인 func1 함수는 자식클래스인 Child에서 재정의하여 주소값이 바뀌어있는 것을 볼 수 있습니다. (func2 함수는 재정의하지 않아서 부모와 자식의 함수 주소값이 동일하다)
즉, 자식 클래스의 __vfptr 은 부모클래스의 __vfptr 값이 그대로 복사되며, 오버라이딩 된 함수만 주소가 새로 업데이트됩니다. 만약 자식클래스에 부모클래스에는 없는 새로운 가상함수를 추가하게 된다면, 객체의 __vfptr 마지막 부분에 추가됩니다.
이처럼, 가상함수를 갖는 클래스의 경우 가상 함수 주소들이 배열 형태로 존재하는 가상함수 테이블 __vfptr을 가지고 있고, 클래스 안에서 테이블이 지시할 수 있는 함수 주소를 참조하는 포인터를 가지고 있습니다.
이때 참조되는 주소값들은 프로그래머가 직접 참조할 수 있는 주소값이 아니라, 내부적으로 필요에 의해 참조되는 주소값입니다.
가상함수 테이블 동작 원리
프로그램 실행 시 컴파일 타임에 호출할 함수의 목록을 가상함수 테이블에 '미리 작성'해두고, 실행 중에 객체와 그 객체의 가상함수 테이블에서 호출할 함수의 주소를 찾는 방법으로 동작하게 됩니다.
가상함수를 호출하면, 컴파일러는 먼저 해당 객체의 vtable을 찾아갑니다. 그리고 vtable에서 호출할 가상함수의 주소를 찾아 해당 함수를 호출합니다. 이렇게 vtable을 사용하면, 함수 호출이 런타임에 결정되므로 다형성을 구현할 수 있습니다. 즉, 코드는 같지만 해당 객체의 타입에 따라 다른 함수가 호출될 수 있습니다.
참고로, 가상함수 테이블은 객체의 생성과 상관없이 메모리 공간에 할당됩니다. 이는 가상함수 테이블이 멤버함수의 호출에 사용되는 일종의 데이터이기 때문입니다.
Parent 클래스에 Func1 함수를 호출하는 과정
Parent 객체 → Parent 클래스의 가상함수 테이블 참조 → 100번지에 위치하는 Func1 함수 실행
- 가상함수 테이블에서 '테이블'은 배열을 뜻합니다. 즉, 가상함수 테이블은 함수 포인터 배열이라고 생각하면 됩니다.
- 가상함수 테이블은 주소값 즉, 번지를 가져야 하므로 가상함수는 인라인 함수가 될 수 없습니다.
- 가상함수 생성 시 가상함수 테이블이라는 여분의 메모리를 더 소모하게 된다. (비트 수준에 따라 4, 8바이트)
- 가상 함수 테이블은 항상 자식클래스가 가진 부모클래스 영역에 존재합니다.
class A { public: string name; }; class B : public A { public: string name2; }; int main() { B b; // 객체 b는 부모 A영역을 가지고 있기 때문에 A의 name에 접근할 수 있다. b.A::name = "aaa"; // 보통 쓰는 b.name = "aaa"; 와 같은 코드를 컴파일러가 풀어서 쓴 것 // 지금 예제에서는 클래스 A, B가 가상화되어있지 않지만 // 가상화가 되어있다면 부모 A 영역에 가상화 테이블이 들어간다. }
추상클래스
추상클래스는 순수가상함수를 하나라도 가지고 있는 클래스를 말합니다.
특징
추상클래스는 객체화할 수 없고, 추상클래스를 상속받은 클래스는 반드시 순수가상함수를 재정의해야만 합니다. 추상클래스는 다형성을 구현하기 위해 사용되는데, 자식클래스는 부모의 순수가상함수를 원하는 기능으로 오버라이딩하고 부모클래스의 포인터나 참조를 통해 동적바인딩하여 사용할 수 있습니다.
추상클래스를 사용하는 이유는?
부모 추상 클래스를 선언하는 과정에서, 부모 클래스는 자식클래스에서 어떤식으로 순수가상함수를 재정의하는지 모릅니다. 따라서 이름을 강제로 똑같게 만들어줌으로써 해당 이름으로 순수가상함수를 사용할 수 있도록 만들어줄 수 있습니다.
또한 추상클래스는 자식클래스가 반드시 구현해야만 하는 기능들을 담아 알리기 위해 사용합니다.
인터페이스
인터페이스는 모든 함수가 순수 가상 함수로 이루어져 있습니다. 순수 가상 함수는 구현이 없고 인터페이스를 상속받는 클래스가 반드시 재정의해야 합니다. 따라서, 이러한 특성을 이용하여 인터페이스를 상속받는 클래스가 꼭 가져야 하는 기능들을 담아 상속하는 식으로 코드를 설계합니다.
(언리얼 인터페이스는 가상함수를 사용할수도있고, 인터페이스 내 함수 기능을 구현해도 된다..!! C++과 다름)
그러나, C++에서는 Java나 C#과 같은 '인터페이스'라는 키워드가 존재하지 않기 때문에, 비슷한 역할을 수행하는 추상 클래스(abstract class)를 인터페이스의 역할처럼 수행하도록 구현할 수 있습니다.
인터페이스와 추상클래스의 차이는?
인터페이스는 다중상속을 지원하고, 추상클래스는 다중상속을 지원하지 않습니다. 또한, 추상 클래스는 순수가상함수뿐만 아니라 일반 함수, 가상함수를 모두 가질 수 있지만 인터페이스는 순수가상함수만 가질 수 있습니다.
가상소멸자
소멸자는 생성자의 역순으로 호출됩니다. 자식클래스의 소멸자가 먼저 호출되고, 이후에 부모클래스의 소멸자가 호출되는 방식입니다.
그러나 만약 부모클래스의 포인터를 통해 자식클래스를 해제하는 경우엔 부모클래스의 소멸자만 호출되고, 자식클래스의 소멸자는 호출되지 않습니다. 따라서 소멸자에 virtual 키워드를 붙인 가상소멸자를 만들어 사용하는 것이 좋습니다.

> 실행결과
Base의 생성자 호출
Derived의 생성자 호출
Derived의 소멸자 호출
Base의 소멸자 호출
virtual 키워드를 사용하면 C++ 런타임은 객체의 실제 타입을 확인하고 적절한 소멸자를 호출합니다. 즉, 파생 클래스의 객체에 대해서는 파생 클래스의 소멸자가 먼저 호출되고, 그 다음으로 기본 클래스의 소멸자가 호출됩니다.
예를 들면, GameObject라는 클래스가 있고, 이를 상속받는 Player, Enemy 클래스가 각각 존재한다고 합시다.
만약, GameObject를 delete 시킨다면 GameObject에 할당된 메모리를 해제하면서 가상소멸자가 호출되어 파생클래스들의 소멸자가 정상적으로 호출되게 됩니다. 만약 GameObject의 소멸자가 가상소멸자가 아니라면, GameObject를 담은 배열을 순회하며 delete 할 때 자식클래스인 Player와 Enemy의 소멸자는 호출되지 않고 넘어가 메모리 누수가 발생할 수 있습니다.
https://coding-factory.tistory.com/699
[C++] 가상함수(virtual) 사용법 총정리
가상함수란? 가상함수는 부모 클래스에서 상속받을 클래스에서 재정의할 것으로 기대하고 정의해놓은 함수입니다. virtual이라는 예약어를 함수 앞에 붙여서 생성할 수 있으며 이렇게 생성된 가
coding-factory.tistory.com
https://srdeveloper.tistory.com/57
C++ 멤버함수, 가상함수 동작원리
멤버함수 동작원리 - C++의 객체의 멤버변수는 객체 내에 존재하지만 멤버함수는 다음과 같은 관계를 갖는다. 멤버함수는 메모리의 한 공간에 별도로 위치한다. 멤버함수가 정의된 클래스의 모
srdeveloper.tistory.com
'👩🏻💻기초지식 > C++' 카테고리의 다른 글
[C++] RTTI (RunTime Type Information, 실시간 타입 정보)란? (0) | 2023.09.05 |
---|---|
[C++] Cast 4종류 (static, dynamic, const, reinterpret) (0) | 2023.09.04 |
[C++] 빌드 컴파일 과정, C++ 빌드과정과 언리얼 빌드과정의 차이점은? (0) | 2023.08.17 |
[C++] 가변 파라미터 함수를 구현하는 방법은? (0) | 2023.08.17 |
[C++] 스택프레임이란? EBP와 ESP (0) | 2023.08.17 |