포인터 (pointer)
pointer. 무엇을 point 하는 걸까? C 언어의 포인터는 특정 주소를 가르키고 있다는 개념이다. (즉 해당 주소값을 갖고 있다)
이 주소는 '메모리'주소를 의미하기 때문에 잠시 메모리에 대해 짚고 넘어가보자.
컴퓨터의 주 메모리는 바이트 (= 8개 비트 정보를 저장할 수 있는 크기) 단위로 이루어져 있다. 바이트 단위라는 의미는 각각의 바이트마다 주소가 정해져 있어서 이 주소로서 서로 구분될 수 있다는 것이다.
실행가능한 C프로그램은 code와 data로 이루어져있다. code는 C 소스코드의 각 명령어들을 번역한 기계어 인스트럭션들을 의미하고, data는 C 소스 코드의 변수들을 의미한다. 이 변수들이 선언되고 초기화될 때, 각 변수에 자료형에 맞는 크기의 (적어도 한 바이트 이상의) 메모리가 할당된다. (아래 그림 참조)
이때, 변수가 저장된 바이트(들)의 첫번째 바이트 주소가 바로 해당 변수의 주소이다. 위 그림을 기준으로 하면 a의 주소는 0x01, b의 주소는 0x02, c의 주소는 0x06이 되는 것이다. 이 주소는 숫자로 표현되긴 하지만, 값의 범위가 정수와는 다르므로 일반적인 정수 자료형의 변수가 아니라, 특별한 포인터 변수의 형태로 저장할 수 있는 것이다.
어떤 포인터변수 p가 변수 c의 주소(= 위 그림에서의 0x06)를 저장하고 있을 때, 우리는 p가 "point to c" (C를 가르킨다) 고 말할 수 있다. 즉 포인터란 단지 메모리의 주소이고, 포인터 변수란 단지 메모리 주소를 저장하는 변수인 것이다. 메모리 주소를 일일히 기억하고 표기하는 것은 번거롭기 때문에 포인터의 개념을 사용하여 포인터 변수 p가 변수 i의 주소를 저장한다는 것을 나타내기 위해 변수 p의 값(contents)이 i를 가리키는 화살표를 사용할 수 있다.
포인터 변수의 선언 (declaration)
포인터 변수의 선언은 다른 변수의 선언과 동일하다. 딱 한가지만 빼고. 포인터 변수는 변수 이름 앞에 항상 * 가 온다.
int *p ;
위와 같은 선언은, p가 int 자료형의 object를 가르킬(point) 수 있는 포인터 변수라는 것을 명시한다. char 자료형의 object를 가르킬 때는 char *p; double 자료형의 object를 가르킬 때는 double *p; 와 같이 사용한다. 여기서 object 라는 단어를 사용하는 이유는, 포인터 p는 변수에 속하지 않는 메모리 영역을 가르킬 수도 있기 때문이다. (일반적으로 object를 객체라고 번역하는데, wikidocs에 이 책을 번역하신 분은 이를 '개체' 라고 번역하셨다. 아직 이 책에서의 object가 어떤 것을 의미하는지 정확히 알 수 없어서 섣불리 객체라고 적지 않고 원서 그대로 object 라는 단어를 사용하였다. 추후 단어의 의미를 더 정확하게 알게되면 수정 예정)
또한 아래와 같이 포인터 변수들 또한 다른 변수들의 선언과 함께 선언해줄 수 있다.
int i, j, a[10], b[20], *p, *q ; // 하지만 이렇게 여러 변수를 한번에 선언하는 게 바람직한 형태는 아니다
위의 코드들에서 볼 수 있는 것과 같이 포인터 변수도 자료형을 가진다. C언어는 모든 포인터 변수가 특정 타입(referenced type)의 objects 들만 가르킬 수 있게 하기 때문이다. 다만 포인터가 가르킬 수 있는 타입이 정해져있지는 않다. 포인터 변수가 다른 포인터를 가르키는 것도 가능하다.
이렇게 포인터 변수를 선언하면, (자료형에 관계없이) 항상 4 바이트의 메모리가 할당된다. 이는 컴퓨터의 기본적인 주소 체계가 기본적으로 4 바이트로 표현되기 때문이다.
포인터 변수의 초기화(initialization)와 & (address) 연산자
다른 변수들과 동일하게 포인터 변수를 선언하면 변수에 메모리 영역이 할당된다. 값을 초기화 하지 않고 변수만 선언한 경우에는 쓰레기값(garbage)이 채워지기 때문에 포인터 변수를 사용하기 전에 값을 초기화 해주는 것이 중요하다.
포인터 변수를 초기화할 때는 & 연산자(주소 연산자)를 써서 변수의 주소를 저장할 수 있다.
💡 & 연산자 : 변수 x 에 대해, &x는 메모리에 x가 저장되어있는 주소를 의미
int i;
int *p;
p = &i ; // 변수 i의 메모리 주소를 포인터 변수 p에 대입한다 (초기화)
위의 코드는 아래와 같은 방식으로 사용할 수도 있다.
int i;
int *p = &i // 선언과 동시에 초기화 (선언과 초기화를 따로 하는 것보다 더 권장되는 방식)
형태는 달라도 어쨌든 위의 코드들은 모두 '포인터 변수 p의 값이 i object를 가르키게' 한다.
포인터 변수의 사용과 *(indirection) 연산자
포인터 값이 초기화되고나면 * 연산자를 통해 포인터가 가르키는 object에 저장된 값에 접근할 수 있게 된다. (포인터 변수가 초기화 되기 전에 접근하면, 위에서 언급한 것과 같이 포인터 변수의 쓰레기값에 접근하게 되는데 이는 프로그램을 종료시키는 등의 문제를 발생시킨다. Never do this! 🙅🏻♀️ 🙅🏻♀️ 🙅🏻♀️ )
💡 * 연산자 : 포인터 p에 대해, *p는 p가 현재 가르키는 object를 나타냄
포인터 변수 p가 변수 i를 가르키고 있을 때, i가 저장한 값을 프린트 하는 방법은 아래와 같다.
printf("%d \n",i); // 변수를 통해 직접 값에 접근 하는 방법을 '직접 접근' 이라 한다.
printf("%d \n",*p); // 포인터변수와 *연산자를 통해 값에 접근하는 방법을 '간접 접근' 이라 한다.
쉽게 표현하자면 p가 i를 가르키고 있는 한, *p는 i의 별명(alias) 라고 할 수 있는 것이다. *p의 값을 바꾸면, 실제로 i의 값까지 바뀐다!
포인터의 연산
포인터는 주소값을 다루는 독특한 변수라서 다른 변수들의 산술 연산과는 다른 규칙들이 적용된다. 간단히 살펴보자.
1. 포인터끼리 덧셈 ❌
- 사실상 포인터끼리 더했을 때의 정보는 쓰잘데기 없는 정보다. 포인터끼리는 더할 수 없다.
2. 포인터끼리 뺄셈 ⭕
- 포인터끼리의 뺄셈은 두 요소간의 거리를 나타내주기 때문에 의미가 있다. 그래서 포인터끼리 뺄 수 있다.
- 단, 두 포인터가 같은 타입이고 같은 배열내의 다른 요소를 가리키고 있을 때만 뺄 수 있다.
3. 포인터에 정수 더하기, 빼기 ⭕
- 포인터에 정수를 더하거나 빼는 것은, 특정 요소의 메모리 주소로 부터 정수값의 거리만큼 다음 주소로 이동한 주소의 위치를 의미.
- 주의) 정수값 n이 산술적으로 n을 더하는 데 쓰이는게 아닌, 다음 n번째 주소를 의미할 때 쓰인다(= 실제 주소 값의 메모리 단위로 연산) 엄밀히 얘기하면 포인터가 가진 주소값을 기준으로 정수*sizeof(포인터 변수의 자료형) 만큼을 더하거나 빼는 것이다.
e.g. 정수형 배열 nums[10]과, nums을 가르키는 포인터변수인 ptr1 이 존재할 때, ptr1+1은 nums[0]의 메모리 주소 +4 가 된다. 이는 정수형 배열에서는 한 요소가 4바이트이므로 ptr1+1은 ptr1에서 4바이트 만큼 떨어진 다음 주소를 의미하기 때문이다! (만약 문자형 배열 이었다면 1바이트만 떨어진 다음 주소를 의미했을 것이다)
- 이렇게 정수를 더하거나 뺀 포인터 값을 다른 포인터에 대입하는 것도 가능하다!
포인터에 여러가지 자료형이 있는 이유
위에서 'p가 i를 가르키고 있는 한, *p는 i의 별명(alias)' 이라고 언급한 적이 있다. *연산자를 사용하면 포인터변수를 통해 변수 i의 값에 접근(참조)할 수 있는 것인데 결국 이게 포인터에 여러가지 자료형이 있는 이유이다.
모든 포인터는 자료형에 관계없이 4바이트의 공간을 차지하지만, 우리가 포인터를 통해 값을 참조하는(간접 접근) 변수들은 다양한 메모리 크기를 갖고 있다. 포인터를 통해 변수의 메모리를 참조하기 위해 포인터 자체에도 자료형이 존재하는 것이다!
Reference
1. C Programming : A Modern Approach, 2/E K. N. King | W. W. Norton & Company
'C' 카테고리의 다른 글
[C] 문자열 리터럴 (문자열 상수) | 문자열 리터럴의 저장, 포인터, 문자열 변수와 문자열 상수 (1) | 2022.10.23 |
---|---|
[C] 배열과 포인터 | 포인터와 배열의 관계, 배열 이름을 포인터처럼 사용하기, 포인터를 배열의 이름으로 사용하기 (0) | 2022.10.22 |
[C] 반복문 | while문, do-while문, for 문, break, continue (0) | 2022.10.21 |
[C] 조건문 | if문 & switch문 (0) | 2022.10.21 |
[C] 연산자 - 연산자 종류, 연산 규칙, 형 변환 (0) | 2022.10.21 |
댓글