반응형

 

ctrl +, 또는 ctrl + t  전체 페이지중 이동

 

 

 

ctrl +. :빠른작업  리팩토링

 


ctrl +j :  자동완성 표시

 

 

 

ctrl + r +r : 함수나 변수 명을 한번에 변경

ctrl + k +d : 줄 자동 정렬

 

ctrl + k +c  : 주석 으로 변경

 

ctrl + k + u : 주석 해제 - 하나씩 해제

 

 

ctrl + m + o :  정의 부분만 보이기

 

 

f12 : 정의 탐색

 

 

 

 

shift +f12 : 모든 참조 찾기

 

 

 

디버그 중 중단점이후

f11 : 한단계씩 코드 실행 - 함수내부까지 들어감

f10 : 프로시저  단위 실행

 

 

'언어' 카테고리의 다른 글

증감연산자  (0) 2021.05.31
오버로딩과 오버라이딩  (0) 2021.05.15
메서드 와 함수가 다른가?  (0) 2021.05.15
반응형

오랜만에 친구가 cpp 공부를 하고있다 하여  미로만들기를 추천하며 나역시 미로를 만들어 보았다

 

위 영상은 이번에 제작물이다 {

    1. 맵 램덤생성

    2.방향키 입력시 플레이어 움직임

    3.플레이어의 시야 제한

}

 

우선 맵 헤더 파일이다. 

class  Mazemap
{

public:
	enum PlayerMoverDirection
	{
		UP,
		DOWN,
		LEFT,
		RIGHT,
		MovendPointeEndPoint
	};
	enum MapState {
		road = 0,
		wall,
		startPoint,
		EndPoint
	};

	const char MapStateChar[4][4] = { "□","■","○","☆" };

	int MapSizeLW = 25;
	int PlayerEyesight = 5;
	int PlayerPos[2] = { 0,0 };


	std::vector<std::vector<int>> Map{};


	void init();
	void PlayerMove(PlayerMoverDirection m);
	void MapRestart();


private:
	// 맵생성 완료 확인
	bool IsGenerateMap = false;
	/// <summary>
	/// [4][2]  [1]상 [2]하 [3]좌 [4]우
	/// </summary>
	const int MoverDirection[4][2] = { {1,0} ,{-1,0} ,{0,-1} ,{0,1} };

	//start point end point  의  거리차이
	const int Sp_Ep_MDistance = 3;
	//랜덤 관련 클래스
	uniform_int_distribution<int> RandomMapPoint;
	random_device  rd;
	mt19937_64 mt;

	void AutomaticMapMaker(int MapSize);
	void ProceduralGeneration(int generatex, int generatey, PlayerMoverDirection goback = PlayerMoverDirection::MovendPointeEndPoint);

	std::vector<PlayerMoverDirection> canDoitGeneration(int x, int y, PlayerMoverDirection goback);
};

 



맵 클레스엔 맵과 플레이어의 이동을 표현할 변수들을 만들어준다.

 

우선 맵 생성을 만들었는데.

 

맵 램덤 생성에서 어떤식으로 생성할지 생각하다 가장 기본적인 생각은 DFS 였고

 

 플레이어 위치에서 시작을한뒤 

 

 

      

{

 왔던길을 제외한 갈수있는길을 탐색한다. 

               갈수 있는길은 특정 방향으로 2칸이 벽 일경우 가능

 

 상하좌우 전부 길이없을경우 도착지점 배열에 추가한다.(막다른 길을 도착지점으로 하기위함)

 


갈수 있는길의 방향을 배열로 받은뒤 섞어준다.

 

배열의 순서대로 다음 방향으로 2칸이동후 사이의 1칸도 길로 채워준다.

}

 

 

 

   

 

 

맵 초기화 시켜주기 

맵을 생성후 start point를 지정해준다.

void Mazemap::MapRestart()
{
	Map.clear();
	AutomaticMapMaker(MapSizeLW);
}

void Mazemap::AutomaticMapMaker(int MapSize)
{
	uniform_int_distribution<int> Maprange(0, MapSize-1);
	int  P_x, P_y, EndP_x, EndP_y = 0;

	if (SpareEndPoint.size() > 0) SpareEndPoint.clear();

	P_x = Maprange(mt);
	P_y = Maprange(mt);

	PlayerPos[0] = P_x;
	PlayerPos[1] = P_y;

	
    
	for (int i = 0; i < MapSize; i++) {
		Map.push_back(std::vector<int>());
		for (int j = 0; j < MapSize; j++) {
			Map[i].push_back(1);
		}
	}
	Map[P_x][P_y] = MapState::startPoint;
	//Map[EndP_x][EndP_y] =MapState::EndPoint;
	ProceduralGeneration(P_x, P_y);
	
	int randPoint = Maprange(mt) % (SpareEndPoint.size() - 1);

		Map[SpareEndPoint[randPoint][0]][SpareEndPoint[randPoint][1]] = MapState::EndPoint;

	IsGenerateMap = true;
}

 

 

 

맵생성 코드의 기본적인 아이디어는

 

1.가는 방향으로 길을 채워준다.

2. 갈수 있는길을 탐색한다

3. 왔던길을 제외한다(받은 갈수있는길 배열에서).

4.만약 갈길이 있다면 queue 길찾기 배열에 추가한다.

5. 받은 방향 배열을 섞어준다

6.재귀함수로 현재위치와 queue의 일전에 추가한 다음방향으로 이동한다.



더보기

맵 생성 코드

