본문 바로가기
C언어/포인터

[C언어] 배열과 포인터 사이의 관계 - 1 (포인터 - 3)

by 프링글's 2022. 7. 8.

Ⅰ. 배열과 포인터

배열의 각 원소들은 메모리 상에 연속적으로 위치해 있게 된다. 즉, int형 배열을 선언을 하면 각 원소들은 4바이트씩을 차지한체로 줄지어서 놓여있게 된다는 의미이다. 우리는 이전에 포인터의 덧셈을 어떻게 하는지 보았고, int형 데이터를 가리키는 포인터 p에 1을 더하면 4바이트가 더해진다는 사실을 알고 있다. 그런데 배열은 메모리 상 연속적이기 때문에 arr[0]을 가리키는 포인터가 p라면 arr[1]을 가리키는 포인터는 p+1이 된다.

#include <stdio.h>
int main(){
    int arr[3] = {1,2,3};
    int *p = &arr[0];
    
    printf("%p %p\n", p, &arr[0]);
    printf("%p %p", p+1, &arr[1]);
    
    return 0;
}

 

000000000062FE00 000000000062FE00
000000000062FE04 000000000062FE04

위 결과를 보면 &arr[0]과 &arr[1]의 결과가 4바이트 차이인 것을 알 수 있다. 즉, &arr[0]의 주소를 담은 포인터 p에 1을 더한 결과가 &arr[1]과 같다. 이를 통해, arr[1]과 *(p+1)이 같은 의미인 것 또한 알 수 있다.


Ⅱ. 배열의 이름과 비밀

 

#include <stdio.h>
int main(){
    int arr[3] = {1,2,3};
    printf("%p", arr);
    
    return 0;
}

위 코드를 실행시키면 어떤 값이 나올까? arr은 배열이기 때문에 배열의 원소들이 튀어나올까? 하지만 실행시켜보면 다음과 같은 결과가 나온다.

000000000062FE10

즉, 배열의 이름을 출력하면 배열의 첫번째 원소의 주소값이 나온다. 배열의 이름은 배열의 첫번째 원소의 주소값과 같은 값을 가지게 된다는 의미이다. 그럼 배열의 이름은 배열의 첫번째 원소를 가리키는 포인터구나! 라고 생각한다면 또 그렇게 말할 수는 없다. 미묘하게 다른 부분이 존재하고 있기 때문이다.

즉, 배열은 배열이고, 포인터는 포인터이다. 다음 코드를 실행시켜보자.

#include <stdio.h>
int main() {
    int arr[6] = {1, 2, 3, 4, 5, 6};
    int* parr = arr;

    printf("Sizeof(arr) : %d \n", sizeof(arr));
    printf("Sizeof(parr) : %d \n", sizeof(parr));
  
    return 0;
}

 

Sizeof(arr) : 24
Sizeof(parr) : 8

arr와 parr모두 arr의 첫번째 원소의 주소값을 가지는 것은 같다. 그런데 왜 arr의 크기는 24바이트이고 parr의 크기는 8바이트일까?
우선 arr은 int형 원소 6개를 가지는 배열이므로 4바이트*6인 24바이트라는 크기를 가지고 parr은 그저 arr의 첫번째 원소를 가리키는 포인터이므로 주소의 크기인 8바이트(64비트 컴퓨터 기준 주소는 항상 8바이트이다)가 나오게 된다. 따라서 배열의 첫번째 원소의 주소와 배열의 이름은 엄연히 다른 것이다.
그런데 int* parr = arr;이라고 해줬으므로 parr도 배열을 가리키는거 아닌가요? 라는 질문이 생길 수 있다. 하지만 이 부분에서의 arr은 배열의 첫번째 원소의 주소값을 나타내는 것이 맞다. 언제는 둘이 다르고 언제는 같은 값을 가지고... 정말 헷갈릴 수 있다. 하지만 한마디로 정리해보면 sizeof연산자나 주소값 연산자(&)를 사용하는 경우를 제외하면 배열의 이름을 사용할 때 암묵적으로 첫번째 원소를 가리키는 포인터로 타입변환된다고 할 수 있다.

아니 또 &연산자를 쓸 때는 무엇이 다르다는 말일까? 이 부분은 배열을 가리키는 포인터에 대한 이야기로 다음 포스트에서 다시 이야기해보도록 하자.


Ⅲ. [ ]연산자의 역할

