sw사관학교정글

[week05] 포인터 이해하기 - part.1

D cron 2021. 12. 9. 21:55

포인터의 악명

  • C언어를 조금이라도 공부해 본 사람들은 악명 높은 포인터에 대해 잘 알고 있을 것이다. 심지어 C언어를 공부하지 않았던 사람이라도 주변 사람들이 포인터로 고통받는 것을 목격했을지도 모른다.
  • 이 포인터란 놈은 헷갈리기 쉬운 개념이기 때문에 이번 기회에 차근차근 알아가 보려고 한다.

포인터를 알아보자

  • 포인터는 특정한 데이터가 저장된 (시작)주소값을 보관하는 변수다.
  • 우리가 예전에 다뤄왔던 int나 float을 생각해보자.
    • int형 변수는 정수를 보관한다.
    • float형 변수가 실수를 보관한다.
    • 마찬가지로 포인터는 주소값을 보관한다.
  • 포인터는 아주 새로운놈은 아니었던 것이다.
  • 뭐야? 포인터도 그냥 int, char 같이 어떤 형태의 값을 보관하는 변수잖아? 별거 아녔네!라고 생각하자(그렇지 않더라도).

포인터의 type

  • 그런데 이 포인터란 놈은 이상하게 형(type)을 가지고 있다.
  • 이 말은 int형 데이터의 주소 값을 저장하는 포인터와 float형 데이터의 주소값을 저장하는 포인터가 서로 다르다는 말이다!
  • 아까 전에 포인터는 int, char와 비슷하게 주소 값을 보관하는 변수라고 했는데, type이 도대체 왜 필요한 거지?라는 생각이 자연스럽게 들 수 있다.
  • 그런데 이 의문은 다시 정의를 보면 알 수 있다.
    • 포인터: 특정한 데이터가 저장된 (시작)주소 값을 보관하는 변수
  • 괄호속의 '시작'이 보이는가?
    • 이 힌트를 가지고 뒤에 가서 설명하도록 하겠다.

포인터의 정의

int *p //(포인터에 주소값이 저장되는 데이터의 형) *(포인터의 이름)
int* p //라고 하면 된다.(포인터에 주소값이 저장되는 데이터의 형)* (포인터의 이름)
// 둘중에 아무거나 사용하면 된다.

포인터의 &연산자 와 *연산자

  • 포인터는 주소값을주소 값을 저장하는 변수라고 배웠다. 그러면 주소 값을 어떻게 알 수 있을까?
    • &연산자가 주소값을 알려주는 연산자이다.
  • &연산자의 사용
  •   
    • 주소 값이 잘 찍힌다.
  • #include <stdio.h> int main(){ int a; a = 2; printf("%p \n", &a); return 0; } // 0x7ffc54f8d504
  • 포인터에게 주소값 넣어주기
  • #include <stdio.h>
    int main(){
        int *p;
        int a;
        p = &a;
        printf("포인터 p에 들어 있는 값: %p \n",p);
        printf("int 변수 a가 저장된 주소: %p \n",&a);
        return 0;
    }
    // 포인터 p에 들어 있는 값: 0x7ffd0653013c 
    // int 변수 a가 저장된 주소: 0x7ffd0653013c
  • *연산자
    • &연산자가 어떠한 데이터의 주소값을 얻는 연산자라면,
    • *연산자는 거꾸로 주소값에서 해당 주소값에 대응되는 데이터를 가져오는 연산자이다.
    • *연산자는 쉽게 말하면
      • 나를 내가 가진 주소값에 위치한 데이터로 생각해줘! 이다.
    • 예시
    • #include <stdio.h>
      int main(){
          int *p;
          int a;
          
          p = &a;
          a = 3;
      
          printf("a의 값: %d \n",a);
          printf("*p의 값: %d \n", *p);
      
          return 0;
      }
      // a의 값: 3
      // *p의 값: 3
      • 여기서 *p와 a는 정확히 동일한 값이다.

포인터에 type이 있는 이유

위의 예시를 그림으로 나타내면 다음과 같다.

포인터도 특정 공간을 차지하며, 자기 자신만의 주소를 가지고 있다.

  • 여기서 포인터를 자세히 살펴보면, a의 [시작] 주소값이 들어있는 것을 볼 수 있다(정의도 그러하다). 문제는 포인터에는 항상 시작 주소만 있다는 점이다.
  • 그런데 만약 포인터에 형이 없다면?
  • *p를 실행했을 때 포인터의 주소 값인 0x12A3BE72로 가서 a에 들어있는 값이 얼마인지 읽어 들여와야 하는데 얼만큼 읽어 들일지 알 방법이 없다.
  • 그러니까 int *p와 같이 포인터에도 형(type)이 존재해서
    • "나는 포인터인데 int형을 가리키니까 시작 주소로부터 4byte를 읽어와 줘!"
    • 라고 말해주어, a의 값인 3을 가져올 수 있게 되는 것이다.

포인터도 변수다

  • 앞서 말했듯이 포인터는 변수다
  • #include <stdio.h>
    int main(){
        int a;
        int b;
        int *p;
    
        p = &a;
        *p = 2;
        p = &b;
        *p = 4;
        printf("a : %d \n",a);
        printf("b : %d \n",b);
        return 0;
    }
    // a : 2
    // b : 4
    • 포인터에 들어간 주소 값이 바뀔 수 있다.
    • 처음에 a를 가리켰다가(p에 변수 a의 주소값이 들어갔다가)
    • 나중에 b를 가리킬(p에 변수 b의 주소값이 들어감) 수 있다는 것이다.

