[자료구조 C 언어] C 프로그래밍 기초 - 5 : 포인터 뿌시기 (Pointer)

Programming/C · 2020. 2. 23. 23:13

벌써 지긋지긋합니다.

 

뭔가 이해한 것 같으면 새로운 게 생겨나는 포인터...

 

차근차근 알아봅시다.

 

제 게시물은 쭉 읽어보면서 이해하기 좋게 한다가 목적입니다.

 

천천히 이해하면서 읽어주세요.

 

대부분의 내용은 제가 나중에 사용하기 쉽게 이해한 개념인 것을 참고해 주세요!! (정확한 사전적인 뜻을 알고 싶으신 분들은 위키피디아로~)

 

1. 개념

 

포인터 (Pointer) : 어떤 주소를 가리켜서 그 속의 내용물을 가져올 수 있는 도구.

 

2. 표기

 

int *p = &a;

 

포인터 p는 앞에 *를 붙임으로써 변수 a의 주소를 기리킬 수 있다.

 

*p는 변수 a의 주소 안에 있는 변수 a의 값을 반환한다.

 

p는 변수 a의 주소를 가진다.

 

변수 앞에 &를 붙이면 (&a) 해당 변수의 주소를 나타낼 수 있다.

 

당연히 &p는 포인터 p의 주소를 가리킨다.

 

 

여기까지가 기본적인 개념입니다.

 

제가 제일 헷갈렸던 건 배열과 포인터의 관계입니다.

 

포인터 p가 가리키는 주소에 있는 값은 *p로 반환할 수 있습니다.

 

이와 동일하게 p[0]로도 반환이 가능합니다.

 

예를 들어

 

int b  = 3;

 

int *pb = &b;

 

printf("%d\n", *pb);

 

printf("%d\n", pb[0]);

 

위의 코드를 실행했을 때 동일하게 3이 출력되는 것을 확인할 수 있습니다.

 

어떤 사정이 있길래 저렇게 호환이 되는 건지 궁금해서 확인하기 위한 코드를 작성해 봤습니다.

 

자세하게 한번 보겠습니다.

 

편의상 주소는 %p로 출력해야 하지만 %d로 출력해서 보는 게 편하더라고요.

 

일단 b에는 3이란 정수가 저장되어 있습니다.

 

b의 주소인 &b는 pb와 동일한 결과를 보여줍니다.

 

당연히 포인터 pb 자체의 주소인 &pb는 다른 값을 가지고 있습니다.

 

위에서 설명드린 것과 같죠!

 

신기한 건 *pb와 pb[0]은 모두 3을 출력하고 있습니다.

 

왜일까 궁금하죠?

 

pb[0]의 주소인 &pb[0]은 b의 주소인 &b 값과 같습니다.

 

그럼 다음과 같이 정리할 수 있습니다.

 

"포인터에 괄호 []를 붙인 것은 *와 동일한 역할을 한다."

 

아래와 같이 정리할 수 있습니다.

 

*pb = pb[0]

 

pb = &pb[0]

 

 

 

 

 

또 하나 주목할 점은 pb[0]과 pb[1]의 주소는 4만큼 차이가 납니다.

 

왜 4일까요?

 

int 자료형 크기가 4 byte라서 그렇습니다.

 

여기서 잠시 배열에 대해 알아보고 가겠습니다.

 

배열은 다음과 같이 선언할 수 있습니다.

 

int a[4] = {1, 2, 3, 4};

 

처음 선언할 때 괄호 안에 숫자는 배열의 크기를 나타냅니다.

 

그리고 배열의 원소는 0부터 시작합니다.

 

즉, 다음과 같이 배열의 원소를 가리킬 수 있습니다.

 

a[0]  :   1

a[1]   :   2

a[2]  :   3

a[3]  :   4

 

얘네들의 주소를 봅시다.

앞에서 한 거랑 뭔가 비슷하죠?

 

마찬가지로 a[0]과 a[1]의 주소는 int 형의 크기인 4만큼 차이가 납니다.

 

포인터나 배열이나 동일한 방식을 출력할 수 있는 것이 주목할 점입니다.

 

** 배열의 이름인 a는 첫 번째 배열의 주소값을 가지고 있습니다. (지금은 그냥 그렇다고 알아두세요!)

 

더욱 신기하고 짜증 나는 점은 *a는 배열의 첫 번째 값인 1을 반환합니다.

 

진짜 포인터랑 똑같죠.

 

어떻게 보면 당연한 결과입니다.

 

*를 붙임으로써 a에 저장되어 있는 주소 값의 내용물을 가리킬 수 있으니까요.

 

제가 느낀 것은 그럴 거면 뭐 하러 *를 붙인 포인터를 선언을 해주나 싶었습니다.

 

굳이 포인터로 선언을 안 했더라도 변수에 주소값 저장하고 *만 붙이면 그 주소에 있는 값을 가리킬 수 있으니까 말이죠.

 

근데 그건 또 아니더라구요... 하하

 

그럼 우린 또 하나 알게 되었네요.

 

일반적인 변수로 선언된 것은 *를 붙여서 주소값을 가리킬 수 없지만 포인터로 선언된 것과 배열로 선언된 것은 *를 붙여서 해당 변수가 가지고 있는 주소 값의 내용물을 가리킬 수 있다!!

 

이제 거의 다 됐습니다.

 

 

 

마지막으로 하나만 더 알아보고 이번 게시물은 끝냅시다.

 

앞서서 확인해 본 결과

 

int a = 10;

 

int *p = &a;

 

일 때 

 