void Mazemap::ProceduralGeneration(int generatex, int generatey, PlayerMoverDirection goback)
{
	//길로 바꿔주기
	if (Map[generatex][generatey] == MapState::wall){
		Map[generatex - (MoverDirection[goback][0])][generatey - MoverDirection[goback][1]] = MapState::road;
		Map[generatex][generatey] = MapState::road;
	}

	else if (Map[generatex][generatey] == MapState::road) 
	{
		return;
	}


	////갈수있는 방향 받기
	////같던길 제외 해주기
	auto nextRoad = canDoitGeneration(generatex, generatey, goback);
	////섞기
	for (int i = 0; i < nextRoad.size(); i++) {
		int a = RandomMapPoint(mt) % nextRoad.size();
		if (a == i)continue;
		PlayerMoverDirection m = nextRoad[i];
		nextRoad[i] = nextRoad[a];
		nextRoad[a] = m;
	}

	if (!(nextRoad.size() > 0)) {
		SpareEndPoint.push_back({ generatex, generatey });
	}

	for (int i = 0; i < nextRoad.size(); i++) {
		ProceduralGeneration(generatex + MoverDirection[nextRoad[i]][0] * 2, generatey + MoverDirection[nextRoad[i]][1] * 2, nextRoad[i]);
	}
}

 

 

canDoitGeneration 함수 (갈수있는길을 탐색한뒤 [상하좌우] enum 배열을 반환한다)

std::vector<Mazemap::PlayerMoverDirection> Mazemap::canDoitGeneration(int x, int y, PlayerMoverDirection goback)
{
	std::vector<PlayerMoverDirection> v;

	if (x + 2 < MapSizeLW && Map[x + 2][y] == Mazemap::MapState::wall &&
		Map[x + 1][y] == Mazemap::MapState::wall &&
		goback != DOWN)v.push_back(UP);
	if (x - 2 >= 0 && Map[x - 2][y] == Mazemap::MapState::wall &&
		Map[x - 1][y] == Mazemap::MapState::wall &&
		goback != UP)v.push_back(DOWN);
	if (y + 2 < MapSizeLW && Map[x][y + 2] == Mazemap::MapState::wall &&
		Map[x][y +1] == Mazemap::MapState::wall &&
		goback != LEFT)v.push_back(RIGHT);
	if (y - 2 >= 0 && Map[x][y - 2] == Mazemap::MapState::wall &&
		Map[x][y - 1] == Mazemap::MapState::wall &&
		goback != RIGHT)v.push_back(LEFT);
	return v;
}

 

이후 움직이는 코드를 만들어준다

맵을 탈출하지 못하게 배열의 크길 방향 제한을 걸어준다.

std::vector<Mazemap::PlayerMoverDirection> Mazemap::canDoitGeneration(int x, int y, PlayerMoverDirection goback)
{
	std::vector<PlayerMoverDirection> v;

	if (x + 2 < MapSizeLW && Map[x + 2][y] == Mazemap::MapState::wall &&
		Map[x + 1][y] == Mazemap::MapState::wall &&
		goback != DOWN)v.push_back(UP);
	if (x - 2 >= 0 && Map[x - 2][y] == Mazemap::MapState::wall &&
		Map[x - 1][y] == Mazemap::MapState::wall &&
		goback != UP)v.push_back(DOWN);
	if (y + 2 < MapSizeLW && Map[x][y + 2] == Mazemap::MapState::wall &&
		Map[x][y +1] == Mazemap::MapState::wall &&
		goback != LEFT)v.push_back(RIGHT);
	if (y - 2 >= 0 && Map[x][y - 2] == Mazemap::MapState::wall &&
		Map[x][y - 1] == Mazemap::MapState::wall &&
		goback != RIGHT)v.push_back(LEFT);
	return v;
}

그리고 main에서 움직이는 코드를 사용해서 이동한다.

void MazeMain::Update()
{
	int keyInput;
	keyInput = _getch();
	if (keyInput == 224) {
		keyInput = _getch();
	}
	if (keyInput == 115 || keyInput == 80) {
		map.PlayerMove(Mazemap::PlayerMoverDirection::UP);
	}
	if (keyInput == 97 || keyInput == 75) {
		map.PlayerMove(Mazemap::PlayerMoverDirection::LEFT);
	}
	if (keyInput == 100 || keyInput == 77) {
		map.PlayerMove(Mazemap::PlayerMoverDirection::RIGHT);
	}
	if (keyInput == 119 || keyInput == 72) {
		map.PlayerMove(Mazemap::PlayerMoverDirection::DOWN);
	}

	if (keyInput == 'r' || keyInput == 'R') {
		map.MapRestart();
	}
}

 

 

이제 메인에서 각종 코드들을 실행시킨다.

 

랜더는 더블버퍼 win api 코드를 들고와서 작성했다.

 

main

더보기
class MazeMain
{
public:
	Mazemap	map;
	GameBoard GameRender;

	void init();

	void a_main();

	string Render();

	void Update();

	~MazeMain();
	bool Game = true;
private:

};

int main() {


	MazeMain m;
	m.init();
	m.a_main();
}


void MazeMain::init()
{
	map.init();

	GameRender.Map_size = map.MapSizeLW;
	GameRender.ScreenInit();

	GameRender.func = [this]() {
		return Render();
	};

}

void MazeMain::a_main()
{
	while (Game)
	{
		while (_kbhit() == 0) {
			GameRender.Render();
			Update();
		}
	}

}

 

그런다음 

원하는 시야거리를 잡아준뒤 그이상을 넘어갈경우 맵을 벽과 같은 문자로 넣어버린다.

 

