khstar
C-포인터와 변수 확실히 알자.(1) 본문
서론
C Language를 공부하면서 가장 이해하기 어려운 것이 무어냐고 물어본다면,
아마도 거의 대부분 "바로 포인터(Pointer)라는 녀석이다!" 라고 말할 것입니다.
C에서 다른 건 다 이해가 되어도 "포인터 만큼은 죽어도 모르겠다" 라고 말하는 경우를 제 주변에서도 심심치 않게 봐왔습니다.
사실 C는 어려울 수밖에 없습니다.
C를 그저 프로그래밍의 기초 과정에서 배우는 옛날 언어라고 생각하고 가볍게 볼만큼 만만한 녀석이 전혀 아닙니다.
C는 어셈블리(Assembly)의 사촌입니다.
다시 말하면, C는 하드웨어 - 특히 마이크로프로세서(CPU)와 친한 녀석입니다.
때문에 하드웨어에 대한 기반 지식없이는 C를 제대로 이해할 수가 없습니다.
포인터라는 녀석이 특히 그렇습니다.
C를 강력하게 만드는 무기가 바로 포인터라고 흔히 말하지만,
이걸 이해하는 것은 왜 그리 어려운 걸까요.
조엘('조엘 온 소프트웨어'의 저자)은 이 '포인터'의 개념을 이해하는 게 '그냥 불가능'한 인간이 있다고 말합니다만, 저는 절대로 이해 불가능한 것이 아니라고 굳게 믿습니다.
제가 여기서 그것을 감히 증명해 보려고 합니다.
일단 기초의 기초부터 들어갑니다.
아주 유치할 정도로 기초부터 다시 설명하겠습니다.
1. 메모리(Memory)와 어드레스(Address)
일단 포인터의 동작은 전부 메모리(Memory)와 관련이 있습니다.
따라서 우리는 먼저 메모리에 대해서 알아야 합니다.
폰 노이만 구조(프로그램 내장 방식)를 따르는 현대의 모든 컴퓨터들은 기본적으로 모든 것이 주기억장치 - '메모리'에 올라가야 CPU가 실행을 시킬 수 있습니다.
프로그램 수행 코드도 메모리에 올라가고, 그에 필요한 데이터 역시 메모리에 올라갑니다.
일단 여기서 우리는 '데이터'에 관심을 가져보기로 합니다.
'100' 이라는 숫자가 메모리에 올려졌다고 해보죠.
그러면 메모리의 어딘가에 이런식으로 데이터가 저장이 될겁니다.
─┬───┬───┬───┬─
…│100│ │ │…
─┴───┴───┴───┴─
이제 메모리에 저장된 '100'이란 숫자를 꺼내오려고(읽어오려고) 합니다.
그런데 문제는 이 100이란 숫자가 드넓은 메모리 영역의 어디에 존재하는지 알아야 한다는 겁니다.
그래서 데이터가 저장되는 메모리 공간에는 일종의 일련번호가 부여되어 있습니다.
그것을 메모리의 주소 - 어드레스(Address)라고 부릅니다.
메모리는 기본적으로 바이트(Byte = 8bit) 단위로 데이터가 저장되기 때문에,
하나의 어드레스는 메모리 상의 하나의 바이트 공간과 대응됩니다.
이 말이 이해가 안된다면, C에서의 배열(Array)을 연상하면 되겠습니다.
int A[10];
이렇게 하면 C에서는 10개의 정수(int) 데이터를 저장하는 배열이 만들어집니다.
이것은 마치 10개의 int형 변수를 하나로 묶어서 다루는 것과 같습니다.
배열의 각 공각에 데이터를 기억시키기 위해서는 이렇게 하죠.
A[0] = 100;
A[1] = 200;
A[2] = 300;
...
A[9] = 1000;
이때 [ ]안에 넣는 숫자, 이것은 곳 배열에서 어느 영역을 지칭하는 '번호'인 셈인데,
이것을 메모리의 관점으로 바꿔보면...
메모리 전체가 하나의 거대한 배열이라고 가정하면,
이 거대 배열의 각 영역에 접근하기 위한 '번호'가 필하요게 되고,
바로 이것이 주소(Address)라는 것으로 보면 됩니다.
2. 변수
우리가 C 코드를 만들때, main() 함수를 작성하면서 가장 먼저하는 일이 무엇입니까?
────────────────────────────────────────
void main()
{
int a, b;
...
}
────────────────────────────────────────
바로 변수를 선언하는 거죠.
바로 이 선언이라는 것은, 자신이 사용할 메모리 공간을 예약하는 것입니다.
─┬───┬───┬───┬─
…│ a │ b │ │…
─┴───┴───┴───┴─
그럼 예약도 되었겠다, 값을 써보도록 하죠.
a = 10;
b = 20;
─┬───┬───┬───┬─
…│ 10│ 20│ │…
─┴───┴───┴───┴─
그럼 이번엔 a, b의 값을 한번 출력해 봅니다.
printf("%d %d", a, b);
[결과]
10 20
C에서는 이처럼 변수 이름을 통해서 메모리에 값을 읽고 쓸 수가 있습니다.
이 당연한 걸 왜 새삼스레 다시 설명하냐고 태클이 들어올 것 같습니다만, 잠시 참아주세요.
앞서 메모리 상의 데이터는 '어드레스'를 통해서 접근할 수 있다고 얘기했습니다.
C에서 변수는 메모리의 한 부분입니다.
그런데 C에서는 '변수 이름'을 가지고 접근하고 있군요.
바로 이 '변수 이름'이라는 것은 메모리 어드레스를 추상화시킨 것입니다.
실제 C에서는 변수앞에 '&' 연산자를 붙여서 그 변수의 어드레스를 바로 얻을 수 있습니다.
────────────────────────────────────────
#include <stdio.h>
void main()
{
int a, b;
a = 10;
b = 20;
printf("Address: %d %d\n", &a, &b);
}
[결과]
────────────────────────────────────────
이 결과를 잘 보면, a와 b의 어드레스에는 4만큼의 차이가 있습니다.
이것은 둘다 int형(정수형)이고, 일반적으로 32bit CPU에서 정수는 4byte의 크기를 같습니다.
그리고 두 변수가 나란히 선언되었기 때문에, 이 둘은 메모리상에서도 연속해서 공간을 차지하게 됩니다.
참고로 x86 계열의 CPU는 어드레스가 높은 쪽에서 낮은쪽으로 채워집니다.
때문에 먼저 선언된 쪽이 높은 주소값을 가지게 됩니다.
3. 데이터와 타입
기본상식이지만, 컴퓨터에서는 모든 데이터는 2진수(Binary)로 표현됩니다.
이말은 즉, 그 어떤 종류의 데이터라할지라고 메모리에 저장될 때는 2진수로 기록된다고도 할 수 있습니다.
그럼 반대로, 메모리에 저장된 데이터를 읽어 올때도 역시, 이것을 "문자열"로도 "정수"로도 "실수"로도, 기타 얼마든지 다른 형식으로 읽어 들일 수 있다는 것이 됩니다.
그렇다면, 메모리에 저장된 자료를 과연 어떤 형식(Type)으로 해석해야 하는지 문제가 됩니다.
그렇기 때문에 우리는 변수를 선언할 때, 반드시 그 변수가 취급하는 데이터형(Type)을 명시하게 됩니다.
int number; // 정수형
float real_num; // 실수형
double bigsize_real; // 큰 실수형
char* string; // 문자열
또한 이 데이터 타입은, 변수의 선언에 있어서, 그 변수에게 얼마만큼의 메모리 공간을 할당해야 하는가를 결정하는 정보이기도 합니다.
보통 int와 float은 4 Byte 크기를 할당받게 되고, double은 8Byte, 문자열의 경우에는 문자열의 길이에 따라 다릅니다.
4. 포인터
자, 이제 드디어 포인터에 대해 이야기할 차례입니다.
"포인터(Pointer)란 무엇인가?"
라고 묻는 다면, 그 정의는 간단합니다.
"메모리의 어드레스(Address)를 데이터로 취급하는 변수"
즉 포인터 변수에는 어드레스 값을 저장할 수 있습니다.
그래서 포인터 변수는 다음처럼 특정 변수의 어드레스를 기억하는 것이 가능합니다.
int V;
int *P;
P = &V;
포인터 변수가 어느 변수의 주소(Address)값을 가지고 있는 상태.
이 상태를 우리는 포인터 변수 P가 일반 변수 V를 "가리키고 있다"라고 표현합니다.
앞서 메모리의 어드레스를 통해서 메모리에 있는 데이터에 접근할 수 있다고 했습니다.
변수 역시 자신이 어드레스를 가지고 있기 때문에, 우리가 변수를 통해서 데이터를 읽고 쓸수 있는 것입니다.
그렇다면, 포인터 변수가 데이터로서 가지고 있는 어드레스를 이용해서도 메모리에 접근 할 수 있지 않겠습니까?
변수 V가 가지고 있는 데이터에 접근 하려 한다고 하면, 보통은 변수 V를 직접 호출해서 바로 접근할 수 있습니다.
그런데 포인터 변수 P가 V를 가리키고 있는 상태에서는 포인터 변수 P를 통해서 간접적으로 V에 접근할 수도 있게 됩니다.
예를 들면,
V = 10;
이렇게 V에 10 이란 상수를 바로 기록할 수도 있고,
*P = 10;
이렇게 포인터 변수를 통해서 간접적으로 V에 10을 저장시킬 수도 있습니다.
여기서 포인터 변수 앞에 쓰인 '*' 연산자는 보통 "P가 가리키고 있는 변수의 값을 가져온다" 라고 말하는데, 사실 정확한 정의는 "P가 가리키고 있는 곳=장소" 그 자체입니다.
간단히 말하면 '*' 연산자가 붙은 포인터 변수 = (*P) 자체가 하나의 변수처럼 취급될 수 있다는 겁니다.
따라서 아래처럼 값을 가져오는데도 쓸 수 있고,
A = *P;
이처럼 값을 저장할 때도 쓸 수 있습니다.
*P = A;
5. 포인터와 타입
C에서 포인터 변수를 선언은 이런식으로 하게됩니다.
int *p;
자 그럼, 이 p 라는 변수의 타입(Type)은 무엇일까요? 정수(int) 일까요 ??
잠깐 다른 문제를 내보겠습니다.
정수형(int) 변수의 타입(Type)은? → 정수(int)
실수형(float) 변수의 타입은? → 실수(float)
자, 그럼 포인터 변수의 타입은?
네, 당연히 포인터(Pointer)입니다.
int *ip;
float *fp;
double *dp;
char *cp;
void *vp;
이렇게 선언된 변수들은 죄다 '포인터' 형입니다.
결국 그 정체는 그저 '어드레스'를 저장하는 변수인 겁니다.
32bit CPU에서 이 어드레스는 32bit 정수(4 Byte)로 표현됩니다.
따라서 결국은 포인터 변수라는 건 int형 변수나 별반 다를게 없습니다.
단지 그 정수를 '어드레스'라고 해석할 뿐인 겁니다.
C에서는 변수 선언시에, 이름 앞에 '*' 라는 표시를 붙여서 이것이 포인터형 변수라고 컴파일러에게 알려줍니다.
그럼 * 앞에 있는 int 같은 타입 정보는 대체 무엇인가?
앞서 메모리에 저장된 데이터를 해석하려면 타입(Type) 정보가 필요하다고 했습니다.
포인터 변수의 타입은 포인터이니까, 포인터 변수의 데이터는 어드레스로 인식하면 됩니다.
그치만, 포인터 변수가 가리키는 곳(포인터 변수에 저장된 어드레스)에 있는 데이터는 어떻게 해석해야 하는가가 문제가 됩니다.
따라서 포인터 변수를 선언할 때는 두 가지 타입 정보가 필요합니다.
정확히 말하면, 포인터 변수가 가리키는 것에 대한 타입 정보가 추가로 필요해 지는 겁니다.
int *p;
이렇게 선언된 포인터 변수는 가리키는 대상이 정수(int) 타입이라는 것을 의미합니다.
앞서 '*' 가 붙은건 다 포인터형 변수라고 말했습니다만...
int *ip;
float *fp;
그 말대로라면 여기서의 ip와 fp는 같은 타입이라는 얘기가 됩니다.
그럼 이런 것도 가능할까요.
int a = 10;
int *ip;
float *fp;
ip = &a;
fp = ip;
마지막 문장 "fp = ip"에서는 ip가 가진 값이 변수 a에 대한 어드레스이므로, fp에는 a의 어드레스 값이 기록됩니다. "fp = &a"와 같은 결과가 되죠.
결론적으로 ip와 fp에 둘 다 같은 정수형 변수 a의 어드레스를 저장시켰습니다.
뭔가 불안해 보이는 코드이지만, 사실 이렇게 해도 컴파일은 됩니다.
다만 컴파일러는 정중하게 경고(warring) 메세지를 보냅니다.
'int ' differs in levels of indirection from 'float *'
실수형 자료를 가리키는 포인터 fp에 정수형 변수의 어드레스를 할당시켰다는 거지요.
그래서 데이터를 잘못 해석해버릴 가능성이 있기 때문에 컴파일러가 경고를 보낸겁니다.
실제로 저렇게 사용하면 큰일(?)날 수 있습니다(...)
마무리
결론적으로 포인터라는 것은 메모리, 그 중에서도 '어드레스'를 다루는 테크닉입니다.
그렇기에 포인터를 알려면 메모리와 어드레스에 대한 이해가 필수인 거지요.
그래서 본격적으로 포인터에 대해 얘기하기 전에,
메모리와 연관지어서 기초적인 부분을 다시 정리해본 겁니다.
여기까진 그다지 어렵진 않을 겁니다.
다음에는 포인터를 처음 접할때 조금씩 햇갈리기 시작하는 배열과 포인터의 관계에 대해서 알아보겠습니다.
[출처] C 포인터, 확실히 알자(1) - 변수와 포인터|작성자 시즈하