본문으로 바로가기

바다야크

  1. Home
  2. 컴퓨터/프로그래밍
  3. C언어 포인터 사용하는 이유

C언어 포인터 사용하는 이유

· 댓글개 · 바다야크

C언어 포인터 사용한다 vs. 사용하지 않는다

C언어 포인터를 쉽게 이해하는 방법 시리즈 3부입니다. 꽤 오래전에 개발자 커뮤니티에서 댓글 싸움이 심하게 벌어진 적이 있는데요, C언어라고 해서 포인터를 반드시 사용할 필요가 없다는 글에 반박과 찬성 의견이 이어진 것이죠. C언어에서 포인터를 사용하지 않으면 왜 C언어로 코딩하느냐로 시작해서 포인터를 사용하지 않는다면 C언어 개발자라고 어디 가서 떠들지 말라며 다소 과격한 글이 올라왔고, 이에 대해 포인터 없는 언어도 많고 C로 코딩하지만, 포인터 없이도 잘만 작성한다는 공격적인 답글이 서로 엉켜 싸우듯 했습니다.

연산자 []로만 사용했다고 포인터를 안 썼다고?

맞습니다. 포인터 없는 언어도 많은데 C언어라고 꼭 포인터를 써야 할까요? 당연히 포인터 없이도 프로그래밍이 가능합니다. 실행 에러를 유발하는 포인터는 피하고 대신에 C언어의 장점을 이용해도 되겠지요. 그러나 포인터를 사용하지 않는다고 해도 포인터에 대한 이해는 반드시 필요합니다.

예를 들어서 포인터 '*' 연산자 없이 배열 '[]' 연산자만으로 작성했다고 해서 포인터를 사용하지 않은 프로그램일까요?

void print_value( int ary[10]){

   int ndx;

   for ( ndx= 0; ndx < 10; ndx++){
      printf( "%d\n", ary[ndx]);
   }

   ary[0] = 200;
}

