포인터란 무엇일까?
포인터의 구조와 원리에 대한 분석과 설명
1. CPU와 메모리
컴퓨터에서 프로그램이 동작 할때,
CPU는 HDD에 있는 프로그램을 RAM이라 불리우는 주기억장치로 가져와서 그 프로그램이 종료되거나 다른 프로그램을 실행하기 전까지 계속 주기억장치에 상주 시켜놓는다.
(물론 OS의 필수 구성 요소들은 항시 상주다.)
그리고 이 CPU를 구성하고 있는 레지스터들과 RAM은 그 구조및 공법상의 이유로 레지스터의 속도가 훨씬 더 빠른데
이러한 속도 차이로 인한 전체적인 데이터 처리속도의 하향을 막기위해
CPU와 RAM사이에 캐쉬라 불리우는 메모리가 들어가 있다.
즉 HDD에 있는 프로그램을 RAM으로 가져오면,
이 RAM상에 올라온 데이터들 중에 자주 엑세스 되는 것들을 다시 캐쉬 메모리가 읽어 오게 되고,
CPU는 RAM까지 자료를 가지러 가지 않고 그 중간의 캐쉬 까지만 가서 RAM으로부터 읽혀온 데이터를 읽어 오게 되는 것이다,
엑세스 속도에 의한 CPU IDLE TIME이 줄어들게 되어 전체적인 시스템의 데이터 처리 속도가 DOWN 되지 않는 것이다.
2. 메모리와 주소
CPU가 데이터를 요구할때 이 캐쉬 메모리에 자료가 있어서 즉각 그 데이터에 엑세스하게 되는 비율을 적중률이라고 하는데
데이터가 요구된 그 순간에 그 요구된 데이터가 캐쉬메모리에 존재하고 있을 확률이라고 생각하면 편할 것이다.
그렇다는 것은 적중률이 높을수록 캐쉬메모리에 반드시 그 자료가 있을 확률이 높다는 것임과 동시에
그만큼 CPU가 RAM까지 가서 데이터를 가져올 일이 없을테니 자료처리 속도가 빠르다는 이야기가 될 것이다.
반대의 경우로,
CPU가 데이터를 요구했을때, 이 요구된 데이터가 캐쉬메모리에 없어서 다시 RAM으로 가서 그 요구된 데이터를 찾아, 읽어 오는 경우가 많을 때는 적중률이 낮다라고 표현한다.
위 처럼, 데이터를 읽어오고 하는 등의 자료처리를 하기 위해서는 이 데이터의 메모리상에서 정확하게 어디에 있는지 알아야만 할 것이다.
무수히 많은 자료가 위치하는 메모리 상에서 그 위치를 정확하게 구분하고, 원하는 부분에 접근하여 그 원하는 부분만을 가져오기 위해서는 이를 구분해 줄 수 있는 어떠한 기준이 필요할 것이다.
그 기준은 기호가 될 수도 있을 것이고, 숫자가 될 수도 있을것이다.
다행히 CPU는 전기적인 신호로 인해 0과 1만을 구분하여 동작하는데
이를 사용하여 각각의 위치를 구분해 준다면 더할나위 없이 편하고 좋을 것이다.
그래서 등장한 것이 이 주소(ADDRESS)라는 개념이다.
내부적으로, “0” 과 “1”로 구성되는 2진수로 구분되며 편의상 사람들은 16진수로 표현한다.
3. 변수와 메모리 할당
int A = 10;
이라는 간단한 코드를 보자. 이것은 정수형 변수 A를 선언하고, 이 정수형 변수 A에 정수값 10을 할당하겠다는 코드이다.
int는 정수형 데이터 타입으로 4바이트의 크기를 갖는다. (컴퓨터 마다 다를 수도 있다.)
프로세서가 이러한 코드를 만나게 되면
프로세서는 메모리에 A라는 이름의 정수형 데이터 타입 크기인 4바이트 공간을 할당하고 거기에 정수값 10을 저장한다.
이 부분은 다른 프로세서들이 할당 받지 못하게 되어 안전하게 사용할 수 있게 된다.
그리고 이 4바이트의 각 바이트는, 위에서 말한 번지로 구분되어 지는데 이것이 주소이다.
위의
int A= 10;
을 그림으로 표현하면 다음과 같다.
프로세서가 알아서 메모리상 어딘가에 4바이트 공간을 만들고 그 안에 정수형 데이터 10을 할당해 놓은 것이다.
4. 포인터의 기초
포인터명
“포인터란 특정 메모리 영역을 가리키기 위해 선언하는 특별한 변수”라고 할 수 있다.
가리킨다는 것은 특정한 변수의 주소를 가지고, 언제이든 이 포인터를 가지고 대상 변수에 접근할 수 있다는 말이다.
포인터는 기본적으로
데이터타입 변수명;
일반 변수 선언 방식을 따르고 있다.
포인터타입 포인터명;
의 형식으로 선언할 수 있다는 것이다.
포인터명 앞에는 이것이 포인터 변수인지, 일반 변수인지를 컴파일러가 쉽게 구분할 수 있게 해주기 위해서 * 연산자를 붙여준다.
포인터타입 *포인터명;
이라고 선언해주면 되는 것이다.
포인터 타입
포인터 타입은 포인터의 데이터 형식이며, 가리킬 대상의 데이터 형식과 맞추면 된다.
하지만 포인터에 가리킬 대상을 알려주지 않으면 이 포인터 변수 안에는 쓰레기 값이 가득차서 쓸모가 없게 된다.
포인터 변수도 변수이기 때문에 이를 올바르게 사용하기 위해서는
변수에 값을 할당해 주듯이 아래와 같이 포인터 변수에도 가리킬대상이라는 값을 할당해 주어야 한다.
포인터타입 *포인터명 = 가리킬대상;
이 포인터타입은 가리킬 대상의 데이터의 형식에 따라 그 데이터 형식이 바뀌는데
정수형 변수를 가리키려면
int *포인터명 = 가리킬대상;
문자형 데이터를 가리키려면
char *포인터명 = 가리킬대상;
과 같이 선언하면 되는 것이다.
이 포인터타입은 후에 나올 배열 등을 엑세스 할때 중요하게 사용 되어진다.
포인터 변수가 차지하는 공간
int *포인터명, char *포인터명 으로 선언했다고 해서
일반 변수처럼 각각 int 형인 4바이트와 char형인 1바이트로 메모리상에 할당되어지지는 않는다.
포인터 변수는 대상체와 포인터 타입에 상관없이 무조건 4바이트의 공간에 포인터 변수가 할당되어 지고,
이 4바이트의 공간안에는 가리키고있는 대상의 주소값이 할당되어져 있다.
그리고 가리킬대상을 포인터 변수에 가르쳐 주지 않고 단지 선언만 해서는 임의의 쓰레기 값이 이 포인터 변수내에 들어가게 된다.
이를 그대로 사용하게 되면 심각한 문제를 초래할 수도 있다.
가령 시스템 메모리를 건드리는 등의 말이다.
앞서 말했듯이 포인터형 변수도 일단은 변수이기 때문에 프로세서로 부터 메모리상에 4바이트의 공간을 임의로 할당 받으며
가리키고 있는 대상의 주소값을 가지고 있다.
가리킬 대상은 주소 값이라야만 한다.
그럼 위에서 언급되었던, 특정 메모리를 가리키는 변수이긴 한데 “특별한 변수”라니?
이건 또 무슨 소리일까?
그렇다. 여기서 말하는 특별한 변수라는 것은,
말 그대로 특별한 변수라는 의미로,
일반 변수가 그 데이터 값을 할당 받을 수 있는 것과는 달리, 오로지 주소 값만을 가질 수 있고 일반 데이터 값은 가질 수 없다는 말이다.
포인터타입 *포인터명 = 10;
이라는 형식은 용납되지 않는다는 것이다.
만일 위의 형식을 따라 선언할 경우 컴파일러는 Warning을 낸다.
무리 없이 컴파일 하기 위해서는 아래와 같은 형식을 따르면 된다.
포인터타입 *포인터명 = 주소값;
그런데 잠깐, 이 주소 값이라는 것은 어떻게 얻어내야 하는 것인가??
분명 우리는
int A = 10;
으로 정수형 변수 A를 만들어서 이 변수에다가 10이라는 값을 할당 하였다.
어딜 봐도 주소 값을 찾아 볼 수 없는 것이다.
잠시 위의 그림을 살펴보면 4바이트 공간이 메모리상의 임의의 위치에 마련되어져있는데
이 4바이트 공간의 이름을 우리는 A라고 선언했었다는 사실을 기억할 수 있을 것이다.
임의의 위치에 마련되어진다는 것은 프로그램이 매번 처음부터 실행되어질 때마다
프로세서가 정수형 변수 A를 그 위치를 바꿔가며 메모리 어딘가에 할당한다는 의미로써
바꾸어 말하면, 정확한 위치가 어디인지는 프로세서만 알지, 프로그래머는 알 수 없다는 것이다.
그럼 위에서 던진 질문에 대한 답이 없다는 소리인가?
정녕 평생토록 그 위치를 알 수 없다는 소리인가?
다행히 그렇지는 않다.
&연산자
우리는 “&” 연산자를 이용하여 해당 변수가 어디에 위치하고 있는지
할당되어져 있는 메모리상에서의 위치, 즉 그 변수의 주소 값을 추적할 수 있다.
변수 A를 보면
0x6038d10 ~ 0x6038d13 으로 총 4바이트의 공간이 변수 A라는 이름으로 할당되어져 있는데,
이 변수의 시작주소 0x6038d10가 변수A를 대표하는 주소 값이니
이 값만 알아내면, 우리는 변수A에 접근할 수 있을 것이다.
실제로 &연산자를 사용하면 변수A의 시작주소 값인 0x6038d10가 리턴되지,
0x6038d10 ~ 0x6038d13 주소 전부가 리턴되는 것은 아니다. 첫 주소 값만을 얻어와 각 바이트당 담겨져 있는 데이터값에 대한 엑세스는 프로세서가 알아서 해준다.
“&” 연산자의 사용방법은 간단하다.
주소를 알아내고자 하는, 주소를 얻고자 하는 변수의 이름 앞에 써주기만 하면 된다.
가령 정수형 변수 A의 주소 값을 얻으려면
&A;
라고 작성하면 되는 것이다.
즉 주소값은 &가리킬대상과 같다는 소리이니
포인터타입 *포인터명 = &가리킬대상;
이라고 할 수 있을 것이다.
포인터 변수와 대상체
정수값 10을 가지고 있는 정수형 변수 A와 이를 가리키는 포인터 변수 ptr을 선언해보자.
1
2
3
int A = 10;
int *ptr;
ptr = &A;
또는
1
2
int A = 10;
int *ptr = &A;
이를 그림으로 확인해 보면 다음과 같다.
이제 포인터 변수 ptr은 정수형 변수 A의 주소값을 가지고 있으므로
ptr을 “정수형 변수 A에 대한 포인터”
혹은
“포인터 변수 ptr이 변수 A을 가리키고 있다”
“A는 포인터 ptr의 대상체이다.”
라고 말한다.
“*” 역참조자
1
2
3
int A = 10;
int *ptr = &A;
printf("%#010x",ptr);
라고 작성하고 컴파일, 실행하면 ptr이 가리키고 있는 주소값이 출력된다.
그럼, ptr이 가리키고 있는 변수의 주소값 말고 데이터 값을 가져올 수도 있을까?
* 연산자를 사용해보자.
1
2
3
int A = 10;
int *ptr = &A;
printf("%d",*ptr);
정수형 변수 A가 가지고 있는 데이터 값인 숫자 “10”이 출력되는 것을 볼 수 있을 것이다.
이렇게 *연산자를 이용하면 해당 데이터의 값을 출력할 수도 있다.
이것을 일반적으로 편의상 “역참조자”로 지칭하고 있다.
이 역참조 연산자를 사용하면
포인터가 가리키고 있는 대상체의 값을 마음대로 주무를 수 있는데
이것이야 말로 포인터의 궁극적인 활용의 정점이라고 본인은 생각한다.
역참조자를 이용해서 변수A의 값을 바꿔보자.
1
2
3
4
5
6
int A = 10;
int *ptr = &A;
printf("1번 : %d \n",A);
*ptr = 5;
printf("2번 : %d \n",A);
1번 : 10
2번 : 5
라는 결과가 출력된다.
소스코드의
*ptr = 5;
라는 부분을 설명하자면
ptr이 가리키고 있는 주소의 변수”값”에 정수 “5”를 할당하겠다는 뜻이다.
A = 5;
와 같은 뜻을 가진 코드라는 소리다.
혹시나 하여 &연산자를 ptr에 붙였을 경우
&ptr 했을 경우는
ptr 변수의 주소가 나오므로 유의하자.
마지막으로 정리하며 포스팅을 마친다.
1
2
3
4
5
6
7
*ptr == A;
ptr == &A;
&ptr != &A;
sizeof(ptr) == 4; //byte of count
sizeof(A) == 4; //byte of count
sizeof(int) == 4; //byte of count
sizeof(double) == 8; //byte of count