본문 바로가기
Software/C++&MFC 핵심노트

[C++/MFC 핵심노트] 배열과 포인터 그리고 문자열

by lovey25 2018. 11. 12.
반응형

2018-12-20 update log: 실습용 더미프로그램 수정으로 인한 실습코드 변경
2018-11-21 update log: 오류사항 수정


배열, 포인터, 문자열은 서로 개념적으로 긴밀한 관계가 있습니다. 개념과 연관성을 자연스럽게 이해하도록 하기위해 이 3가지를 하나의 주제로 묶어서 설명하려 합니다.

핵심은 포인터라고 할 수 있을 것 같습니다. 포인터란 개념은 C/C++에서 매우 중요하다고들 합니다. 그리고 언어를 공부해 본적 있는 분들은 포인터가 나오기 전까지는 별 거부감 없이 잘 따라가다가 포인터 부터는 이게 뭔소리가 했던 경험들 다들 있으실 겁니다.

저도 아직 포인터란 개념을 완벽히 이해했다고는 말하지 못하지만, 지금 다시 생각해보면 어려운 개념이라서 이해를 못한것이 아니라 책의 설명이 너무어려웠것이 아닌가 싶습니다. 메모리의 공간개념을 활자기반의 전통적인 교육자료로는 정보전달의 한계가 있었다라고 스스로 위안하고 있습니다. 

자 지금부터 제가 그 어려운걸 한번 정리해 보겠습니다. ㅡㅡ;

Arrays (배열)

앞에서 변수의 기본형들에 대해서 알아봤는데 이제 이 변수들을 묶어서 사용하는 배열에 대해서 알아보겠습니다. 변수의 사용을 위해서는 선언을 해야 한다고 했습니다. 그렇다면 엑셀같이 표로된 무수히 많은 data를 다루기 위해서, 음... 간단하게 만개정도의 변수가 필요한 상황을 가정해 봅시다. 

이런 경우 만개의 변수이름을 일일히 지정해서 만번의 선언을 해주어야 하는데 너무 비효율 적이죠. 그리고 이렇게 만들어진 변수는 관리도 힘듭니다. 그래서 이런 변수묶음을 배열이란 것으로 관리할 수 있습니다.

아래 배열 선언과 초기화 예제를 먼저 보시죠.