포인터의 덧셈

  • 포인터의 덧셈을 진행해보자.
    • #include <stdio.h>
      int main(){
          int a;
          int* pa;
          pa = &a;
      
          printf("pa의 값: %p \n", pa);
          printf("(pa + 1)의 값: %p \n",pa + 1);
      
          return 0;
      }
      // pa의 값: 0x7ffef5073b1c 
      // (pa + 1)의 값: 0x7ffef5073b20
      • 나는 분명 pa에 1을 더했는데
        • 0x7ffef5073b1c+1인 0x7ffef5073b1d가 나오지 않고,
        • 0x7ffef5073b20가 나왔다!!!!
        • 0x7ffef5073b20 - 0x7ffef5073b1c = 4(16진수임을 생각)
      • 이유가 무엇일까?
        • 바로 포인터의 형이 int*이기 때문이다.
        • 포인터가 int*라면, 포인터에 1을 더해주는 행위는 4byte를 더해주는 것과 같이 취급한다.
        • 포인터가 char*라면, 포인터에 1을 더해주는 행위는 1byte를 더해주는 것과 같이 취급한다.

배열과 포인터

  • 배열은 변수가 여러 개 모인 것이다.
  • 배열의 각 원소는 메모리 상에 연속되게 놓인다.
  • 배열을 정의해보자.
  • int arr [10] = {1,2,3,4,5,6,7,8,9,10};
  • 그림으로 보면 다음과 같다.
    • 위와 같이 메모리 상에 연속되게 나타나고, 한 개의 원소는 int형 변수이기 때문에 4byte를 차지하게 된다.
    • 이제 우리가 왜 바로 앞에서 포인터의 덧셈을 배웠는지 알 수 있다.
    • 포인터의 덧셈으로 배열의 원소에 아주 쉽게 접근할 수 있다!!
      • 배열의 시작 부분을 가리키는 포인터를 정의한 뒤에 포인터에 +1을 하면 그다음 원소를 가리킬 것이다. 그리고 +2를 하면 그 다음다음 원소를 가리킨다!!
  • 위와 같은 일이 가능한 이유는 포인터는 자신이 가리키는 데이터의 '형'의 크기를 곱한 만큼 덧셈을 수행하기 때문이다. 즉 p라는 포인터가 int a;를 가리킨다면 p+1을 할 때 p의 주소 값에 사실은 1*4가 더해지고, p+3을 하면 p의 주소값에 3*4인 12가 더해진다.
  • 실제로 잘 작동되는지 확인해보자.
  • #include <stdio.h>
    int main(){
        int arr[10] = {1,2,3,4,5,6,7,8,9,10};
        int* parr;
    
        parr = &arr[0]; //시작 주소값을 가리킨다.
        printf("arr[3] = %d, *(parr + 3) = %d \n", arr[3],*(parr + 3));
        return 0;
    }
    //arr[3] = 4, *(parr + 3) = 4
    • 포인터로 배열의 원소에 쉽게 접근 가능하다!

배열의 이름의 비밀

  • arr 자체를 출력하면 뭐가 나올까?
  • #include <stdio.h>
    int main(){
        int arr[3] = {1,2,3};
        printf("arr의 정체 : %p \n",arr);
        printf("arr[0]의 주소값 : %p \n",&arr[0]);
        return 0;
    }
    // arr의 정체 : 0x7ffcb84acf2c 
    // arr[0]의 주소값 : 0x7ffcb84acf2c
    • arr와 arr [0]의 주소 값이 동일하다!!!
    • 배열의 이름은 배열의 첫 번째 원소의 주소 값을 나타내고 있다.
    • 그렇다면, 우리가 배워왔던 대로
      • 배열의 이름이 배열의 첫 번째 원소를 가리키는 포인터라고 할 수 있을까??
        • 아니다.

배열은 배열이고 포인터는 포인터다.

  • 배열의 이름을 포인터라고 할 수 없는 명확한 증거를 제시한다.
  • #include <stdio.h>
    int main(){
        int arr[6] = {1,2,3,4,5,6};
        int* parr = arr;
    
        printf("Sizeof(arr): %ld \n",sizeof(arr));
        printf("Sizeof(parr): %ld \n",sizeof(parr));
    }
    // Sizeof(arr) : 24
    // Sizeof(parr): 8
    • 둘의 size가 다르므로 같지 않다!
  • 배열의 이름과, 첫 번째 원소의 주소 값은 다른 것이다. 근데 왜! 두 값을 출력 했을 때 같은 값이 나올까?
    • 그 이유는 C언어 상에서 배열의 이름이 sizeof 연산자나 주소값 연산자(&)와 사용될 때(&arr)의 경우를 빼면, 배열의 이름을 사용 시 암묵적으로 첫 번째 원소를 가리키는 포인터로 타입 변환된다.

[ ]연산자

  • [ ]가 사실 연산자였다.
  • #include <stdio.h>
    int main(){
    	int arr[5] = {1,2,3,4,5};
    	printf("a[3] : %d \n", arr[3]);
    	printf("*(a+3) : %d \n",*(arr + 3));
    	return 0;
    }
    • 컴퓨터는 C에서 [ ]라는 연산자가 쓰이면 자동적으로 아래의 형태로 바꿔서 처리하게 된다.
      • arr [3] → *(arr+3)
      • 그리고 arr는 +연산자와 사용되기 때문에 첫 번째 원소를 가리키는 포인터로 변환되어 arr+3이 포인터 덧셈을 수행하게 된다.
      • 이는 배열의 4번째 원소를 가리키는 것과 동일하다.

Part.2에서 계속...


참고자료

https://modoocode.com/231

 

씹어먹는 C 언어 시작하기

 

modoocode.com