sw사관학교정글

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

D cron 2021. 12. 9. 23:02

포인터의 포인터

  • 포인터의 포인터는 어떻게 표현할까?
  • 쉽게 상상할 수 있듯이 다음과 같이 정의한다.
  • int **p
    • 위는 int를 가리키는 포인터를 가리키는 포인터라고 할 수 있다.
  • 포인터의 포인터 예제
  • #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 : 0x7ffc740e74a4 // pa : 0x7ffc740e74a4 // *ppa : 0x7ffc740e74a4 
    // &pa : 0x7ffc740e74a8 // ppa : 0x7ffc740e74a8
    • 같은 행에 있는 값들은 모두 같다는 점에 주목하자.
    • **ppa를 다시 써보면 *(*ppa)가 되는데, *ppa는 pa를 지칭하는 것이기 때문에 *pa가 되어 결국 a를 지칭한다.
    • 그림으로 나타내면 다음과 같다.

배열 이름의 주소값

  • 포인터 이해하기 part.1에서 배열 이름에 sizeof 연산자와 주소값 연산자를 사용할 때 빼고는 전부다 포인터로 암묵적 변환이 이뤄진다고 했다. 그렇다면 주소값 연산자를 사용하면 어떻게 될까?
  • #include <stdio.h>
    
    int main(){
        int arr[3] = {1,2,3};
        int (*parr)[3] = &arr;
        
        printf("arr[1] : %d \n",arr[1]);
        printf("parr[1] : %d \n",(*parr)[1]);
        return 0;
    }
    // arr[1] : 2
    // parr[1] : 2
    • 이 코드에서 &arr는 무슨 의미를 가질까? 이전에 arr은 int*로 암묵적 변환된다고 했으니까 &arr은 int**가 되는 것일까?? 아니다!! 암묵적 변환은 주소값 연산자가 왔을 때는 이뤄지지 않는다.
    • arr는 크기가 3인 배열이기 때문에 &arr을 보관할 포인터는 크기가 3인 배열을 가리키는 포인터가 되어야 할 것이다. 그리고 C언어 문법상 이를 정의하는 방식은 다음과 같다.
    • int (*parr)[3]
      참고로 괄호를 꼭 써줘야 한다. 그렇지 않으면 
    • int *parr[3]
      이렇게 되어서 C 컴파일러가 int* 원소 3개를 가지는 배열을 정의한 것으로 오해하게 된다.
    • printf("parr[1] : %d \n", (*parr)[1]);
      parr은 크기가 3인 배열을 가리키는 포인터이기 때문에 배열을 직접 나타내기 위해서는 *연산자를 통해 원래 arr을 참조해야 한다. 
      • 따라서 (*parr)[1]과 arr[1]은 같은 문장이 된다.

포인터를 지금껏 배운 이유

  • 다음 예시를 살펴보자.
#include <stdio.h>
int change_val(int i){
    i = 3;
    return 0;
}
int main(){
    int i = 0;

    printf("호출 이전 i의 값: %d \n",i);
    change_val(i);
    printf("호출 이후 i의 값: %d \n",i);

    return 0;
}
// 호출 이전 i의 값: 0
// 호출 이후 i의 값: 0
  • i의 값은 전혀 바뀌지 않음을 확인할 수 있다.
  • 왜 바뀌지 않았을까?
  • 왜냐하면 change_val()을 호출할 때 change_val() 함수 안에서 정의된 변수 i는 main() 함수의 i의 값을 전달받은 후에, change_val() 함수 안에서 정의된 변수 i의 값을 3으로 변경하게 된다.
    • 그런데 여기에서 중요한 점은 main() 함수의 i가 아닌, change_val() 함수 안에서 정의된 변수 i의 값이 3으로 변경된다는 점이다. 결론적으로 main() 함수의 i는 change_val()함수에 전달만 될 뿐, 값이 변경되지 않는다.
  • 그렇다면 함수간의 변수의 변경은 불가능한 것일까?
  • 그럴리가! 포인터를 사용하면 된다!!
  • 이전의 방법을 통해서 다른 함수에 정의된 변수들의 값을 변경할 때 직면했던 문제는 바로 각 함수는 다른 함수의 변수들에 대해 아는 것이 아무것도 없다는 것이었다.
  • 그러니까 우리는 어떤 값을 변경시키기 위해서 주소값을 전달해주면 된다.

 

포인터를 써먹어 보자.

#include <stdio.h>
int change_val(int *pi){
    printf("----- chage_val 함수 안에서 -----\n");
    printf("pi 의 값 : %p \n", pi); // 0x1234
    printf("pi 가 가리키는 것의 값 : %d \n", *pi); // 0
    *pi = 3;
    printf("----- change_val 함수 끝~~ -----\n");
    return 0;
}
int main(){
    int i = 0;
    printf("i 변수의 주소값: %p \n", &i); // 0x1234
    printf("호출 이전 i의 값: %d \n", i); // 0
    change_val(&i);
    printf("호출 이후 i의 값: %d \n", i); // 3
    return 0;
}
// i 변수의 주소값: 0x7fff9001f854 
// 호출 이전 i의 값: 0 
// ----- chage_val 함수 안에서 -----
// pi 의 값 : 0x7fff9001f854 
// pi 가 가리키는 것의 값 : 0 
// ----- change_val 함수 끝~~ -----
// 호출 이후 i의 값: 3
  • 드디어 포인터를 배운 이유를 찾았다. 
  • 천천히 무슨 일을 했는지 들여다보자.
  • int change_val(int *pi) ;
    일단 함수의 정의부분을 살펴보면 int형의 변수를 가리키는 pi라는 이름의 포인터를 인자로 받는다. 그리고 main 함수에서 함수를 이렇게 호출한다.
    • change_val(&i);
      • 즉, 인자에 main함수에서 정의된 i라는 변수의 '주소값'을 인자로 전달하고 있다. 따라서 change_val() 함수를 호출했을 때 pi에는 i의 주소값이 들어가게 된다. 
    • {
          printf("----- chage_val 함수 안에서 -----\n");
          printf("pi 의 값 : %p \n", pi); // 0x1234
          printf("pi 가 가리키는 것의 값 : %d \n", *pi); // 0
          *pi = 3;
          printf("----- change_val 함수 끝~~ -----\n");
          return 0;
      }
       
    • pi가 i의 주소값을 가지고 있으므로 pi를 출력했을 때 그 값은 i의 주소값과 같을 수밖에 없다. 또한 그 아래 *pi를 통해 i를 간접적으로 접근할 수 있다. 왜냐하면 *연산자는 '내가 가지는 주소값에 해당하는 변수를 의미'하기 때문이다.
      • pi를 통해서 main과 change_val 함수의 사이에 연결고리가 생기는 것이다!
    • 여기서 pi는 change_val 함수 내에서 정의된 변수이다.
    • *pi = 3을 통해 pi가 가리키고 있는 변수의 값을 3으로 변경할 수 있다. 여기서 pi는 i를 가리키고 있기 때문이다.

결론적으로

어떠한 함수가 특정한 타입의 변수나 배열의 값을 바꾸려면 함수의 인자는 반드시 그 타입을 가리키는 포인터를 이용해야 한다.


참고자료

https://modoocode.com/231

 

씹어먹는 C 언어 시작하기

 

modoocode.com