string MazeMain::Render()
{

	string s;

	//map.PlayerPos[0] map.PlayerPos[1]
	for (int i = 0; i < map.Map.size(); i++)
	{
		for (int j = 0; j < map.Map[i].size(); j++)
		{

			if (map.PlayerPos[0] == i && map.PlayerPos[1] == j) {
				s += map.MapStateChar[2];
				continue;
			}
			if (
				map.PlayerPos[0] + map.PlayerEyesight > i &&
				map.PlayerPos[0] - map.PlayerEyesight < i &&
				map.PlayerPos[1] + map.PlayerEyesight > j &&
				map.PlayerPos[1] - map.PlayerEyesight < j
				) {
				s += map.MapStateChar[map.Map[i][j]];
			}
			else {
				s += map.MapStateChar[Mazemap::MapState::wall];

			}
			s += map.MapStateChar[map.Map[i][j]];

		}
		s += "\n";
	}
	return s;

}

'언어 > C++' 카테고리의 다른 글

cpp 전처리기  (0) 2023.09.05
c++ std::function 사용하기  (0) 2023.01.10
함수 포인터! (Function Pointer)!  (0) 2021.07.13
인라인 함수(Inline Function)  (0) 2021.06.15
네임스페이스  (9) 2021.05.18
반응형

 

전처리기 : 프로그램을 컴파일 하기전 선행 처리되는 부분.

 

소스파일->전처리기->컴파일러->링커->exe 파일 

 

 

전처리기는 

앞에 # 을쓰며 맨뒤에 세미콜론; 을 붙이지 않는다.

 

지시문: 전처리기 지시문들은 프로그램 소스를 쉽게 변경하고 다른 환경에서 컴파일하기 쉽게 만든다.

 

전처리 지시문들을 간단하게 사용법을 적어두었다. 

 

전처리기 지시문 종류: #include #define #error #import #pragma #elif #if  #undef #else #ifdef #line #using #endif #ifnedf

아래의 내용은 micro내용에 있음

 


#include

처음부터 계속해서 나오는 include은 한글로 포함하다 라는 뜻을 가지고있으며 

지시문의 선언된 지점부터 지정된 파일의 내용을 포함하도록 전처리기에 지시 하는 문장이다.

 

c언어를 처음 배웠을때 나오는 #include <stdio.h> 을 처음으로 접하는 지시문이였을겄이다. 

 stdio standard input output 의 약자로 입출력을위해 우리가 초반부터 사용해 오던 헤더파일 이다.

이처럼 다른 소스코드를 쉽게 추가하여 사용할수 있다.

 

include의 사용법으로는

 

350: #include <stdio.h>

351: #include "std.h"

 

이런식으로 사용한다.

위 코드의 경우 #include <stdio.h> 파일 내용과 "std.h" 파일내용을 350번,351번 아래부터 적용한다. 로 볼수있다.

보통은 맨위에 적어서 모를수도 있겠지만 코드의 라인에 맞추어 적용한다.

 

#include <> 와 "" 의 차이는 파일을 읽는 방법에서 차이가난다

 

 

<>  컴파일러 옵션의 경로에 따직른 위치에서 헤더파일을 찾는다.

       <d:\file\a.h> 와 같이 지정된 경로를 찾는다.

 

""     컴파일러인 경우 현재 정의한 위치의 파일을 찾아본다.

        파일에서 상위 파일로 올라가며 찾아본다.

        이후 <> 와 같이 지정된 경로에 따른 위치에서 찾는다.

         지정된 경로를 찾는다. 

    

 

지정된 경로 예시

이러한 식으로 명확한 경로를 묶은 경우 해당 경로만 검색한뒤 표준 검색을 무시한다.

 

그럼으로 직접 만든 헤더파일일 경우 " "으로 사용하는걸 볼수있다.

 

 


#define  매크로

define 은 쉽게 사용하기위한 식별자(별명) 으로 볼수있다.

 

#define IN int
#define pi 3.141592
#define multiplyPI(x)(x*pi)

 

 IN in = 50;
 cout<< multiplyPI(in);
 cout<< in*pi;

 

위 처럼 간단한 함수 상수 등을  간단한 이름으로 변경하여서 사용할수 있다.

 

위 상수와 비슷한 매크로는 상수는

간단하게 변경할수 있으며 3.14 와 같은 숫자를 보기좋게 PI 와 같이 사용하여 가독성을 높힐수 있다.

 

#define pi 3.141592

 

매크로 와 비슷한 함수의 경우 일반함수와 다른점이 몇가지 있다.

 

매크로는  코드가 기계어로 변경될때 일반 함수와 다르게 하드코딩 한것 처럼 확장되어 넘어간다.

#define PI 3.141592

 

int p = PI ;

일경우

p = 3.141592 와 같다.

 

일반함수와 다르게 함수 공간(stack frame)이 생성안되기 때문에 속도가 빠르고
변경시 매크로 부분이 선언했을때 처럼 변경되며 실행파일 크기가 늘어난다.

 

계산을 하고 나오는 함수와달리

define 함수 는 하드코딩 처럼 변경 해주는 것이기때문에 다르게 계산된다.

 

#define SQR(x) x*x

int main() {

	int x = 10;
	cout << SQR(10)<<endl;
	cout << SQR(x+10) << endl;
	cout << SQR(++x) << endl;
}

위 와 같이 실행했을경우

차례대로 

100

120

144

순서대로 출력된다.

이렇게 출력되는 이유는 일전에 계속말 한것 처럼 함수가 아니라 실행전 치환 해주는 것이기 떄문인다.

