오늘은 1주차 숙제인 다형성과 상속의 개념을 이해하고 반복문을 활용하여 문제를 풀어보았다.

#include <iostream>
using namespace std;
// TODO: Phone 클래스 구현
// - displayBrand()와 showFeature() 순수 가상 함수를 포함하도록 구현하세요
// - 소멸자를 반드시 virtual로 선언하세요
// Samsung 클래스 작성 (Phone 클래스를 상속받음)
class Samsung : public Phone {
public:
void displayBrand() {
cout << "Samsung" << endl;
}
void showFeature() {
cout << "Galaxy S 시리즈" << endl;
}
};
// pple 클래스 작성 (Phone 클래스를 상속받음)
class Apple : public Phone {
public:
void displayBrand() {
cout << "Apple" << endl;
}
void showFeature() {
cout << "iPhone Pro 시리즈" << endl;
}
};
// Xiaomi 클래스 작성 (Phone 클래스를 상속받음)
class Xiaomi : public Phone {
public:
void displayBrand() {
cout << "Xiaomi" << endl;
}
void showFeature() {
cout << "Redmi Note 시리즈" << endl;
}
};
int main() {
// TODO: main 함수 구현
// - Phone* 타입의 배열을 생성하여 Samsung, Apple, Xiaomi 객체를 저장
// - 반복문을 사용하여 각 객체의 displayBrand()와 showFeature()를 호출
// - 반복문을 사용하여 메모리 해제를 위해 delete 호출
return 0;
}
기본 뼈대 코드를 보면서 주석을 채워나가는 숙제이다. 우선 Phone 클래스를 구현해주기위해
class phone
{
virtual void displayBrand() = 0;
virtual void showFeature() = 0;
virtual ~Phone();
};
virtual을 사용하여 순수가상함수로 이루어진 클래스 Phone을 만들어주었다. 다음으로 main 으로 넘어와서 추상클래스를 포인터 타입의 배열로 선언하여 상속클래스들의 객체를 저장해주어야하는데
Samsung s;
Apple a;
Xiaomi x;
Phone* p[3] = {&s,&a,&x};
처음엔 이렇게 구현을 하였고 그다음
for(int i = 0; i < 3; i++)
{
p[i]->displayBrand();
p[i]->showFeature();
}
for(int i = 0; i < 3; i++)
{
delete p[i];
}
return 0;
의 형태로 구현을 해주었다. 이렇게 구현한후 실행시 소멸자에서 읽기 엑세스 위반이 계속 생겼다.
그래서 이유를 찾아보니 delete 함수는 오직 Heap에만 할당된 동적 메모리를 해제하는 용도로 사용을해야하는데 객체를 인스턴스화 하는과정에서 변수들을 스택에 할당을 해버린상태이다. 때문에 스택에 할당된 변수들을 동적메모리에서 해제를 하려고하니 에러가 날 수 밖에없었다. (소멸자는 동적메모리에서 할당해제할때 즉 객체가 소멸할때 호출이 되는것인데 스택에있는 메모리는 해제불가능함으로 에러가 났던것) 이를 해결하려면 변수들을 생성할때 new 키워드를 이용해서 동적으로 할당을 해주어야한다.
Phone* phone[3] = {new Samsung(), new Apple(), new Xiaomi()};
이렇게 포인터 배열의 변수 인자 하나하나에 동적메모리를 할당을 해주어야 반복문을 사용하여 delete 함수를 호출해서 메모리 해제를 할 수 있었던 것이었다. 이번숙제를 수행하면서 동적메모리에 관련된 지식이 부족했음을 경험했다 또한 추상클래스를 포인터로 동적할당하는 방법도 마찬가지이다. 나에게 부족한 부분을 알 수 있어서 좋은 경험이었다.
스택 메모리
- 정적메모리 스택메모리는 컴파일시점에 크기가 정해지고 자동으로 메모리가 할당 및 해제되는 영역 ex) int main() { int x = 0; } 메인함수가
끝날 때 자동으로 해제가 됨
힙 메모리
- 동적메모리 힙메모리는 컴파일 이후 사용자가 직접 메모리를 할당할 수 있으며 자동 할당및 해제가되지않아 사용자가 직접 해제해줘야함
int main() { int* ptr = new int(10) delete ptr; } new 와 delete 는 짝으로 이루어져야하며 메모리 할당해제를 하지 않을시 예외적인 오류가 발생하여
프로그램이 정상적으로 작동하지 않을 수 있음 / 컴파일 이후에 동적으로 할당가능한 것이 장점
Dangling Pointer
- 서로 같은 주소를 가르키고 있는 동적메모리 할당된 두 포인터가 있다 이 상황에서 한개의 포인터만 메모리 해제를 하면 다른 하나의 포인터는
해당주소의 메모리가 해제된걸 모르기때문에 이상한 값을 가리킬 수 있다. 이러한 상황을 댕글링 포인터 라고 한다.
메모리 Leak 릭
- 메모리 누수 ) 식당에서 손님이 식사를 하고 떠난 자리를 치우지않으면 사용할 수 있는 식기가 점점 줄어드는 것처럼 메모리 또한 사용 후
할당 해제를 하지않는다면 점점 사용할 수 있는 용량이 줄어든다 이를 메모리 누수 / 릭 현상이라고 한다.
스마트 포인터
앞서말한 Dangling pointer 가 발생하지 않도록 자동으로 관리해주는 포인터이다.
unique_ptr
- 객체에 대한 단일 소유권을 관리한다. 한개의 메모리 주소에 한개의 소유권만을 주장한다 move 함수로 소유권을 이전할 수 있다.
shared_ptr
- 레퍼런스 카운터를 관리한다. 하나의 메모리에 레퍼런스 카운터를 이용하여 몇개의 포인터가 가리키고 있는지 확인 할 수 있다.
단, 서로의 포인터가 서로를 가리킬 수 있는 순환참조가 발생할 수 있다.
weak_ptr
- 객체의 소유권을 공유하지 않는다. 다른 스마트 포인터와 다르게 레퍼런스 카운터를 증가시키지않는 약한 참조를 하는 포인터이다.
sharedptr에서 발생하는 순환참조 문제를 다른하나의 포인터를 weakptr로 대체하면 순환참조의 문제를 해결 할 수 있다.
Lock() 함수
- weakptr 는 약한참조이므로 참조하는 대상을 직접적으로 사용할 수 없다. 때문에 lock 함수를 이용해서 유효성 검사를 한 후
해당 참조대상을 유효성 검사를 통해서 읽기가 가능하다.
얕은복사
- int형 변수 10이 있다고 가정했을때 해당변수의 주소를 가리키는 a 가있다고하면 b = a 를하면 10을 가리키는 주소가 2개가 되었으니
앝은복사이다 해당 복사는 Dangling pointer 문제를 발생 시킬 수 있다.
깊은복사
- 위와 같은상황에서 int a =new int(10) , int b = new int(*a) 를하게되면 10을 가리키는 메모리 공간하나전체가 복사가 되어서
두 변수가 독립적인 공간과 주소 값을 가지게 된다. 이렇게 될경우 dangling pointer는 발생하지 않지만 메모리공간이 2개가 되었으므로
두 공간 전부 해제 해주어야한다.
언리얼 엔진의 메모리 관리
- 가비지 컬렉션 : 언리얼 에서 제공하는 자동으로 메모리를 정리해주는 기능
마크 앤 스윕 알고리즘 방식 더 이상 사용하지 않는다고 판단되는 언리얼엔진의 객체 타입들을 식별하여 제거
루트 셋에 포함된 객체들을 식별 해당 객체들은 항상살아있다고 간주됨 : 가비지 컬렉션의 대상에서 제거(절대 버려서는 안되는 물건)
마크 = 표시하다 루트셋 객체에서 시작해서 직간접적으로 참조하는 UOBJ 객체들을 마킹(표시) 하고 사용중임을 나타냄
스윕 = 쓸어내다 마크단계에서 표시가 된 객체들을 제외한 나머지 오브젝트들을 자동으로 제거하고 메모리를 회수
해당 과정에서 객체의 소멸자가 호출되고 메모리가 반환됨.
가비지 컬렉션의 동작 방식을 제어하는 다양한 플래그 존재
[ GUObjectArray 라는 전역 배열에 저장된 아래 플래그 3개를 통해 게임 내에서 사용되는 오브젝트들을 관리]
- RF_Rootset
이 객체는 루트셋에 포함되어있다 (가비지 컬렉션 대상이 아님) AddToRoot 함수를 이용해 사용자가 직접 가비지컬렉션대상에서 제외시킬 수 있음
지워도 되는 타이밍에 RemoveFromRoot 를통해 함수를 해제 할 수 있음. (마크 앤 스윕은 주기적으로 돌기 때문에 가능)
- RF_BeginDestroyed (사용자 설정 불가능)
[ 현재 메모리 회수가 진행중이다 라고 알림 ] 시스템 표시 플래그 / 해당 객체가 가비지 컬렉션을 통해서 메모리 회수가 될 때
- RF_FinishDestroyed (사용자 설정 불가능)
[ 메모리 회수가 전부 완료되었다 알림 ] 시스템 표시 플래그 / 해당 객체가 가비지 컬렉션을 통해서 메모리 회수가 완료되었을 때
언리얼 엔진의 리플렉션 시스템
- 리플렉션 : 프로그램이 실행중일 때 자신의 구조와 상태를 검사하고 수정할 수 있는 능력 [c++ 의 경우 자체적인 기능이 없음]
언리얼 엔진모듈에 포함되지않은 사용자 가 정의한 객체들을 언리얼엔진에서 사용할 수 있게 변환 해주는것을 리플렉션 이라고 함
- UHT : 언리얼 엔진 툴로 사용자가 작성한 코드를 바로실행시키게되면 언리얼 엔진에서 이해할 수 없는 객체들이 있을 수 있음
때문에 사용자가 작성한 코드를 분석해서 언리얼 엔진이 이해할 수 있도록 메타데이터를 추가 및 생성해주는 도구 자동으로 변환은 불가능함
때문에 언리얼에서 제공하는 매크로를 이용해야함 [ 이건 클래스다, 이건 변수다 등등 을 나타냄 ]
- GENERATED_BODY UHT에 의해서 코드가 생성되려면 해당 매크로는 반드시 추가되어야함
리플렉션 매크로를 통해서 모든 코드를 하나하나 설명해주어야함
위 작업이 끝난 후 메타데이터를 추가해 컴파일을 실행 하게됨