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]
-
이렇게 되어서 C 컴파일러가 int* 원소 3개를 가지는 배열을 정의한 것으로 오해하게 된다.int *parr[3]
-
parr은 크기가 3인 배열을 가리키는 포인터이기 때문에 배열을 직접 나타내기 위해서는 *연산자를 통해 원래 arr을 참조해야 한다.printf("parr[1] : %d \n", (*parr)[1]);
- 따라서 (*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형의 변수를 가리키는 pi라는 이름의 포인터를 인자로 받는다. 그리고 main 함수에서 함수를 이렇게 호출한다.int change_val(int *pi) ;
-
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를 가리키고 있기 때문이다.
-
결론적으로
어떠한 함수가 특정한 타입의 변수나 배열의 값을 바꾸려면 함수의 인자는 반드시 그 타입을 가리키는 포인터를 이용해야 한다.
참고자료
씹어먹는 C 언어 시작하기
modoocode.com