이말은 위으 코드는 결국 아래와 같이 변경된다는 뜻이다.

	cout << 10*10 << endl;
	cout << x + 10 * x+10 << endl;
	cout << ++x *++x << endl;

 

이를 방지해 주기위해

	cout << SQR((x+10)) << endl;

이런식으로 넣으면 정상적으로 400이 나온다

 

위 코드처럼 define의 경우 틀렸을때 쉽게 오류를 찾기 힘든경우가 있기에 조심해서 사용해야 한다.

 


#undef 

undef는 식별자의 정의를 제거하여 , 이후 컴파일러에서도 식별자를 인식하지 못한다.

 

undef 의경우 define 와 같이 사용하여 영역처럼 사용할수 있다.

#define SQR(x) (x)*(x)
int main() {

	int x = 10;
	cout << SQR(10)<<endl;
#undef SQR
	cout << SQR(++x) << endl; //err 식별자가 정의되어 있지 않습니다.
}

 


 

 #if     #elif   #else  #endif #ifdef #ifndef

전처리기  조건부 컴파일 지시자.

사용법의 경우 아래 이미지 처럼 일반 if , else if , else 문과 비슷하게 사용한다.

다른점은 #if문의경우 마지막에 #endif로 끝맺음을 지어줘야한다.

 

그리고 전처리기 에서 처리하는 지시문 이기때문에 

위의 이미지 처럼 회색 처리되며 컴파일 조차 되지않는다.

#if SQR(10) 처럼 define 함수를 쓸수도 있다.

 

여러 경우로 사용할수 있는데 

 

컴파일 되지않기 때문에 테스트 모드와 실행모드를 설정하여 다르게 동작하게 할때 사용할수있다.

 

 

#ifdef 와 ifndef 는 

식별자가 존재할경우 #if 와 같은 기능을 하지만 정의 되지않거나 제외되었을경우 #undef와 같은 기능을하여

제외한다.

 

이러한 기능으로 헤더파일 중복을 막을수 있다.

 

#ifndef HD_FILE

#define HD_FILE

 


#line

 

#line 은 미리 정의된 매크로 중 __LINE__  과 __FILE__을 변경한다.

22: cout << __LINE__ << endl;
23:#line 12 "hello.cpp"
24:cout << __LINE__  +","+ hello.cpp<< endl;
25:cout << __LINE__ << endl;

output : 

22

12

13

에러 났을경우 라인과 파일을 출력할수 있고 

특정 상황에 라인을 재정의 하여 마지막에 도달하였을경우

line의 수를 보고 어떤 방식으로 실행하였는지 확인할수 있다.


 

#using #import

 

 

  #import

GCC (C 컴파일러 에서는) include 의 중복을 피할수 있는 용도로 사용하지만

cpp msvc 에서의 #import 는.tlb .olb 와같이 COM 객체를 사용하기위해 형식 라이브러리 type library 를 불러오는데 사용합니다.

 

#using 은 dll 파일을 참조할수 있습니다.

#using 되는 구성 요소는 컴파일 시간에 가져온 다른 버전의 파일로 실행할 수 있으므로 클라이언트 애플리케이션에서 예기치 않은 결과를 제공할 수 있습니다.

 


#paragma 

파라그마는 많은 기능을 내포하고 있다.

 

뒤에 붙는 명령어에 따라 다른 기능을 가지고 있는데

예시로

pragma omp 의 경우 스레드 즉 병렬 처리 관련한 기능들을 가지고 있다.

#pragma omp parallel for num_threads(4) 처럼 사용하여  병렬 처리할수 있다

내부엔 mutex semapor lock 과 비슷한 기능들도 구현 되어있다.

#pragma omp parallel for num_threads(4)
    for (i = 0; i < SIZE; i++)
    {
        a[i] = i;
        printf_s("%d\n", a[i]);
    }

 

progma once  는 소스코드 파일을 컴파일할때 컴파일러에 헤더 파일이 중복되지 않도록 지정한다.

 

 

 


미리 정의된 매크로

 

__DATE__ : Mmm dd yyyy 형식의 상수 문자열

__FILE__ : 현재 소스 파일의 이름

__LINE__: 소스파일의 줄 번호

 

위 와 같은 사용하기 쉽게 미리 정의 해둔 매크로 들이 존재한다. 

 

visual studio 의 최신 미리정의된 매크로의 경우 

microsoft 의 공식문서에서 확인할수 있다.

https://learn.microsoft.com/ko-kr/cpp/preprocessor/predefined-macros?view=msvc-170 

 

미리 정의된 매크로

Microsoft C++ 컴파일러의 미리 정의된 전처리기 매크로가 나열 및 설명되어 있습니다.

learn.microsoft.com

 

 

 

 

전처리기 연산자 

#,#@,## 

 

# 문자열화 연산자

매크로 매개변수를 문자열 리터럴로 변환한다.

예제 learn.microsoft.com

 

#@ charizing 연산자

매크로 인수에서만 사용가능하며 인수를 작은따음표로 묶어 매크로가 확장될때 문자로 처리된다.

 

 

 

 

 

 

 

## 토큰 붙여녛기 연산자

아래 처럼 ## 을 사용하면 매크로가 확장되지않는다 

n이 인수로 전달 되는것이 아니라 실제 token9와 같이 작동한다.

숫자 10을 넣는경우 오류가 나는것을 확인할수 있다.

<cassert> 라이브러리

는 위에서 사용했던 매크로가 만들어져 있는 라이브러리다.

 

'언어 > C++' 카테고리의 다른 글

