포인터의 악명
- 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를 더해주는 것과 같이 취급한다.
- 나는 분명 pa에 1을 더했는데
-
배열과 포인터
- 배열은 변수가 여러 개 모인 것이다.
- 배열의 각 원소는 메모리 상에 연속되게 놓인다.
- 배열을 정의해보자.
- 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번째 원소를 가리키는 것과 동일하다.
- 컴퓨터는 C에서 [ ]라는 연산자가 쓰이면 자동적으로 아래의 형태로 바꿔서 처리하게 된다.
Part.2에서 계속...
참고자료
'sw사관학교정글' 카테고리의 다른 글
[week05] C언어 시작하자마자 red-black tree 구현하기 (1) | 2021.12.10 |
---|---|
[week05] 포인터 이해하기 - part.2 (0) | 2021.12.09 |
[week05] 코드리뷰 - 류석영 교수님 (0) | 2021.12.09 |
[week04] 백준 9084번 python DP 천천히 이해해보기 (4) | 2021.11.30 |
[week03] 백준 11725번 python 풀이 (0) | 2021.11.29 |