썸네일 사진은 한 스택오버플로우 게시물에서 찾았는데, 읽어보면 좋을 법한 내용이다.
📢 42서울 7기 라피신을 대비하여 모두의 코드 '씹어먹는 C 언어' 강좌의 내용을 정리한 게시물로, 이전 블로그로부터 옮겨 온 글입니다. (원문)
앞에서 우리는 포인터가 메모리 상에서 특정 데이터가 위치해있는 주소를 담고 있다는 사실을 알아보았다.
컴퓨터는 데이터를 메모리에 저장할 때 바이트 단위로 나눠서 저장하는데, 각 자료형은 char
를 제외하고 대부분 2바이트 이상이다. int
자료형이 4바이트인 것처럼 말이다.
이처럼 데이터란 메모리에 연속된 바이트 형태로 저장될텐데, 포인터는 어느 바이트를 가리키고 있는 것이며, 어디까지 한 개의 데이터로 인식할 수 있는 것일까?
여기서 포인터의 타입과 관련된 문제가 발생한다. 각 자료형이 차지할 수 있는 바이트의 크기를 명시하여 포인터에도 동일한 타입을 부여해주지 않으면, 메모리에서 얼마만큼을 읽어들여야 할 지 알 수 없게 된다.
왜냐하면 포인터에게 전달되는 주소값은 해당 변수(데이터)가 메모리에서 차지하는 모든 주소들의 위치가 들어있는 것이 아니라 시작 주소만 들어가 있기 때문이다.
포인터도 변수 라는 사실을 잘 기억하자. 즉, 포인터에 들어간 주소값은 바뀔 수 있다. 하나의 포인터가 항상 같은 위치를 가리킬 필요는 없다는 뜻이다.
/* 포인터도 변수다 */
#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;
}
바로 위의 내용과는 반대로, 한 번 가리킨 주소값을 절대 바꾸지 않는 포인터 즉 상수 포인터를 만들 수도 있다.
상수 포인터 이전에, 상수 즉 const
그 자체는 프로그래밍에서 아주 중요한 역할을 한다는 것부터 이해하면 좋을 것이다.
이는 프로그래밍 상에서 프로그래머들의 실수를 줄여주고, 실수가 발생했을 시 이를 잡아내는 데 아주 중요한 역할을 하게 된다. 왜냐하면 상수는 최초의 선언(초기화) 이후 그 어떤 경우에도 대입 연산자(=
) 또는 포인터에 의한 값 변경 (ex. *{포인터}
) 이 불가능하기 때문이다.
const int a = 3;
a = 3;
심지어는 이와 같은 대입조차 불가능하다. 그래서 중요한 값이며 절대로 바뀌지 않을 것 같은 값에는 무조건 const
키워드를 붙여주는 습관이 중요하다.
/* 상수 포인터? */
#include <stdio.h>
int main() {
int a;
int b;
const int *pa = &a;
*pa = 3; // 올바르지 않은 문장
pa = &b; // 올바른 문장
return 0;
}
위 코드에서 포인터 pa
의 선언에 const
키워드를 더해 줌으로써 상수 포인터를 만든 것처럼 보인다.
그런데, 이 const
키워드는 아래와 같이 경우를 나누어 위치할 수 있다:
const int *pa = &a; // const int 형 변수를 가리키는 포인터 pa
int const *pa = &a; // int 형 변수를 가리키는 const 포인터 pa
먼저 첫 번째 const int* pa = &a;
의 경우, 우리가 여기서 말하고 싶은 상수 포인터는 아니다. 상수를 가리키는 포인터가 맞다. 즉, 포인터에 담기는 주소값이 변하면 안되는 것이 아니라, 포인터가 가리키는 변수가 값이 바뀔 수 없는 상수값인 경우다.
/* 상수 포인터? */
#include <stdio.h>
int main() {
int a;
int b;
const int *pa = &a;
*pa = 3; // 올바르지 않은 문장
pa = &b; // 올바른 문장
return 0;
}
여기서 중요한 것은 위의 이 코드에서 int a
는 초기화 시 상수가 아니었다는 점이다. 변수 a
의 자료형은 그저 int
였지만 포인터 pa
를 통해서 간접적으로 가리킬 때에는 컴퓨터 입장에서 'const
인 변수를 가리키고 있구나' 라고 생각하게 되어 값을 바꿀 수 없게 된다.
그래서 *pa = 3
과 같이 pa
가 가리키는 변수(a
)의 값은 바꿀 수 없지만, pa = &b
와 같이 pa
에 담기는 값(주소값) 자체는 바꿀 수 있다.
/* 상수 포인터? */
#include <stdio.h>
int main() {
int a;
int b;
int const *pa = &a;
*pa = 3; // 올바른 문장
pa = &b; // 올바르지 않은 문장
return 0;
}
그럼 동일한 원리로, 이처럼 int const *pa = &a;
즉 const
가 나중에 적힌 포인터의 경우 const *
가 핵심이 되어 pa
에 담기는 값 자체가 변경될 수 없다는 의미임을 알 수 있다.
그래서 pa = &b;
처럼 pa
가 가리키는 대상을 변경할 수는 없지만, *pa = 3;
와 같이 pa
가 가리키는 대상의 '값'은 변경할 수 있게 된다.
#include <stdio.h>
int main() {
int a;
int* pa = &a;
printf("포인터 pa의 값: %p\n", pa);
printf("(포인터 pa) + 1의 값: %p\n", pa + 1);
return 0;
}
위 코드를 컴파일해보면, 16진수이지만 어쨌든 pa
와 pa + 1
의 값은 4만큼 차이난다는 것을 확인할 수 있다.
1만큼 차이날 것이라고 예상했다면, 포인터가 가지는 값인 주소는 그냥 숫자의 나열이 아니라 해당 주소에 들어가 있는 데이터를 위한 값이라는 점을 상기하자.
int
자료형은 4바이트의 메모리 공간을 필요로 하므로, int형 변수 a
의 주소를 가리키는 포인터 pa
는 4바이트 중 첫 번째 바이트의 주소를 담고 있다.
예를 들어 이를 0x12345678
이라고 하자. 이 때 변수 a
m는 이 주소부터 시작해서 총 4바이트만큼, 즉 0x12345678
~ 0x1234567B
까지의 메모리 공간을 차지하고 있는 것이다. 그러므로 pa + 1
은 a
에게 할당된 4바이트만큼의 공간 다음으로 연속된 주소, 즉 원래 주소값 + 4 만큼의 주소값을 반환한다.
a
가 char
였다면 1바이트 뒤, double
이었다면 8바이트 뒤의 주소값을 반환했을 것이다.
다만, 포인터 + 상수
가 아닌 포인터 간의 덧셈은 C에서 허용되지 않는다. 사실, 포인터끼리의 덧셈은 아무런 의미가 없을 뿐더러 필요 하지도 않다. 두 변수의 메모리 주소를 더해서 나오는 값은 이전에 포인터들이 가리키던 두 개의 변수와 아무런 관련이 없는 메모리 속의 임의의 지점일 뿐이다.
그런데, 포인터끼리의 뺄셈은 가능하다. 왜 그런지는 다음 글에서 자세히 다루고자 한다.