본문으로 바로가기
homeimage
  1. Home
  2. 컴퓨터/프로그래밍
  3. C언어 포인터와 배열 쉽게 이해하는 방법 1부

C언어 포인터와 배열 쉽게 이해하는 방법 1부

· 댓글개 · 바다야크

이 글은 C의 포인터를 학습 수준에서 알기는 아는데 정확히 모르는 분을 위한 글입니다. C언어를 학습하다 보면 대부분 포인터가 제일 어렵다고 합니다. 프로그래밍 경력자도 C언어는 자유도가 높아서 불만인 분이 많습니다. 너무 헷갈린다는 것이죠. 오죽하면 읽기 어렵고 이해하기 어려운 C언어 프로그래밍 대회 IOCCC(International Obfuscated C Code Contest)가 열렸겠습니까. 출품 작을 보았는데, 오우~ 도대체 뭔 소리인지. 당연히 포인터가 들어갑니다.

문제 1)

printf( "%c\n", 1["badayak.com"+1]);

아주 간단한 코드인데 결과가 예상되시나요? 포인터 없이 배열로 작성되었지만, 포인터를 알아야 쉽게 이해되는 내용입니다. C언어를 학습하시다 보면 포인터는 어렵고 배열은 쉽게 넘어가는데요, C언어로 프로그래밍을 하다 보면 배열을 포인터로 처리하는 경우가 많아서 오류 없는 프로그래밍을 하려면 배열의 특성도 정확히 알아야 합니다.

C언어 포인터와 배열 이해하기

C언어의 포인터와 배열을 말씀드리기 전에 얼마나 이해하고 계시는지 문제를 내겠습니다.

문제 2)

char  ary[100];

ary[0]       = 'b';
1[ary]       = 'a';
*(ary+2)     = 'd';
*(&ary[0]+3) = '\0';

printf( "%s\n", ary);

실행하면 어떤 내용이 출력될까요? 위 코드를 이해하지 못한다고 해도 대충 "bad"가 출력될 것 같습니다. 자, 이 문제는 아래의 내용을 이해하는지를 따지는 것입니다.

   ary[0], 1[ary], *(ary+2), *(&ary[0]+3)

조금 더 쉽게 말씀드리기 위해서 ary[0]을 C언어에서 똑 같이 표현하는 방법입니다.

   ary[0] = 0[ary] = *(ary+0) = *(&ary[0]+0)

타 언어 경력자가 C언어를 어려워하는 것은 독특한 특성이 있기 때문입니다. 다른 언어에서는 배열을 알리는 '[]' 문자는 그냥 배열을 선언하고 숫자에 따라 요소 값을 표현합니다. 그러나 C언어에서 '[]' 문자는 연산자입니다. 그것도 최상위 그룹에 속한 연산자입니다.

엄중히 따지면 C언어에는 다중 배열이 없습니다. 다중 배열처럼 처리하는 것이죠. 그래서 생김새부터 다른데요, 다른 언어는 [100,100,100]으로 선언하는 것을 C언어에서는 [100][100][100] 이렇게 요상하게 작성합니다. 자바에서도 '[]'는 연산자이지만, 1[ary] 이런 식으로,  또는 [100][100][100] 이렇게 작성하는지 모르겠습니다. 여하튼 C언어는 다른 언어에 비해 많이 튑니다.

C언어 배열 특성

문제로 돌아와서 ary[0]과 0[ary]부터 알아보겠습니다. ary[0]을 풀이하면 ary의 첫 번째 요소의 주소에 0을 더한 주소의 요소 값이 됩니다. 즉, '[]'는 참조에 대한 주소 값을 가감하는 연산자입니다.

포인터를 이해하려면 변수의 주소부터 이해하라고 하는데요, 배열도 마찬가지입니다. 주소를 쉽게 설명한다면, 컴퓨터는 메모리의 집합체이고, 변수는 특정 메모리에 자리를 잡은 위치이며, 그 위치를 주소라고 합니다. 그림으로 설명하면 이렇습니다.

C언어 변수와 주소
C언어 변수와 주소

위 그림은 변수 ch1, ch2, ch3 세 개의 변수가 정의되었고 각각의 주소는 100, 101, 102입니다. 그리고 각 변수의 값은 'A', 'B', 'C'입니다. 이처럼 메모리의 주소에 따라 변수가 배치되는 모습을 머릿속에 그릴 수 있다면 포인터와 배열을 이해하기가 매우 쉽습니다. ary 배열의 모습은 이럴 것입니다.

