포인터를 이용하면 메모리에 직접 접근이 가능합니다. 아래의 글을 코드로 작성해 포인터의 선언방법과 변수의 주소 값을 얻을 때 사용하는 &연산자에 대해 보겠습니다.
'정수 7이 저장된 int형 변수 num을 선언하고 이 변수의 주소 값 저장을 위한 포인터 변수 pnum을 선언하자. 그리고 나서 pnum에 변수 num의 주소 값을 저장하자'
int main(void)
{
int num = 7;
int *pnum; // 포인터 변수 pnum의 선언
pnum = # // num의 주소값을 포인터 변수 pnum에 저장
}
위 코드를 보면 &연산자는 '오른쪽에 등장하는 피연산자의 주소 값을 반환하는 연산자'입니다. 따라서 위 문장에서는 &연산자의 결과로 변수 num의 주소값이 반환되며, 이를 포인터 변수 pnum에 저장하게 됩니다. 포인터를 선언할 때는 가리키고자 하는 변수의 자료형에 맞춰 선언해야합니다. 위 코드는 int형 변수를 가리키기 때문에 따라서 int형 포인터를 선언했습니다.
지금부턴 포인터의 가장 중요한 &연산자와 *연산자를 살펴보겠습니다.
&연산자
&연산자는 피연산자의 주소 값을 반환하는 연산자입니다. &연산자의 피연산자는 변수이어야하며, 상수는 피연산자가 될 수 없습니다. 그리고 다음과 같이 변수의 자료형이 맞지 않는 포인터 변수의 선언은 문제가 될 수 있습니다.
int main(void)
{
int num1 = 5;
double *pnum1 = &num1;
double num2 = 5;
int *pnum2 = &num2;
}
*연산자
*연산자는 포인터가 가리키는 메모리 공간에 접근할 때 사용하는 연산자입니다.
int mian(void)
{
int num = 10;
int *pnum = # // 포인터 변수 pnum이 변수 num을 가리키게 하는 문장
*pnum = 20; // 포인터 변수 pnum이 가리키는 메모리 공간인 변수 num에 20을 저장해라
printf("%d", *pnum); // 포인터 변수 pnum이 가리키는 메모리 공간인 변수 num에 정장된 값을 출력해라
}
이렇듯 사실상 *pnum은 포인터 변수 pnum이 가리키는 변수 num을 의미하는 것입니다.
포인터 선언시 주의할 점
포인터 변수를 선언할 때 초기화를 동시에 진행하는 습관을 들여야합니다. 그렇지않으면, 포인터 변수는 쓰레기 값으로 초기화 됩니다. 아래의 코드를 보고 흔히 하는 실수를 확인해보겠습니다.
int main(void)
{
int *ptr; //포인터 변수 ptr은 쓰레기 값으로 초기화 된다.
*ptr = 200;
}
이 경우 포인터 변수를 선언만하고 초기화하지않아 쓰레기 값으로 초기화 됐습니다. 이때, ptr이 가리키는 메모리 공간이 매우 중요한 위치였다면, 시스템 전체에 심각한 문제를 일이클 수도 있는 상황입니다.
int main(void)
{
int *ptr = 125;
*ptr = 10;
}
이 경우도 위의 실수와 동일한 문제가 발생합니다. 125번지에 저장하는건 결국 쓰레기값에 저장한 경우와 다르지 않습니다. 그러면 포인터를 선언만 하고 이후에 유효한 주소 값을 채워 넣는 방법을 생각해보겠습니다.
int *ptr1 = 0; // 여기서 '0'은 0번지가 아닌 널포인트를 의미한다.
int *ptr2 = NULL; // NULL은 사실상 0을 의미한다.
물론 이렇게 진행하면 프로그램이 멈추는 현상이 동일하게 일어나지만, 잘못된 메모리의 접근에 대해 보호장치가 없는 운영체제에서도 시스템에 치명적인 영향을 주지 않습니다.
포인터와 배열의 관계
배열의 이름도 포인터입니다.단, 그 값을 바꿀 수 없는 '상수 형태의 포인터'입니다.
비교조건 \ 비교대상 |
포인터 변수 |
배열의 이름 |
이름이 존재하는가? |
존재한다 |
존재한다 |
무엇을 나타내거나 저장하는가? |
메모리의 주소 값 |
메모리의 주소 값 |
주소 값의 변경이 가능한가? |
가능하다 |
불가능하다(=상수 형태란 의미) |
포인터와 배열의 관계를 간단하게 정리해봤습니다. 거의 동일함을 알 수 있습니다. 다만, 주소값의 변경 유무에 차이가 있습니다. 이렇든 배열도 포인터이므로 배열의 이름을 피연산자로 하는 *연산도 가능합니다.
int arr1[3] = {1, 2, 3};
printf("%d \n", *arr1);
*arr1 += 100;
printf("%d \n", *arr1[0]);
return 0;
이와 같이 코드를 입력하면 arr1의 값은 1과 101로 출력됩니다. 1차원 배열이름의 포인터 형은 배열의 이름이 가리키는 대상을 기준으로 결정하면 됩니다. 그리고 포인터를 배열의 이름처럼 사용할 수도 있습니다. 이는 아래 코드를 통해 설명하겠습니다.
int arr[3] = {15, 25, 35};
int *ptr = &arr[0]; // int *ptr = arr; 과 동일한 의미
printf("%d %d \n, ptr[0], arr[0]); // 결과적으로 15 15
printf("%d %d \n, ptr[1], arr[1]); // 결과적으로 25 25
printf("%d %d \n, ptr[2], arr[2]); // 결과적으로 35 35
printf("%d %d \n", *ptr, *arr); // 결과적으로 15 15
return 0;
이렇듯 배열과 포인트 둘 다 포인트이기 때문에 나온 결과입니다. 이처럼 사용할 일은 거의 없지만, 이러한 일이 가능하다는 사실은 알고 있어야합니다.
포인터 연산
포인터는 *연산 이외에 증가 및 감소연산도 가능합니다. 결과는 포인터의 특성에 맞춰 진행합니다. 이전에 설명했듯이 TYPE형 포인터를 대상으로 n의 크기만큼 값을 증가 및 감소 시, n x sizeof(TYPE)의 크기만큼 주소 값이 증가 및 감소합니다. 즉, int형 포인터면 1증가시 4가 증가하고 double형은 8이 증가합니다.
*(++ptr) = 20; // ptr에 저장된 값 자체를 변경
*(ptr+1) = 20; // ptr에 저장된 값은 변경되지 않음
위 문장의 차이를 잘 이해해야 합니다. ++연산의 결과로 인해 포인터 변수 ptr에 저장된 값이 4만큼 증가합니다. 하지만 +연산의 결과로 인해 포인터 변수 ptr에 저장된 값이 증가하지 않습니다. 다만 증가된 값을 연산의 결과로 얻어서 *연산을 진행할 뿐입니다. 즉 arr[i] == *(arr+i)란 의미를 알 수 있습니다.
두 가지 형태의 문자열 표현
char str1[ ] = "My STring"; // 변수 형태의 문자열
char *str2 = "Your String"; // 상수 형태의 문자열
첫 번째 경우 배열을 기반으로 하는 '변수 형태의 문자열' 선언입니다. 반면 두 번째 경우 포인터를 기반으로 문자열을 선언하는 방법입니다. 이렇게 선언을 하면 메모리 공간에 문자열 "Your String"이 저장되고, 문자열의 첫 번째 문자 Y의 주소값이 반환됩니다. 그리고 그 반환 값이 포인터 변수 str2에 저장됩니다. 그래서 str2를 char형 포인터로 선언한 것입니다. 그렇다면 위 두 문자열 선언의 차이점은 무엇인지 보겠습니다.
M |
y |
|
S |
t |
r |
i |
n |
g |
\0 |
<--------------배열 str1--------------> |
ㅁ-------------> |
Your String \0 |
↑포인터 str2 |
자동 할당된 문자열 |
str1은 그 자체로 문자열 전체를 저장하는 배열이고, str2는 메모리상에 자동으로 저장된 문자열 "Your String"의 첫 번째 문자를 단순히 가리키고만 있는 포인터 변수입니다. str1, str2 모두 문자열의 시작 주소 값을 담고 있다는 측면에서 동일하나, '배열이름 str1은 계속해서 문자 M이 저장된 위치를 가리키는 상태이어야 하지만 포인터 변수 str2는 다른 위치를 가리킬 수 있다는 차이'가 있습니다.
이전에 말한대로 str1은 변수 형태의 문자열로 변경이 가능하고, str2은 상수 형태의 문자열로 변경이 불가능합니다.
포인터배열
포인터 변수로 이뤄진, 그래서 주소 값이 저장이 가능한 배열을 포인터 배열이라 합니다. 이러한 배열의 선언방식은 기본 자료형 배열의 선언방식과 동일합니다.
int *arr1[2] // 길이가 20인 int형 포인터 배열 arr1
double *arr2[30] // 길이가 30인 double형 포인터 배열 arr2
그리고 포인터 배열또한 문자열 배열이 가능합니다. 이전에 설명한것처럼 큰따움표를 기준으로 문자열의 주소 값을 저장할 수 있습니다.
char *strArr[3] = {"Simple", "String", "Array"};
printf("%s \n", strArr[0]); //출력값 Simple
printf("%s \n", strArr[1]); //출력값 String
printf("%s \n", strArr[2]); //출력값 Array