Post

C언어 :: 포인터에 관하여

C언어 :: 포인터에 관하여

C언어의 포인터에 대한 이해도를 높일 수 있는 글입니다.

포인터란?

포인터는 메모리 주소를 저장하는 변수이다. C언어가 Low 레벨 언어의 특성을 지닌다고 할 수 있게 하는 장본인으로, 포인터를 이용하여 메모리에 직접 접근할 수 있다.

예제를 통해 살펴보겠다.

1
2
3
4
5
6
7
8
9
10
11
/*
1. int형 변수 num을 선언한다.
2. num의 주소값을 저장하기 위한 포인터 변수 pnum을 선언한다.
3. pnum에 num의 주소 값을 저장한다.
*/

int main(void) {
    int num = 7;
    int * pnum;
    pnum = #
}

위 예제를 통해 알수있는 사실은 다음과 같다.

  • pnum은 포인터 변수의 이름이다.
  • int*는 int형 변수의 주소 값을 저장하는 포인터 변수의 선언이다.
  • &num은 변수 num의 시작번지 주소 값이 저장된다.

참고로 주소 값은 동일한 시스템에서 크기가 동일하며 모두 정수 형태이다. 시스템에 따라 포인터 변수의 크기가 달라지는데 32비트 시스템에서는 포인터 변수의 크기가 4바이트이고, 64비트 시스템에서는 8바이트이다.


& 연산자

& 연산자는 피연산자의 주소 값을 반환하는 연산자이다. 따라서 & 연산자의 피연산자는 변수이어야 하며, 상수는 피연산자가 될 수 없다. 상수는 메모리에 저장되지 않거나 읽기 전용 메모리(ROM; Read Only Memory)에 저장되기 때문이다.

1
2
3
int main(void) {
    int * pnum1 = 5; // 컴파일 에러 발생
}


변수의 자료형과 일치하지 않는 포인터 변수를 선언할 경우, 컴파일 에러는 발생하지 않지만 포인터 관련 연산 시 문제가 발생할 수 있다.

1
2
3
4
int main(void) {
    int num1 = 5;
    double * pnum1 = &num1;
}


* 연산자

* 연산자는 포인터가 가리키는 메모리 공간에 접근 시 사용하는 연산자이다.

1
2
3
4
5
6
int main(void) {
    int num = 10;
    int * pnum = #
    *pnum = 20; // pnum이 가리키는 변수에 20을 대입
    printf("%d", *pnum);
}


위에서 모든 주소는 정수형이고, 같은 시스템이라면 동일한 크기를 가진다고 설명했다. 그럼에도 포인터 형이 존재하는 이유는 무엇일까? 그 이유는 포인터 변수가 가리키는 ‘메모리 공간에 접근해서 어떻게 값을 읽어야 할지 알기 위함’이다.

예를 들어 다음과 같은 포인터 변수가 있다고 하자.

return *pnum;

위 문장을 통해 값을 반환하려면 우선 pnum이 가리키는 메모리 공간에 접근할 것이다. 하지만 몇 바이트를 읽어야 하는지, 정수로 해석해야 할지 또는 실수로 해석해야 할지에 대한 정보가 부족하다. 즉 포인터형이 존재하는 이유는 * 연산자를 통한 메모리 공간의 접근 기준을 마련하기 위함이다. 포인터에 형이 존재하지 않는다면 * 연산을 통한 메모리의 접근은 불가능할 것이다.

예제

포인터의 이해를 돕기 위해 다음 주석을 따르는 예제를 작성해 보겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/*
1. int형 변수 num1, num2를 선언하고 각각 10과 20으로 초기화한다.
2. int형 포인터 변수 ptr1, ptr2를 선언하고 각각 num1과 num2를 가리키게 한다.
3. 포인터 변수 ptr1, ptr2를 이용하여 num1의 값을 10 증가시키고 num2의 값은 10 감소시킨다.
4. 두 포인터 변수 ptr1, ptr2가 가리키는 대상을 서로 바꾼다.
5. ptr1, ptr2가 가리키는 변수에 저장된 값을 출력한다.
*/

#include <stdio.h>

int main() {
    int num1 = 10;
    int num2 = 20;
    int *ptr1 = &num1;
    int *ptr2 = &num2;

    int *temp = ptr1;

    *ptr1 += 10;
    *ptr2 -= 10;

    ptr1 = ptr2;
    ptr2 = temp;

    printf("num1: %d\n", num1);
    printf("num2: %d\n\n", num2);

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

    return 0;
}


포인터 변수를 이용하여 메모리 공간에 접근할 수 있기 때문에 포인터 사용 시 각별한 주의가 필요하다. 특히 포인터 변수를 선언만하고 초기화하지 않으면 쓰레기 값이 저장되는데, 해당 쓰레기 값이 가리키는 메모리에 중요한 값이 저장되어 있다면 값 변경 시 큰 문제가 발생할 것이다. 때문에 포인터 변수를 선언과 동시에 초기화하지 않을 것이라면 다음과 같이 0 또는 null을 이용하여 초기화하는 것이 좋다. 참고로 0은 널 포인터를 의미한다.

1
2
3
4
int main(void) {
    int * ptr1 = 0;
    int * ptr2 = NULL;
}


배열

배열의 이름도 포인터이다. 단, 그 값을 바꿀 수 없는 ‘상수 형태의 포인터’이다.