C언어 배열 변수
C언어 배열 변수

ary 배열을 주소로만 따진다면, ary[0]은 ary 첫 번째 요소의 주소 100에 0을 더한 주소가 됩니다. ary[1]은 ary 첫 번째 요소의 주소 100에 1을 더한 요소의 주소가 되고요. 다시 말씀드리지만, C언어의 '[]'는 시작 주소에 '[]' 사이에 있는 값을 더하는 연산자인 것입니다.

 

다른 언어의 배열은 이렇게 이해합니다.

  • ary[0]은 ary 첫 번째 요소
  • ary[1]은 ary 두 번째 요소
  • ary[2]은 ary 세 번째 요소

그러나 C언어의 '[]'는 주소를 계산하는 연산자라는 것을 알고 이해한다면 아래와 같이 해석할 수 있습니다.

  • ary[0]은 ary 첫 번째 요소의 주소 값에 0을 더한 주소의 요소입니다.
  • ary[1]은 ary 첫 번째 요소의 주소 값에 1을 더한 주소의 요소입니다.
  • ary[2]은 ary 첫 번째 요소의 주소 값에 2를 더한 주소의 요소입니다.

C언어에서 '[]'는 주소를 계산하는 연산자이므로 아래와 같이 작성할 수 있으며 해석할 수 있습니다.

  • 0[ary]는 0에 ary 첫 번째 요소의 주소 값을 더한 주소의 요소입니다.
  • 1[ary]는 1에 ary 첫 번째 요소의 주소 값을 더한 주소의 요소입니다.
  • 2[ary]는 2에 ary 첫 번째 요소의 주소 값을 더한 주소의 요소입니다.

이제 아래의 코드가 이해되시죠?

ary[0]       = 'b';
1[ary]       = 'a';

첫 번째 문제를 보겠습니다.

printf( "%c\n", 1["badayak.com"+1]);

어떤 문자가 출력될까요?  실행하면 'd'가 출력됩니다.

  • 0["badayak.com"]은 'b', 1["badayak.com"]은 'a', 2["badayak.com"] 는 'd'가 됩니다.
  • 0["badayak.com"+0] 은 'b', 0["badayak.com"+1] 은 'a', 0["badayak.com"+2] 는 'd'가 됩니다.
  • 그러므로 1["badayak.com"+1]  은 'd'입니다.

그러나 C언어에서 누가 1["badayak.com"+1] 이런 식으로 작성하겠습니까. 그래서 다른 언어처럼 "ary[0]은 ary 첫 번째 요소" 식으로 단순히 이해해도 될 것입니다. 그러나 앞서도 말씀드렸지만, C언어에서는 배열을 포인터로 다루는 경우가 많고, 포인터로 받았다면 배열과는 다른 방식으로 생각해야 하므로 '[]'가 연산자라는 것을 알고 있는 것이 도움이 됩니다.

C언어 포인터 특성

이번에는 C언어의 포인터를 알아보겠습니다. 먼저 포인터 변수 선언과 정의를 하겠습니다.

char  ary[] = "badayak.com";
char *ptr = ary;

글보다는 그림으로 설명하겠습니다.

C언어 포인터 메모리
C언어 포인터 메모리

C언어 입문서에 자주 보이는 그림 설명입니다. 우선 C언어의 포인터를 쉽게 이해하려면 포인터를 특별하게 생각하는 것부터 없애야 합니다. 메모리에서 주소는 숫자이므로 포인터 변수는 단지 숫자가 들어가는 정수 변수일뿐입니다. 주소라고 해서 어렵게 생각하지 마시고 숫자일 뿐이다라고 단순히 평가하세요. 다른 것이 안 들어갑니다. 복작한 실수가 아니라 단순한 정수가 들어가고, 그 정수는 메모리의 특정 주소일 뿐입니다.

C언어 변수와 메모리
C언어 변수와 메모리

ch1, ch2, ch3와 ary[0], ary[1], ary[2]처럼 포인터 변수 ptr도 값을 넣고 빼는 변수일뿐입니다. ptr은 변수이므로 당연히 자기 주소(100)을 가지고 있습니다. ptr 변수의 값은 101일뿐 다른 변수와 차이가 없습니다. 다만, 포인터 변수 앞에 '*' 연산자를 놓으면 ptr 변수가 가지고 있는 정수 값을 주소로 생각하여 값을 구할 수 있다는 것이 차이일 뿐입니다.