CPP 콘솔 미로 만들기  (0) 2023.11.15
c++ std::function 사용하기  (0) 2023.01.10
함수 포인터! (Function Pointer)!  (0) 2021.07.13
인라인 함수(Inline Function)  (0) 2021.06.15
네임스페이스  (9) 2021.05.18
반응형

C++ 의 std::function 을 이용하여 코드를 사용하기 쉽게 변경하기.


std::Function은 무엇인가

함수 포인터    =  반환값이 같은 타입이여야 하고 코드위치를 가르키기 때문에 메모리를 할당,회수가 불가하지만 빠르다.

 

std::Function  =  암시적 형변환이 가능하고

                           일반적인 함수포인터와 다르게 람다함수나 bind 또한 맴버함수에 대한 포인터도 사용가능하다

 

선언방법

int Func(int n){ cout<<n<<endl; };

std::function<void(int)> funcName1 = Func; //함수 대입
std::function<void(int)> funcName2 = [](int a) {}; // 람다함수
std::function<void(int)> funcName3 = std::bind(Func, 1); //std::bind

 

사용방법은 일반 함수와 같다

funcName1(10);

 

사용하는 이유

cpp로 player을 개발하던 도중 enum swich case 를 여러번 사용하는 경우를 보았다.

 메인업데이트 부분의 player 상태 처리와 렌더링부분의 player 상태처리 처럼  업데이트 상황을 나눠서 계산할때

state를 계속 추가해주었다.

업데이트 루프마다 플레이어의 상태들을 관리해 주는것은 player의 상황이 늘어났을경우 각종업데이트에서

swich를 써둬서 상황을 생성할때마다 swich문을 추가하는것과 커플링의 문제가보였다.

 이러한 문제는 코딩을 할때 일이 많아지고 코드의 길이가 길어져 비효율적이다.

state 패턴

 swich case를 state 패턴처럼 이용하며 함수들을 좀더 유동적으로 다루기위해

객체를 등록하여 사용이 필요했다.

위의 사진처럼 swich 문과 update를 하나의 루프에 묶어서 돌린다.

 

하나의 클래스에서  처음시작할때 원하는 위치에 함수를 받아서 저장 해둔뒤 실행하면 state에 따라 실행하게 고안하였다.

다른 오브젝트를 새로 생성하거나 제작할때 오브젝트의 init 함수에 특정 event를 걸어주게 만들면 알아서 돌아갈것이다.

 

장점

이런식으로 구조를 제작하면 새로운 상황을 추가할때 추가해주는 코드가 줄어들고

한쪽에서 코드를 지워도 문제없이 코드가 실행될것이다.

또한 하드코딩이 아니라 프로그램 내에서도 코드를 유동적으로 사용가능할것이다.

 

단점

유동적인 코드가 한번에 확인하기 어렵고, 필요한 순간 할당 제거를 할수있기에

나뉘어서 동작하는 함수들을 많이 추가하는 경우 서순이 꼬여서 생기는 찾기 어려운 오류가 생길수있다.

 

 

테스트 코드

//테스트 함수 추가를 위한 player 클래스
class Player
{
	
public:
	Player(int c) : b(c) { cout << a<<endl; };
	int b = 21;
	void playerMove( );
	void playerIdle( );
private:
	int a =10;

};

void Player::playerMove( )
{
	this->a+=this->b;
}

void Player::playerIdle( ) {
	cout << a<<endl;
}
//std::function 을 이차원 배열로 생성
class Deleg {
public:
	std::vector<std::function<void()>> Deleg;
};

//플레이어의 상태만큼 미리 event update의 크기를 만들어둘 예정
enum PlayerState 
{
	Move,
	Idel,
	Skill,
	StateCount
};



int main() {
	//Render update,LateUpdate,등 업데이트 순서
	const int UpdateRate= 2; 
	Player p = {Player(4)};
  	Player* _P = &p;
  
  Deleg StateDelg[PlayerState::StateCount][UpdateRate] = { { {} ,{} } ,{ {} ,{} } ,{ {}, {} } };
	StateDelg[0][0].Deleg.push_back([=]() {cout << endl; _P->playerMove(); cout << endl;  });
	StateDelg[0][1].Deleg.push_back([_P]() {_P->playerMove(); _P->playerIdle(); });


	StateDelg[1][0].Deleg.push_back([_P]() {_P->playerMove(); _P->playerIdle(); });
	StateDelg[1][1].Deleg.push_back([_P]() {_P->playerMove(); _P->playerIdle(); });


	StateDelg[2][0].Deleg.push_back([_P]() {_P->playerMove(); cout << "1Update End" << endl; });
	StateDelg[2][0].Deleg.push_back([_P]() {_P->playerMove(); cout << "1Update End" << endl; });
	StateDelg[2][0].Deleg.push_back([_P]() {_P->playerMove(); cout << "1Update End" << endl; });
	StateDelg[2][0].Deleg.push_back([_P]() {_P->playerMove(); cout << "1Update End" << endl; });
	StateDelg[2][0].Deleg.push_back([_P]() {_P->playerMove(); cout << "1Update End" << endl; });

	StateDelg[2][1].Deleg.push_back([_P]() {_P->playerMove(); _P->playerIdle(); });
  
  
  //전체 코드를 실행하지만 i나 j를 바꿔줌으로써 특정 state만 실행시켜줄수있다
	for (int i = 0; i < StateCount; i++)
		for (int j = 0; j < UpdateRate;j++) {
			for (int k = 0; k < StateDelg[i][j].Deleg.size(); k++) {
				(StateDelg[i][j].Deleg[k]());
			}

		}
  
 }

 

