포인터 자유자재로 다루기

자료구조

C언어에서 포인터 다루기를 어려워하는 사람이 많다. 이번 포스트에는 포인터를 다루는 방법을 정리하고자한다.

포인터의 연산 #

기본적으로 포인터는 다음과 같이 선언한다.

int a = 0;
int* pointer = &a;

이때 포인터가 가르키는 주소값이 0x0000000이라고 하자. pointer+1의 값은 무엇일까? 0x0000001이라고 생각할 수 있겠지만, 0x0000004값을 가진다. 포인터에서 +, -연산시에는 주소값이 이동하게되는데, 이동되는 정도는 포인터가 가르키는 타입의 크기에 의해 결정된다.

아래 예시를 보자.

int main(void) {
    char *pc = 0;
    double *pd = 0;
    short **pps = 0;
    float *pf = 0;

    pc++; // 1byte 이동
    pd += 5; // 5*8 byte 이동
    pps += 2; // 8*2 byte 가르키고있는 것이 포인터이므로.
    pf = (char*) pf + 1;
    printf("%x %x %x %x \n", pc, pd, pps, pf);
    return 0;
}

위의 예시에서 pc++;부분을 분석하자.

  1. pc포인터가 가르키는 자료형 타입은 char이다.
  2. char의 크기는 1byte이다.
  3. 따라서 pc포인터는 1byte떨어진 주소를 가르키게 된다.

이제 pf = (char*) pf + 1;부분을 보자

  1. pf의 타입을 char*형으로 형변환한다.
  2. (char*) pf가 가르키는 타입은 char형이다.
  3. char의 크기는 1byte이다.
  4. pc포인터는 1byte 떨어진 주소를 가르킨다.

연산자 우선순위: [] . -> 캐스팅 *(역참조) & sizeof * / %

배열과 포인터 #

기본적으로 다음과 같이 배열 선언이 가능하다.

int *score;
score = (int*) malloc(sizeof(int)*100); // length: 100
score[0] = 10;

아래와 같이 2차원 배열도 선언이 가능하다.

int data[3][3] = { {1, 2, 3}, 
                   {4, 5, 6},   
                   {7, 8, 9} };
data[0][1];

컴퓨터의 메모리는 1차원 선형구조이다.그려면 다차원 배열은 어떻게 처리될까? 아래코드를 보자

data[0][1];
*(data+0)+1;

data[0][1]같은 부분은 개발자가 배열에 직관적으로 처리하기 위한 것이고, 컴파일시 2번째 줄처럼 변환되어진다. 아래 코드의 경우 2번째줄처럼, 첨자가 아닌 포인터 이동을 통해 배열 요소에 접근하는 경우를 보여준다.

int* data;
int m;
scanf("%d", &m);
data = malloc(sizeof(int)*4*m);

for (int i = 0; i < m; i++)
    for (int j = 0; j < 4; j++)
        scanf("%d", data+4*j+i);
        // scanf("%d", &data[i][j]); ERROR!

위의 코드를 보면 scanf("%d", &data[i][j]);에 에러가 표시된 것을 볼 수 있다. 왜 배열을 선언했는데 첨자로 접근할 수 없을까? 배열을 저장한 변수의 타입이 1차원 포인터이기 때문이다. data[i]로 접근을 할 수 있지만, 그 너머는 규격이 없어 계산할 수 없다.

다차원 배열 #

int (*data)[4];
int *data[4];

위의 두 코드의 차이점이 뭘까? 힌트를 주자면 첨자가 포인터연산보다 우선순위가 높다.

첫번쨰 코드는 int 자료형을 저장하는 크기 4인 배열의 포인터를 정의한 것이고, 두번째는 int 포인터형을 저장하는 크기 4인 배열을 저장한 것이다. 첫번째는 배열의 포인터고, 두번째는 포인터 배열이다.

2차원 배열은 다음과 같이 두가지 방식으로 동적할당을 할 수 있다.

int (*list)[5];
list = malloc(row*coloumn*sizeof(int)); // 연속된 메모리 공간
list+3*row+column;
int **list;
list = malloc(row*sizeof(int*)); //흩어진 배열의 포인터를 연속적으로 저장
for (int i = 0; i < row; i++)
    list[i] = malloc(column*sizeof(int));
list[row][column];

malloc은 메모리공간을 할당할 수 없으면 NULL을 리턴한다. 다음과 같이 매크로를 이용해 더 안전하게 사용할 수 있다.

#define MALLOC(pointer, size) \
    if (!((pointer)=malloc(size))) { \
        fprintf(stderr, "Insufficient memory"); \
        exit(EXIT_FAILURE); \
    }

포인터의 크기는 보통 64bit시스템에서는 8byte, 32bit시스템에서는 4byte다.