&p[0]은 a의 주소를 가지고 있고, &a[1]은 &a[0] 보다 4만큼 큰 주소값을 가지고 있었습니다.

 

마찬가지로

 

int a[2] = {10, 11};

 

일 때

 

&a[0]과 &a[1]은 4만큼 차이가 나는 주소값을 가지고 있습니다.

 

배열의 인덱스가 1 증가할 때마다 (포인터에 괄호를 붙인 거나 배열이나 모두 다) 선언된 변수 형의 크기만큼 주소값이 차이가 납니다.

 

이런 주소를 가리키는 방법이 또 한 가지 있습니다.

 

쓰임에 따라 이게 편한 경우도 있겠지만 뭐 하러 이런 게 되게 만들었는지 모르겠습니다.

 

괜히 헷갈리게.......

 

바로 *(p+n) 입니다.

 

괄호가 정말 중요합니다.

 

왜 그런지는 또 결과를 보시죠

 

int a[10] = {11, 22, 33, 44, 55, 66, 77, 88, 99, 1010};

 

저는 이렇게 배열을 선언했습니다.

 

결과는 이렇게 나옵니다.

 

괄호를 맨 위 처럼 했을 때만 인덱스 1에 해당하는 배열 값이 반환됩니다.

 

즉, *(a+n)은 a[n]와 동일한 결과를 가집니다.

 

*(a+n) = a[n] 

 

괄호가 잘못되면 *a의 값인 a[0]이 가리키는 값에 그냥 숫자 1을 더해준 결과가 나오게 됩니다. (11 + 1 = 12)

 

뭐 이렇게 생략된 거라고 생각하시면 편합니다.

 

*(a) + 1 = *(a+0) + 1 = a[0] + 1

 

왜 그런지를 또 따져봅시다.

 

a+0은 a[0]의 주소인 &a[0]과 동일한 값을 가지고 a+1도 마찬가지로 a[1]의 주소인 &a[1]과 동일한 값을 가지고 있습니다.

 

따라서 (a+n)에 *를 붙임으로써 (a+n)이 가지고 있는 a[n]의 주소 내부의 값을 가리킬 수 있는 거죠.

 

포인터도 동일합니다.

 

    int *pa;

 

    pa = &a;

 

이렇게 선언했을 때의 경우를 알아보겠습니다.

 

pa라는 포인터에는 배열의 이름인 a의 주소가 저장되어 있고, 위에서 살펴본 대로 배열의 이름인 a와 a의 주소 &a는 배열의 첫 번째 값인 a[0]의 주소와 동일한 값을 가지고 있습니다.

 

예상대로라면 *pa는 배열의 첫번째 원소인 a[0]의 값과 동일한 값을 반환할 것입니다.

 

 

예상과 똑 떨어지네요.

 

순조롭습니다.

 

이제 하나 궁금한 점이 있습니다.

 

과연 *(pa+n)은 a[n]의 값을 반환할까?

 

pa는 배열이 아니니까 안 될 수도 있지 않을까 의문이 생겨서 해봤습니다.

 

다행히 이변은 없었습니다.

 

포인터가 배열로 선언되지 않았어도 *(pa+n)을 통해 n이 1 씩 커질수록 포인터의 자료형의 크기만큼 주소가 변화하고 그 주소의 내용물을 *을 통해 가리킬 수 있습니다.

 

머리가 뒤죽박죽 정신이 없으시죠?

 

정리해 드리겠습니다.

 

처음에 포인터 pb는 배열의 이름인 a를 통해 배열의 첫 번째 원소의 주소 값을 가지게 되었습니다.

 

이후로 pb+n으로 n이 증가할 때마다 자료형의 크기만큼 주소값이 변화하게 되고, 배열의 원소도 인덱스가 증가할 때 마다 배열의 자료형 크기만큼 주소가 변화하기 때문에 n과 동일한 인덱스를 가진 배열값을 포인터 pb+n이 *을 통해 가리킬 수 있게 됩니다.

 

아! 아까 처음 정리한 거 기억나시나요?

 

[]는 *과 동일한 역할을 한다.

 

이것도 한번 증명해 봅시다.

 

 

포인터로 선언된 변수도 괄호를 붙임으로써 해당 주소가 가리키는 값을 반환할 수 있습니다.

 

 

 

 

총 정리!!

 

*(p+n) = p[n] 

 

p가 배열이건 포인터건 위와 같이 표현함으로써 p+n 또는 p[n]이 주소의 내용물을 가리킬 수 있다.

 

그럼 둘의 차이점은 뭐냐!!

 

배열과 포인터의 차이점은 딱 한 가지만 기억하시면 됩니다.

 

int *pa;

int a[3] = {1, 2, 3};
int b[3] = {11, 22, 33};

 

pa = &a; // 배열 a의 주소를 가리킨다.

 

이때 *(pa+1)은 a[1]의 값을 가집니다.

 

pa = &b; // 배열 b의 주소를 가리킨다.

 

이렇게 다시 선언해 주면 이제부터는 *(pa+1)은 b[1]의 값을 가질 수 있습니다. 

 

다시 말하면, 포인터는 위와 같이 다른 주소를 가져옴으로써 다른 배열을 가리킬 수 있습니다.

 

하지만 배열로 선언된 변수는 다음과 같이 다른 배열을 가리킬 수 없고 오직 자기 자신만 가리킬 수 있습니다.

 

a = &b; // 이렇게 선언하면 에러가 뜹니다.

 

위와 같이 선언한다고 해서

 

*(a+1)을 했을 때 b[1]의 값을 가질 수 없습니다.

 

끝!!

 

다음엔 배열 포인터, 포인터 배열 등등을 알아봅시다..

반응형