위 코드는 테스트 코드로써

1. 이차원 배열[플레이어 상태][ 업데이트 ] 를 만들어주었다.

    역으로 업데이트종류 마다 지정된 플레이어 상태에 맞는 함수를 실행시켜 주어도 좋다.

2. 함수(애니메이션 스크립트,스킬등)를 시작할때 원하는 [상태 또는 업데이트] 에 맞는 함수 리스트에 넣어준다.

3. 상황에 맞게 돌린다. 

 

위 소스코드의 결과 값이다.

 

이제 다른 함수들이 특정 state부분만 잠시 실행시키기 원해도 배열의 위치만 맞게 실행시켜주면 간단하게 사용가능하다.

'언어 > C++' 카테고리의 다른 글

CPP 콘솔 미로 만들기  (0) 2023.11.15
cpp 전처리기  (0) 2023.09.05
함수 포인터! (Function Pointer)!  (0) 2021.07.13
인라인 함수(Inline Function)  (0) 2021.06.15
네임스페이스  (9) 2021.05.18
반응형

n Nulnud-coalescing operator  

   NULL 병합 연산자

   ??  , ??=

 

특징  : 오른쪽 결합

 

 

null 병합 연산자 라고 부르는 ?? 연산자는

null이 아닌 경우 왼쪽 값을 반환하고 null인 경우 오른쪽 값을 확인 하고 반환하는 연산자다.

만약 왼쪽값이 null 이 아닌경우  오른쪽 값을 확인하지 않는다.

 

 

            int? a = 17;
            int b = 15;
            int? c = null;
            
            Console.WriteLine(a ?? b); // Output :17,  왼쪽값이 null이 아니기때문에 17.
            Console.WriteLine(c ?? b); //Output :15,   왼쪽(null)->오른쪽 값을 확인해 본뒤 b반환
            Console.WriteLine(c ?? c); //Output : ,    둘다 null 이기 때문에 null
            //Console.WriteLine(b ?? c); Err ?? 연산자는 int 및 int? 형식의 피연산자에 적용할수 없다

int? 가 뭔지 모르겠다면 이 블로그의 Nullable 을 보고오는게 좋다.

https://programing-note.tistory.com/entry/c-Nullabe-%EA%B0%92-%ED%98%95%EC%8B%9D

 

 

c#8.0 이상에서 사용할수 있는null 병합 할당 연산자(??=)는  null 병합 연산자 와 비슷하다. 

  왼쪽 값이 null인경우 우측 값을 할당시켜준다. 물론 좌측값이 null이 아닌경우 우측값을 확인하지 않는다.

            int? a = null;
            int b = 15;
            a ??= b; 좌측값이 null 이기때문에 우측b 값을 확인후 a값에 대입한다.
            Console.WriteLine(a); // 15

 이 연산자는 가끔 유용하게 쓰일수있다.

 

 

예시로는 null인지 확인하는 구문을 간단하게 만들수있다.

            if (isNull is null)
            {
                isNull = 10;
            }

이러한 연산을

            isNull ??= 10;

이렇게 간단하게 쓸수 있다.

 

 

 

Null 조건부 연산자 ?. 및 ?[]

C# 6이상부터 사용할수 있는 연산자다.

이는 Null이 아닌것으로 평가되었을 때만 멤버 엑세스 ?. ,또는 요소 엑세스?[], 연산을 피연산자 에게 적용하고

 Null 인것으로 평가되었을경우 Null을 반환한다.

 

  조건부 멤버나 요소 액세스 작업에서 null 을 반환하면 나머지 체인은 실행되지 않는다.

 

A?.B?.c

위 와 같을때 A가 Null일때 B는 평가되지 않는다.

이와 같이 A또는 B가 Null일경우 C는 평가되지 않는다.

 

아래 윈도우 예제 코드를 보면 쉽게 이해할수 있다.

            
            double SumNumbers(List<double[]> setsOfNumbers, int indexOfSetToSum)
            {
                return setsOfNumbers?[indexOfSetToSum]?.Sum() ?? double.NaN;
            }

            var sum1 = SumNumbers(null, 0);
            Console.WriteLine(sum1);  // output: NaN
            var numberSets = new List<double[]>
                 {
                     new[] { 1.0, 2.0, 3.0 },
                     null
                  };

            var sum2 = SumNumbers(numberSets, 0);
            Console.WriteLine(sum2);  // output: 6

            var sum3 = SumNumbers(numberSets, 1);
            Console.WriteLine(sum3);  // output: NaN
            
                //var sum4 = SumNumbers(numberSets, 2); 예외발생

위 코드처럼 코드를 짤때 Null 이 들어가 오류가 생기는 상황을 간단하게 처리할수있다.

 

 

주의 할점

a.x 또는 a[x] 가 예외를 던지면 a?.x 또는 a?[x] 는 null이 아닌 예외를 던진다.

위의 예제 마지막줄 처럼 x가 a의 배열의 크기 보다 큰경우 a?[x] 는 IndexOutOFRangeExcption 예외를 던진다.

 

참고

?. 연산자는 왼쪽 연산자를 한번만 계산하고 null이 아닌것을 확인한 후에는 null로 변경할수 없도록 보장한다.

 

또한 스레드를 사용할때도 안전하게 호출할수 있다.

스레드를 호출할때 null인지 확인하고 호출하는 것을

var handler = this.PropertyChanged;
if (handler != null)
{
    handler(…);
}
 
 를 아래처럼 변경할수있다

PropertyChanged?.Invoke(…)

위 상황처럼 간단하게  사용할수있다

 

