리메이크 중/C,C++ 이론 중심

C(&C++) 이론 18. 포인터 응용

라이피 (Lypi) 2023. 1. 18. 18:09
반응형

 


내용 참고

혼자 연구하는 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번지의 값을 변경하려고 시도해서 런타임 에러가 발생한다.

 


반응형