티스토리 뷰
2023.03.02. 내용 수정
안녕하세요? 이화입니다. 오늘은 포인터에 대한 내용을 써 볼 거에요. C언어를 처음 배우시는 분 중에는 포인터가 정말 어렵고, 큰 벽이라고 생각하시는 분도 있습니다. 하지만 포인터는 쉽습니다. 거짓말 아니고 인터넷에서 겁 주는 것보다는 정말로 간단한 개념입니다. 그렇지만 포인터를 배우면서 자칫 오개념을 얻거나 하는 경우가 많기에, 포인터에 대한 설명은 최대한 단순화되거나, 너무 복잡해지는 경우가 많다고 생각합니다.
이 글을 쓰며 저는 최대한 오개념을 배제하고, 컴퓨터 개론에 관한 내용을 첨가하면서 쉽지만 깊이있는 글을 쓰려고 노력해 보았습니다. 포인터는 응용이 어렵지 이론은 큰 어려움이 없다고 생각합니다. 이 글이 포인터를 통해 심화된 무언가를 할 때 이론적 기반을 충분히 갖출 수 있도록 도와드릴 수 있으면 좋겠습니다.
이 글은 다음에 해당하는 분들을 위한 글입니다.
- C언어에 대한 기초적인 지식을 알고 있습니다.
- 포인터에 대한 내용을 거의 모릅니다.
그리고 이 글은 다음 내용을 담고 있습니다.
- C언어에서 변수란 무엇인가
- C언어에서 사용되는 포인터 개념
- 함수의 호출과 포인터
배열과 포인터에는 무슨 관계가 있는가, 포인터 연산다음 글에 쓸게요.
자. 시작해 볼게요.
0. 포인터(Pointer)
포인터라는 이름의 사냥개 품종도 있습니다. 사냥꾼을 위해 사냥감의 위치를 발로 가리킨다고 하네요.
물론, 우리가 배우고자 하는 포인터는 동음이의어입니다.
1. 변수란 무엇인가.
자. 포인터에 대해 배우러 오신 분들은 왜 갑자기 변수를 설명할까 궁금할 수 있습니다. 하지만 변수에 대한 이해가 가벼우면 포인터 개념을 알기 어려워요. 많은 사람들이 변수를 단지 값을 저장할 수 있는 공간이라고 알고 있습니다. 포인터에 대해 정확히 알려면, 컴퓨터가 변수를 어떻게 다루는지부터 알아볼 필요가 있습니다.
아래 코드는 int형의 변수를 하나 선언합니다.
int i;
이 코드는 정확히 어떻게 동작할까요? 어떤 과정을 거쳐 값을 저장할 수 있는 공간이 생기는 걸까요?
우선, 아주 기본적이고 어쩌면 무시할 수 있는 부분부터 생각해봅시다. 컴퓨터가 처리에 사용되는 데이터를 저장하는 곳은 바로 주기억장치(Computer memory)입니다. 우리가 사용하는 컴퓨터는 주기억장치에 여러 값들을 저장하는데, 그 역할을 하는 부품으로 RAM(Random Access Memory)이 사용됩니다. 영어 뜻을 그대로 풀어 보면 "임의 접근 기억 장치"입니다. 메모리의 아무 데나 임의로 접근할 수 있고, 어디에 접근하든 동일한 시간이 소요된다는 뜻입니다. 이 메모리에서 우리가 원하는 위치에 접근하기 위해서는 어떻게 해야 할까요? 이를 위해 RAM의 각 부분은 주소를 가지고 있습니다. 우리가 건물을 찾기 위해 주소를 보고 찾아가듯이, 컴퓨터는 주소를 가지고 저장된 데이터를 찾아갑니다.
우리가 짠 프로그램이 변수를 만들고 사용한다는 것은, 정확히 말해서 다음과 같습니다.
- 메모리에 변수의 종류에 따라 필요한 크기만큼 공간을 냅니다.
- 변수의 이름에, 그 공간의 주소를 연결해 둡니다.
- 그 이름을 사용할 때, 주소를 참조하여 메모리에서 데이터를 읽고 씁니다.
변수가 가진 주소는 역참조 연산자( & )를 통해서 알 수 있습니다.
printf("%p", &i); //printf에 대해 모르신다면, 그냥 수를 출력한다는 것만 알고 있어 주세요.
궁금하면 한번 실행시켜 보세요. 0x로 시작하는 16진수 숫자가 하나 출력되는 걸 확인할 수 있습니다. 이게 메모리에서 변수가 위치하는 곳을 나타내는 수, 주소입니다. 프로그램은 실행될 때마다 다른 메모리 위치에서 실행되기 때문에, 이 수는 실행될 때마다 매번 변하게 됩니다.
2. 포인터란 무엇인가.
우린 프로그램이 변수를 읽어오기 위해 주소를 사용한다는 것을 알았습니다. 하지만 우리가 이 주소를 가지고 다른 변수를 찾아가기 위해서는 이 주소를 어디 저장해 놓을 필요가 있을 겁니다. 우리는 이걸 위해 포인터, 포인터 변수를 사용합니다. 포인터 변수(짧게 포인터라고도 합니다)는 다른 변수의 주소를 저장하는 변수입니다. 그리고 포인터 변수는 다음과 같이 선언할 수 있습니다.
int *ptr;
변수 이름으로 사용한 ptr은 pointer의 약자입니다. 코드를 보시면 변수 이름에 *이 붙은 걸 알 수 있습니다. 이 *은 이 변수가 자신 자료형의 값이 아닌, 자신 자료형의 값을 가리키는 주소를 저장한다는 뜻입니다. 이런 변수를 보통 포인터 변수라고 부릅니다.
이 포인터 변수는 보통 숫자 형태의 값을 가지지만, 아무 값이나 넣을 순 없습니다. 위에서 이야기했듯 프로그램은 실행될 때마다 다른 메모리 위치에서 실행되고, 포인터가 엉뚱한 주소를 읽어온다면 운영체제는 "이 프로그램이 허락받지 않은 곳을 건드렸다!" 하면서 프로그램을 꺼 버립니다. 그래서 우리는 다른 변수의 주소를 찾아서 포인터 변수에 넣어줘야 합니다.
그럼 포인터 변수에 다른 변수의 주소를 저장하려면 어떻게 해야 할까요? 우리는 아까 변수의 주소값을 알아내는 연산자를 배웠습니다. 그렇게 얻어낸 주소를 다음과 같이 단순히 대입하면 됩니다.
int i;
int *ptr;
ptr = &i;
포인터 변수 이외의 변수에 메모리 주소를 저장하지 마세요. 작동하는 것 처럼 보일 때도 있지만 대부분 문제를 일으킵니다.
포인터에 저장된 주소가 가리키는 메모리에 어떻게 접근해야 할까요? 참조 연산자( * )를 사용하면 포인터가 가리키는 변수의 메모리를 읽고, 수정할 수 있습니다.
*ptr += 1;
printf("%d", *ptr);
위 코드를 정상적으로 실행했다면, 프로그램은 ptr이 가리키는 변수에 1을 더하고, ptr이 가리키는 변수의 값을 출력합니다.
포인터 변수도 물론 변수이기 때문에, 포인터를 가리키는 포인터를 만들 수 있습니다. 이 경우 선언은 다음과 같습니다.
int **dptr;
위 코드에서, 포인터 변수 dptr은 int형 포인터를 가리키는 포인터입니다. 간단히 int형 이중 포인터라고도 합니다.
이 경우를 좀 더 자세히 알아봅시다.
dptr = &ptr;
*dptr = &i;
**dptr += 1;
printf("%d", **dptr);
첫 번째 줄에서 dptr에 ptr의 주소를 대입합니다. 두 번째 줄에서는 dptr이 가리키고 있는 변수(ptr)에 i의 주소를 대입합니다. 세 번째 줄에서는 dptr이 가리키고 있는 변수(ptr)가 가리키고 있는 변수(i)의 값을 1 늘립니다. 네 번째 줄 역시 그 변수를 출력합니다. 이중 포인터는 사실 특별할 게 없습니다. *을 여러 번 써서 가리키고 있는 변수가 가리키고 있는 변수가... 이렇게 참조를 반복할 수 있다는 것만 알고 계시면 됩니다. 포인터의 포인터의 포인터의 포인터... 원하는 만큼 참조를 반복할 수 있습니다.
03. 함수의 호출과 포인터
자, 여기까지 충분히 이해하면서 오셨으면 포인터의 기초적인 개념은 다 마무리하신 겁니다. 포인터의 첫 고비를 넘기셨습니다!
이제 마지막으로 함수의 호출과 포인터에 대해 간단히 다뤄보도록 하겠습니다. C언어에서는 많은 곳에 포인터를 쓰지만, 제일 먼저 만나는 건 보통 함수의 호출과 관련된 부분일 것입니다. scanf에 대해 아신다면 더욱이요.
void f(int i)
{
i ++;
}
c설명을 위해 약간 멍청해보이는 함수 친구를 만들었습니다. 값을 받아서 거기 1을 더하네요. 하지만 많은 분들이 이 함수는 아무 것도 하지 않는다는 걸 아실 겁니다. 함수를 호출할 때 전해지는 값은 복사되어서 전해지기 때문에, 복사된 값을 아무리 바꿔도 원본 변수를 바꿀 순 없거든요. 이런 호출 방식을 값에 의한 호출(Call by value)이라고 합니다. 제일 기본적인 방법이죠. 하지만 포인터를 이용하면, 이제 함수에 전해진 변수를 수정할 수 있습니다. 이런 호출 방식은 참조에 의한 호출(Call by reference)이라고 합니다. 1
그럼 이런 함수는 어떻게 만들까요? 아래 함수를 봅시다.
void f(int *i)
{
*i ++;
}
바로 이렇게, 포인터를 통해 변수의 주소를 받고, 참조 연산자를 통해 그 포인터가 가리키는 값을 수정할 수 있습니다. 이 함수를 호출할 때, 변수 자체가 아닌 변수의 주소를 전달해 줘야 함을 기억하세요.
f(&i);
변수의 주소를 역참조 연산자를 통해 전달해 주었습니다. 이 함수가 실행되면 i는 1이 더해지겠네요. 참조에 의한 호출에 대한 예시를 조금 더 들고 싶지만, 여러분이라면 충분히 응용할 수 있다고 믿습니다.
이렇게 포인터에 대해 기초적인 부분을 알아봤습니다. 배열과 포인터 연산에 대한 내용은 다음 글에서 다루도록 하겠습니다.
다음 내용이 급하시다면 팁을 하나 드릴까요? 배열은 사실 포인터로 취급할 수 있는 값입니다. 배열의 첫 번째 요소의 주소와 두 번째 요소의 메모리 값을 출력해 보세요. 그리고 배열을 인덱스 연산자( [ ] )없이 출력시켜 보세요. 그리고, int형 배열을 역참조 연산자 없이 int형 포인터에 대입하고 배열 쓰듯이 써 보세요. 정확히 어떻게 되는지는 다음 글에 알려드리겠습니다.
미진한 글이지만 도움이 되셨다면 좋겠습니다. 궁금하신 내용은 덧글 또는 트위터 계정 으로 연락주시면 재빠른 대답 드리겠습니다. 읽어주셔서 감사합니다.
-1. 덤
귀여운 개발자 잇창명님이 작성한 C의 타입 시스템에 대한 글입니다. https://eatch.dev/s/ctype
포인터뿐만 아니라 다양한 타입들에 대해 다룹니다. 포인터에 충분히 익숙한 분이거나 호기심이 이는 분들이라면 읽어보시면 좋겠습니다.
-
사실, 엄밀히 말해서 이런 방식의 호출은 참조에 의한 호출이 아니라 포인터를 매개로 참조에 의한 호출을 흉내내는 방식이라고 할 수 있습니다. c++에서는 참조자 &를 이용해 제대로 된 참조에 의한 호출을 사용할 수 있습니다. 이 글에서는 자세한 방법은 다루지 않겠습니다.
🍮 "`void f(int *i) { *i++; }`"
👤 "와 이건 원리가 뭔가요?"
🍮 "사실 C언어에는 참조로 호출(call by reference)이라는 개념이 없고 전부 값으로 호출(call by value)되는데 포인터는 같은 주소면 같은 메모리 공간을 가리키고 포인터도 값으로 호출돼서 같은 주소가 복사되니까 `*` 연산자로 참조처럼 쓸 수 있"
👤 "뭐라는거야"
🍮 "참조로 호출했어요"