C언어 포인터 개인 공부 정리

이형준·2023년 5월 5일
0

TIL

목록 보기
19/37

reference: https://modoocode.com/231 (씹어먹는 C 언어)

  • 포인터 기초

    포인터는 특정한 데이터가 저장된 주소값을 보관하는 변수!!!

    C언어에서 포인터는 다음과 같이 정의할 수 있다.

    • (포인터에 주소값이 저장되는 데이터의 형) *(포인터의 이름);

      예를 들어 p 라는 포인터가 int 데이터를 가리키고 싶다고 하면,

      int *p;
      int* p;  

      라고 하면 올바르게 된다. 즉, 포인터 p 는 int 형 데이터의 주소값을 저장하는 변수가 된다!

      포인터를 정의했으면 주소값을 넣아야 할 터, 넣기 위해 우선 & 연산자에 대해 알아보자.

    • AND 연산자 & 와 생긴 건 같지만, 그 친구는 두 항 사이에 쓰는 이항 연산자!

    • 포인터에 쓰는 & 는 단항 연산자!

    • &변수 형태로 사용하면 그 변수의 주소를 알 수 있다.

      그렇다면 반대로, 해당 주소값의 데이터를 가져오고 싶다면? *

      p = &a;
      *p = 3;

      위와 같이 3을 담고 있는 변수 a의 주소를 가리키는 p가 있을 때, *p는 a와 동일한 의미❗

    포인터도 엄연한 변수이기에 특정한 메모리 공간을 차지한다는 것도 잊지말자!

    • 포인터에는 왜 타입이 있을까?

      포인터가 주소값만 보관하는데 왜 굳이 타입이 필요할까? 어차피 주소값은 32 비트 시스템에서 항상 4 바이트이고, 64 비트 시스템에서는 8 바이트 인데 그냥 pointer 라는 타입을 만들어버리면 안될까?

      포인터에는 시작 주소만이 담겨 있다. int, char과 같은 데이터형이 아닌 pointer라는 타입으로 선언하면, 포인터는 메모리에서 얼마만큼을 읽어들여야 하는 지 알 길이 없다. 하지만 int *p 와 같이 선언한다면, 이 포인터는 int 데이터를 가리키니까 시작점에서 4바이트만 읽어들이면 되겠구나! 를 알 수 있다는 것.
  • 상수 포인터

    const int* pa = &a;
    
    *pa = 3;  // 올바르지 않은 문장
    pa = &b;  // 올바른 문장
    
    포인터 pa가 가리키는 값은 바뀌면 안된다!
    int* const pa = &a;
    
    *pa = 3;  // 올바른 문장
    pa = &b;  // 올바르지 않은 문장
    
    포인터 pa가 바뀌면 안된다!
    const int* const pa = &a;
    
    *pa = 3;  // 올바르지 않은 문장
    pa = &b;  // 올바르지 않은 문장
    
    둘다 const -> pa와 pa가 가리키는 값 둘 다 바뀌면 안된다! 
  • 포인터의 덧셈

    /* 포인터의 덧셈 */
    #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 의 값 : 0x7ffd6a32fc4c 
    (pa + 1) 의 값 : 0x7ffd6a32fc50

    분명 포인터에 + 1을 하라고 명령했으나, 두 수의 차이는 다름아닌 4! 이유가 뭘까?

    바로 int 데이터형을 가리키는 포인터이기 때문이다. int 는 4바이트니까… 아니 근데 왜 4가 더해지거나 빠지는건데….

    배열과 포인터

    배열은 변수가 여러개 모인 것으로 생각할 수 있다 라고 공부했다. 여기에 또다른 놀라운 특성이 있는데, 그건 바로 배열들의 각 원소는 메모리 상에 연속되게 놓인 다는 점이다.

    int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    이라는 배열을 정의한다면 메모리 상에서 다음과 같이 나타납니다.

    https://modoocode.com/img/152460454D5FA71108EA6F.webp

    • int arr[10]의 한 원소는 int 형 변수이기 때문에, 4바이트를 차지한다.

      #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
    • parr + 3 을 수행하면, arr[3] 의 주소값이 되고, 거기에 * 를 붙여주면 * 의 연산자의 역할이 '그 주소값에 해당하는 데이터를 의미해라!' 라는 뜻이므로 *(parr + 3) 은 arr[3] 과 동일하다!

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

      #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 의 정체 : 0x7fff1e868b1c 
      arr[0] 의 주소값 : 0x7fff1e868b1c

      arr 과 arr[0] 의 주소값이 동일하다. 그렇다면 배열에서 배열의 이름은 배열의 첫 번째 원소의 주소값을 나타내고 있다는 사실을 알 수 있다. 어? 그렇다면…

      배열의 이름이 배열의 첫 번째 원소를 가리키는 포인터인가? ❌

      C 언어 상에서 배열의 이름이 sizeof 연산자나 주소값 연산자(&)와 사용될 때 (예를 들어 &arr) 경우를 빼면, 배열의 이름을 사용시 암묵적으로 첫 번째 원소를 가리키는 포인터로 타입 변환되기 때문에, 배열의 시작 원소와 주소값이 동일하게 나온 것.

      arr 이 sizeof 랑도, 주소값 연산자랑도 사용되지 않았기에, arr 은 첫 번째 원소를 가리키는 포인터로 타입 변환되었기에, &arr[0] 와 일치하게 된다.

      []는 연산자였다?!

      a[3] : 4 
      *(a+3) : 4

      [] 라는 연산자가 쓰이면 자동적으로 위 처럼 형태로 바꾸어서 처리하게 됩니다. 즉, 우리가 arr[3] 이라 사용한 것은 사실 *(arr + 3) 으로 바뀌어서 처리가 된다.

  • 고급 개념들

    • 포인터의 포인터
      #include <stdio.h>
      int main() {
        int a;
        int *pa;
        int **ppa;
      
        pa = &a;
        ppa = &pa;
      
        a = 3;
      
        printf("a : %d // *pa : %d // **ppa : %d \n", a, *pa, **ppa);
        printf("&a : %p // pa : %p // *ppa : %p \n", &a, pa, *ppa);
        printf("&pa : %p // ppa : %p \n", &pa, ppa);
      
        return 0;
      }

      실행 결과

      a : 3 // *pa : 3 // **ppa : 3 
      &a : 0x7ffd26a79dd4 // pa : 0x7ffd26a79dd4 // *ppa : 0x7ffd26a79dd4 
      &pa : 0x7ffd26a79dd8 // ppa : 0x7ffd26a79dd8
  • 2차원 배열의 [] 연산자

     /* 정말로? */
     #include <stdio.h>
     int main() {
       int arr[2][3];
     
       printf("arr[0] : %p \n", arr[0]);
       printf("&arr[0][0] : %p \n", &arr[0][0]);
     
       printf("arr[1] : %p \n", arr[1]);
       printf("&arr[1][0] : %p \n", &arr[1][0]);
     
       return 0;
     }
  • 실행 결과

         arr[0] : 0x7ffda354e530 
         &arr[0][0] : 0x7ffda354e530 
         arr[1] : 0x7ffda354e53c 
         &arr[1][0] : 0x7ffda354e53c

    arr[0] 의 값이 arr[0][0] 의 주소값과 같고, arr[1] 의 값이 arr[1][0] 의 주소값과 같습니다. 이것을 통해 알 수 있는 사실은 기존의 1 차원 배열과 마찬가지로 sizeof 나 주소값 연산자와 사용되지 않을 경우, arr[0] 은 arr[0][0] 을 가리키는 포인터로 암묵적으로 타입 변환되고, arr[1] 은 arr[1][0] 을 가리키는 포인터로 타입 변환된다라는 뜻이다.

    • 1 차원 배열 int arr[] 에서 arr 과 &arr[0] 는 그 자체로는 완전히 다른 것이였던 것처럼 2 차원 배열 int arr[][] 에서 arr[0] 과 &arr[0][0] 와 다릅니다. 다만 암묵적으로 타입 변환 시에 같은 것으로 변할 뿐입니다. 따라서 sizeof 를 사용하였을 경우 2 차원 배열의 열의 개수를 계산할 수 있습니다.
      int main() {
        int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
        printf("전체 크기 : %d \n", sizeof(arr));
        printf("총 열의 개수 : %d \n", sizeof(arr[0]) / sizeof(arr[0][0]));
        printf("총 행의 개수 : %d \n", sizeof(arr) / sizeof(arr[0]));
      }

      실행 결과

      전체 크기 : 24 
      총 열의 개수 : 3 
      총 행의 개수 : 2
      sizeof 연산자의 경우 포인터로 타입 변환을 시키지 않기 때문에 sizeof(arr[0]) 는 마치 sizeof 에 1 차원 배열을 전달한 것과 같습니다. 그리고 그 크기 (3) 을 알려주겠지요. 그리고 sizeof(arr[0][0]) 을 하게 된다면 int 의 크기인 4 를 리턴하게 되어서 총 열의 개수를 알 수 있게 됩니다.
    • 2차원 배열을 가리키는 포인터 2차원 배열을 가리키는 포인터는 다음과 같이 쓴다.
      /* (배열의 형) */ (*/* (포인터 이름) */)[/* 2 차원 배열의 열 개수 */];
      // 예를 들어서
      int (*parr)[3];
      크기가 3인 배열을 기리키는 포인터와 동일한데, 어떻게 2차원 배열을 가리킬 수 있는걸까? 이게 말이 되는게, 1 차원 배열에서 배열의 이름이 첫 번째 원소를 가리키는 포인터로 타입 변환이 된 것 처럼, 2 차원 배열에서 배열의 이름이 첫 번째  을 가리키는 포인터로 타입 변환이 되어야 합니다. 그리고 그 첫 번째 행은 사실 크기가 3 인 1 차원 배열이지요!
    • 포인터 배열
      • 포인터 배열은 말 그대로 포인터들의 배열

      • 포인터 배열은 정말로 배열이고, 배열 포인터는 정말로 포인터

        /* 포인터배열*/
        #include <stdio.h>
        int main() {
          int *arr[3];
          int a = 1, b = 2, c = 3;
          arr[0] = &a;
          arr[1] = &b;
          arr[2] = &c;
        
          printf("a : %d, *arr[0] : %d \n", a, *arr[0]);
          printf("b : %d, *arr[1] : %d \n", b, *arr[1]);
          printf("b : %d, *arr[2] : %d \n", c, *arr[2]);
        
          printf("&a : %p, arr[0] : %p \n", &a, arr[0]);
          return 0;
        }

        실행 결과

        a : 1, *arr[0] : 1 
        b : 2, *arr[1] : 2 
        b : 3, *arr[2] : 3 
        &a : 0x7ffe8a2fa4e4, arr[0] : 0x7ffe8a2fa4e4

        배열의 형을 int, char 등등으로 하듯이, 배열의 형을 역시 int* 으로도 할 수 있다.

        다시말해, 배열의 각각의 원소는 int 를 가리키는 포인터 형으로 선언된 것!

profile
저의 미약한 재능이 세상을 바꿀 수 있을 거라 믿습니다.

0개의 댓글