메모리
게임도 컴퓨터 프로그램입니다.
컴퓨터 프로그램의 핵심은 데이터를 산술 가공한후 입출력(Read/Write, Get/Set, Input/Output) 하는 것 입니다.
여기서 데이터의 입출력은 컴퓨터 주기억장치에 기록된 데이터만 가능합니다.
주기억 장치란 일반적으로 컴퓨터 RAM 공간이며 OS 커널에서 관리를 해주는 한도 내에서 사용 가능합니다.
이중 프로그래머가 직접 다룰 수 있는 영역은 HEAP(힙) 이라고 불리는 영역이고,
OS 커널의 관리하에 접근 가능한 영역을 얻어 와야만(할당) 사용 가능합니다.
프로그래밍 언어에서 alloc, malloc, new 등의 함수나 연산자로 할당한 메모리가 바로 힙영역입니다.
즉 OS 커널은 컴퓨터 휘발성 공간에 포함되어 있는 영역을 힙이라는 이름으로 관리하며, 메모리 할당
요청이 들어오면 사용가능한 메모리 위치(주소, Address)를 리턴해주는 것입니다.
데이터
데이터에는 두종류가 있습니다.
바로 텍스트 와 바이너리 입니다.
텍스트 데이터는 '.txt' 같은 텍스트 파일을 떠올리면 되고,
바이너리는 '.png' 같은 이미지 파일을 떠올리면 됩니다.
두 데이터 모두 바이트 스트림(1차원 배열)이라는 점에서 거의 같은 공통점이 있습니다.
최신 언어에서는 간과하기 쉽지만 바이너리이냐 텍스트 이냐는 아주 작은 차이가 있는데..
바로 널문자 ('\0') 의 포함 여부입니다. (\가 붙은 이유는 실제 문자 '0'과 구분하기 위함입니다, )
(숫자 값으로는 0(NULL) 이고 작은 따옴표는 아스키 문자를 표현하기 위함입니다.)
이 널문자가 마지막에 있는 바이트 배열을 스트링이라고 부릅니다.
따라서 스트링 배열의 실제 바이트수는 문자길이에 +1이 됩니다.
스트링은 약속된 인코딩으로 해석하면 사람이 읽을 수 있는 형태로 표시할 수 있습니다.
위의 아스키 문자로만 이루어진 텍스트는 1바이트 단위로 문자 의미를 가지게 되며 스트링으로 저장된
텍스트 파일을 메모장 같은 텍스트 전용뷰어 툴로 열면 바이트가 문자로 변형된 모습으로 볼 수 있습니다.
2바이트를 필요 하는 한글 같은 문자(EUC-KR)들은 아스키와 혼용하거나 각 언어별 정해진 코드로 저장되는데
이것을 멀티바이트(Multi Byte) 형식 이라 부릅니다.
최근에는 이런 나라별 언어 형식이 달라 한글이든 영어든 2바이트로 통일된 규약을 쓰는데 이것을
유니코드(Wide Char) 형식이라 부릅니다.
UTF-8, UTF-8 Bom 등이 유니코드 입니다.
이렇게 텍스트 스트링은 정해진 코드 규약에 따라 기록이 되어 있습니다.
하지만 텍스트 역시 바이트 배열 데이터 이며 사람이라면 쓸 수 밖에 없는 언어를 표현하기 위한
바이트 덩어리입니다.
최신의 언어에서는 바이트를 직접 다루는 함수의 형태 보다는 하나의 자료구조 처럼
사용하기 수월하게 클래스 형태(string, String, CString, std:string 등)로 제공하고 있습니다.
// 스트링 함수 사용 (c style)
char *pTestString = malloc(20);
strcpy(pTestString, "hahaha");
strcat(pTestString, "hoho");
// 문자 하나씩 대입하여 스트링 조립 (c style)
char *pTestString2 = malloc(20);
pTestString2[0] = 'h';
pTestString2[1] = 'a';
pTestString2[2] = 'h';
pTestString2[3] = 'a';
pTestString2[4] = 'h';
pTestString2[5] = 'a';
pTestString2[6] = '\0';
pTestString2[7] = 'h';
pTestString2[8] = 'o';
pTestString2[9] = 'h';
pTestString2[10] = 'o';
pTestString2[11] = '\0';
// 스트링 클래스 사용 (c++, c#, java style)
string strTestString3 = "hahaha";
strTestString3 += "hoho";
// 아스키 값을 대입하여 스트링 조립 (c style)
char *pTestString4 = malloc(20);
pTestString4[0] = 104; // h
pTestString4[1] = 97; // a
pTestString4[2] = 0x68; // h
pTestString4[3] = 0x61; // a
pTestString4[4] = 104; // h
pTestString4[5] = 97; // a
pTestString4[6] = 0; // \0
pTestString4[7] = 104; // h
pTestString4[8] = 111; // o
pTestString4[9] = 0x68; // h
pTestString4[10] = 0x6f; // o
pTestString4[11] = 0; // \0
cout << pTestString
=> hahahahoho
cout << pTestString2
=> hahahahoho
cout << pTestString3
=> hahahahoho
cout << pTestString4
=> hahahahoho
클래스 내부적의 데이터는 가변 배열의 형태로 구현되어 있습니다.
또한 멀티바이트와 유니코드의 별도 구현 없이 손쉽게 스트링 처리를 지원합니다.
텍스트 데이터 다음으로 많이 쓰는 데이터는 이미지 데이터 입니다.
이미지는 대표적인 바이너리 데이터 입니다.
단위는 픽셀이며 픽셀들이 1차원 배열로 이루어져 있는것이 이미지라고 할 수 있습니다.
픽셀은 1바이트 단위일 수도 있고 2바이트 단위일 수도 있고 3,4바이트 단위일 수 도 있고 다양합니다.
컬러 사용범위와 압축 여부에 따라서 한 픽셀의 바이트 단위가 달라집니다.
압축을 하지 않았다고 가정하면 RGB 채널로 이루어진 이미지는 각 채널당 1바이트씩 사용합니다.
한 바이트는 0~255 값을 가지므로 각 채널당 256단계의 명암을 가지게 되는 것이죠.
RGB 무압축은 24bit, 여기에 알파채널까지 사용하면 32bit를 한픽셀로 가지게 됩니다.
32bit는 4바이트 이므로 100 * 100 이미지가 32bit 무압축 이미지 라면 10000 * 4 = 4만 바이트(39k)가 됩니다.
이미지는 가로 세로 2차원 배열일것 같은데 1차원 배열입니다.
마치 텍스트 파일에 개행으로 인해서 여러줄로 보는것 처럼 이미지도 그런 약속을 하여 그렇게 출력했기 때문에
2차원으로 보이는것입니다.
위 100*100 이미지를 예로 들면 (x,y) 가 위치라고 할 때
(99,0)의 위치의 픽셀 다음이 (0, 1)위치의 픽셀이 됩니다.
바이트로 표현하면 Image[99]가 99,0 이고 Image[100]이 0,1 이 됩니다.
배열안에 숫자가 오프셋(offset)이라는 1차원 배열의 순번이라는 의미라고 가정한다면
offset = y * width + x
라는 공식으로 말할 수 있습니다.
위의 예에 대입하면 이미지의 가로 해상도는 100이므로
//(99, 0)
offset = 0 * 100 + 99 = 99
//(0, 1)
offset = 1 * 100 + 0 = 100
//(33, 44)
offset = 44 * 100 + 33 = 4433
이런식으로 계산됩니다.
Image라는 배열이 있다고 가정했을때
Image[4433] = 0;
이렇게 코드에 입력하면 100*100 이미지의 x 33, y 44 위치의 컬라가 검정색으로 바뀌게 됩니다.
픽셀은 스트링 처럼 널문자 같은것이 없습니다.
바이트 버퍼에 있는 그대로가 데이터인 샘입니다.
스트링과 구분하기 위해 이미지 같은 원시 바이트 덩어리 데이터를 RAW 데이터라고 부릅니다.
어떤 압축이나 암호화 따위를 하기 전의 의미로도 사용되지만 바이트 덩어리를 RAW 데이터라고 부릅니다.
또 바이트 데이터, 바이너리 데이터 같은 말입니다.
일반적으로 RAW 데이터를 처리하는 일이 잘 없지만,
memset, copy(memcpy,BlockCopy), move, blt 등의 유사한 함수 형태로 각 언어에서 제공되고 있습니다.
바이너리 데이터는 그 단위가 픽셀이면 이미지 일뿐 반듯이 이미지라고 생각할 필요는 없습니다.
바이너리 데이터는 용도에따라서 다양한 형태로 저장됩니다.
struct SaveData
{
int _index;
DATE _saveDate;
int _key;
int _switch;
char[256] _temp;
}
SaveData data;
// data 입력
...
..
SaveFile(&data); // c style
위의 예 처럼 가변 단위로 바이트데이터가 구성되어 있을 수도 있습니다.
포인터
포인터는 메모리의 주소입니다.
최신의 언어에서는 잘쓸이 없고 지원하지 않는 언어가 많지만
메모리의 주소라는 개념은 알고 있는것이 도움이 됩니다.
이제까지 데이터의 종류와 그 데이터를 저장하고 읽기 위해서는 메모리 공간이 필요함을 알았습니다.
컴퓨터에 8기가 짜리 RAM이 꼽혀있다고 가정하고 기본적으로 OS를 사용 하기 위해 2기가 정도는
항상 쓰는 공간이고 남은 6기가를 우리가 사용하고 있다고 가정합니다.
하지만 OS 위에서는 우리가 만든 프로그램 뿐만 아니라 다양한 프로그램들이
실행되었다가 종료되었다를 반복 합니다.
따라서 우리가 사용할 수 있는 공간은 커널의 메모리 관리에 의해 수시로 변화합니다.
그 사용 가능한 위치가 수시로 변화하기 때문에 우리는 원하는 공간 만큼 분양 받을 필요가 있습니다.
그리고 그 분양받은 공간은 다른 프로그램이 사용하지 못하게 해야하는 것이구요.
우리는 그 공간을 할당 받았다고 표현합니다.
메모리를 할당 받으면 그 메모리의 시작 주소값을 리턴해줍니다.
그리고 그 리턴 받은 주소값을 담을 수 있는 변수를 포인터라고 말합니다.
포인터가 메모리의 주소값을 알고 있으므로 그 주소가 가르키고 있는 곳에
데이터를 읽고 쓸수가 있는것입니다.
// 20byte alloc
char *pTest1 = malloc(20);
// 40byte alloc (20 * 2)
short *pTest2 = malloc(20 * sizeof(short));
// 80byte alloc (20 * 4)
int *pTest3 = malloc(20 * sizeof(int));
cout << sizeof(pTest1);
=> 4
cout << sizeof(pTest2);
=> 4
cout << sizeof(pTest3);
=> 4
char getTest1Value = pTest1[0];
getTest1Value = pTest1[10]; // get
pTest1[11] = 0x02; // set
short getTest2Value = pTest2[0];
getTest2Value = pTest2[10]; // get
pTest2[11] = 0x0202; // set
int getTest3Value = pTest3[0];
getTest3Value = pTest3[10]; // get
pTest3[11] = 0x02020202; // set
위의 예시는 메모리를 각각 20,40,80 byte 할당후
해당 시작주소를 가져와서 원하는 위치에 값을 쓰는 예제 입니다.
그리고 메모리를 20,40,80 바이트를 할당 받았지만,
포인터변수 자체 사이즈는 4바이트를 차지하고 있는 변수
라는것도 보여주고 있습니다.
32bit 언어에서 그 주소값의 사이즈는 4바이트가 됩니다.
즉 포인터 자체는 버퍼(바이트공간) 전체를 의미하는 변수가 아닙니다.
해당 버퍼의 시작 주소값을 가지고 있는 것이죠.
포인터 자체는 자료형과 관계 없이 4바이트 변수가 됩니다.
포인터도 배열처럼 인덱스 참조가 가능합니다.
인덱스의 순번이 가르키고 있는 주소는 자료형에 따라 달라집니다.
1바이트 자료형 포인터는 1바이트씩 증감이 됩니다.
(2바이트 자료형은 2바이트씩 증감, 4바이트는 4바이트씩 증감, sizeof(형) 만큼)
&pTest1[0] 의 주소값이 0x00000001 이였다면 &pTest1[1]의 주소값은 0x00000002이 됩니다.
&pTest2[0] 의 주소값이 0x00000001 이였다면 &pTest2[1]의 주소값은 0x00000003이 됩니다.
&pTest3[0] 의 주소값이 0x00000001 이였다면 &pTest3[1]의 주소값은 0x00000005이 됩니다.
(여기서 & 표시는 해당 배열 위치의 주소값을 의미합니다.
pTest1 자체는 포인터 변수 이며 할당 받은 메모리 버퍼의 시작 주소값를 담고 있습니다.
따라서 pTest1 값과 &pTest1[0]의 값은 같습니다.)
즉 포인터의 자료형은 증감되는 단위를 결정합니다.
pTest1~3이 이미지 버퍼의 시작 주소를 가지고 있는 포인터 라면
pTest1 은 픽셀이 1바이트인 이미지, pTest2는 픽셀이 2바이트인 이미지,
pTest3은 픽셀이 4바이트인 이미지라고 할 수 있습니다.
저것들이 이미지라고 한다면 그래픽 카드에 스크린 버퍼를 가르키고 있는 주소에 복사하면
저들이 가지고 있는 이미지가 화면에 그려지게 되겠지요.
Blt 관련 함수들이 내부적으로 메모리 카피로 이루어져 있는 함수들입니다.
이해를 돕기위해 이미지를 예로 들었는데 포인터는 결과적으로 메모리의 주소값이며
그 주소값이 가르키고 있는 공간에 데이터를 읽고 쓸수 있는 것입니다.