ptr 변수의 값은 101이고, 그 주소의 값을 구하는 *ptr 값은 'b'가 됩니다. 우선 연산자 '*'부터 헷갈리는 것을 정리하겠습니다.

 

포인터 변수 앞에 '*'가 없다면? 정수를 갖는 숫자 변수일 뿐입니다.

  • 포인터 변수 선언   char *ptr;
  • 포인터 변수 주소 대입  ptr = ary;
  • 포인터 변수 주소가 가리키는 메모리의 값  *ptr;

포인터 변수에 대해서 '*' 문자는 딱 두 가지에만 사용됩니다. 포인터 변수로 선언할 때와 포인터 변수의 값을 주소로 치환해서 해당 주소의 값을 구하거나 대입할 때입니다. '*' 문자가 없는 포인터 변수는 그저 정수 값을 갖는 int 변수로 생각하십시오. int 변수와 다른 점은 '*' 쓸 수 있느냐 없느냐의 차이일 뿐입니다.

ptr의 값은 ary[0]의 주소이며 *ptr은 ary[0]의 값이다.

위 코드가 이제 이해되시나요? 좀 더 쉽게 적는다면 이렇습니다.

ptr+0은 ary[0]의 주소 값에 0을 더한 값이며 *(ptr +0)은 ary[0]의 값이다.

그래서 이렇게 대응할 수 있습니다.

*(ptr +0) 은 ary[0]의 값
*(ptr +1) 은 ary[1]의 값
*(ptr +2) 은 ary[2]의 값
*(ptr +3) 은 ary[3]의 값

혹시 *(ptr +0)에서 괄호를 왜 사용했을까? 궁금하시다면 아직도 ptr이 정수 변수라는 생각을 못하신 것입니다. 그리고 '*' 문자는 뒤에 오는 숫자를 주소 값으로 해서 값을 구한다는 것이 아직 낯설게 느껴져서입니다.

위 그림에서 ptr의 값은 ary[0]의 주소인 101입니다. 그러므로 아래와 같이 해석됩니다.

*(ptr +0)은 *( 101 +0)이며 a[0]의 값 'b'
*(ptr +1)은 *( 101 +1)이며 a[1]의 값 'a'
*(ptr +2)은 *( 101 +2)이며 a[2]의 값 'd'
*(ptr +3)은 *( 101 +3)이며 a[3]의 값 'a'

그러나 괄호를 사용하지 않는다면 이렇게 될 것입니다.

*ptr +0 은 'b' +0 즉, 'b'
*ptr +1 은 'b' +1 즉, 'c'
*ptr +2 은 'b' +2 즉, 'd'
*ptr +3 은 'b' +3 즉, 'e'

어떻습니까? 이해되시죠?

char  ary[100];

ary[0]       = 'b';
1[ary]       = 'a';
*(ary+2)     = 'd';
*(&ary[0]+3) = '\0';

printf( "%s\n", ary);

이제가지의 설명을 이해하셨다면 문제 2)의 프로그램을 실행하면 어떤 내용이 출력될지를 아실 것입니다.

포인터 & 연산자 사용의 차이

포인터 변수는 메모리의 특정 위치를 가리키는 주소의 정수 값을 갖는 변수라고 했습니다. 정수 변수라서 직접 정수 값을 넣을 수 있습니다.

int   ndx;          // ndx 변수는 주소 5000에 생성된다고 가정
int  *ptr;

ptr = 5000;         // 5000은 ndx의 주소

위의 코드는 ndx의 주소가 5000이라고 가정하고 포인터 변수 ptr에 대입했습니다. 문제는 ndx의 주소를 5000이라고 가정한 것이지 실제로 실행하면 장담할 수 없는 숫자입니다. 즉, 주소가 어떻게 될지 모릅니다.

int ndx;
char *ptr;

ptr = &ndx;

 그래서 '&' 연산자로 프로그램을 실행할 때 ndx의 주소를 구합니다.

int  ndx = 99;
int  jdx;
int *ptr;

ptr = &ndx;
printf( "ndx = %d\n", *ptr);

jdx = *ptr;

그리고 포인터 ptr의 값인 ndx의 주소가 어떤 값인지 알려면 '*' 연산자를 사용합니다.

 

그렇다면 아래 코드는 어느 부분이 잘못되었을까요?

문제 3)

