간접 주소 지정 방식은 값을 저장할 '주소'를 메모리에 저장하는 것이다.
그러면 4바이트 정수형으로 addr 변수를 선언하고 addr 변수에 0x0000006C 값을 대입해보자.
int addr = 0x0000006C ;
이렇게 선언한 addr 변수에 주소를 저장할 수는 있다.
하지만 일반 변수라서 실제로 해당하는 주소의 메모리에 가서 값을 읽거나 저장할 수 있는 기능은 없다.
이것은 C언어의 일반 변수가 자신이 위치한 메모리에서만 값을 읽거나 쓸 수 있는 직접 주소 지정 방식으로 동작하기 때문이다.
그래서 C언어는 간접 주소 지정 방식으로 동작하는 특별한 변수를 선언하기 위해 포인터(Pointer) 문법을 추가로 제공한다.
포인터 문법을 사용해 선언한 포인터 변수는 메모리 주소만을 저장하기 위해 탄생한 특별한 변수이다.
포인터 변수는 일반 변수와 다르게 *기호를 추가로 사용하여 선언한다.
일반 변수는 자료형이 변수의 크기를 의미하지만, 포인터 변수는 자료형을 선언하지 않아도 무조건 크기가 4바이트(32비트 운영체제 기준)으로 정해져 있기 때문에 포인터 변수의 크기를 적을 필요가 없다.
즉, short
는 ptr
변수에 저장된 주소에 저장될 값의 자료형을 의미한다.
다양한 장소를 옮겨 다니며 진행하는 게임을 할 때 다음 장소의 주소를 종이에 적어서 바로 전달 -> 직접 주소 지정 방식
게임을 더 흥미롭게 만들기 위해서 종이를 또 다른 장소에 있는 사물함에 넣어놓기 -> 간접 주소 지정 방식
종이에 적은 주소는 한 번 전달되면 바꿀 수 없다.
하지만 사물함에 들어있는 주소는 사물함을 열어 보기 전까진 다른 주소로 바꿀 수 있다.
결국 간접 주소 지정 방식은 중간에 사물함이라는 매개체를 하나 더 사용해서 진행에 다양성을 추구할 수 있다는 뜻이다.
컴퓨터에는 사물함이 없으니 메모리를 하나 정해서 그 메모리를 사물함처럼 이용한다.
이 메모리는 주소를 저장하기 위해 크기를 4바이트로 고정해야 한다.(32비트 운영체제 기준)
간접 주소 지정 방식을 이용한 예를 들어보자.
102번지에 4바이트 크기의 '주소'가 저장되어 있는데
이 주소에 가서 '값' 1042를 2바이트 크기로 대입하라.
사물함 역할을 하는 102번지에 값을 저장할 메모리 주소인 108이 적혀 있다.
따라서 1042 값은 108번지에 2바이트 크기로 저장된다.
결국 간접 지정 방식을 사용하면 1042값을 저장할 실제 메모리 주소를 명령에 적지 않아도 된다.
이렇게 간접 주소 지정 방식을 사용하면 1042 값이 108번지가 아니라 120번지로 변경되더라도 명령을 바꾸지 않아도 된다.
왜냐하면 102번지에 저장되어 있는 주소를 108번지에서 120번지로 바꾸면 되기 때문이다.
명령을 바꾼다는 뜻은 기계어를 바꿔야 한단 뜻이고 이것은 코드를 다시 번역해서 실행 파일을 만들어야 한다는 뜻이다.
하지만 메모리에 있는 주소를 변경하는 것은 단순한 데이터 조작이기 때문에 코드를 다시 번역할 필요가 없고 프로그램이 실행되는 중에도 변경할 수 있다.
그래서 좀 더 복잡하더라도 간접 주소 지정 방식을 사용하면 명령어를 바꿀 필요가 없기 때문에 이를 활용하는것이 좋다.
#include <stdio.h>
void main(){
short birthday; /* short형 변수 birthday를 선언 */
short *ptr; /* 포인터가 가리키는 대상의 크기가 2바이트인 포인터 변수를 선언 */
ptr = &birthday; /* birthday 변수의 주소를 ptr 변수에 대입함 */
*ptr = 0x0412; /* 번지 지정 연산자. ptr 포인터가 가리키는 대상에 가서 1042 값을 대입하겠다는 의미 */
printf("birthday = %d (0x%04X)\n", birthday, birthday)
}
'ptr = ' 형태로 사용하면 포인터 변수의 값(가리키는 대상의 주소)이 변경되고,
'*ptr = ' 형태로 사용하면 '포인터가 가리키는 대상'의 값이 변경된다.
/* A형 - 다른 함수의 변수 사용 시 오류 */
#include <stdio.h>
void Test(){
short soft = 0;
soft = tips; /* 오류 발생 */
}
void main(){
short tips = 5;
Test();
}
tips
변수는 main
함수에서 선언된 변수이므로 Test 함수에서 사용할 수 없다.
/* B형 - 매개변수로 다른 함수의 변수 값 받기 */
#include <stdio.h>
void Test(short data){ /* data = tips; */
short soft = 0;
soft = data; /* 가능, soft = 5 */
}
void main(){
short tips = 5;
Test(tips);
}
main
함수의 tips
변수값을 넘겨받았을 뿐이기 때문에 Test 함수에서 main
함수의 tips
변수값을 직접 수정할 수는 없다.
/* C형 - 매개변수로 다른 함수의 변수 주소 받기 */
#include <stdio.h>
void Test(short *ptr){ /* ptr = &tips; */
short soft = 0;
soft = *ptr; /* soft = tips; */
*ptr = 3; /* tips = 3; */
}
void main(){
short tips = 5;
Test(&tips);
}
C형은 B형과 달리 매개변수로 포인터 변수 ptr
을 선언해서 main
함수의 tips
변수 주소를 받았다.
따라서 Test
함수에서 tips
변수 이름은 사용할 수 없지만 tips
변수의 주소를 ptr
포인터가 가지고 있기 때문에 *ptr
을 사용해서 해당 주소에 저장된 값을 가져오거나 대입할 수 있다.
결국 간접 주소 지정 방식을 사용하면 Test
함수를 호출한 main
함수에 선언된 변수의 값만 받아 오는것이 아니라 해당 변수의 값을 변경할 수도 있다.
콜 바이 벨류(Call by Value)
콜바이벨류(Call by Value)는 함수가 인수로 전달받은 값을 복사하여 처리하는 방식이다.
이때, 전달된 값은 기본형, 즉 JS의 원시 타입(primitive type) 데이터인 경우에 해당하며, 변수가 가진 값을 복사하여 전달하므로 함수 내에서 값을 변경해도 원본 값은 변경되지 않는다. 따라서 값의 불변성(Immutability)을 유지하는 데에 용이하다.
ex)
직접 주소 지정 방식으로 변수 값을 교환하면,
우리의 기대와 달리 start
와 end
의 값이 변경되지 않았다.
main
함수의 지역 변수인 start
, end
변수 값은 계속 96과 5로 유지되고 있기 때문에, 포인터 문법을 사용하여 이 문제를 해결해야 한다.
콜 바이 레퍼런스(Call by Reference)
콜바이레퍼런스(Call by Reference)란 함수 호출 시 인수로 전달되는 변수의 참조 값을 함수 내부로 전달하는 방식이다.
이 방식에서는 함수 내에서 인자로 전달된 변수의 값을 변경하면, 호출한 쪽에서도 해당 변수의 값이 변경된다. 이는 인자로 전달되는 값이 변수의 주소이므로, 함수 내에서 변수의 값을 변경하면 해당 주소에 저장된 값이 변경되기 때문이다.
ex)
포인터 문법을 이용해 값 대신 각 변수의 주소를 넘겨 이 문제를 해결할 수 있다.
main
함수의 start
, end
변수 주소를 Swap
함수로 전달하려면 Swap
함수의 매개변수에는 포인터 변수가 와야 하기 때문에 Swap
함수가 다음과 같이 변경된다.
Swap
함수에서 pa
, pb
포인터를 *pa
, *pb
와 같이 사용하면 pa
, pb
포인터가 가리키는 대상 메모리인 start
, end
값을 변경할 수 있기 때문에 정상적으로 값이 서로 바뀐것을 확인할 수 있다.
포인터 변수는 일반 변수와 const
키워드를 사용하는 위치가 좀 다르다.
포인터 변수는 포인터 변수에 저장된 값을 변경하거나 포인터 변수가 가리키는 대상의 값을 변경하는 두 가지 형태로 사용할 수 있기 때문이다.
int * const p ;
p 앞에 const
키워드를 사용했다. 따라서 p가 가지고 있는 주소를 변경하면 번역할때 오류가 발생한다.
int data = 5, temp = 0;
int *const p = &data; /* 변수 p는 data 변수의 주소를 저장 */
*p = 3; /* p 변수가 저장하고 있는 주소에 가서 3을 대입하면 data 변수의 값이 3으로 변경됨 */
p = &temp; /* 오류 발생 : 변수 p에 const 속성이 적용되어 p에 저장된 주소는 변경할 수 없음 */
const int *p;
p가 주소에 접근할 때 사용하는 크기 앞에 const
키워드를 사용했기 때문에 *p를 사용하여 대상의 값을 변경하면 번역할 때 오류가 발생한다.
int data = 5;
const int *p = &data; /* 변수 p는 data 변수의 주소를 지정 */
*p = 3; /* 오류 발생 : 변수 p가 가리키는 대상에 const 속성이 적용되어 대상의 값을 변경할 수 없음 */
const int *const p;
자신과 대상에 모두 const
키워드를 사용했기 때문에 p가 가지고 있는 주소를 바꾸거나 *p를 사용하여 대상의 값을 바꾸면 번역할 때 오류가 발생한다.
int data = 5, temp = 0;
const int *const p = &data; /* 변수 p는 data 변수의 주소를 저장 */
*p = 3; /* 오류 발생 : 변수 p가 가리키는 대상에 const 속성이 적용되어 대상의 값을 변경할 수 없음 */
p = &temp; /* 오류 발생 : 변수 p에 const 속성이 적용되어 p에 저장된 주소는 변경할 수 없음 */
결론적으로 일반 변수를 다룰 때보다 포인터 변수를 다룰 때 실수할 확률이 더 높기 때문에, const
키워드를 적절하게 잘 활용하면 실수로 인한 버그를 줄일 수 있다.
자신이 사용할 메모리의 범위를 기억하는 방법은 크게 두 가지이다.
1. 시작 주소와 끝 주소로 메모리 범위 기억하기
2. 시작 주소와 사용할 크기로 메모리 범위 기억하기
포인터는 자신이 가리킬 대상 메모리의 시작 주소만 기억하면 되기 때문에 갖게 되는 특성이 있다.
short data = 0;
short *p = &data;
p = p + 1; /* 포인터 변수에 저장된 주소 값을 1만큼 증가시킴 */
위 예시에 사용한 포인터 변수 p에 저장된 주소가 100번지인 경우에 이 변수를 1만큼 증가시키면 변수에 저장되어 있던 주소 값은 101번지가 되는 게 아니라 102번지가 된다.
왜냐하면 포인터에서 +1의 의미는 단순히 주소값에 1을 더한다는 뜻이 아니고 그 다음 데이터의 주소를 의미하기 때문이다.
위 예시에서 포인터 변수 p가 가리키는 대상의 크기가 2바이트인데 이 포인터로 다음 데이터를 가리키려면 주소 값이 1이 아닌 2가 증가되어야 정상적으로 그 다음 데이터를 가리킬 수 있다.
이처럼 포인터 변수에 +1을 하면 자신이 가리키는 대상의 크기만큼 증가하는데 이것을 '포인터 변수의 주소 연산'이라고 한다.
char *p1 = (char *)100; /* p1에 100번지를 저장 */
short *p2 = (short *)100; /* p2에 100번지를 저장 */
int *p3 = (int *)100; /* p3에 100번지를 저장 */
double *p4 = (double *)100; /* p4에 100번지를 저장 */
p1++; /* 가리키는 대상의 크기가 char형(1바이트)이기 때문에 p1에 저장된 주소 값이 101이 됨 */
p2++; /* 가리키는 대상의 크기가 short형(2바이트)이기 때문에 p2에 저장된 주소 값이 102가 됨 */
p3++; /* 가리키는 대상의 크기가 int형(4바이트)이기 때문에 p3에 저장된 주소 값이 104가 됨 */
p4++; /* 가리키는 대상의 크기가 double형(8바이트)이기 때문에 p4에 저장된 주소 값이 108이 됨 */
포인터 변수가 가리키는 대상의 크기를 모를 때 사용하는것이 void
키워드 이다.
void
키워드는 '정해져 있지 않다'는 의미를 가지고 있으며,
void *p;
와 같이 변수를 선언하면 포인터 변수 p에 주소 값을 저장할 수는 있지만 해당 주소에서 값을 읽거나 저장할 때 사용하는 크기는 정해져 있지 않다.
즉, 사용할 메모리의 시작 주소만 알고 끝 주소를 모를 때 사용하는 포인터 형식이다.
int data = 0;
void *p = &data; /* data의 시작 주소를 저장 */
*p = 5; /* 오류 발생 : 대상 메모리의 크기가 지정되지 않음 */
void*는 포인터가 가리킬 대상의 크기를 정한 것이 아니라서 말 그대로 어떤 크기의 메모리가 오든지 상관없다.
다만 주소를 사용할 때 반드시 '사용할 크기'를 표기해야 한다.
int data = 0;
void *p = &data;
*(int *) p = 5; /* 형 변환 문법을 사용하여 대상의 크기를 4바이트로 지정하므로 data 변수에 5가 저장됨 */
(int )p 라고 사용하면 일시적으로 포인터 변수 p의 형식이 int*가 되기 때문에 포인터 변수 p에 저장된 주소에 가서 4바이트(int) 크기로 5를 저장한다.
void MyFunc(char *p_char, short *p_short, int*p_int){
if(p_char != NULL) *p_char = 1;
else if(p_short != NULL) *p_short = 1;
else *p_int = 1;
}
void main(){
short data = 5;
/* data 변수는 short형이기 때문에 short*를 사용하는 두 번째 매개변수에 주소를 넘겨줌 */
MyFunc(NULL, &data, NULL);
}
위는 main
함수에 선언한 지역 변수의 주소 값을 매개변수로 받아서 그 주소에 해당하는 메모리에 값 1을 대입하는 MyFunc
함수이다.
그런데 MyFunc
로 전달되는 주소의 형식이 char*
, short*
, int*
중 하나이고 이 형식은 사용할 때마다 달라질 수도 있다면,
어떤 형식의 주소 값이 전달될지 모르기 때문에 위처럼 세개의 포인터를 매개변수에 추가해야 한다.
만약 MyFunc
로 전달되는 주소의 형식이 더 많아지면 매개변수가 더 많아져서 불편하기 때문에 void*형
을 사용하여 수정할 수 있다.
void MyFunc(void *p, char flag){
/* flag에 전달된 값에 따라 형 변환하여 전달된 주소에 1을 대입 */
if(flag == 0 ) *(char *)p = 1; /* flag가 0이면 char*형 */
else if(flag == 1) *(short *)p = 1; /* flag가 1이면 short*형 */
else *(int *)p = 1; /* flag가 0과 1이 아니면 int* 형 */
}
void main(){
short data = 5;
/* data 변수는 short형이기 때문에 short*를 의미하는 1을 같이 전달함 */
MyFunc(&data, 1);
}
void형 포인터를 매개변수로 사용하면 어떤 형식의 주소이든 저장할 수 있지만 3가지 주소 형식 중 무엇을 사용했는지 알 수 없다.
따라서 char는 0, short는 1, int는 2를 의미하는 값을 매개변수로 함께 전달해야 한다. (매개변수 이름 -> flag)