
내용 참고
혼자 연구하는 C/C++ (Soen.kr/와우북스)
포인터 응용
■ 포인터의 기본 개념은 단순한 만큼 파생될 수 있는 내용이 많다.
■ 특히 포인터와 메모리에 대한 내용과 이중 포인터, 포인터 배열, 배열 포인터, 함수 포인터 등이 중요하다.
■ 여기서는 *ptr++ 표현에 대한 내용과 void형 포인터에 대한 내용을 담았다.
Ⅰ. *ptr++
■ 처음보면 당황스러울 수 있는 표현이다. 차근차근 분석해보자.
ⅰ. 예제
#include <iostream>
using namespace std;
int main()
{
int ar[] = {1,2,3,4,5};
int arMAX = sizeof(ar)/sizeof(ar[0]);
//배열의 이름은 상수 포인터로 배열의 첫번째 요소를 가리키지만,
//sizeof() 연산자에서는 배열 전체의 크기를 리턴한다.
int* ptr;
//*ptr++은 포인터의 값을 리턴하고 다음 요소로 하나씩 넘어간다.
ptr = ar;
for (int i = 0; i < arMAX; i++) {
cout << "ptr = " << *ptr++ << " ";
}
cout << endl;
//*ptr++을 풀어쓰면 다음과 같다.
ptr = ar;
for (int i = 0; i < arMAX; i++) {
cout << "ptr = " << *ptr << " ";
ptr++;
}
cout << endl;
}
ⅱ. 포인터로서의 배열의 이름과 sizeof()
■ 앞에서 언급했지만 배열의 이름은 포인터 상수이며, 배열의 첫번째 요소를 가리킨다.
■ 즉, int* ptr = ar; 과 int* ptr = &ar[0];은 같다.
■ 하지만 배열의 이름을 sizeof()로 계산하면 배열의 전체 크기가 나온다.
■ 그러므로 sizeof(ar)은 배열의 전체 크기를 반환하고 sizeof(ar[0])은 배열 요소 하나의 크기를 반환한다.
ⅲ. (*ptr)과 (ptr++)을 합치면 *ptr++
■ *ptr++; 는 1) *ptr;로 포인터 ptr이 가리키는 값을 리턴하고, 2) ptr++로 다음 주소의 요소로 이동한다.
■ 왜냐하면 후위 증감 연산자는 값이 리턴된 뒤에야 계산되기 때문이다.
■ 이 표현은 공식 문서 등에서도 많이 사용되는 표현이기 때문에 알아둘 필요가 있다.
Ⅱ. void형 포인터
■ C 및 C++에서 void는 타입이 없음을 나타내는 예약어이다.
■ 일반 변수를 선언할 때는 당연히 쓸 수 없고, 함수와 포인터에만 쓸 수 있다.
■ void형 포인터는 일반 포인터와는 다른 특성을 갖는다.
ⅰ. 임의의 대상체를 가리킬 수 있다.
■ void형 포인터는 일반 포인터와는 달리 임의의 대상체를 가리킬 수 있다.
■ 이때 명시적인 캐스팅은 필요로 하지 않는다.
■ 반대로 일반 포인터에 void형 포인터를 대입할 때는 반드시 명시적인 캐스팅을 해야한다. (C++ 기준)
ⅱ. 대상체의 값을 바로 가져올 수 없다.
■ void 포인터인 상태로는 대상체가 무슨 타입인지 모르기 때문이다.
■ 대신에 명시적 캐스팅을 한 상태에서는 대상체의 값을 가져올 수 있다.
#include <iostream>
using namespace std;
int main()
{
void* vp;
int i = 68;
char c = 'k';
float f = 0.1;
vp = &i;
//cout << *vp << endl; // 에러
cout << *(int*)vp << endl;
vp = &c;
cout << *(char*)vp << endl;
vp = &f;
cout << *(float*)vp << endl;
}
■ 이렇게 대상체의 타입을 정확하게 알고, 그 타입의 포인터로 캐스팅을 하면 사용할 수 있다.
ⅲ. 증감 연산자를 사용할 수 없다.
■ 대상체의 값을 가져올 수 없는 것과 같은 이유이다.
■ 대상체의 타입을 모르므로 얼마나 이동해야하는지 모르기 때문이다.
#include <iostream>
using namespace std;
int main()
{
int ar[] = {1,2,3,4};
int arMAX = sizeof(ar)/sizeof(ar[0]) - 1;
void* vp = ar;
for (int i = 0; i <= arMAX; i++) {
cout << *(int*)vp << endl; //윗 내용
vp = (int*)vp + 1; //이번 내용
}
}
■ void형 포인터를 이용하여 증감 연산자처럼 쓰고 싶으면 위의 방법밖에 없다.
ⅳ. 정리
■ void형 포인터는 대상체가 정해져 있지 않으므로 임의의 번지를 지정하는 것만 가능하다.
■ void형 포인터는 *연산자로 값을 읽거나, 증감 연산자를 사용하려면 반드시 명시적 캐스팅이 필요하다.
Ⅲ. NULL 포인터
ⅰ. 정의
■ null 포인터란 포인터에 저장된 주소값이 null, 즉 0인 포인터이다.
■ 주소값이 0이라는 소리는 절대 주소로 메모리 번호가 0인 위치이다.
■ #define NULL 0 이라는 명령어로 숫자 0 대신 NULL을 사용하는 것이 더 직관적이다.
■ 헤더파일에 정의되어 있으므로 직접 정의해야할 일은 거의 없을 것이다.
ⅱ. 실제 의미
■ 절대 번지가 0인 메모리는 시스템 영역이기 때문에 응용 프로그램에서 이 위치를 건드려서는 안된다.
■ 그래서 null 포인터는 에러를 나타내도록 약속되어 있다.
■ 포인터를 반환하는 함수들은 에러가 발생했을 때 이 NULL값을 리턴한다.
ⅳ. 예외 처리
■ null은 0이기 때문에 에러 상황에서 반환된 값을 그대로 사용하면 문제가 생길 여지가 있다.
■ 그래서 포인터를 반환하는 함수가 null을 반환하는지 확인하는 예외 처리를 할 필요가 있다.
■ 예를 들어 문자열의 특정 문자를 찾아서 변환하는 함수인 strchr() 함수의 경우를 보자.
#include <iostream>
#include <string.h>
int main()
{
char str[] = "korea";
char *p;
p = strchr(str,'s');
//if(p != NULL) {
*p = 'r';
//}
puts(str);
}
■ 8번째 줄에서 char형 포인터에 strchr() 함수의 반환값을 담고 있다.
■ "korea" 라는 문자열에는 's'가 없으므로 strchr() 함수는 null값을 반환할 것이다.
■ 여기서 if문으로 예외처리를 하지 않았다면 10번째 줄은 0번지의 값을 변경하려고 시도해서 런타임 에러가 발생한다.