[예제#1]

int i_val[5] = {11, 40, 3, 9, 22};    // 크기가 [5]인 정수형 배열을 선언하고 초기화 값 지정
int D2[3][4];                         // 크기가 [3][4]인 2차원 배열 선언
char D3[4][5][6];                     // 크기가 [4][5][6]인 3차원 배열 선언

먼저 첫번째 배열은 크기가 5인 정수형 배열입니다. "i_val"이라고 된 변수이름 뒤에 있는 대괄호("[ ]") 안에 숫자 "5"가 적혀 있습니다. 여기서 "[5]"가 배열을 만드는 키워드 입니다. 숫자 5는 배열의 크기를 말하죠.

다시말해 정수형인 "i_val"변수를 5개 연속해서 선언한다는 뜻이고 5개의 변수를 구분하기 위해서 각 변수뒤에 숫자로 번호를 붙여주는겁니다. 그리고 "=" 기호 뒤에 중괄호로 표시한 집합표시에 5개의 정수 초기값이 있습니다. 

아래 표를 보시죠.

배열이름 i_val[0] i_val[1] i_val[2] i_val[3] i_val[4]
Data 11 40 3 9 22

i-val[0] 부터 i_val[4]까지 5개의 변수에 초기값 "11"부터 좌측에서 우측으로 차례대로 "22"까지를 초기값으로 저장이되게 됩니다.  이렇게 크기가 5인 정수형 배열이 선언되었고 각 값도 초기화가 되었습니다.

여기서 기억할 점은, 배열의 첫번째는 "0번( [0] )"부터 시작한다는 것이고, 배열이름 다음에 오는 숫자로 해당 배열내 내가 원하는 지점으로 바로 접근할 수 있다는 사실입니다.

그리고 두번째 줄의 2차원 배열인 "D2"를 풀어서 쓰면 정수형 변수 D2를 크기가 [3]인 배열로 만들고 다시 그 배열을 크기가 [4]인 배열로 만든다 가 됩니다. 그러니까 이 배열은 정수형 변수가 12개 ( 3 x 4 = 12 )를 저장할 수 있는 거죠. 

그리고 세번째 줄에 있는 3차원 배열도 마찬가지 입니다. 2차원의 배열을 한번더 3차원까지 확장시킨거죠.

이제 배열이 뭔지 알았으니 머리속에서 오른쪽으로 잠시 밀어놓고 이어서 문자열 개념을 알아보도록 하겠습니다.

string, CString (String, 문자열)

다음은 문자열 변수입니다. 앞서 "[C++/MFC 기본강좌] 변수형, Data type의 기본" 에서 char/wchar_t와 같은 문자변수를 봤었는데요. 이 문자변수를 배열처럼 만들어서 "문자 + 배열 = 문자열" 이라고 한다라고 보시면 됩니다.

그러니까 문자 하나씩만 저장하는 문자변수(char)를 시리즈로 묶어서 단어나 문장을 한번에 표현할 수 있도록 배열을 만들고 그 배열을 문자열이란 변수형으로 별도로 만들어서 처리하는 거죠.

그리고 CString은 string과 같은 개념이지만 MFC에서 편하게 사용할 수 있도록 다양한 기능을 추가해서 만든 유용한 문자열형입니다. 우리는 MFC를 공부하고 있기 때문에 앞으로 주로 문자열은 CString을 많이 이용하는 걸 보실 수 있으실겁니다.

[예제#2]

char ch_val = 'a';                      // 문자 변수
CString str_val = _T("It is string!");  // 문자열 변수

사용은 이렇게 하면 되는데 여기에 생소한  "_T("---")" 이런 형태의 표현이 있네요. 이건 문자의 인코딩 때문에 사용하는 메크로라는 건데, 따옴표 안에 있는 문자를 프로그램이 사용하는 인코딩에 맞게 유니코드일때는 유니코드로 멀티바이트일 때는 멀티바이트로 알아서 변경시켜주는 편리한 도구입니다. 

이전에 올린 포스팅 "문자 인코딩 - 유니코드와 멀티바이트란 무엇인가?" 를 읽어 보시면 이해가 되실껍니다. 결론적으로 말해서 앞으로 코딩을 할때 문자열은 모두 _T("") 매크로를 사용해서 표현을 해주면 만사 OK다 가 되겠습니다.

자 아래 그림을 보시죠. 아래 그림을 컴퓨터의 메모리라고 생각합시다. 그리고 위의 코드에 따라서 변수가 어떻게 만들어 졌는지를 간단히 설명하기 위해서 개념을 설명한 것이니 실제와는 차이가 있을 수 있습니다. ("CString"도 그냥 "string"의 개념으로 설명하겠습니다.)

변수 "ch_val"은 문자변수로 char형입니다. char형은 1바이트의 크기를 가지는 변수 입니다. 그래서 위의 소스코드의 1번줄처럼 변수를 선언하게 되면 컴퓨터는 임의의 위치(위 그림에서는 "B"라는 주소를 가지는 위치)에 1바이트의 크기를 할당합니다. 그리고 "ch_val"이라는 이름표가 붙겠죠. 

그리고 CString 문자열 변수이기 때문에 1바이트의 문자가 연속적으로 이어지는 변수입니다. 따라서 위의 예제의 경우는 총 14개의 공간에 변수가 할당됩니다. 저장할 문자열이 "It is string!" 인데, 띄어쓰기 포함해서 "13개"인데 왜 14개이냐면 마지막에 문자열이 끝났다는 걸 표시하는 NULL문자가 필요하기 때문에 실제 문자열 길이보다 +1 만큼 더 필요합니다.

자 여기까지가 문자열의 개념이었습니다. 문자열 개념은 머리속에서 왼쪽으로 살짝 밀어두겠습니다.

Pointer (포인터)

드디어 포인터 입니다.

우리가 지금까지 봤던 변수나 배열들은 모두 컴퓨터의 메모리에 물리적인 공간을 차지하고 있습니다. 컴퓨터는 메모리 공간을 관리하기 위해서 나름의 규칙으로 구획을 나누고 각 구획을 주소로 지정합니다. 주소만 알면 특정 변수에 접근할 수 잇게 되는 것이죠.

포인터는 바로 이런 주소를 저장하는 변수를 말합니다. 다음과 같이 변수 이름앞에 "*" 키워드를 사용해서 포인터를 선언합니다.

[예제#3]

int *ip;  // pointer to an integer
double *dp;   // pointer to a double
float *fp;  // pointer to a float
char *ch;  // pointer to a character

말이 길어지면 복잡해지기만 하기 때문에 예제를 통해서 차근차근 이해를 해 보도록 하겠습니다. 이전 글에서 소개한 실습용 더미프로젝트를 활용해서 실습해 보시면 되겠습니다.

[실습코드 #1]

int i_val = 5;              // 정수형 변수 선언 후 숫자 5로 초기화

myprint(_T("i_val = "));
myprint(i_val);             // i_val 변수에 저장된 값을 전달
myprint(_T("\r\n&i_val = "));
myprint((int)&i_val);       // i_val 변수의 주소를 전달

이 코드를 실습용 더미프로젝트에 집어넣고 결과를 확인해 보면 이런 결과가 나옵니다.

예제코드 결과는 "i_val = 5" 이고 7, 8번 라인의 결과가 "&i_val = 17821208" 입니다. 

코드와 결과를 비교해 보겠습니다.

우리는 1행 코드에서  "i_val"이라는 정수형 변수를 선언하고 숫자 5로 초기화를 시켰습니다. 

이말인 즉, 컴퓨터 메모리 공간 어딘가에 int형으로 정해진 크기만큼, int는 4바이트 랍니다, 자리를 확보한 다음에 i_val이라는 이름을 붙이고 거기에 5라는 숫자를 저장한 상태가 되는 것입니다. 

예제 4번 라인에서는 "i_val"이라는 이름으로 변수를 호출해서 저장된 값을 출력하도록 했고,

에제 6번 라인에서는 "&i_val" 이라는 표현을 써서 해당 변수의 주소값을 가져오도록 했습니다. 이때 더미프로그램에서 편의상 만들어 둔 함수 "myprint"가 정수형만 받을 수 있어서 앞부분에 (int)를 사용해서 정수형으로 강제변환해주었습니다.

(5번행에서 "\r\n&i_val = " 의 \r\n 이라는 표현은 개행문자를 나타냅니다. 출력할 때 줄바꾸기를 해주기 위해서 집어넣었습니다.)

"17821208"는 "i_val"이라는 이름을 가진 변수의 주소값이 되는 것입니다. 

이렇게 불러온 변수의 주소는 "*"키워드를 사용해서 해당 주소에 접근하는데 사용할 수 있습니다. (컴퓨터에서 주소값은 보통 16진수로 표현합니다. 여기서는 편의상 10진수로 진행되는 점 참고해주세요. 그리고 숫자도 상황에 따라 달라질 수 있습니다.)

역시 예제를 보겠습니다.

[실습코드 #2]

int i_val = 5;                                // 정수형 변수 선언 후 숫자 5로 초기화
int *pt_val;                                  // 포인터 변수 선언
pt_val = &i_val;

myprint(_T("pt_val = "));
myprint((int)pt_val);                        // 결과 : pt_val = 8318376
myprint(_T("\r\n*pt_val = "));
myprint((int)*pt_val);                       // 결과 : *pt_val = 5

앞의 실습코드 #1과 동일하게 "i_val" 변수를 만들어주었고, 2번 라인에서 "*"키워드를 사용해서 "pt_val"이라는 이름의 포인터 변수를 선언해 주었습니다.

3번 라인에서는 "i_val" 변수의 주소값을 포인터 변수에 대입했습니다.

6번 라인은 "pt_val"을 출력하기 때문에 당연히 저장되어 있는 주소값이 그대로 출력이 되었습니다.

8번 라인은 "*pt_val" 을 출력합니다. "pt_val"포인터는 i_val 변수의 주소값을 가지고 있습니다. 그런데 앞에 "*"표시를 써서 저장된 주소값이 지시하는 곳을 호출하도록 했습니다. 그 말인 즉, "*pt_val" 은 "i_val"과 같은 얘기가 되기 때문에 결과로 5가 나오는 것입니다.

아래 그림이 메모리의 상태를 도식화 한것입니다.

"i_val"이라는 변수가 메모리 주소 "8318376"이라는 위치에 있고 안에는 5라는 숫자가 저장되어 있습니다. 

그리고 "pt_val"은 어떤 다른 위치에 자리 잡고 있는데 안에는 "i_val"의 주소값인 "8318376"이 들어 있네요.

그래서 *pt_val (8318376에 있는 변수) = i_val = 5 가 되는 겁니다.

마무리

자 오래동안 달려서 배열, 문자열, 포인터가 무엇인지 알아봤습니다.

이제 머리속 오른쪽, 왼쪽으로 밀어두었던 배열과 문자열을 가져와서 가운데 있는 포인터와 짬뽕을 시켜, 처음 언급했던 배열, 문자열, 포인터가 긴밀한 관계에 있다는 이유를 알아보겠습니다.

다음 코드에서 변수 "a"와 "b"에는 각각 어떤 숫자가 들어갈까요?

[예제#4]

Int arr[4] = {1,2,3,4};
Int a = arr[0];
Int b = *arr;

1번라인에서 4개짜리 배열을 만들고 앞에서부터 차례대로 1, 2, 3, 4를 대입했습니다.

그리고 "arr[0]"은 배열의 첫번째 요소를 가르키는 표현이라고 했으니 당연히 첫번 째 요소인 "1"이 되어 변수"a"에는 숫자 "1"이 저장됩니다.

그럼 3번 라인의 "*arr"은 뭘까요? "*" 키워드는 포인터라고 했는데 포인터는 변수의 주소를 저장한다고 했습니다. 그럼 어떤 주소인지가 관건인데, 바로 시작 주소입니다.

다시말해 "*arr"은 "arr"이란 변수(배열)의 시작 주소를 가르키게 되고 정수형 변수이기 때문에 딱 정수의 크기만큼만 끊어서 가르키게 됩니다.

설명이 배열과 비슷하죠?

"arr[0]" 과 "*arr"은 동일한 개념이었습니다.

따라서 변수 "b"에도 "1"이 들어가게 됩니다.

자 이제 감이 오신분들은 문자열과 포인터의 관계도 눈치 채셨겠네요.

앞에 있던 예제#2 코드에서 "It is string!"을 담고 있던 문자열 변수 "str_val"에서 포인터나 배열을 이용하여 첫번째 문자인 "I"를 호출하려면 어떻게 해야 할까요?

자 아래 실습코드로 답을 확인해 보세요~ (실습은 더미프로젝트에서)

[실습코드 #3]

CString str_val = _T("It is string!");

myprint((CString)str_val[0]);
myprint(_T("\r\n"));
myprint((CString)*str_val);

(역시 급조한 myprint라는 함수의 부족함으로 (CString) 캐스팅을 사용한점 양해바랍니다. ㅡ.,ㅡ)

 

끝!

반응형

댓글