오늘 학습 진행 내용
라이브 세션
알고리즘 강의
게임 개발자를 위한 C++ 문법
챕터2-1.자원 관리하기
스택 메모리
스택 메모리 예시 코드
#include <iostream>
using namespace std;
void func2() {
int b = 20; // 지역 변수 'b', stack 메모리에서 관리됨
cout << "func2: b = " << b << endl;
}
void func1() {
int a = 10; // 지역 변수 'a', stack 메모리에서 관리됨
cout << "func1: a = " << a << endl;
fun2(); //fun2() 호출
} // func1()이 종료되면 'a'는 소멸, fun2() 종료 후 'b' 도 소멸
void func3() {
for (int i = 0; i < 3; ++i) { // 반복문 안에서 지역 변수 'i'가 매번 새로 생성됨
int temp = i * 10; // 반복문 안에서만 유효한 'temp'
cout << "Iteration " << i << ": temp = " << temp << endl;
} // 반복문 끝날 때마다 'temp'는 소멸됨
}
int main() {
func1(); // func1() 호출
// 'a'는 func1() 호출 중에만 존재하고, 함수 종료 후 소멸됨
fun3(); //fun3() 호출
return 0;
}
힙(동적) 메모리
힙(동적) 메모리 예시 코드
#include <iostream>
using namespace std;
//변수 하나의 영역에 대한 동적 할당
void func1() {
int* ptr = new int(10); // 힙 메모리에 정수 10 할당
cout << "Value: " << *ptr << endl;
delete ptr; // 메모리 해제
}
//배열의 동적 할당과 해제
void func2() {
int* arr = new int[5]; // 힙 메모리에 정수 배열 5개 할당
for (int i = 0; i < 5; ++i) {
arr[i] = i * 10;
cout << "arr[" << i << "] = " << arr[i] << endl;
}
delete[] arr; // 배열 메모리 해제
}
//메모리 해지를 하지 않은 예시
void func3() {
int* ptr = new int(20); // 힙 메모리에 정수 20 할당
cout << "Value: " << *ptr << endl;
// 메모리 해제를 하지 않음
}
//사용자 입력 기반 배열 동적 할당
void createDynamicArray() {
int size;
cout << "Enter the size of the array: ";
cin >> size; // 배열 크기를 사용자로부터 입력받음
if (size > 0) {
int* arr = new int[size]; // 입력받은 크기만큼 동적 배열 생성
for (int i = 0; i < size; ++i) {
arr[i] = i * 2; // 배열 초기화
cout << "arr[" << i << "] = " << arr[i] << endl;
}
delete[] arr; // 동적으로 할당한 배열 메모리 해제
} else {
cout << "Invalid size!" << endl;
}
}
int main() {
func1();
fun2();
func3();
createDynamicArray();
return 0;
}
Dangling Pointer
이미 해제된 메모리의 주소를 계속 가지고 있는 포인터를 지칭
Daling Pointer 예시 코드
#include <iostream>
using namespace std;
//이중 해제 코드 오류 발생
void func4() {
int* ptr = new int(30); // 힙 메모리에 정수 30 할당
cout << "Value: " << *ptr << endl;
delete ptr; // 첫 번째 해제
// delete ptr; // 두 번째 해제 (코드 활성화 시 문제 발생)
}
//Dangling Pointer 예시
void func5() {
int* ptr = new int(40); // 힙 메모리에 정수 40 할당
int* ptr2 = ptr; //힙 메모리의 ptr 주소를 ptr2에 저장
cout << "ptr adress = " << ptr << endl;
cout << "ptr2 adress = " << ptr2 << endl;
cout << *ptr << endl;
delete ptr; //힙 메모리 ptr 해제
cout << *ptr2 << endl; //이미 해제된 ptr의 주소를 할당하고 있는 ptr2 불러오면 쓰레기값 존재
}
int main() {
fun4()
func5();
return 0;
}
Memory Leak
스마트 포인터
스마트 포인터의 핵심 원리는 new/delete를 사용하지 않는 자동 메모리 관리
각 스마트 포인터의 목적에 맞게 자동으로 메모리를 관리(Dangling Pointer, Memory Leak 해결)
unique_ptr
객체에 대한 단일 소유권을 관리(단 하나의 포인터만 객체를 소유), 복사 혹은 대입 불가능
객체의 소유권을 명확히 하고 소유권 이전(move)를 통해 효율적인 자원 관리
unique_ptr 예시 코드
#include <iostream>
#include <memory> // unique_ptr 사용
using namespace std;
//Smart 포인터 사용 예제(unique_ptr)
class uniqueClass {//일반 클래스에서 unique_ptr 사용 예시
public:
uniqueClass(int val) : value(val) {
cout << "uniqueClass 생성: " << value << endl;
}
~uniqueClass() {
cout << "uniqueClass 소멸: " << value << endl;
}
void display() const {
cout << "값: " << value << endl;
}
private:
int value;
};
int main() {
// unique_ptr 생성
unique_ptr<int> ptr1 = make_unique<int>(10); //객체에 대한 단일 소유권을 관리
// unique_ptr이 관리하는 값 출력
cout << "ptr1의 값: " << *ptr1 << endl;
// unique_ptr은 복사가 불가능
// unique_ptr<int> ptr2 = ptr1; // 컴파일 에러 발생!
// 소유권 이동 (move 사용) 복사 대신
unique_ptr<int> ptr2 = move(ptr1);
if (!ptr1) { //move로 ptr1에서 ptr2로 옮겨져서 ptr1이 비어지게 됨
cout << "ptr1은 이제 비어 있습니다." << endl;
}
cout << "ptr2의 값: " << *ptr2 << endl;
// unique_ptr로 MyClass 객체 관리
unique_ptr<uniqueClass> uniqueObject = make_unique<uniqueClass>(42);
// MyClass 멤버 함수 호출
uniqueObject->display();
// 소유권 이동
unique_ptr<uniqueClass> newOwner = move(uniqueObject);
if (!uniqueObject) {
cout << "uniqueObject는 이제 비어 있습니다." << endl;
}
newOwner->display();
// 범위를 벗어나면 메모리 자동 해제
return 0;
}
shared_ptr
레퍼런스 카운트를 관리, 현재 객체를 참조하는 포인터의 개수를 카운팅
레퍼런스 카운트가 0이 되면 객체는 자동으로 메모리 해제
하나의 객체를 여러 개의 포인터가 함께 참조할 수 있는 스마트 포인터(복사 및 대입 연산으로 여러개 포인터 하나의 객체 공유)
use_count()를 활용하여 현재 객체를 참조하는 포인터 수 확인
reset()으로 소유 중인 객체를 해제하거나 다른 객체로 변경 가능
shared_ptr 예시 코드
#include <iostream>
#include <memory> // shared_ptr 사용
using namespace std;
//Smart 포인터 사용 예제(shared_ptr)
class sharedClass {//일반 클래스에서 shared_ptr 사용 예시
public:
sharedClass(int val) : value(val) { //클래스 기본 생성자
cout << "sharedClass 생성: " << value << endl; // 출력: sharedClass 생성: 42
}
~sharedClass() { //클래스 소멸자
cout << "sharedClass 소멸: " << value << endl; // 출력: sharedClass 소멸: 42
}
void display() const { //출력 매개 함수
cout << "값: " << value << endl; // 출력: 값: 42
}
private:
int value;
};
class B; // Forward declaration
class A {
public:
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destroyed\n"; }
};
class B {
public:
std::shared_ptr<A> a_ptr;
~B() { std::cout << "B destroyed\n"; }
};
int main(){
// shared_ptr 생성
shared_ptr<int> ptr3 = make_shared<int>(10);
// ptr3의 참조 카운트 출력
cout << "ptr3의 참조 카운트: " << ptr3.use_count() << endl; // 출력: 1
// ptr4가 ptr3과 리소스를 공유
shared_ptr<int> ptr4 = ptr3;
cout << "ptr2 생성 후 참조 카운트: " << ptr3.use_count() << endl; // 출력: 2
// ptr4가 범위를 벗어나면 참조 카운트 감소
ptr4.reset();
cout << "ptr2 해제 후 참조 카운트: " << ptr4.use_count() << endl; // 출력: 1
// shared_ptr로 MyClass 객체 관리
shared_ptr<sharedClass> obj1 = make_shared<sharedClass>(42);
// 참조 공유
shared_ptr<sharedClass> obj2 = obj1;
cout << "obj1과 obj2의 참조 카운트: " << obj1.use_count() << endl; // 출력: 2
obj2->display(); // 출력: 값: 42
// obj2를 해제해도 obj1이 객체를 유지
obj2.reset();
cout << "obj2 해제 후 obj1의 참조 카운트: " << obj1.use_count() << endl; // 출력: 1
//shared_ptr을 사용해서 순환 참조가 발생하는 예시
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a;
// main 함수가 끝나도 A와 B는 서로 참조 중이라 메모리 해제가 안 됨
}
weak_ptr
객체의 소유권을 공유하지 않고 레퍼런스 카운트를 증가시키지 않는 약함 참조
shared_ptr은 두 개 이상의 객체가 서로 shared_ptr 가리켜 순환 참조 발생(메모리 누수 유발)
서로 순환하고 있는 shared_ptr 중 하나를 weak_ptr로 대체하면 순환고리 끊어지게 됨
weak_ptr은 레퍼런스 카운트를 증가시키지 않는 약한 참조 방식 동작 스마트 포인터
lock() 호출 후 반환된 shared_ptr이 유효한지 확인 후에 사용
레퍼런스 카운트 영향 미치지 않으므로 반드시 lock 함수로 내부객체 유효성 확인 후 사용
weak_ptr 예시 코드
#include <iostream>
#include <memory> // weak_ptr 사용
using namespace std;
class B; //Forward declaration
class A {
public:
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destroyed\n"; }
void say_hello() {
std::cout << "Hello from A\n";
}
};
class B {
public:
std::weak_ptr<A> a_ptr; // weak_ptr로 변경
~B() { std::cout << "B destroyed\n"; }
void useA() {
if (auto a_shared = a_ptr.lock()) { // 유효한지 확인
a_shared->say_hello();
} else {
std::cout << "A is no longer available.\n";
}
}
};
int main(){
std::shared_ptr<B> b = std::make_shared<B>();
{
std::shared_ptr<A> a = std::make_shared<A>();
b->a_ptr = a;
b->useA(); // A가 유효하므로 Hello 출력
} // A는 scope을 벗어나며 소멸됨
b->useA(); // A는 이미 소멸되었기 때문에 메시지 출력
auto weak_a = std::make_shared<A>();
auto weak_b = std::make_shared<B>();
weak_a->b_ptr = weak_b;
weak_b->a_ptr = weak_a; // weak_ptr로 참조
}
얕은 복사와 깊은 복사
일반적으로 포인터나 동적으로 할당된 자원을 관리하는 객체는 메모리 안정을 위해 깊은 복사 사용이 바람직
얕은 복사
클래스 내의 포인터 멤버를 복사할 때 포인터가 가리키는 데이터가 아닌 포인터가 저장하고 있는 주소값만 복사하는 것
두 객체가 동일한 동적 메모리 영역을 가리키게 됨
얕은 복사는 원복 객체 메모리 해제하면 복사된 객체의 포인터는 해제된 메모리 영역 가리킴(dangling pointer 발생)
깊은 복사
클래스의 포인터 멤버가 가리키는 동적 데이터를 새로 할당된 독립적인 메모리 영역에 복제하는 것
원본 객체와 복사된 객체는 서로 독립적인 메모리 공간을 소유하므로 dangling pointer가 발생하지 않음
얕은 복사와 깊은 복사 비교 예시 코드
#include <iostream>
using namespace std;
int main() {
// 포인터 A가 동적 메모리를 할당하고 값을 30으로 설정
int* A = new int(30);
// 포인터 B가 A가 가리키는 메모리를 공유(얉은 복사)
int* B = A;
// 포인터 B가 A가 가리키는 값을 복사 (깊은 복사)
int* deep_B = new int(*A);
cout << "A의 값: " << *A << endl; // 출력: 30
cout << "B의 값: " << *B << endl; // 출력: 30
cout << "deep_B의 값: " << *deep_B << endl; // 출력: 30
// A가 동적 메모리를 해제
delete A;
// 이제 B는 Dangling Pointer(해제된 메모리를 가리키는 포인터)
// 이 시점에서 B를 통해 접근하면 Undefined Behavior 발생
cout << "B의 값 (dangling): " << *B << endl; // 위험: 정의되지 않은 동작
// B는 여전히 독립적으로 자신의 메모리를 관리
cout << "deep_B의 값 (깊은 복사 후): " << *deep_B << endl; // 출력: 30
// B의 메모리도 해제
delete deep_B;
return 0;
}
언리얼엔진의 메모리 관리
언리얼엔진의 리플렉션 시스템
| 매크로 | 리플렉션에서의 목적 | 일반적인 위치 |
|---|---|---|
UCLASS() | C++ 클래스를 UObject 기반의 리플렉션 시스템에 등록 | 클래스 정의 앞 |
UPROPERTY() | 멤버 변수를 리플렉션 시스템에 노출 | 멤버 변수 선언 앞 |
UFUNCTION() | 멤버 함수를 리플렉션 시스템에 노출 | 멤버 함수 선언 앞 |
USTRUCT() | C++ 구조체를 리플렉션 시스템에 등록 | 구조체 정의 앞 |
GENERATED_BODY() | UHT가 생성하는 리플렉션 및 엔진 지원 코드를 위한 삽입 지점 | 클래스/구조체 본문 첫 줄 |
USTRUCT() 예시
// FMyStructData.h (or within another .h)
#pragma once
#include "CoreMinimal.h"
#include "UObject/ObjectMacros.h"
#include "MyStructData.generated.h"
USTRUCT(BlueprintType) // 블루프린트에서 이 구조체를 타입으로 사용할 수 있도록 합니다.
struct FMyStructData
{
GENERATED_BODY() // UHT가 필요한 코드를 생성하도록 합니다.
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MyStructData")
int32 SampleInt;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MyStructData")
FString SampleString;
FMyStructData() : SampleInt(0), SampleString(TEXT("Default")) {}
};
UCLASS() 예시
UCLASS(Blueprintable, BlueprintType) // 블루프린트에서 생성 가능하고 타입으로 사용 가능하도록 합니다.
class YOURPROJECT_API UMyReflectedObject : public UObject // UObject를 상속받습니다.
{
GENERATED_BODY() // UHT가 필요한 코드를 생성하도록 합니다.
public:
// ... Properties and Functions will go here ...
UMyReflectedObject(); // Constructor
};
자원 관리하기 실습 1.
메모리 누수 발생 코드 분석하고 보완하기
C++로 구현된 코드를 분석해서 메모리 누수가 우려되는 부분을 확인한 후, 코드를 수정하세요.
#include <iostream>
using namespace std;
class MyClass {
private:
int* ptr;
public:
// 생성자
MyClass() {
ptr = new int(10); // 동적 메모리 할당
cout << "메모리 할당 완료!" << endl;
}
// 소멸자
~MyClass() {
}
void print() const {
cout << "값: " << *ptr << endl;
}
};
int main() {
MyClass obj;
obj.print();
// main 함수 종료
return 0;
}
실습 코드
#include <iostream>
using namespace std;
class MyClass {
private:
int* ptr;
public:
// 생성자
MyClass() {
ptr = new int(10); // 동적 메모리 할당
cout << "메모리 할당 완료!" << endl;
}
// 소멸자(생성자에서 포인터 ptr에 동적 메모리 할당했으므로 소멸자에서 delete로 삭제해줌으로써 메모리 누수 방지)
~MyClass() {
delete ptr;
cout << "MyClass 소멸: " << *ptr << endl; // 출력: MyClass 소멸
}
void print() const {
cout << "값: " << *ptr << endl;
}
};
int main() {
MyClass obj;
obj.print();
// main 함수 종료
return 0;
}
자원 관리하기 실습 2.
스마트 포인터를 활용한 로그분석기 구현
로그 기록기는 프로그램에서 발생한 중요한 사건을 기록합니다.
로그 기록기의 몇 가지 기능을 구현해 봅시다.
요구사항은 아래와 같습니다.
📌로그 메시지는 중요도에 따라(Info, Warning, Error)로 분류되어 기록될 수 있습니다. 로그 앞에 중요도가 태그가 붙어 표시됩니다.완성된 모습
[로그 출력 예시]
[INFO]: System is starting.
[WARNING]: Low disk space.
[ERROR]: Unable to connect to the server.
Total logs recorded: 3
Logger instance destroyed.
실습 코드
#include <iostream>
#include <memory>
#include <string>
using namespace std;
class Logger{
public:
Logger() : logCount(0) {}
~Logger(){
cout<<"Logger instance destroyed"<<endl;
}
void logInfo(const string& message){
logCount++;
cout<<"[INFO]: "<<message<<endl;
}
void logWarning(const string& message){
logCount++;
cout<<"[WARNING]: "<<message<<endl;
}
void logError(const string& message){
logCount++;
cout<<"[ERROR]: "<<message<<endl;
}
void showTotalLogs(){
cout<<"Total logs recorded: "<<logCount<<endl;
}
private:
int logCount;
};
int main(){
unique_ptr<Logger> logger = make_unique<Logger>();
logger->loginfo("System is starting.");
logger->logWarning("Low disk space.");
logger->logError("Unable to connect to the server.");
logger->showTotalLogs();
}
CH2 학습 가이드
미니 실습
실습 코드
#include <iostream>
using namespace std;
class Car{
public:
Car(string modelname){
this->modelname=modelname;
this->speed=0;
};
void Accelerate(){
if(speed>300){
speed=300;
}
else{
speed+=10;
}
}
void Break(){
if(speed-10<0){
speed=0;
}
else{
speed-=10;
}
}
void DisplayInfo(){
cout<<"자동차 이름 : "<<modelname<<endl;
cout<<"현재 속도 : "<<speed<<endl;
}
private:
string modelname;
int speed;
};
int main(){
string modelname = "Aventador";
Car car(modelname);
int input=0;
while(input!=3){
cout<<"1) 가속\n";
cout<<"2) 브레이크\n";
cout<<"3) 종료\n";
cin>>input;
switch(input){
case 1:
{
car.Accelerate();
car.DisplayInfo();
break;
}
case 2:
{
car.Break();
car.DisplayInfo();
break;
}
case 3:
{
cout<<"종료됩니다."<<endl;
break;
}
default:
{
cout<<"잘못된 입력입니다.\n";
break;
}
}
}
}
도전 실습
온도계를 클래스로 만들어요.
멤버 변수는 섭씨 기준의 현재 온도에요.
현재 온도에 대한 게터 함수와 세터 함수를 각각 만들어요.
세터 함수에서 입력한 값에 범위를 걸어주세요.
섭씨 -100도에서 100도 까지만 값 입력이 가능해요.
출력 모드 전환이 되는 bool 타입의 멤버 변수, 그리고 변수의 세터를 만들어주세요.
이 멤버 변수가 TRUE이면 [섭씨 현재 온도] 게터 함수가 [화씨 현재 온도]로 반환해줘요.
아래의 인터페이스를 만들어 주세요.
[--- 메시지 표시하는 라인 ---]
1) 온도 설정
2) 표시 모드 전환
3) 온도 표시
실습 코드
#include <iostream>
using namespace std;
class Thermometer {
public:
Thermometer(double temperature){
this->temperature = temperature;
this->printTempF = false;
}
double getTemperature(){
if(!printTempF){
return temperature;
}
else{
return temperature*1.8 + 32;
}
}
void setTemperature(double temperature){
if(temperature>=-100 && temperature<=100){
if(printTempF){
this->temperature = (temperature - 32) / 1.8;
cout<<"온도가 화씨 " <<temperature <<"도로 설정됩니다."<<endl;
}
else{
this->temperature = temperature;
cout<<"온도가 섭씨 " <<temperature <<"도로 설정됩니다."<<endl;
}
}
else{
cout<<"잘못된 온도 입력입니다."<<endl;
}
}
void setPrimtTempF(bool tempF){
this->printTempF = tempF;
}
bool getPrintTempF(){
return printTempF;
}
private:
double temperature;
bool printTempF;
};
int main(){
double temperature =36.5;
Thermometer thermometer(temperature);
int input=0;
while(true){
cout<<"1) 온도 설정\n";
cout<<"2) 표시 모드 전환\n";
cout<<"3) 온도 표시\n";
cin>>input;
switch(input){
case 1:
{
cin>>temperature;
thermometer.setTemperature(temperature);
break;
}
case 2:
{
bool printTempF = !thermometer.getPrintTempF();
thermometer.setPrimtTempF(printTempF);
if(printTempF){
cout<<"화씨 기준 온도로 표시 모드 전환합니다."<<endl;
}
else{
cout<<"섭씨 기준 온도로 표시 모드 전환합니다."<<endl;
}
break;
}
case 3:{
if(thermometer.getPrintTempF()){
cout<<"현재 온도는 화씨 "<<thermometer.getTemperature() <<"도 입니다."<<endl;
}else{
cout<<"현재 온도는 섭씨 "<<thermometer.getTemperature() << "도 입니다."<<endl;
}
break;
}
default:
{
cout<<"잘못된 입력입니다. 종료됩니다.\n";
return 0;
}
}
}
}
도전X도전 실습
비행기에 파일럿 2명을 태울거에요.
파일럿 클래스는 아래와 같은 규격이에요.
클래스: Pilot
멤버 변수: string name, int licenseID
생성자: Pilot(std::string name, int id);
멤버 함수: void printPilotInfo();
비행기 클래스는 다음과 같은 규격을 가지고 있어요.
클래스 이름: Aircraft
멤버 변수: std::string modelName, Pilot crew[2]
생성자: Aircraft(std::string name, const Pilot& captain, const Pilot& coPilot);
멤버 함수: void displayAircraftInfo(); void displayCrewInfo();
main() 함수에서 캡틴과 코파일럿을 생성하고, 비행기를 생성해주세요.
비행기와 파일럿 정보를 출력해주세요.
실습 코드
#include <iostream>
using namespace std;
class Pilot {
public:
Pilot(string name, int id) : name(name), licenseID(id) {}
void printPilotInfo() {
cout << "파일럿 이름: " << name << endl;
cout << "파일럿 라이센스 ID: " << licenseID << endl;
}
private:
string name;
int licenseID;
};
class Aircraft {
public:
Aircraft(string name, const Pilot& captain, const Pilot& coPilot)
: modelName(name), crew{ captain, coPilot } // 배열 초기화 OK
{
}
void displayAircraftInfo() {
cout << "비행기 모델명: " << modelName << endl;
}
void displayCrewInfo() {
cout << "\n[캡틴]\n";
crew[0].printPilotInfo();
cout << "\n[코파일럿]\n";
crew[1].printPilotInfo();
}
private:
string modelName;
Pilot crew[2]; // crew[0], crew[1]
};
int main() {
Pilot captain("James", 123456);
Pilot copilot("Jim", 678910);
Aircraft Asiana("BOEING 727", captain, copilot);
Asiana.displayAircraftInfo();
Asiana.displayCrewInfo();
}