참고 링크 :docs.microsoft.com;

'언어 > C#' 카테고리의 다른 글

c# Nullable값 형식  (0) 2022.03.24
c# Tuple  (0) 2022.03.23
C# Ref,Out 키워드 와 in 키워드  (0) 2022.03.16
반응형

Nullable 값 형식 : 기본 값 형식의 모든 값과 추가 null 값을 나타낼수 있다.

 

예시로 

 bool 변수는 true false 두가지를 나타내는게 가능하고 Null은 표현불가능하다.

하지만 null 이 필요할수 있기때문에  자료형 뒤에 ? 를 붙이면 null 표현이 가능해진다.

 

사용 방식 

값 타입 자료형 ? 변수명

    int? a = 10;
    int? b = null;
   // int c = null;  null을 허용하지 않는 값 형식
   
   class A{}
  // A a?; null 을 허용하지 않는  값 형식 이여야 한다.
  
  int?[] arr = new int?[10];
  //배열 선언방법

 

 

Nullable 값 형식에서 기본 형식으로 변환 

   int? a = 10;
   int b = 0;
   b = a ?? -1;
   // a가 null 이면 -1이 들어가고  null이 아닐경우 a값이 그대로 들어가 10이 들어간다.

 

?? 연산자는 왼쪽 값이 null 이면  오른쪽 값을 사용하고 null이 아닐경우 왼쪽 값을 사용하는 

일종의 null 을 검사하는 연산자다. 

 

 

 

 

 

is

is 연산자로는 Nullable 형식인지 아닌지 구분 불가하다

마소 예제코드

int? a = 1;
int  b = 2;

if ( a is int )
{
  Console.WriteLine(int? instance is compatible with int);
}
if (b is int?)
{
    Console.WriteLine("int instance is compatible with int?");
}

// Output:
// int? instance is compatible with int
// int instance is compatible with int?

 

 

 

GetType 

Object.GetType은 Null 허용값 형식의 boxing으로 기본형식 값의 boxing과 동일하게 나온다.

GetType은 기본형식 Type 인스턴스를 반환한다.

int? a = 17;
Type typeOfA = a.GetType();
Console.WriteLine(typeOfA.FullName);
// Output:
// System.Int32

 

그럼으로 아래 코드 처럼 구분할수 있다

마소코드

Console.WriteLine($"int? is {(IsNullable(typeof(int?)) ? "nullable" : "non nullable")} value type");
Console.WriteLine($"int is {(IsNullable(typeof(int)) ? "nullable" : "non-nullable")} value type");

bool IsNullable(Type type) => Nullable.GetUnderlyingType(type) != null;

// Output:
// int? is nullable value type
// int is non-nullable value type

 

 

 

참고 링크 :docs.microsoft.com;

'언어 > C#' 카테고리의 다른 글

c# ?? 연산자 (Null 병합 연산자)  (0) 2022.07.12
c# Tuple  (0) 2022.03.23
C# Ref,Out 키워드 와 in 키워드  (0) 2022.03.16
반응형

c# 튜플 : 간단한 데이터 구조로 여러 데이터 요소를 그룹화 할수있다.

 

튜플 변수 선언

(원하는 자료형 , 원하는 자료형 , ... ) 변수명;

// 선언과 동시에 초기화
(double,int) t = ( 1.1 , 3 );
 t.Item1 = 10;

(string name , int age) t2;
t2 = (name: "Mok", age: 5);
t2.age = 20;

이런식으로 간단하게 그룹화 할수있다

또한 튜플은 함수의 반환형식 에서도 사용할수 있다.

 

 

튜플 함수 사용

(원하는 자료형 , 원하는 자료형 , ... ) 함수명 (매개변수s..) { 내부구현...  retrun(반환값,...) };

  
   (int min, int max) FindMinMax(int[] input)
        {
         //... 내부구현
            return (min, max);
        }

위 함수처럼 자료형 클래스 말고도 묶어서 사용이 가능하다.

 

 

c# 에서 제공한 튜플의 사용 사례를 보면 더욱 쉽게 알수있다.

       //마소 c# 튜플 사용 사례
       var ys = new[] { -9, 0, 67, 100 };
        var (minimum, maximum) = FindMinMax(ys);
        // Output:
        // Limits of [-9 0 67 100] are -9 and 100

        (int min, int max) FindMinMax(int[] input)
        {
            if (input is null || input.Length == 0)
            {
            }

            var min = int.MaxValue;
            var max = int.MinValue;
            foreach (var i in input)
            {
                if (i < min)
                {
                    min = i;
                }
                if (i > max)
                {
                    max = i;
                }
            }
            return (min, max);
        }

위 사용 사례에서 보이는것 처럼 minimum 과 maximum 을 따로 구하지 않고 한번의 묶음으로 처리하여

하나의 함수로 처리하는것을 볼수있다.

 

 

또다른 사용 예시로는 비교문에서 알수있다.

//튜플 비교문


(int a, byte b) left = (5, 10);
(long a, int b) right = (5, 10);
Console.WriteLine(left == right);  // output: True
Console.WriteLine(left != right);  // output: False

var t1 = (A: 5, B: 10);
var t2 = (B: 5, A: 10);
Console.WriteLine(t1 == t2);  // output: True
Console.WriteLine(t1 != t2);  // output: False

보이는것 처럼 두개의 묶음으로 비교하여 비교문을 줄일수 있다.

 

 

튜플은  Dictionary 에서도 사용이 가능하다.

var limitsLookup = new Dictionary<int, (int Min, int Max)>()
{
    [2] = (4, 10),
    [4] = (10, 20),
    [6] = (0, 23)
};

