no image
[ C++ ] 프로그래머스 비밀지도
문제 링크https://school.programmers.co.kr/learn/courses/30/lessons/17681 프로그래머스SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프programmers.co.kr 문제 나의 풀이1. 각 배열이 들어가있는 10진수를 2진수로 변환2 . 2진수로 변환된 배열의 값들을 순회하면서 비교, 두 배열 중 하나라도 1이 있다면 temp에 1로 저장3. temp 순회하면서 1이면 #, 0이면 공백을 answer에 추가+ 처음엔 배열에 들어가있는 수를 하나하나 이진수로 바꾸는 작업을 했었는데 하다보니 코드가 길어지기도 하고, 가독성이 너무 떨어졌다. 바꾸는 과정에서 함수를 만들고 이 함수는 vector를 리턴하..
2025.04.25
C++
no image
[UE, C++] 캐릭터 움직여보기
드디어 언리얼 에디터 상에서 캐릭터를 움직여보려고 한다오늘은 애니메이션 적용까지는 하지않고 간단한 이동까지만 진행했다. 먼저 캐릭터를 만들기 전에 알아야하고, 캐릭터 이동에 쓰이는 이론들이 있기 때문에 내가 이해한대로 쉽게 정리를 해보았다. 월드 상의 무언가를 움직일 수 있게 하는 클래스월드 상에서 움직임을 구현하는 클래스 중에서는 Pawn Class가 있고, Character Class가 있다.이렇게 클래스를 검색했을 때 Pawn이 Character보다 더 상위 클래스인 것을 알 수 있다.이것만 보고 예상할 수 있는것은 Character 클래스는 Pawn 클래스보다 제공하는 함수가 더 많으려나?하고 예상을 해보았다. Pawn Class플레이어가 소유할 수 있는 가장 부모의 클래스하나하나 손수 로직을 짜..
2025.02.03
C++
[C++ ] C++의 꽃, 다형성에 대한 모든 개념 정리
매번 코드를 짜긴 짰는데.. 구조는 대강 설계할 수 있었으나 코드를 짤 때에는 gpt의 도움을 살짝씩 받았었다.그래서 인터넷 검색없이 코드를 짜라고 하면 헷갈리고 아직 이해가 부족한 부분이 많다고 생각했다.언리얼 엔진 문법을 익히기 전에 C++ 문법부터 제대로 익히고 들어가는게 이해하기 수월하고 맞을 것 같다는 생각이 들어 찐하게 학습을 진행하게 되었다. 이해하면서 정리를 했기 때문에 내가 어떻게 이해했는지와 비전공자들도 이해할 수 있을 정도로 아주아주 자세하게 과정을 적어놓았다.부디 이 글을 읽는 대부분의 사람들이 다형성을 이해하고 돌아가면 좋겠다.  다형성객체지향 프로그래밍의 핵심 개념 중 하나하나의 인터페이스로 다양한 객체를 다룰 수 있다는 특징코드의 재사용성과 확장에 굉장히 용이 가상 함수(vir..
2025.01.21
C++
no image
[ C++ ] Text_RPG 팀플 완료
오늘은 드디어 팀프로젝트로 진행하던 텍스트 게임 프로젝트를 끝냈다.간단한 소감과 배웠던 내용을 기록하려고 한다.  나의 담당파트- 좀비 타입별 무작위 스탯 설정- 팩토리를 사용한 좀비 랜덤 생성- 랜덤 유틸 함수 구현- 프로젝트 아스키 아트 추가 및 전반적인 멘트 통일화  소감전부터 팀플을 몇 번하긴 했었는데 오랜만에 하기도 하고, 낯을 많이 가려서 초반에는 말을 잘 못했던 것 같다.프로젝트 팀원은 컴퓨터공학을 전공한 사람과 비전공자가 섞인 6명으로 진행했다.이렇게 다양한 팀원들이 섞이면서 내가 몰랐던 부분도 배워가고 아는 부분은 또 알려주고, 문법이나 개발에 대한 이해가 더 증가한 것 같다. 팀원들이 다 착하고 열심히 해서 다음 프로젝트에도 같이 하면 좋을 것 같다. 게임 컨셉은 좀비로 진행했다.(사실..
2025.01.17
C++
[Visual Studio] 한글 깨짐 현상
오늘은 진행하던 팀 프로젝트를 병합하는 과정에서 한글 깨짐 현상이 발생했다.좀비 생성 부분은 게임 매니저와 다 병합했고 손이 남아서 프로젝트 전체 문구와 꾸미기를 담당했다. 아스키 아트라는 것을 알게 되었고, 얘로 꾸미니까 볼품없는 콘솔창이 뭔가 귀여워진 느낌이 들었다. 아스키아트아스키 아트(ASCII Art)는 ASCII(미국 표준 문자 코드)로 표현할 수 있는 문자, 숫자, 기호 등을 조합하여 만든 그림이나 디자인.컴퓨터의 텍스트 환경에서 그림이나 도형을 표현할 수 있는 방법으로 발전해왔고 아스키 아트는 주로 문자 기반 환경에서 이미지나 아이콘을 표현하기 위해 사용된다. /\_/\ ( o.o ) > ^ 간단하게 고양이를 출력해보았다. 귀여워.. 그리고 전체적으로 멘트를 통일화 시켰다. 어디는 ~ ..
2025.01.15
C++
no image
[C++] 진짜 랜덤값? 가짜 랜덤값?
저번 포스팅에서 올렸던 좀비 클래스를 구현하다가 랜덤함수를 검색했는데 진짜 랜덤값 이라는 키워드가 궁금증을 자극해서 알아보게 되었다. 먼저 랜덤값하면 말 그대로 어떤 값이 나올지 모르는, 출력이 되어야만 알 수 있는 그런 숫자라고 생각한다.사실 게임 속에서도 아이템을 획득하거나 뽑기를 할 때 랜덤으로 확률을 정하고 결과를 얻는 부분이 많이 있다.그래서 구현할 때는 모두 랜덤함수를 사용했을거라 생각했던 나... 하지만 랜덤값에도 가짜가 있다는 사!실! 알고있었나요...? 먼저 rand 함수와 srand()는 무엇인지 궁금해졌다.Seed 숫자(=시드)컴퓨터가 난수를 생성할 때 사용하는 특정한 시작 숫자.이 숫자를 기준으로 정해진 알고리즘을 실행하면서 마치 난수처럼 보이는 수열을 생성하는 것 srand()ra..
2025.01.14
C++
no image
[C++] Text RPG 몬스터 생성 로직 설계
간단하게 Text를 통해 RPG 게임을 만드는 팀 프로젝트를 진행 중이다.프로그램을 시작하면 플레이어 생성을 하고 플레이어의 레벨에 맞는 몬스터를 생성한다.그리고 플레이어 또는 몬스터의 체력이 0이 될 때까지 전투를 진행한 뒤 보상을 받고 상점 방문 여부를 받는다.그 후부터는 보스를 처치하거나 플레이어의 체력이 0이 될 때까지 계속 반복하는 흐름이다. 나는 여기서 몬스터 구현을 담당했다.로직을 꼼꼼하게 설계하면 코드 작성이 쉬워지기 때문에 설계에 많은 시간을 쏟았다.아래는 기능의 흐름과 어떤 것들을 구현하면 좋을지 몬스터 생성 관련 로직들을 설계한 내용이다.각 클래스가 가지고 있어야할 멤버 변수와 멤버 함수들을 정의했다.팀 회의를 통해 공통된 특성은 Actor라는 상위 클래스로 정의해서 상속받아 사용하는..
2025.01.13
C++
no image
[C++] 컴파일러와 인터프리터?
사실 코딩을 하다보면 절대 안들어본 사람이 없는 "컴파일"이게 대체 뭘까?? 사실 개발자에게도 컴파일러가 뭐에요?라고 물어보면 대답 못하는 사람도 있다...기초부터 탄탄하게!!! 컴파일러(Compiler)프로그래밍 언어로 작성된 소스 코드(흔히 우리가 에디터를 사용해서 쓰는 언어들)를 컴퓨터가 이해할 수 있는 기계어로 변환하는 소프트웨어 도구이다.프로그래머가 작성하는 코드는 고수준 언어 즉, High Level Language라고 불리고 기계어는 컴퓨터가 이해할 수 있는 언어 즉, Low Level Language라고 불린다.이 고수준 언어는 컴파일러라는 고마운 친구 덕분에 기계어 코드로 번역이 되는 것인데 이 과정은 컴파일(Compile)이라고 한다. 그렇다면 컴파일러는 이 고수준 언어를 저수준 언어로..
2025.01.03
C++
no image
[C++] Template, STL
2025년 1월 2일 목요일(2025년이 밝았따..!)신년인데도 집중이 잘 되지 않는데.. 왜 그럴까? 아직 이해를 잘 못해서 겁부터 먹는듯하다겁먹지마 파이팅!!! 오늘은 Template, STL에 관해 공부를 했다.Template일반화된 코드를 작성할 수 있는 문법정의할 함수 위에 template 를 추가하고, 일반화할 타입에 실제 타입 대신 T로 작성하면 끝!어떤 타입이 올지 모르지만 그 타입을 그냥 T라고 정의하겠다! 라는 의미근데 모든 매개변수 타입을 일반화할 필요는 없음배열의 크기 같은 경우는 항상 정수형이므로 그냥 int를 사용해줘도 문제 없음T Add(T arr[], int size) {}#include using namespace std;// general programming(일반화된 ..
2025.01.02
C++

[ C++ ] 프로그래머스 비밀지도

닿메_dahme
|2025. 4. 25. 16:20

 

문제 링크

https://school.programmers.co.kr/learn/courses/30/lessons/17681

 

프로그래머스

SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프

programmers.co.kr

 

문제

 

나의 풀이

1. 각 배열이 들어가있는 10진수를 2진수로 변환

2 . 2진수로 변환된 배열의 값들을 순회하면서 비교, 두 배열 중 하나라도 1이 있다면 temp에 1로 저장

3. temp 순회하면서 1이면 #, 0이면 공백을 answer에 추가

+ 처음엔 배열에 들어가있는 수를 하나하나 이진수로 바꾸는 작업을 했었는데 하다보니 코드가 길어지기도 하고, 가독성이 너무 떨어졌다. 바꾸는 과정에서 함수를 만들고 이 함수는 vector<int>를 리턴하는데 이러면 2차원 벡터에 이진수들이 하나하나 들어가있어서 arr1과 arr2를 비교하기에 굉장히 비용이 많이들고, 뎁스도 깊어지는 설계를 하고있었다.

그러던 와중에 까맣게 잊고 있었던 비트 연산을 보게되었고, 기존 설계보다 훨씬 간결하고 가독성 좋게 문제를 풀 수 있었다.

 

최종 코드

#include <string>
#include <vector>
#include <iostream>
#include <algorithm>

using namespace std;

vector<string> solution(int n, vector<int> arr1, vector<int> arr2) {
    vector<string> answer;
    
    for (int i = 0; i < n; i++)
    {
        string temp = "";
        arr1[i] = arr1[i] | arr2[i];
        
        while (temp.size() < n)
        {
            if (arr1[i] % 2 == 1)
                temp.push_back('#');
            else if (arr1[i] % 2 == 0)
                temp.push_back(' ');
            
            arr1[i] /= 2;
        }
        
        reverse(temp.begin(), temp.end());
        answer.push_back(temp);
    }
    return answer;
}

 

 

다른 코드

#include <string>
#include <vector>
#include <iostream>
#include <bitset>

using namespace std;

vector<string> solution(int n, vector<int> arr1, vector<int> arr2) {
    vector<string> answer;

    for (int i = 0; i < n; i++) {
        bitset<16> bits(arr1[i] | arr2[i]);

        string row = bits.to_string().substr(16 - n);
        for (char& c : row)
            c = (c == '1') ? '#' : ' ';

        answer.push_back(row);
    }

    return answer;
}

여기서는 따로 이진수 변환작업을 해주지않고 바로 bitset을 활용하여 넣어준 예시이다.

substr은 16개의 비트 00000000011111 이런식으로 저장되는데 결국은 마지막 n 번째의 비트만 필요하기 때문에 잘라주었다.

 

 

마지막 말

처음엔 10진수에서 2진수로 변환해야한다는 생각과 OR을 써야겠다는 생각까지는 접근했는데 비트연산을 생각못하고 2진수로 변환하는데 하나하나 하느라 이 과정에서 막혀버린 것 같다.

비트 연산을 공부했지만 막상 코드를 짤 때는 사용해봤던 적이 적은데 이렇게 진법 계산할 때나, 부분집합 구할 때 많이 사용한다고 한다.

검색하다가 비트 마스트라는 용어도 보았는데 어떻게 구현하는지 공부해봐야겠다.

[UE, C++] 캐릭터 움직여보기

닿메_dahme
|2025. 2. 3. 21:24

드디어 언리얼 에디터 상에서 캐릭터를 움직여보려고 한다

오늘은 애니메이션 적용까지는 하지않고 간단한 이동까지만 진행했다.

 

먼저 캐릭터를 만들기 전에 알아야하고, 캐릭터 이동에 쓰이는 이론들이 있기 때문에 내가 이해한대로 쉽게 정리를 해보았다.

 

월드 상의 무언가를 움직일 수 있게 하는 클래스

월드 상에서 움직임을 구현하는 클래스 중에서는 Pawn Class가 있고, Character Class가 있다.

이렇게 클래스를 검색했을 때 Pawn이 Character보다 더 상위 클래스인 것을 알 수 있다.

이것만 보고 예상할 수 있는것은 Character 클래스는 Pawn 클래스보다 제공하는 함수가 더 많으려나?하고 예상을 해보았다.

 

Pawn Class
  • 플레이어가 소유할 수 있는 가장 부모의 클래스
  • 하나하나 손수 로직을 짜줘야함

 

Character Class
  • 캐릭터를 움직이기 위한 여러 컴포넌트 붙어져 있음
  • 이족보행의 인간형 캐릭터만 적용할 수 있음
  • 장점
    • Jump, StopJump, Sprint 같은 캐릭터 이동을 구현할 때 필수적으로 구현해야하는 함수들이 내장되어있어 빠르고 간편하게 구현 가능함

 

PlayerComtroller Class
  • 만들어둔 캐릭터에 PlayerController가 빙의하는 것
    • 캐릭터가 3개가 있다고 할 때, PlayerController에 따라 어떤 캐릭터를 조작하는지 결정됨
  • 주요기능
    • Enhanced Input Sysytem - 입력처리(언리얼 엔진 5)
      • Input Mapping System (IMC)
        • IA들을 총괄해서 관리하는 역할
      • Input Action (IA) - 특정 동작 추상화(특정 동작 1개에 대해서만 전문적으로 처리) ← 한 동작 하나하나를 따로따로 저장하는 방식
        • 점프 → IA_Jump → Jump 함수
        • 마우스 회전 → IA_Look → Look 함수
        • 이동 → IA_Move → Move 함수
      • Enhanced Input Sysytem 장점
        1. IA로 동작별로 독립적으로 구현해놓고, IMC로 총괄하면 나중에 키 입력을 A → F로 변경하고 싶을 때 IA의 내부로직을 건드리지 않고도 IMC에서 간편하게 변경(관리) 가능
        2. 캐릭터가 자동차를 탔을 때 자동차의 입력으로 변경되어야하는 상황에서 IMC만 갈아끼울 수 있음(간단하게)
          • 사람 전용 IMC
          • 자동차 전용 IMC
          • 이렇게 있는데 if 문으로 조건에 맞게 활성화/비활성화 하는 방식
    • 카메라 제어 로직
    • UI 상호작용
      • 플레이어가 버튼을 누르거나 드래그 앤 드랍 이벤트가 발생했거나 하는 상황(어떤 상황인지 잘 이해안됨)
    • Possess / Unpossess - 빙의
      • 해당 캐릭터나 폰을 움직일 수 있게 해줌

 

 

예제 코드
#include "MyCharacter.h"
#include "MyPlayerController.h"
#include "EnhancedInputComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Camera/CameraComponent.h"

AMyCharacter::AMyCharacter()
{
	PrimaryActorTick.bCanEverTick = false;

	SpringArmComp = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
	SpringArmComp->SetupAttachment(RootComponent);
	SpringArmComp->TargetArmLength = 300.0f;
	SpringArmComp->bUsePawnControlRotation = true;
	
	CameraComp = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
	CameraComp->SetupAttachment(SpringArmComp, USpringArmComponent::SocketName);  // 소켓네임이 스프링암의 끝부분을 나타냄
	CameraComp->bUsePawnControlRotation = false;

	NormalSpeed = 600.0f;
	SprintSpeedMultiplier = 1.7f;
	SprintSpeed = NormalSpeed * SprintSpeedMultiplier;

	GetCharacterMovement()->MaxWalkSpeed = NormalSpeed;


}

void AMyCharacter::BeginPlay()
{
	Super::BeginPlay();
	
}

void AMyCharacter::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	if (UEnhancedInputComponent* EnhancedInput = Cast<UEnhancedInputComponent>(PlayerInputComponent))
	{
		if (AMyPlayerController* PlayerController = Cast<AMyPlayerController>(GetController()))
		{
			if (PlayerController->MoveAction)
			{
				EnhancedInput->BindAction(PlayerController->MoveAction, ETriggerEvent::Triggered, this, &AMyCharacter::Move);
			}
			if (PlayerController->JumpAction)
			{
				EnhancedInput->BindAction(PlayerController->JumpAction, ETriggerEvent::Triggered, this, &AMyCharacter::StartJump);
				EnhancedInput->BindAction(PlayerController->JumpAction, ETriggerEvent::Completed, this, &AMyCharacter::StopJump);
			}
			if (PlayerController->LookAction)
			{
				EnhancedInput->BindAction(PlayerController->LookAction, ETriggerEvent::Triggered, this, &AMyCharacter::Look);
			}
			if (PlayerController->SprintAction)
			{
				EnhancedInput->BindAction(PlayerController->SprintAction, ETriggerEvent::Triggered, this, &AMyCharacter::StartSprint);
				EnhancedInput->BindAction(PlayerController->SprintAction, ETriggerEvent::Completed, this, &AMyCharacter::StopSprint);
			}
		}
	}

}

void AMyCharacter::Move(const FInputActionValue& value)
{
	if (!Controller) return;

	const FVector2D MoveInput = value.Get<FVector2D>();

	if (!FMath::IsNearlyZero(MoveInput.X))
	{
		AddMovementInput(GetActorForwardVector(), MoveInput.X);
	}
	if (!FMath::IsNearlyZero(MoveInput.Y))
	{
		AddMovementInput(GetActorRightVector(), MoveInput.Y);
	}
}

void AMyCharacter::StartJump(const FInputActionValue& value)
{
	if (value.Get<bool>())
		Jump();
}

void AMyCharacter::StopJump(const FInputActionValue& value)
{
	if (!value.Get<bool>())
		StopJumping();
}

void AMyCharacter::Look(const FInputActionValue& value)
{
	FVector2D LookInput = value.Get<FVector2D>();

	AddControllerYawInput(LookInput.X);
	AddControllerPitchInput(LookInput.Y);  // ia 만들 때 y값은 반전시켜놓은 상태
}

void AMyCharacter::StartSprint(const FInputActionValue& value)
{
	if (GetCharacterMovement())
	{
		GetCharacterMovement()->MaxWalkSpeed = SprintSpeed;
	}
}

void AMyCharacter::StopSprint(const FInputActionValue& Value)
{
	if (GetCharacterMovement())
	{
		GetCharacterMovement()->MaxWalkSpeed = NormalSpeed;
	}
}
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "MyCharacter.generated.h"

class USpringArmComponent;
class UCameraComponent;
struct FInputActionValue;

UCLASS()
class PRACTICE_API AMyCharacter : public ACharacter
{
	GENERATED_BODY()

public:
	AMyCharacter();

protected:	
	virtual void BeginPlay() override;
	virtual void Tick(float DeltaTime) override;

	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
	USpringArmComponent* SpringArmComp;
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
	UCameraComponent* CameraComp;

	UFUNCTION()
	void Move(const FInputActionValue& value);  // 참조로 가져오는 이유 : 구조체는 엄청 큰 사이즈이기 때문에 성능, 복사 비용이 크다. 그래서 참조로 가져옴
	UFUNCTION()
	void StartJump(const FInputActionValue& value);  // Trigger로 받는 애들은 시작과 끝을 나눠주는 것이 좋음(EnhancedInputSystem의 취지와도 맞음)
	UFUNCTION()
	void StopJump(const FInputActionValue& value);
	UFUNCTION()
	void Look(const FInputActionValue& value);
	UFUNCTION()
	void StartSprint(const FInputActionValue& value);
	UFUNCTION()
	void StopSprint(const FInputActionValue& value);

private:
	float NormalSpeed;
	float SprintSpeedMultiplier;  // 몇 배 곱해줄건지
	float SprintSpeed;


};

 

이렇게 구현을 해보았다. 지금은 Character 클래스를 상속받아 구현하는 중이다.

 

 

결과

 

자세가 좀 경직되어있지만 잘 움직이는 것을 볼 수 있다.

 

 

마지막 말

Character 클래스가 Jump나 여러 함수를 제공해줘서 여러모로 편했다.

Pawn 클래스를 상속받아 하나하나 구현해보면 더욱 실력이 늘 것 같다.

또한, 언리얼의 장점인 오픈소스답게 Character 클래스에서 제공하는 Jump 함수는 어떤 로직을 사용하는지, 엔진 내부 코드들을 하나하나 뜯어볼 수 있어서 재밌었던 것 같다.

 

 

매번 코드를 짜긴 짰는데.. 구조는 대강 설계할 수 있었으나 코드를 짤 때에는 gpt의 도움을 살짝씩 받았었다.

그래서 인터넷 검색없이 코드를 짜라고 하면 헷갈리고 아직 이해가 부족한 부분이 많다고 생각했다.

언리얼 엔진 문법을 익히기 전에 C++ 문법부터 제대로 익히고 들어가는게 이해하기 수월하고 맞을 것 같다는 생각이 들어 찐하게 학습을 진행하게 되었다.

 

이해하면서 정리를 했기 때문에 내가 어떻게 이해했는지와 비전공자들도 이해할 수 있을 정도로 아주아주 자세하게 과정을 적어놓았다.

부디 이 글을 읽는 대부분의 사람들이 다형성을 이해하고 돌아가면 좋겠다.

 

 

다형성
  • 객체지향 프로그래밍의 핵심 개념 중 하나
  • 하나의 인터페이스로 다양한 객체를 다룰 수 있다는 특징
  • 코드의 재사용성과 확장에 굉장히 용이

 

가상 함수(virtual)
  • 부모 클래스에서 선언된 함수이지만 파생 클래스에서 재정의(override)할 수 있음
    • 파생 클래스에서 정의해도 되고, 안 해도 됨
  • 파생 클래스에서 재정의를 하게 된다면 파생 클래스에서 재정의한 함수가 호출
  • 재정의 하지 않았다면 부모 클래스의 기본 구현된 함수가 호출

 

가상 함수 선언 문법
virtual void makeSound() const;  // 가상 함수 선언

// 파생 클래스에서 재정의하는 것은 선택

 

순수 가상 함수(virtual)
  • 파생 클래스에서 반드시 재정의해야 하는 멤버 함수
  • 재정의하지 않으면 사용할 수 없음(컴파일 에러 발생)
  • 다형성을 활용하여 파생 클래스마다 반드시 다른 동작을 구현하도록 강제

 

순수 가상 함수 선언 문법
virtual void makeSound() const = 0;  // 순수 가상 함수 선언

// 반드시 파생 클래스에서 makeSound를 재정의해야만 사용할 수 있음

(자세한 예시는 추상 클래스 예제 코드와 함께)

 

추상클래스(abstract class)
  • 특별히 기본 클래스로 사용되도록 설계된 클래스
  • 하나 이상의 순수 가상 함수가 포함되어 있음
    • 추상클래스로 정의하고 싶다면 멤버 변수 없이 순수 가상 함수로만 이뤄진 클래스 형태가 가장 바람직
  • 객체를 자체적으로 생성할 수 없고 반드시 상속을 거쳐야만 사용 가능(아래에서 자세히 설명)
    • 상속받은 파생 클래스 또한 반드시 부모의 순수 가상 함수를 override 해줘야 됨

 

객체 생성이 안되는 예제 코드

#include <iostream>
#include <string>

using namespace std;

class Animal
{
public:
    Animal() {};
    ~Animal() {};
    virtual void makeSound() const = 0;  // 순수 가상 함수
    //virtual void makeSound() const {};  // 가상 함수
};

class Cat : public Animal
{
};

class Dog
{
public:
    void makeSound()
    {
        cout << "Animal을 상속받지 않은 Dog 입니다." << endl;
    }
};

int main()
{
    Animal animal;  // 에러
    Cat cat;  // 에러
    Dog dog;

    /*animal.makeSound();
    cat.makeSound();
    dog.makeSound();*/

}

이유 : Animal 클래스에는 makeSound라는 순수 가상 함수가 선언되어있음.

즉, Animal 클래스는 추상 클래스가 됨. 따라서 Animal 클래스 자체적으로는 객체 생성이 불가능함

Cat 클래스는 Animal을 상속받는 자식 클래스가 되는데, 순수 가상 함수는 상속받은 객체에서 무조건 재정의해야하지만 Cat 클래스에서는 순수 가상 함수인 makeSound를 재정의하지 않아 객체 선언 불가능함.

 

그럼 이 코드가 정상적으로 동작할 수 있게 하는 방법은?

#include <iostream>
#include <string>

using namespace std;

class Animal
{
public:
    Animal() {};
    ~Animal() {};
    virtual void makeSound() const = 0;  // 순수 가상 함수
    //virtual void makeSound() const {};  // 가상 함수
};

class Cat : public Animal
{
public:
    void makeSound() const override
    {
        cout << "Animal을 상속받았고, Cat에서 makeSound 순수가상함수 overriding 했음." << endl;
    }
};

class Dog
{
public:
    void makeSound()
    {
        cout << "Animal을 상속받지 않은 Dog 입니다." << endl;
    }
};

int main()
{
    //Animal animal;
    Cat cat;
    Dog dog;

    //animal.makeSound();
    cat.makeSound();
    dog.makeSound();

}

Cat에서 상속받은 추상 클래스 Animal의 순수가상함수 makeSound를 override으로 재정의 해줌으로써 객체 선언이 가능해짐.

결과

 

부모 클래스 포인터
  • 다형성을 구현하기 위해서 부모 클래스 타입의 포인터를 사용함
  • 이를 통해 해당 부모 클래스를 상속받는 다른 파생 클래스에도 쉽고 유연하게 접근 가능

 

부모 클래스 포인터 타입으로 객체를 관리하는 예제 코드 1 (다형성 활용)

Animal* animals[3];
animals[0] = new Cat();
animals[1] = new Dog();
animals[2] = new Bird();

for (int i = 0; i < 3; i++) {
    animals[i]->makeSound();  // 각 클래스의 makeSound()가 호출됨.
    delete animals[i];        // 동적 할당된 메모리 해제.
}

 

부모 클래스 포인터 타입으로 객체를 관리하는 예제 코드 2 (다형성 활용)

Animal* cat = new Cat();
cat->makeSound();  // Cat 클래스의 makeSound() 호출.

Animal* dog = new Dog();
dog->makeSound();  // Dog 클래스의 makeSound() 호출.

 

이렇게 부모 클래스 포인터 타입으로 하면 좋은 이유
Animal* animal = new Cat();  // Animal 포인터로 Cat 객체 다룸.
animal->makeSound();         // Cat의 makeSound()가 호출됨.

animal = new Dog();          // Animal 포인터로 Dog 객체 다룸.
animal->makeSound();         // Dog의 makeSound()가 호출됨.

  1. 부모 클래스인 Animal 타입의 animal 포인터 하나로 Animal을 상속받고 있는 파생 클래스에 접근하기 유용
  2. 메모리 낭비 최소화할 수 있음

 

부모 클래스 포인터 타입으로 객체를 관리하지 않는 예제 코드(다형성 활용 x)

Cat* cat = new Cat();
Dog* dog = new Dog();

cat->makeSound();  // Cat의 메서드 호출.
dog->makeSound();  // Dog의 메서드 호출.

delete cat;
delete dog;

 

인터페이스(Interface)
  • 순수 가상 함수로만 이뤄진 추상 클래스를 인터페이스라고 칭함
  • 인터페이스는 구현이 아닌 설계(구조)만 제공
  • 멤버 변수를 가지지 않으며 오로지 메서드만 정의
  • 다중 상속 가능(순수 가상 함수만 포함되어있으므로 충돌 가능성이 낮기 때문)

 

 

마지막 말

이렇게 큰 틀은 다형성이지만 그 안에 우리가 개발하면서 필요한 여러 지식들이 들어가 있는 모습을 볼 수 있다.

다형성을 사용하면 특히 메스드의 이름 같은 부분을 절약할 수 있어서 좋고, 다양하게 재정의하는 부분이 재밌게 느껴지는 것 같다.

'C++' 카테고리의 다른 글

[ C++ ] 프로그래머스 비밀지도  (0) 2025.04.25
[UE, C++] 캐릭터 움직여보기  (0) 2025.02.03
[ C++ ] Text_RPG 팀플 완료  (1) 2025.01.17
[Visual Studio] 한글 깨짐 현상  (0) 2025.01.15
[C++] 진짜 랜덤값? 가짜 랜덤값?  (0) 2025.01.14

[ C++ ] Text_RPG 팀플 완료

닿메_dahme
|2025. 1. 17. 00:17

오늘은 드디어 팀프로젝트로 진행하던 텍스트 게임 프로젝트를 끝냈다.

간단한 소감과 배웠던 내용을 기록하려고 한다.

 

 

나의 담당파트

- 좀비 타입별 무작위 스탯 설정

- 팩토리를 사용한 좀비 랜덤 생성

- 랜덤 유틸 함수 구현

- 프로젝트 아스키 아트 추가 및 전반적인 멘트 통일화

 

 

소감

전부터 팀플을 몇 번하긴 했었는데 오랜만에 하기도 하고, 낯을 많이 가려서 초반에는 말을 잘 못했던 것 같다.

프로젝트 팀원은 컴퓨터공학을 전공한 사람과 비전공자가 섞인 6명으로 진행했다.

이렇게 다양한 팀원들이 섞이면서 내가 몰랐던 부분도 배워가고 아는 부분은 또 알려주고, 문법이나 개발에 대한 이해가 더 증가한 것 같다. 팀원들이 다 착하고 열심히 해서 다음 프로젝트에도 같이 하면 좋을 것 같다.

 

게임 컨셉은 좀비로 진행했다.(사실 내가 좀비를 너무 좋아하고,, 좀보이드도 너무 재밌게해서 적극적으로 의견을 냈다.)

 

초반에 개발 구조를 어떻게 잡을지를 확실하게 정해야했는데, 다들 경험이 부족하다보니 머리부터 박고 시작해서 중간에 조금 난황을 겪었다.

그리고 필수 기능까지 빠르게하고 그 다음에 도전을 하던지 하자 라는 방향으로 흘러갔는데, 중간에 튜터님의 피드백이 있기도 했고... 나도 작업하다가 더 재밌어져서 필수/도전 외에 더 추가적인 기능이 있으면 좋을 것 같다는 생각을 했었다. 

다른 팀원분들도 사실 그렇게 생각했을지도?

왜냐면 추가기능에 대한 회의를 하면서 아이디어를 모았는데 다들 너무 재밌고 참신한 아이디어를 마구마구 내주셔서 실력만 있다면 그걸 전부 다 구현해보고 싶을 정도였다.

 

프로젝트가 진행되면서 다들 낯도 많이 풀리고, 회의도 많이하고, 아이디어도 많이내서 프로젝트가 점점 맛있어져갔다!(뿌듯)

중간에 깃 충돌해결과 머지하는데 살짝 오류가 나긴 했으나,, 회사 다닐 때 배웠던 깃 다루는 법을 사용해서 큰 문제없이 해결했다.(이것도 뿌듯)

 

그리고 깃 머지하는 과정에서 다른 팀원이 풀리퀘스트를 이용하는 방법으로 머지를 하자고 했다.

나도 뿔리퀘를 사용하는걸 선호하지만..(회사다닐 때, 뿔리퀘로 대부분의 코드를 올렸고,, 그때는 경험이 많은 팀장님께서 코드를 봐주셨기 때문에..) 그 때 상황은 각자 기능 개발하느라 코드리뷰할 여유가 없기도 했고 뿔리퀘로 올리면 레포지토리 주인장이 귀찮아지기 때문에...  그냥 깃 데스크탑에서 간단하게 머지하는 방법을 살짝..? 알려줬다..

 

그래도 팀원들 중에서도 깃을 다뤄보신 분들이 계서서 좀 수월했던 것 같다.

 

프로젝트 기간은 일주일 밖에 안되지만, 하루에 많은 시간을 할애하기도 했고 몰입했던터라 짧은 기간이지만 많은 내용을 배워가는 것 같다.

특히 협업과정을 경험한건 진짜 귀중했던 시간이라고 생각한다.

다른 개발자 분들의 설계나 코드 작성 스타일, 일정 관리, 담당자들 간의 소통 등 진짜 배워가는 점이 많은 프로젝트였다.

 

 

 

배운 내용

- 함수 오버라이딩

- 유니크 포인터 사용 방법

- 코드 병합

 

 

트러블슈팅

- 비주얼 스튜디오, 깃헙 한글 인코딩 문제

 

[Visual Studio] 한글 깨짐 현상

닿메_dahme
|2025. 1. 15. 22:15

오늘은 진행하던 팀 프로젝트를 병합하는 과정에서 한글 깨짐 현상이 발생했다.

좀비 생성 부분은 게임 매니저와 다 병합했고 손이 남아서 프로젝트 전체 문구와 꾸미기를 담당했다.

 

아스키 아트라는 것을 알게 되었고, 얘로 꾸미니까 볼품없는 콘솔창이 뭔가 귀여워진 느낌이 들었다.

 

아스키아트

아스키 아트(ASCII Art)는 ASCII(미국 표준 문자 코드)로 표현할 수 있는 문자, 숫자, 기호 등을 조합하여 만든 그림이나 디자인.
컴퓨터의 텍스트 환경에서 그림이나 도형을 표현할 수 있는 방법으로 발전해왔고 아스키 아트는 주로 문자 기반 환경에서 이미지나 아이콘을 표현하기 위해 사용된다.

 /\_/\  
( o.o ) 
 > ^ <

간단하게 고양이를 출력해보았다. 귀여워..

 

그리고 전체적으로 멘트를 통일화 시켰다. 어디는 ~ 했다.로 출력되고 또 어디는 ~ 했음. 이렇게 출력이 되어서 뒤죽박죽인 느낌이 들었었다.

 

// 작성 중

 

[C++] 진짜 랜덤값? 가짜 랜덤값?

닿메_dahme
|2025. 1. 14. 21:49

저번 포스팅에서 올렸던 좀비 클래스를 구현하다가 랜덤함수를 검색했는데 진짜 랜덤값 이라는 키워드가 궁금증을 자극해서 알아보게 되었다.

 

먼저 랜덤값하면 말 그대로 어떤 값이 나올지 모르는, 출력이 되어야만 알 수 있는 그런 숫자라고 생각한다.

사실 게임 속에서도 아이템을 획득하거나 뽑기를 할 때 랜덤으로 확률을 정하고 결과를 얻는 부분이 많이 있다.

그래서 구현할 때는 모두 랜덤함수를 사용했을거라 생각했던 나...

 

하지만 랜덤값에도 가짜가 있다는 사!실! 알고있었나요...?

 

먼저 rand 함수와 srand()는 무엇인지 궁금해졌다.

Seed 숫자(=시드)
  • 컴퓨터가 난수를 생성할 때 사용하는 특정한 시작 숫자.
  • 이 숫자를 기준으로 정해진 알고리즘을 실행하면서 마치 난수처럼 보이는 수열을 생성하는 것

 

srand()
  • rand 함수에서 사용될 수를 초기화하는 역할
  • 매개변수로 seed 값을 받아서 초기화
  • 정해진 값을 넣게되면 rand 값도 항상 정해지기 때문에 랜덤값이 아님
    • 즉 항상 변하는 값을 넣어줘야하는데 사람들이 생각해낸 것이 바로 시간값

 

rand() 함수 특징
  • 랜덤하게 생성되는 숫자의 크기가 최대 32767까지
  • 생성되는 숫자의 분포가 균일하지 않다
  • 알고리즘이 있기 때문에 설정한 Seed 숫자만 알면 어떤 숫자가 랜덤으로 생성되는지 알 수 있다
  • 프로그램이 생성될 때 값이 정해지기 때문에 여러 번 실행시켜도 동일한 값이 나온다
  • 시간(time)으로 시드 생성(C 스타일)

 

랜덤값 테스트

과연 가짜 랜덤값이 무엇인지 확인해보기 위해 랜덤으로 숫자를 추출하는 간단한 코드로 테스트를 진행해보았다.

#include <iostream>
#include <cstdlib>
#include <ctime>

using namespace std;
int main()
{
    time_t now = time(0);
    tm localTime;

    localtime_s(&localTime, &now);

    cout << "현재 시간: ";
    cout << localTime.tm_hour << ":"
        << localTime.tm_min << ":"
        << localTime.tm_sec << endl;

    for (int i = 0; i < 10; ++i) {
        int num = rand() % 10;
        cout << num << endl;
    }

    return 0;
}

 

결과는??

 

1, 7, 4, 0, 9, 4, 8, 8, 2, 4가 나왔다.

 

자 그럼 다시 한 번 실행해보면 랜덤으로 값이 나오는거니까 또 다른 10개의 값이 출력되겠지?

으잉? 이게 무슨 일이야 같은 값이 나온다.

이유는 처음 실행할 때 난수를 지정하고 그게 저장이 되어서 다음 실행을 해도 똑같은 값이 나오기 때문이다.

 

이걸 해결하기 위해 rand로 값을 반환하기 전에 strand에 시드값으로 항상 다른 값이 나오는 시간을 넣어준다.

기존 작성한 코드에 srand(time(nullptr)); 한 줄을 추가해준다.

#include <iostream>
#include <cstdlib>
#include <ctime>

using namespace std;
int main()
{
    time_t now = time(0);
    tm localTime;

    localtime_s(&localTime, &now);

    cout << "현재 시간: ";
    cout << localTime.tm_hour << ":"
        << localTime.tm_min << ":"
        << localTime.tm_sec << endl;

    srand(time(nullptr));
    for (int i = 0; i < 10; ++i) {
        int num = rand() % 10;
        cout << num << endl;
    }

    return 0;
}

 

결과는?

 

 

이렇게 시간값을 시드로 넣어주니 항상 값이 변하는 것을 볼 수 있다.

하지만 rand 함수는 정해진 알고리즘이 실행되면서 값을 반환하기 때문에 시드값이 어떤 것이 들어갔는지 알아낼 수 있다면 어떤 랜덤값이 나오는지 알 수 있다.

 

그럼 과연 진짜 랜덤값은 무엇일까?

 

<random> 라이브러리
  • 더 좋은 시드값을 위해 random_device 제공
  • std::random_device는 균일한 분포(uniformly-distributed)를 가진 비결정적(non-deterministic)인 int 숫자를 생성하는 클래스
  • 다양한 난수 생성 엔진과 분포(distribution)를 제공

 

 

std::random_device는 균일한 분포(uniformly-distributed)를 가진 비결정적(non-deterministic)인 int 숫자를 생성하는 클래스이다.

#include <iostream>
#include <cstdlib>
#include <ctime>
#include <random>

using namespace std;
int main()
{
    time_t now = time(0);
    tm localTime;

    localtime_s(&localTime, &now);

    cout << "현재 시간: ";
    cout << localTime.tm_hour << ":"
        << localTime.tm_min << ":"
        << localTime.tm_sec << endl;

    random_device rd;

    cout << "Min Value : " << rd.min() << ", " << "Max Value : " << rd.max() << endl;

    for (int i = 0; i < 3; i++) {
        cout << rd() << endl;
    }

    return 0;
}

 

 

이렇게 하면 random_device가 제공하는 최대 최소값으로 랜덤값에 접근할 수 있다.

하지만 내가 범위를 지정하고 싶다면?

 

바로 분포를 이용하면 된다

#include <iostream>
#include <random>

int main() {
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> dist(0, 99); // 0~99 범위임의로 지정했따

    for (int i = 0; i < 5; ++i) {
        std::cout << dist(gen) << " ";
    }
    return 0;
}

이렇게 겉으로 보기엔 좀 어려워보이지만 클래스 선언만 하고 분포값만 지정해주면 간단하게 진짜 랜덤한 값을 뽑아낼 수 있는 것이다.

 

정리해보자면 radnom_device로 랜덤한 시드를 받아온 다음, 이 시드를 기반으로 mt19937에서 알고리즘이 동작해서 랜덤값이 나오게 되는 것이다.

 

마무리말

여기까지 랜덤값을 어떻게 가져오면 균등한 확률로 뽑아낼 수 있을지에 대해 알아보았다.

공부하다보니 아직 배울 내용이 너무 많다는 걸 느꼈고 배웠던 내용도 시간이 지나면 잊어버리게 되어 꾸준히 복습하는 것이 중요하다는 것을 느꼈다.

그리고 구현되어있는 내장함수의 구조는 어떤지, 무슨 타입의 값을 받고 리턴하는지 자세히 들여다볼 수 있는 기회가 되었다.

 

'C++' 카테고리의 다른 글

[ C++ ] Text_RPG 팀플 완료  (1) 2025.01.17
[Visual Studio] 한글 깨짐 현상  (0) 2025.01.15
[C++] Text RPG 몬스터 생성 로직 설계  (1) 2025.01.13
[C++] 컴파일러와 인터프리터?  (1) 2025.01.03
[C++] Template, STL  (0) 2025.01.02

간단하게 Text를 통해 RPG 게임을 만드는 팀 프로젝트를 진행 중이다.

프로그램을 시작하면 플레이어 생성을 하고 플레이어의 레벨에 맞는 몬스터를 생성한다.

그리고 플레이어 또는 몬스터의 체력이 0이 될 때까지 전투를 진행한 뒤 보상을 받고 상점 방문 여부를 받는다.

그 후부터는 보스를 처치하거나 플레이어의 체력이 0이 될 때까지 계속 반복하는 흐름이다.

 

나는 여기서 몬스터 구현을 담당했다.

로직을 꼼꼼하게 설계하면 코드 작성이 쉬워지기 때문에 설계에 많은 시간을 쏟았다.

아래는 기능의 흐름과 어떤 것들을 구현하면 좋을지 몬스터 생성 관련 로직들을 설계한 내용이다.

각 클래스가 가지고 있어야할 멤버 변수와 멤버 함수들을 정의했다.

팀 회의를 통해 공통된 특성은 Actor라는 상위 클래스로 정의해서 상속받아 사용하는 방식으로 채택했다.

 

설계한 내용은 아래와 같다.

- 몬스터 생성은 랜덤으로 진행
- 체력과 공격력은 플레이어의 레벨을 받아 플레이어의 수준에 맞게 랜덤값을 부여
- 몬스터 종류는 총 3개의 종류로 설정
- 추후 각 몬스터별 스킬이나 새로운 기능을 추가할 가능성이 있으므로 각각의 파일을 따로 만들어서 관리
- 몬스터 생성은 Factory 클래스를 만들어서 생성 관련 로직만 담당


이렇게 설계한 내용으로 코드를 작성해보았다.

#pragma once
#include "Actor.h"
#include <memory>

class ZombieFactory
{
public:
    ZombieFactory();
    std::unique_ptr<Actor> CreateZombie(const int playerLevel);
};

 

#include "ZombieFactory.h"
#include "NormalZombie.h"
#include "VariantZombie.h"
#include "HalfZombie.h"
#include "BossZombie.h"
#include "RandomUtil.h"
#include <iostream>

using namespace std;

ZombieFactory::ZombieFactory()
{
}

unique_ptr<Actor> ZombieFactory::CreateZombie(const int playerLevel)
{
    int randomValue = RandomUtil::GetRandomInt(1, 3);

    // Player 레벨이 10 이상일 때 보스 좀비 생성
    if (playerLevel >= 10)
        return make_unique<BossZombie>(playerLevel);

    // 랜덤으로 나온 값을 바탕으로 좀비 생성
    // TODO : main에서 랜덤 값 정의하고 좀비 타입만 넣어서 하는 완전히 분리된 좀비팩토리 만들면 좋을듯
    switch (randomValue)
    {
        case 1:
            return make_unique<NormalZombie>(playerLevel);
        case 2:
            return make_unique<VariantZombie>(playerLevel);
        case 3:
            return make_unique<HalfZombie>(playerLevel);
        default:
            return nullptr;
    }
}

 

ZombieFactory는 Actor를 기반으로 세 종류의 좀비 클래스들을 동적 할당해서 포인터로 반환한다.

CreateZombie()는 플레이어의 레벨을 받아서 레벨에 맞게 몬스터들을 생성해주는 역할을 하도록 코드를 짰다.

그리고 매개변수로 받은 플레이어 레벨값은 받아서 전달만 하고 따로 변경하거나 하지는 않아서 const를 붙여줬다.

만약 플레이어 레벨이 10 이상이라면 보스 몬스터를 출현하도록 했다.

 

랜덤값을 부여할 때 rand() 함수를 생각했었는데, 검색해보니 rand()는 비효율적이라고 한다.

rand()는 0 ~ 9까지의 시드?를 사용하는데 만약 사용자가 랜덤 범위를 0에서 3까지로 설정했다면

0, 1, 2, 3, 4, 5, 6, 7, 8, 9 의 공간이
0, 1, 2, 3, 0, 1, 2, 3, 0, 1 로 설정되면서 0과 1이 나올 확률이 더 높아지기 때문에 정밀한 랜덤값을 추출하는데 한계가 있다고 한다.

시드?라는 용어를 사용했고 왜 비효율적인지 자세히 이해는 못했다. 다시 한 번 공부를 해야할 내용이다.

 

그리고 아래는 각 좀비타입별로 분리해놓은 좀비 클래스이다. 이름만 다를 뿐 전체 로직은 같다.

#pragma once

#include "Actor.h"
#include <string>
#include <iostream>

class NormalZombie : public Actor
{
public:
    NormalZombie(const int playerLevel);

    std::string GetName() const;
    int GetMaxHP() const;
    int Attack() const;
    void OnHit(int inputAttackPower) const override;
    void PrintStatus() const;
};

공통 특성들을 정의해놓은 상위 클래스 Actor를 상속받아 진행했다.

Get이라는 키워드가 붙은 멤버함수들은 값을 따로 변경하거나 하는 부분 없이 그냥 값을 반환하는 형태의 로직이라 모두 const를 붙여주었다.

 

#include "NormalZombie.h"
#include "RandomUtil.h"

using namespace std;

NormalZombie::NormalZombie(const int playerLevel) : Actor("", 0, 0)
{
    Name = "NormalZombie";
    MaxHP = RandomUtil::GetRandomInt(playerLevel * 20, playerLevel * 30);;
    AttackPower = RandomUtil::GetRandomInt(playerLevel * 5, playerLevel * 10);
    HP = MaxHP;
}


string NormalZombie::GetName() const
{
    return Name;
}

int NormalZombie::GetMaxHP() const
{
    return MaxHP;
}

int NormalZombie::Attack() const
{
    return AttackPower;
}

void NormalZombie::OnHit(int inputAttackPower) const
{
    Actor::OnHit(inputAttackPower);

    cout << Name << "는 " << inputAttackPower << "의 데미지를 입었다..!" << endl;
    
    if (IsDead())
        cout << Name << "는 마지막 일격으로 사망했습니다 !!!" << endl;
}

void NormalZombie::PrintStatus() const
{
    cout << "Name : " << Name << endl;
    cout << "Attack power : " << AttackPower << endl;
    cout << "Current health point : " << HP << endl;
}

생성자는 플레이어의 레벨을 입력받아 랜덤한 값으로 체력과 공격력을 초기화한다.\

 

체력과 공격력을 부여하는 부분에서 랜덤값이 반복해서 사용되었기 때문에 랜덤값을 뽑아내는 부분을 따로 클래스를 만들어서 유틸리티 형태로 만들었다.
(다른 클래스에서 랜덤값을 사용할 일이 있을 것 같아 다른 팀원분들이 사용하기 편하라고 만들었다)

#include "RandomUtil.h"

using namespace std;

namespace RandomUtil
{
	default_random_engine generator(random_device{}());

	int GetRandomInt(int min, int max)
	{
		uniform_int_distribution<int> randomValue(min, max);
		return randomValue(generator);
	}
}

하지만 위에서 언급했듯이 아직 이 랜덤 엔진을 사용하는 방법이 미숙하고 왜 이 방법을 사용하는 것이 효율적인지 과정에 대해 이해를 못했기 때문에 다시 공부를 해야할 것 같다.

완벽하게 이해를 했다면 값이 들어갔을 때 시드를 공평하게 분배해서 평등한 확률이 나올 수 있도록 로직을 수정할 계획이다.

 

더 공부해야할 내용

- random 라이브러리의 동작 구조
- 포인터
- 업캐스팅, 다운캐스팅

 

마무리 말

글로 로직을 설계하는 것은 할 수 있는데 내가 짠 코드를 막상 설명하려고 하니까 말이 제대로 안나오고 뇌절하는 상황이 벌어졌다.

아직 개념이 제대로 정리되지 않았는데 말로 설명하려니 어렵게 느껴졌던 것 같다.(이럴까봐 gpt는 되도록 사용하지 않았는데...)

좀비 클래스를 구현하면서 사용했던 문법이나 개념을 다시 한번 확실하게 잡고 누군가에게 설명할 수준이 되도록 학습을 진행해야겠다.

 

 

'C++' 카테고리의 다른 글

[Visual Studio] 한글 깨짐 현상  (0) 2025.01.15
[C++] 진짜 랜덤값? 가짜 랜덤값?  (0) 2025.01.14
[C++] 컴파일러와 인터프리터?  (1) 2025.01.03
[C++] Template, STL  (0) 2025.01.02
[C++] 유니크 포인터  (0) 2024.12.31

[C++] 컴파일러와 인터프리터?

닿메_dahme
|2025. 1. 3. 21:40

사실 코딩을 하다보면 절대 안들어본 사람이 없는 "컴파일"

이게 대체 뭘까?? 사실 개발자에게도 컴파일러가 뭐에요?라고 물어보면 대답 못하는 사람도 있다...

기초부터 탄탄하게!!!

 

컴파일러(Compiler)

프로그래밍 언어로 작성된 소스 코드(흔히 우리가 에디터를 사용해서 쓰는 언어들)를 컴퓨터가 이해할 수 있는 기계어로 변환하는 소프트웨어 도구이다.

프로그래머가 작성하는 코드는 고수준 언어 즉, High Level Language라고 불리고 기계어는 컴퓨터가 이해할 수 있는 언어 즉, Low Level Language라고 불린다.

이 고수준 언어는 컴파일러라는 고마운 친구 덕분에 기계어 코드로 번역이 되는 것인데 이 과정은 컴파일(Compile)이라고 한다.

 

그렇다면 컴파일러는 이 고수준 언어를 저수준 언어로 변역하는 일만 할까?

그건 아니다.

 

컴파일러의 역할

  • 구문 분석 (Parsing):
    • 소스 코드를 읽고, 문법적으로 올바른지 검사한다.
    • 코드의 구조를 이해하고, 이를 구문 트리(Syntax Tree)로 변환한다.
  • 의미 분석 (Semantic Analysis):
    • 변수의 타입 검사, 함수 호출의 유효성 검토 등 코드의 의미적 오류를 검사한다.
    • 코드가 논리적으로 올바르게 작성되었는지 확인한다.
  • 최적화 (Optimization):
    • 코드의 실행 효율성을 높이기 위해 불필요한 연산을 제거하거나, 더 효율적인 연산으로 대체한다.
    • 메모리 사용량을 줄이고, 실행 속도를 향상시키기 위한 다양한 최적화 기법을 적용한다.
  • 코드 생성 (Code Generation):
    • 최적화된 중간 코드를 대상 플랫폼의 기계어로 변환한다.
    • 실행 파일이나 목적 파일(Object File)을 생성한다.
  • 링크 (Linking):
    • 여러 개의 목적 파일과 라이브러리를 하나의 실행 파일로 결합한다.
    • 외부 참조를 해결하고, 최종 실행 가능한 프로그램을 만든다.
  • 에러 보고 (Error Reporting):
    • 소스 코드에 존재하는 문법적, 의미적 오류를 개발자에게 알린다.
    • 오류 메시지를 통해 문제를 수정할 수 있도록 도움을 주는 역할을 한다.

 

대표적인 컴파일러와 컴파일 언어

  1. GNU Compiler Collection (GCC)
    • 지원 언어: C, C++, Objective-C 등
    • 특징: 오픈 소스, 다양한 플랫폼 지원
  2. Clang
    • 지원 언어: C, C++, Objective-C, Swift 등
    • 특징: 빠른 컴파일 속도, 우수한 에러 메시지
  3. Microsoft Visual C++ (MSVC)
    • 지원 언어: C, C++
    • 특징: Windows 환경에 최적화, Visual Studio와 통합
  4. rustc
    • 지원 언어: Rust
    • 특징: 메모리 안전성 강조, 고성능
  5. Go Compiler (gc)
    • 지원 언어: Go
    • 특징: 빠른 컴파일 속도, 간결한 문법
  6. Swift Compiler (swiftc)
    • 지원 언어: Swift
    • 특징: 애플 플랫폼 최적화, 안전한 메모리 관리
  7. javac
    • 지원 언어: Java
    • 특징: 자바 소스를 바이트코드로 변환, JVM에서 실행

인터프리터(Interpreter)

고급 프로그래밍 언어로 작성된 소스 코드를 한 번에 하나씩 읽고 실행하는 소프트웨어 도구이다.

인터프리터는 컴파일 단계를 거칠 필요가 없다는 장점이 있지만 컴파일러를 사용한것보다 속도가 느린 편이다.

 

인터프리터의 주요 역할과 기능

  1. 소스 코드 해석 (Source Code Interpretation):
    • 소스 코드를 한 줄씩 읽어들여 즉시 실행한다.
    • 구문 분석과 의미 분석을 실시간으로 수행하여 코드를 실행 가능한 상태로 변환한다.
  2. 실행 속도:
    • 컴파일러가 전체 코드를 사전에 변환하는 과정이 없기 때문에 초기 실행 속도가 빠르다.
    • 매 실행 시마다 코드를 해석해야 하므로 장기적인 실행 속도는 컴파일된 프로그램보다 느릴 수 있다.
  3. 플랫폼 독립성:
    • 인터프리터가 설치된 다양한 플랫폼에서 동일한 소스 코드를 실행할 수 있다.
    • 플랫폼에 따라 별도의 컴파일 과정이 필요 없으므로 이식성이 높다.
  4. 디버깅 및 개발 편의성:
    • 코드의 각 부분을 실시간으로 실행하고 결과를 확인할 수 있어 디버깅이 용이하다.
    • 개발자가 코드를 수정하고 즉시 결과를 확인할 수 있어 개발 속도가 빨라진다.
  5. 동적 기능 지원:
    • 런타임에 코드를 수정하거나 생성하는 동적 기능을 쉽게 구현할 수 있다.
    • 예를 들어, 스크립트 언어에서 자주 사용되는 동적 타이핑과 같은 기능을 자연스럽게 지원다.

 

컴파일러와 인터프리터의 비교

특징 컴파일러 인터프리터
변환 방식 전체 소스 코드를 한 번에 기계어로 변환 소스 코드를 한 줄씩 해석하고 즉시 실행
실행 속도 실행 파일이 최적화되어 있어 빠름 해석 과정이 추가되어 상대적으로 느림
디버깅 컴파일 시점에 오류를 모두 발견 실행 중에 오류를 발견하며, 디버깅이 용이함
이식성 컴파일된 실행 파일은 특정 플랫폼에 종속됨 인터프리터가 있는 모든 플랫폼에서 동일한 소스 코드 실행 가능
개발 주기 컴파일 과정이 필요하여 개발 속도가 상대적으로 느림 코드 수정 후 즉시 실행 가능하여 개발 속도가 빠름
예시 언어 및 도구 C, C++, Rust (컴파일러: GCC, Clang, MSVC) Python, JavaScript, Ruby (인터프리터: CPython, Node.js, MRI)

 

대표적인 인터프리터와 인터프리터 언어

  • Python: CPython, PyPy, Jython 등 다양한 인터프리터가 존재.
  • JavaScript: 브라우저 내장 인터프리터(예: V8, SpiderMonkey) 및 Node.js.
  • Ruby: MRI (Matz's Ruby Interpreter), JRuby, Rubinius.
  • PHP: Zend Engine 등 다양한 PHP 인터프리터가 사용됨.

 

이렇게 컴파일러와 인터프리터에 대해 알아보았다.

 

마무리 말

갑자기 이렇게 가장 기초적인 컴파일러에 대해 왜 알아봤냐 하면..

오늘 기술 면접을 다녀왔는데 컴파일러의 링크에 대한 질문에 답변을 못했기 때문이다.

가장 쉽고 기초적인 개념일수록 그냥 넘어가고 잊어버리기 쉬우니 아무리 쉬운 것도 꼭꼭 한번씩 집고 넘어가는 습관을 들이자!!

참고링크

https://losskatsu.github.io/os-kernel/compiler-interpreter/#2-%EC%9D%B8%ED%84%B0%ED%94%84%EB%A6%AC%ED%84%B0interpreter

 

[리눅스] 컴파일러 vs 인터프리터 차이

[리눅스] 컴파일러 vs 인터프리터 차이

losskatsu.github.io

https://devparker.tistory.com/110

 

[CS] 컴파일러(Compiler)란? - 개념과 역할

컴파일러란? 컴파일러(Compiler)는 프로그래밍 언어로 작성된 소스 코드를 컴퓨터가 이해할 수 있는 기계어로 변환하는 소프트웨어 도구입니다. 프로그래머가 작성한 고수준 언어(High-level Language)

devparker.tistory.com

 

'C++' 카테고리의 다른 글

[C++] 진짜 랜덤값? 가짜 랜덤값?  (0) 2025.01.14
[C++] Text RPG 몬스터 생성 로직 설계  (1) 2025.01.13
[C++] Template, STL  (0) 2025.01.02
[C++] 유니크 포인터  (0) 2024.12.31
[C++] 생성자와 소멸자  (0) 2024.12.30

[C++] Template, STL

닿메_dahme
|2025. 1. 2. 21:14

2025년 1월 2일 목요일(2025년이 밝았따..!)

신년인데도 집중이 잘 되지 않는데.. 왜 그럴까? 아직 이해를 잘 못해서 겁부터 먹는듯하다

겁먹지마 파이팅!!!

 

오늘은 Template, STL에 관해 공부를 했다.

Template

  • 일반화된 코드를 작성할 수 있는 문법
  • 정의할 함수 위에 template <typename T> 를 추가하고, 일반화할 타입에 실제 타입 대신 T로 작성하면 끝!
  • 어떤 타입이 올지 모르지만 그 타입을 그냥 T라고 정의하겠다! 라는 의미
  • 근데 모든 매개변수 타입을 일반화할 필요는 없음
    • 배열의 크기 같은 경우는 항상 정수형이므로 그냥 int를 사용해줘도 문제 없음
    • T Add(T arr[], int size) {}
#include <iostream>
using namespace std;

// general programming(일반화된 프로그래밍)
template <typename T>
T Add(T x, T y) {
    return x + y;
}

int main() {
    cout << Add(3, 4) << endl;         // 정수형 덧셈
    cout << Add(3.3, 4.2) << endl;     // 실수형 덧셈

    return 0;
}

 

STL

STL(Standard Template Library)

  • C++에서 제공하는 표준 템플릿 라이브러리

컨테이너

  • 데이터를 담는 자료구조
  • 모든 컨테이너는 템플릿으로 구현되어있음
  • 타입에 상관없이 사용
  • 메모리 관리를 내부적으로 함
    • 사용시 메모리 관리를 고려하지 않아도 됨
  • 모든 컨테이너는 반복자를 가지고 있음
    • 각 컨테이너를 동일한 문법으로 접근 가능

Vetor

  • 배열과 유사하지만 배열의 업그레이드 버전인 컨테이너
  • 템플릿 클래스로 구현되어 있음
  • 삽입되는 원소 개수에 따라 내부 배열의 크기가 자동 관리됨
  • 임의접근이 가능함(인덱스를 통해 특정 위치에 접근 가능)
  • 벡터 특성상 삽입/삭제는 맨 뒤에 하는게 좋음(배열의 특성)

2차원 Vector

vector<vector<int>> vec(3, vec<int>(4, 1)); // 3x4 행렬이고 모든 원소 1로 초기화

vector의 멤버 함수

  • push_back()
    • vector의 맨 끝에 원소를 추가하는 메서드
    • 원소 개수가 늘어남에 따라 크기는 자동으로 증가하기 때문에 크기를 신경 쓸 필요는 없음
    #include <iostream>
    #include <vector>
    using namespace std;
    
    int main() {
        vector<int> vec;
        vec.push_back(10);
        vec.push_back(20);
    
        for (int num : vec) {
            cout << num << " ";
        }
        
        return 0;
    }
    // output
    // 10 20 30
    
  •  pop_back()
    • vector의 맨 끝에 원소 제거하는 메서드
    • 맨 끝 원소가 줄어드니까 크기도 줄어듬
    #include <iostream>
    #include <vector>
    using namespace std;
    
    int main() {
        vector<int> vec = {10, 20, 30};
        vec.pop_back();  // 마지막 요소(30) 제거
    
        for (int num : vec) {
            cout << num << " ";
        }
        
        return 0;
    }
    // output
    // 10 20
    
  •  size()
    • vector의 크기 확인
    #include <iostream>
    #include <vector>
    using namespace std;
    
    int main() {
        vector<int> vec = {10, 20, 30};
        
        cout << vec.size() << endl;
        
        return 0;
    }
    // output
    // 3
    
  •  erase()
    • 특정 위치의 원소 제거
    • 벡터 특성상 시간이 많이 걸리는 함수이기 때문에 되도록이면 지양
    #include <iostream>
    #include <vector>
    using namespace std;
    
    int main() {
        vector<int> vec = {10, 20, 30, 40, 50};
    
        // 두 번째 요소 제거 (index 1)
        vec.erase(vec.begin() + 1);
    
        cout << "Vector after erasing second element: ";
        for (int num : vec) {
            cout << num << " ";
        }
        cout << endl;
    
        // 2~4 번째 요소 제거 (index 1~3)
        vec.erase(vec.begin() + 1, vec.begin() + 4);
    
        cout << "Vector after erasing range: ";
        for (int num : vec) {
            cout << num << " ";
        }
        return 0;
    }
    // output
    // Vector after erasing second element: 10 30 40 50
    // Vector after erasing range: 10 50
    

 

Map

  • 키-값 쌍이 하나의 타입이 됨
  • 키 값을 기준으로 데이터가 자동 정렬
  • 중복된 키값 허용 x
#include <iostream>
#include <map>

using namespace std;

int main() {
    // map 선언 및 값 추가
    map<int, string> myMap;
    myMap[5] = "E";
    myMap[2] = "B";
    myMap[8] = "H";
    myMap[1] = "A";
    myMap[3] = "C";

    // map 내용 출력
    cout << "Map의 내용 (키 값 기준으로 자동 정렬):" << endl;
    for (map<int, string>::iterator it = myMap.begin(); it != myMap.end(); ++it) {
        cout << it->first << ": " << it->second << endl;
    }

    return 0;
}

 

  • insert()
    • make_pair를 통해 키-값 쌍을 만들고 이를 insert를 만들어서 추가
  • find()
    • 특정 키값이 존재하는지 확인 가능
  • size()
    • 맵에 키-값 쌍의 개수를 반환
  • clear()
    • map에 저장되어있는 모든 원소를 삭제

 

이렇게 각 코드 상황에 맞게 적절한 메소드를 사용하면 좋을 것 같고, 이런 메소드가 있었구나~ 하면 될 것 같다!

2차원 벡터는 아직 조금 어렵게 느껴지는데 코드를 계속 짜봐야 빨리 적응할 수 있을 것 같다