1
2
3
4
5
6
7
8
9
10
int main(void) {
    int arr[3] = {0, 1, 2};

    printf("배열의 이름: %p\n", arr);
    printf("첫번째 요소: %p\n", &arr[0]);
    printf("두번째 요소: %p\n", &arr[1]);
    printf("세번째 요소: %p\n", &arr[2]);

    return 0;
}

위 예제를 통해 알 수 있는 사실은 다음과 같다.

  • int형 배열 요소간 주소 값의 차는 4바이트이다.
  • 배열의 이름은 배열의 시작 주소 값을 의미한다. 즉 값의 저장이 불가능한 상수이다.


배열의 이름과 포인터 변수는 변수냐 상수냐의 특성적 차이가 있을 뿐, 둘 다 포인터이기 때문에 포인터 변수로 할 수 있는 연산은 배열의 이름으로도 할 수 있고, 배열의 이름으로 할 수 있는 연산은 포인터 변수로도 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
int main(void) {
    int arr[3] = {0, 1, 2};
    int * ptr = &arr[0]; // int * ptr = arr; 과 동일한 문장

    printf("%d %d\n", ptr[0], arr[0]);
    printf("%d %d\n", ptr[1], arr[1]);
    printf("%d %d\n", ptr[2], arr[2]);
    printf("%d %d\n", *ptr, *arr);

    return 0;
}


포인터 연산

포인터 변수를 대상으로 다양한 형태의 증가 및 감소 연산을 진행할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
int main(void) {
    int num1 = 10;
    double num2 = 10;

    int *ptr1 = &num1;
    double *ptr2 = &num2;

    printf("%p %p %p\n", ptr1, ptr1 + 1, ptr1 + 2); // 4 증가, 8 증가
    printf("%p %p %p\n", ptr2, ptr2 + 1, ptr2 + 2); // 8 증가, 16 증가

    return 0;
}

위 예제를 통해 알 수 있는 사실은 다음과 같다.

  • int형 포인터를 대상으로 n 증가 : n * sizeof(int)의 크기만큼 증가
  • double형 포인터를 대상으로 n 증가 : n * sizeof(double)의 크기만큼 증가


위 예제는 증가 연산을 수행했지만 감소 연산 역시 동일하게 수행된다. 중요한 것은 이러한 포인터의 연산 특성을 이용하여 다음과 같은 배열 접근을 할 수 있다는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
int main(void) {
    int arr[3] = {0, 1, 2};
    int * ptr = arr;

    printf("%d %d %d \n", *ptr, *(ptr+1), *(ptr+2));
    printf("%d %d %d \n", ptr[0], ptr[1], ptr[2]);

    printf("%d %d %d \n", *arr, *(arr+1), *(arr+2));
    printf("%d %d %d \n", arr[0], arr[1], arr[2]);

    return 0;
}


예제를 통해 포인터와 배열의 관계를 더 깊이 이해해 보겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
1. 길이가 5인 int형 배열 arr을 선언하고 1, 2, 3, 4, 5로 초기화한다.
2. 배열의 첫번째 요소를 가리키는 포인터 변수 ptr을 선언한다.
3. 포인터 변수 ptr에 저장된 값을 증가시키는 형태의 연산을 기반으로 배열요소에 접근하면서 모든 배열요소의 값을 2씩 증가시킨다.
4. 배열을 출력한다.
*/

int main(void) {
    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr = arr;

    for (int i = 0; i < 5; i++)
    {
        printf("%d ", *ptr += 2);
        ptr++;
    }

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
위 예제와 달리 포인터 변수 ptr에 저장된 값을 변경시키지 않고 ptr을 대상으로 덧셈연산을 하여,
그 결과로 반환되는 주소 값을 통해 모든 배열요소에 접근하여 값을 2씩 증가시키도록 한다.
*/

int main(void) {
    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr = arr;

    for (int i = 0; i < 5; i++)
    {
        int *temp = ptr + i;
        *temp += 2;
        printf("%d ", arr[i]);
    }

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
1. 길이가 5인 int형 배열 arr을 선언하고 1, 2, 3, 4, 5로 초기화한다.
2. 배열의 마지막 요소를 가리키는 포인터 변수 ptr을 선언한다.
3. 포인터 변수 ptr에 저장된 값을 감소시키는 형태의 연산을 기반으로 모든 배열요소에 접근하여, 배열에 저장된 모든 정수를 더하여 결과를 출력한다.
*/

int main(void) {
    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr = arr + 4;
    int sum = 0;

    for (int i = 4; i >= 0; i--)
    {
        sum += *ptr;
        ptr--;
    }

    printf("Sum: %d\n", sum);

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*
1. 길이가 6인 int형 배열 arr을 선언하고 1, 2, 3, 4, 5, 6으로 초기화한다.
2. 배열에 저장된 값의 순서가 6, 5, 4, 3, 2, 1이 되도록 변경한다.

단, 배열의 앞과 뒤를 가리키는 포인터 변수 두개를 선언하고 이를 활용하여 저장된 값의 순서를 뒤바꿔야 한다.
*/

int main(void) {
    int arr[6] = {1, 2, 3, 4, 5, 6};
    int *ptr1 = arr;
    int *ptr2 = arr + 5;
    int temp = 0;

    for (int i = 0; i < 3; i++)
    {
        temp = *ptr1;
        *ptr1 = *ptr2;
        *ptr2 = temp;

        ptr1++;
        ptr2--;
    }

    for (int i = 0; i < 6; i++)
    {
        printf("%d ", arr[i]);
    }

    return 0;
}
This post is licensed under CC BY 4.0 by the author.