if (limitsLookup.TryGetValue(4, out (int Min, int Max) limits))
{
    Console.WriteLine($"Found limits: min is {limits.Min}, max is {limits.Max}");
}
// Output:
// Found limits: min is 10, max is 20

이처럼 쉽게 저장하고 사용하는것을 볼수있다.

 

 

참고

 1. 지금 사용한 튜플 형식사용은 기존 Tuple class 와는 다르다.

    ㄴ 지금 사용한 형식은 System.ValueTuple 이고 Tuple class는 System.Tuple 이다.

 2.  System.ValueTuple 형식은 데이터 멤버는 필드이다.

 2.1  System.Tuple 형식은 데이터 멤버는 속성이다.

 

3. anonymous type 과 비교도 가능하다.

var apple = new { Item = "apples", Price = 1.35 };
(string,double) appe = ( "apples", 1.35 );

if(appe == appe)
{
Console.WriteLine("true"); //true
}

 

 

익명 타입, 튜플, vlaueTuple 들의 주요 차이점은 다음과 같다

이름 액세스 한정자 형식 사용자 지정 멤버 이름 분해 지원 식트리 지원
anonymous type internal o x o
Tuple public x x o
ValueTuple public o o x

 

참고자료 : docs.microsoft.com

'언어 > C#' 카테고리의 다른 글

c# ?? 연산자 (Null 병합 연산자)  (0) 2022.07.12
c# Nullable값 형식  (0) 2022.03.24
C# Ref,Out 키워드 와 in 키워드  (0) 2022.03.16
반응형

 

Ref 키워드와 Out 키워드를 더 잘 이해하기 위해서는 call by refurence , call by value를 알고 보는 게 좋다.

Ref , Out 키워드

정의부터 설명하자면

 Ref Pass by Reference

   ㄴ 얕은 복사 매개변수 지정자

Out Output Parameters

   ㄴ 출력용 매개변수 - 내부에서 값을 할당해주어야 한다.

 

 

사용 방법

 ref out 사용법

 

함수 선언 시

 리턴 값 함수명 (ref & out키워드 매개변수 타입  변수 이름 ) <-함수를 선언 또는 구현 시에도 매개변수 앞에 써줘야 한다.

함수 사용 시

 함수명( ref & out 키워드 변수 ) <- 사용할 때에도 매개변수 앞에다 ref 키워드를 써줘야 한다.

    public void FuncRefB(ref int A) 
    {
        A++;
    }

    public void Func( int A)
    {
        A++;
    }

    public void FuncOutB(out int A)
    {
        A = 10; // 내부에서 값을 대입 하지 않으면 오류가 나온다.
    }
   
       ...
       {
        int A;
        
        FuncOutB(out A);
          Console.WriteLine(A); // 내부에서 생성해서 받는다 Call by Refurence + @  10
        Func(A);
          Console.WriteLine(A); // 늘어나지 않는다 Call by Value   10
        FuncRefB(ref A);
          Console.WriteLine(A); // 값이 늘어난다. Call by Refurence  11
        }

이처럼 c나 c++처럼 포인터 참조형처럼 Call by Reference 형식으로 넘겨줄 수 있다.

 

(참고할 점)

1. Ref 키워드는 할당이 되어있어야 한다.

2. Out 은 Ref처럼 할당이 되어있지 않아도 사용 가능하지만. 내부에서 새로 할당한다.(이전 값은 사라짐)

3. Ref int -> int 오버로드는 가능하나 Out -> Ref , in 오버로드는 불가능하다.

 

 

구조체 같은 경우 기본적으로 call by value 형식이기 때문에 구조체를 ref 키워드로 넘겨주면 
얕은 복사(주소 값)처럼 사용할 수 있다.

 

in 키워드

in : 읽기 전용 매개변수 지정자

 

함수 선언 시

 리턴 값 함수명 ( in 키워드 매개변수 타입  변수이름 ) <-함수를 선언 또는 구현 시에도 매개변수 앞에 써줘야 한다.

함수 사용 시

 함수명(  키워드 변수 ) <-  in은 사용할 때 앞에 붙여 주지 않는다.

 

void FuncIn(in int num)
{
  //num++; 내부 수정불가능
  
}

내부에서 수정이 불가능하기 때문에 혹여나 다른 값을 입력하거나 수정하는 실수를 미연에 방지할 수 있다.

 

 

Ref 와 in 키워드는  복사 비용을 참조로 변경하여 성능 향상을 생각할수 있다.

 

 

--추가--

 

out 키워드 의경우 C#7.0 버전부터는 튜블 반환을 지원했다.

튜플 반환이 있기전에 여러값을 반환 할수 없었기 때문에 out을 주로 사용하였다.

 

(int sum, int product) Calculate(int a, int b) => (a + b, a * b);

 

 

void Calculate(int a, int b, out int sum, out int product)
{
    sum = a + b;
    product = a * b;
}

Calculate(2, 3, out int sum, out int product);

 

out 키워드는 성능이 중요한 코드나 오래된 api, 호환성 때문에 쓰인다.

 

 

 

반복적으로 많이 쓰일때 생성되는것보다 out으로 불필요한 메모리할당과 GC부하를 줄일수있다.

 

 

 

 

 

 

 

 

 

 

 

 

 

'언어 > C#' 카테고리의 다른 글

c# ?? 연산자 (Null 병합 연산자)  (0) 2022.07.12
c# Nullable값 형식  (0) 2022.03.24
c# Tuple  (0) 2022.03.23

+ Recent posts