int main( void)
{
   int  ary[10] = {100, 101, 102, 103, 104, 105, 106};
   
   print_value( ary);

위의 프로그램은 정수 10개의 배열을 print_value() 함수에 인수로 전달해서 호출하고 있습니다. print_value() 함수는 정수 10개의 배열을 받기 위해 int ary[10]이라고 했습니다. 데이터를 '[]' 연산자로 처리했는데요, 이렇게 작성하면 포인터 없이 코딩한 것일까요?

우선 함수 print_value()의 인수 선언부터 보겠습니다. ary[10]의 10 이라는 숫자는 의미가 있을까요? 10 대신에 100, 1000, 또는 0으로 바꾸어 컴파일하면 어떻게 될까요? 아예 숫자를 넣지 않고 print_value( int ary[])라고 입력해도 오류와 경고 없이 컴파일이 됩니다. 왜 이럴까요?

print_value( int ary [10])이라고 하면 겉으로 보아서는 ary 변수는 배열 같습니다만, 인수에서 배열 선언은 포인터 변수입니다.

void print_value( int *ary)

이렇게 선언한 것과 마찬가지인 것이죠. 정말? int ary [10]으로 선언했는데도 포인터라고? 

자, print_value( int ary[10])이 실제로는 print_value( int *ary)이라는 것을 설명하기보다는 확인하는 식으로 말씀드리겠습니다.

void print_value( int ary[10]){
   printf( "print_value ary size=%ld\n", sizeof( ary));
}

int main( void)
{
   int  ary[10] = {100, 101, 102, 103, 104, 105, 106};

   printf( "main ary size=%ld\n", sizeof( ary));
   print_value( ary);

위 프로그램을 실행하면 어떤 내용이 출력할까요? sizeof( ary) 값은 둘 다 똑같을까요?

main ary size=40
print_value ary size=8

실행해 보면 main() 함수 내에서는 40이라고 나오지만, print_value() 함수에서는 64bit 시스템에서는 8이 나옵니다. 즉, 포인터 변수의 크기와 같습니다. 64bit 시스템에서 메모리 주소는 8byte이니까요.

void print_value( int ary[10]){

   int ndx;

   for ( ndx= 0; ndx < 10; ndx++){
      printf( "%d\n", *ary++);
   }
}

int main( void)
{
   int  ary[10] = {100, 101, 102, 103, 104, 105, 106};
   int  ndx;

   for ( ndx= 0; ndx < 10; ndx++){
       printf( "%d\n", *ary++);         // 컴파일 에러

   print_value( ary);

이전에 올린 포인터 쉽게 이해하기 1부를 보신 분이라면 이해하실 수 있을 것입니다. main() 함수 내에 선언된 배열 변수 ary[]는 각 요소를 참조하는 ary[0], ary[1], ary[2], .... 각각은 메모리에 할당되지만, ary 이름만으로 할당된 메모리는 없습니다.

포인터 쉽게 이해하기 1부에 올려진 그림을 다시 봐주세요. ary가 차지하는 메모리가 없습니다. 이런 이유로 main() 함수 내에 작성된 ary++는 실행할 수 없습니다. ary++는 ary 변수의 값을 1 증가하는 것인데 ary가 차지하는 메모리가 없으므로 증가할 수도, 증가 값을 넣을 수도 없기 때문에 컴파일 에러가 발생합니다.

그러나 print_value( int ary[10])에서 ary는 모습만 배열이지 실제로는 포인터라서 ary++는 자연스러운 코딩입니다. 이제 print_value( int ary[10])이  print_value( int *ary)라는 것이 이해되시죠? 그래서 함수의 인수에서 int ary[100], int ary[1000] 이렇게 입력할 수 있는 것은 [] 연산자 내의 숫자는 아무런 의미가 없기 때문입니다.

연산자 '[]'만 사용하면 되지 않나?

그렇다고 해도 연산자 '[]'만 사용하면 되지 않냐 하실지 모르지만, 안 됩니다! 앞서 예제에서 main() 함수의 sizeof( ary) 값과 print_value() 함수의 sizeof( ary) 값이 다르다는 것만 보아도 안심할 수 없습니다. 둘 다 똑같이 int ary[10]이라고 선언했고 같은 함수를 호출했는데도 값이 다릅니다.

이와 같은 C언어의 특성에 대한 이해가 부족하면 컴파일이 되어도 실행 중에 이해 안 되는 오류가 발생하게 됩니다.

배열 변수를 인수로 넘겨줄 때는 반드시 배열의 크기도 함께 넘겨주어야 합니다. sizeof()로는 배열의 크기를 알 수 없기 때문인데요, 크기를 생략하는 경우는 받는 함수 쪽에서 크기를 정확히 알고 있거나 데이터의 끝을 확인할 수 있는 경우입니다. 예를 들어 NULL로 끝나는 문자열처럼 말이죠.

void print_value( char ary[10]){

   int ndx;

   for ( ndx= 0; ndx < sizeof( ary); ndx++)   // 실행 에러
       printf( "%d\n", ary[ndx]);
}

int main( void)
{
   char ary[10] = {100, 101, 102, 103, 104, 105, 106};
   int  ndx;

   for ( ndx= 0; ndx < sizeof( ary); ndx++)
       printf( "%d\n", ary[ndx]);

   print_value( ary);

main()에서 for 루프는 10회 루프를 돌지만, print_value()의 for 루프는 32bit 시스템에서는 4회가, 64bit 시스템에서는 8회만 돕니다. 다시 말씀드리지만, print_value( char ary[10])은 print_value( char *ary)와 같고, 포인터 변수는 64비트 시스템에서는 8 바이트 크기이기 때문입니다.

void print_value( char ary[], int sz_data){

   int ndx;

   for ( ndx= 0; ndx < sz_data; ndx++)
       printf( "%d\n", ary[ndx]);
}

int main( void)
{
   char ary[10] = {100, 101, 102, 103, 104, 105, 106};
   int  ndx;

   for ( ndx= 0; ndx < sizeof( ary); ndx++)
       printf( "%d\n", ary[ndx]);

   print_value( ary, sizeof( ary));

그래서 위와 같이 데이터 크기도 함께 전달해 주어야 안전한 코드가 됩니다.

void print_value( char *ptr, int sz_data){

   int ndx;

   for ( ndx= 0; ndx < sz_data; ndx++)
       printf( "%d\n", ptr++);
}

int main( void)
{
   char ary[10] = {100, 101, 102, 103, 104, 105, 106};
   int  ndx;

   for ( ndx= 0; ndx < sizeof( ary); ndx++)
       printf( "%d\n", ary[ndx]);

   print_value( ary, sizeof( ary));

또한, 함수 내에서 포인터로 다루는 만큼 프로그램의 이해를 돕기 위해서도 ary[] 식의 배열보다는 포인터로 선언해서 처리하는 것이 좋습니다.

포인터를 사용하는 이유

그렇다면 포인터를 왜 사용할까요? 이유가 여러 가지이겠습니다만, 대표적으로 세 가지를 들 수 있습니다. malloc() 함수로 동적 메모리를 생성할 수 있고, 큰 용량의 데이터를 처리하는데 효율성이 높으며, 데이터를 분석(parsing)하기에도 편합니다.

동적 메모리 생성과 사용은 malloc() 함수 사용법을 참고하세요.

void print_value( char ary[10000]){
   // 배열을 배열로 받는다면 배열 크기만큼 메모리를 할당 받고
   // 데이터를 복사해야 해서
   // 인계 받으려는 데이터가 매우 크다면 부담이 됩니다.
}

int main( void)
{
   char ary[10000] = {100, 101, 102, 103, 104, 105, 106, ......};

   print_value( ary);

배열을 배열로 받는다면 인수로 받을 때 메모리를 확보하고 데이터를 복사해야 합니다. 그러나 주소만 넘겨 준다면 데이터가 크든 작든 인수인계를 받는 부담이 적습니다. 즉, call by reference 방식이어서 인수로 받은 함수에서는 데이터가 변형이 되지 않도록 조심해야 합니다.

두 번째로 데이터 분석의 용이성인데요, 시리얼 통신이나 TCP/IP로 아래와 같은 데이터를 수신했습니다.

0x01, 0x00, 0x00, 0x00, 0x1f, 0x85, 0xeb, 0x51, 0xb8, 0x1e, 0x29, 0x40, 0x7b, 0x14, 0xae, 0x47, 0xe1, 0x6a, 0x6b, 0x40, 0xb8, 0x1e, 0x85, 0xeb, 0x51, 0x8c, 0x76, 0x40, 0xe1, 0xfa, 0xc5, 0x42

위 데이터는 아래와 같은 내용입니다.

어떻게 분석해야 할까요? 앞에서 4바이트까지 분리해서 id 값을 구한 후 뒤에 8 바이트를 잘라서 전류 값을 구하고, 다시 뒤에 8바이트를 자르고 double 값을 구해야 할까요? 스트럭쳐와 포인터를 사용하면 그렇게 수고롭게 작성하지 않아도 됩니다.

typedef struct
{
    int     id;
    double  current;
    double  voltage;
    double  power;
    float   rate;
} __attribute__ ((packed)) data_t;

char data[] = {0x01, 0x00, 0x00, 0x00, 0x1f, 0x85, 0xeb, 0x51,
               0xb8, 0x1e, 0x29, 0x40, 0x7b, 0x14, 0xae, 0x47,
               0xe1, 0x6a, 0x6b, 0x40, 0xb8, 0x1e, 0x85, 0xeb,
               0x51, 0x8c, 0x76, 0x40, 0xe1, 0xfa, 0xc5, 0x42};

data_t *ptr = ( data_t *)data;
printf( "id  = %d\n", ptr->id);
printf( "cur = %f\n", ptr->current);
printf( "vol = %f\n", ptr->voltage);
printf( "pwr = %f\n", ptr->power);
printf( "r   = %f\n", ptr->rate);

어떻습니까? 간단하지요? 포인터 활용이 이외에도 하나의 함수로 여러 형태의 데이터를 인수로 받아서 처리할 수 있고, 데이터를 분석한 결과를 숫자로 계산해서 if 절 없이 함수를 바꾸어 호출할 수 있습니다. 즉, 함수의 주소를 포인터 변수가 받아서 처리하는 함수 포인터입니다.

이처럼 C언어에서 포인터를 제대로 이해하고 옳게 사용한다면 쉬운 코딩, 강력한 코딩이 가능합니다. 그러므로 앞서 C언어의 포인터 언쟁이 다시 벌어진다면 사용해야 한다는 쪽으로 편을 들겠습니다.

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

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