모각코

[2025 하계 모각코] 3회차 활동 결과 - C언어 포인터

pengine 2025. 7. 22. 02:46

활동 목표: 포인터 학습

 

활동 결과:

 

1. 포인터

 

포인터란 메모리 주소를 저장하는 변수이다.

 

처음에 포인터에 대한 개념을 들은 것은 파이썬을 배우던 도중이었다. 

파이썬의 list를 변수에 저장할 때 변수에 그 위치를 저장한다고 배웠다.

li1 = [1, 2, 3]
li2 = li1
li1[1] = 5
print(li2)

>> [1, 5, 3]

 

C에서 포인터는 메모리 주소를 저장한다.

*로 포인터 변수를 선언하고 그 뒤에는 메모리 주소를 가리키는 &를 사용한다.

예를 들어 아래와 같이 포인터 변수를 선언했으면 ptr은 a의 주소를 나타내고 *ptr을 사용해 주소의 값을 역참조할 수 있다.

#include <stdio.h>

int main(){
    int a = 10;
    int *ptr = &a;
    
    printf("%d\n", a);
    printf("%d\n", &a);
    printf("%d\n", ptr);
    printf("%d\n", *ptr);
    *ptr += 1;
    printf("%d\n", a);
    printf("%d\n", &a);
    printf("%d\n", ptr);
    printf("%d\n", *ptr);
    return 0;
}

>> 10
1952446724
1952446724
10
11
1952446724
1952446724
11

 

2. 배열과 포인터

 

우선 C언어의 배열은 int형 변수는 4바이트를 할당한다.

각 배열 요소의 주소를 int 형 배열이라면 첫 번째 요소의 주소가 100일 때 두 번째 요소는 104인 것이다.

때문에 첫 번째 요소의 주소만 알면 각 요소의 주소 또한 쉽게 알 수 있다.

이러한 이유로 배열은 첫 번째 요소의 주소를 가리키고 있다.

 

#include <stdio.h>

int main(){
    int arr[5] = {1, 2, 3, 4, 5};
    printf("%d\n", &arr[0]);
    printf("%d\n", arr);
    printf("%d\n", &arr[1]);
    return 0;
}

>> -1300235104
-1300235104
-1300235100

 

포인터의 배열에서 인덱스 연산을 할 수 있다. arr[1]과 *(arr + 1)은 같은 결과를 나타낸다.

arr[0]의 주소가 100이라고 하면 arr + 1의 주소는 101이 아닌 104를 나타내는 것이다.

arr 은 주소로 저장되어있기 때문에 역참조(*)를 바로 쓸 수 있다.

#include <stdio.h>

int main(){
    int arr[5] = {1, 2, 3, 4, 5};
    printf("%d\n", *(arr + 1));
    return 0;
}

>> 2

 

 

포인터 또한 위와 똑같이 사용할 수 있다. 

그런데 의문이 하나 생긴다. 배열도 똑같이 주소가 저장되어 있는데 왜 굳이 포인터를 사용할까?

이는 상수와 변수 차이이다.

배열 이름은 바꿀 수 없기 때문에 포인터를 사용하는 것이다.

또한 arr을 그대로 쓴다면 이의 메모리는 (요소의 크기(int형이면 4바이트) * 배열의 요소 개수)의 값을 가지지만

포인터를 사용한다면 첫 번째 요소의 크기만을 가지기 때문이다.

이러한 이유로 배열이 함수로 전달될 때 포인터로 변환된다.

 

#include <stdio.h>

int main(){
    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr = arr;
    printf("%d\n", sizeof(ptr));
    printf("%d\n", sizeof(arr));
    printf("%d", sizeof(ptr[0]));
    return 0;
}

>> 8
20
4

 

sizeof는 메모리의 크기를 바이트 단위로 계산해서 반환하는 함수이다.

참고로 포인터는 8바이트의 크기를 가지길래 의문을 품었는데, 이는 운영체제에 대해 공부할 6회차에서 알아보도록 하겠다.

간단하게 이해한 바는, 32비트 운영체제에서는 4바이트였고 64비트 운영체제에서는 8바이트를 사용하는데  호환성과 효율성 등을 위해 4바이트로 구현한 것이다. 32비트 운영체제에서 포인터는 4바이트의 크기를 가진다.

 

3. 배열 포인터

 

배열 포인터는 배열 전체의 주소를 가리키는 것이다. 

#include <stdio.h>

int main(){
    int arr[5] = {1, 2, 3, 4, 5};
    int (*ptr)[5] = &arr;
    printf("%d\n", (*ptr)[1]);
    printf("%d\n", sizeof(*ptr));
    return 0;
}

>> 2
20

 

배열 포인터의 개념을 돕기 위해 위와 같은 코드를 작성했고, 배열 포인터를 쓰는 이유는 다차원 배열에서 유리하게 요소를 다룰 수 있기 때문이다. 따라서 배열 포인터는 2차원 이상의 배열에서만 의미를 가진다.

#include <stdio.h>

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

    int (*ptr)[5] = arr2d;
    for (int i = 0; i < 2; i++){
        for (int j = 0; j < 5; j++)
            printf("%d ", ptr[i][j]);
        printf("\n");
    }
    return 0;
}

>> 1 2 3 4 5 
6 7 8 9 10

 

4. 포인터 배열

 

놀랍게도 배열 포인터와 다른 개념이다! 

포인터 배열이란 배열 각 요소에 포인터가 존재하는 것이다.

포인터 변수로 이루어진 배열이라는 표현이 적절한 것 같다.

#include <stdio.h>

int main(){
    int a = 97, b = 98, c = 99;
    int *arr[3] = {&a, &b, &c};
    printf("%p %p %p\n", arr[0], arr[1], arr[2]);
    printf("%d %d %d\n", *arr[0], *arr[1], *arr[2]);
    return 0;
}

>> 0000002CF43FFC5C 0000002CF43FFC58 0000002CF43FFC54
97 98 99

 

참고로 %p는 포인터 자료형이다.

포인터 배열은 왜 사용할까? 

앞서 설명한 대로라면 배열에서 각 요소의 메모리 주소는 일정한 간격을 가지는데

포인터 배열은 그러한 간격이 무너지는 것 아닌가?

 

포인터 배열을 사용하는 이유는 동적 메모리와 밀접한 관련을 가지고 있다.

그렇기 때문에....

 

다음 회차의 목표와 충돌하므로

다음 글에서 설명하도록 하겠다.