char ary[100];
int  ndx;
char *ptr1 = &ary;
int  *ptr2 = &ndx;
printf( "%s %d\n", *ptr1, *ptr2);

컴파일하면 두 곳에서 경고가 발생합니다. 컴파일 에러보다 더 무서운 warning이 발생하는 것이죠. 답부터 말씀드리면,

  • char *ptr1 = &ary
  • printf()에서 *ptr1입니다.

왜 잘못되었을까요?  ndx의 변수의 주소를 알기 위해서 &ndx라고 했던 것처럼 ary에 대해서도 &ary라고 했을 뿐입니다. 그런데 경고가 발생합니다.

char ary[100];
char *ptr1 = &ary;

위 코드를 아래와 같이 나누어 적겠습니다.

char ary[100];
char *ptr1;

ptr1 = &ary;

위 코드가 잘못된 이유는 char ary[100]; 로 ary 배열 변수를 생성했다고 해서 ary 만의 주소가 없습니다. ary[0], ary[1], ary[2], ... ary[99]까지 '[]' 연산자를 갖는 주소는 있어도 ary 이름이 차지하는 주소는 없습니다. 위에서 배열을 설명하기 위한 그림을 다시 봐주세요.

C언어 배열 변수
C언어 배열 변수

ary[0], ary[1], ary[2]는 있어도 ary는 없지요? 당연히 &ary로 구할 수 있는 주소 값은 없습니다!!

그렇다면 앞서 예제에서 ptr1 = ary; 는 어떻게 가능할까요? 이는 C언어의 친절한 배려일뿐 실제로는 아래와 같이 작성해야 합니다.

ptr = &ary[0];

이렇게 작성해야 옳습니다. 그런데 C 컴파일러가 메모리에 없는 ary를 ptr = ary; 로 작성하는 것을 허용한 것입니다. 그러니까 이런 것이죠. 포인터에 ary를 대입해? 그렇다면 ary[0]의 주소이겠구먼 하고 컴파일 에러 없이 주소 값을 대입해 주는 것인데요, 이래서 타 언어 경험자도 C언어를 헷갈려합니다.

char ary[100];
int  ndx;
char *ptr1 = ary;
int  *ptr2 = &ndx;

이렇게 작성해야 한다는 것인데, 어떤 것은 &가 붙고 어떤 것은 붙어서는 안 되고 이러는 것이죠.

char ary[100];
int  ndx;
char *ptr1 = &ary[0];       // 또는 char *ptr1 = ary;
int  *ptr2 = &ndx;

printf( "%s %d\n", *ptr1, *ptr2);

원래는 이렇게 적어야 하는 것인데, C언어의 배려인지, 자유도가 높다고 해야 할지 모르겠지만, C언어 입문자에게는 혼란스러운 요소가 됩니다.

자, 그렇다면 printf() 문의 *ptr은 무엇이 잘못되었을까요? printf()의 출력 포맷인 '%s'는 문자가 아닌 문자열을 출력합니다. 문자열을 출력하기 위해서는 문자 하나, 숫자 하나가 아닌 문자 여러 개가 모인 메모리의 시작 위치가 필요합니다.

C언어 문자열
C언어 문자열

printf()는 "%s" 해당하는 주소 값이 가리키는 문자부터 시작해서 null을 만날 때까지 인쇄하게 됩니다. 그러므로 포인터 변수 ptr1은 문자 하나인 *ptr1이 아니라 자신이 가지고 있는 주소 값 101, 즉 ptr 자신의 값을 알려 주어야 합니다. 그래서 *ptr1이 아니라 ptr이어야 합니다.

char ary[100];
int  ndx;
char *ptr1 = &ary[0];       // 또는 char *ptr1 = ary;
int  *ptr2 = &ndx;

printf( "%s %d\n", ptr1, *ptr2);

올바르게 수정하면 위와 같습니다. 

C언어 포인터에 대해서 더 말씀드리고 싶은 것이 있는데, 내용이 너무 길어졌네요. 내용을 나누어서 다음 글에는 char *ptr과 int *ptr의 차이점에 대해서 말씀드리겠습니다. 그리고 포인터가 왜 필요하고 사용하는지도 설명하겠습니다. 기대해 주시면 감사하겠습니다.

SNS 공유하기
💬 댓글 개
최근글
이모티콘창 닫기
울음
안녕
감사해요
당황
피폐

이모티콘을 클릭하면 댓글창에 입력됩니다.