[ ]를 그동안 배열의 원소를 꺼낼 때 자연스럽게 사용했겠지만 사실 [ ]또한 연산자라는 사실을 알기는 쉽지 않았을 것이다. 하지만 이미 우리는 [ ]연산자의 역할을 알고 있다. 때문에 [ ]가 어떤 연산을 해주는지도 쉽게 이해할 수 있다.

#include <stdio.h>
int main(){
    int arr[3] = {1,2,3};
    
    printf("%d\n", arr[2]);
    printf("%d", *(arr+2));
    
    return 0;
}

위 코드의 결과는 다음과 같다.

3
3

arr[2]과 *(arr+2)가 같은 의미라는 건 이미 알고 있다. 사실 [ ]연산자를 사용하면 다음과 같이 형태변환이 되어 처리가 된다.즉, arr[2]과 *(arr+2)가 같은 의미 정도의 관계가 아니라 arr[2]가 *(arr+2)로 형태변환이 되어 완벽하게 같은 값을 나타낸다. 때문에 다음과 같이 이상해보이는 형태의 식도 가능하다

2[arr]

이런식으로 요소를 찾아내는 것은 거의 본적이 없겠지만 가능한 연산이다. [ ]연산자로 인해 위 식이 *(2+arr)로 바뀌기 때문이다. 물론 가독성을 고려해서 잘 사용하지는 않는다.

결국 요점은 [ ]연산자를 사용하면 a[n]을 *(a+n)로 형태변환 해주는 역할을 한다는 점이다.


Ⅳ. 포인터의 포인터

계속해서 강조하듯 포인터도 변수이므로 메모리 상의 공간에 저장되어있고 주소값을 가질 것이다. 그럼 이 포인터의 주소값을 가지는 포인터를 만들순 없을까? 그것이 바로 포인터의 포인터이다. 즉, 포인터의 포인터는 포인터를 가리키고 있는 것이다. 포인터의 포인터는 다음과 같이 선언할 수 있다.

int **p;

이를 이해하는 한가지 팁을 주자면 *p를 제외한 부분을 형(type)으로 보는 것이다. 즉, int (*p)에서 p가 int형 변수를 가리키듯 int* (*p)로 보면 p가 int*형, 즉 int를 가리키는 포인터를 가리키고 있다고 이해할 수 있다.

아무튼, 다음 예제를 보자.

#include <stdio.h>
int main() {
    int a;
    int *pa;
    int **ppa;

    pa = &a;
    ppa = &pa;
    a = 3;
	
    return 0;
}

위 예제에서 pa는 a의 주소값을 가지고 있고, ppa는 pa의 주소값을 가지고 있기 때문에 a , *pa, **ppa는 같은 값이 되며 &a, pa, *ppa도 같은 값이 되고, &pa, ppa도 같은 값이 된다.

#include <stdio.h>
int main() {
    int a;
    int *pa;
    int **ppa;

    pa = &a;
    ppa = &pa;
    a = 3;
	
    printf("%d %d %d\n", a, *pa, **ppa);
    printf("%d %d %d\n", &a, pa, *ppa);
    printf("%d %d", &pa, ppa);
    
    return 0;
}

 

3 3 3
6487572 6487572 6487572
6487560 6487560

 


Ⅴ. 포인터 상수

포인터 상수에 대해 이해하기 전 다음 예제를 살펴보도록 하자

#include <stdio.h>
int main(){
    int a[3] = {1,2,3};
    int *p = a;
    
    printf("%d %d %d\n", a[0], a[1], a[2]);
    printf("%d %d %d", p[0], p[1], p[2]);
    
    return 0;
}

 

1 2 3
1 2 3

p에 a라는 배열의 첫번째 원소의 주소를 넣어놓고 [ ]연산자를 이용해 배열처럼 값을 가져와주었다. p를 이용하면 다음과 같이 출력하는 것도 가능해진다.

int i;
for(i = 0; i<3; i++){
    printf("%d ", *p);
    p++;
}

위와 같이 포인터를 이용하면 p++이라는 연산이 가능하다. 하지만 아쉽게도 a++을 하려고 하면 오류가 뜨게 된다. 그 이유는 a가 포인터 상수이기 때문이다. a가 배열의 첫번째 원소를 가리키는 포인터로 타입변환이 되면 포인터이긴 하지만 그 값을 바꿀 수 없는 상수적 특성을 가지게 된다는 의미이다. 따라서 a에 있는 주소값을 바꾸려하면 오류가 생기게 되는 것이다.

댓글