C++ 포인터


이건 혹시라도 헷갈리면 진짜 큰일나고 보통 제대로 이해 못한다는 구간이라서 따로 문서를 만들었다.


일반적으로 변수는 그 값이 컴퓨터 메모리 상에 저장되는 위치를 식별자를 통해서 물리적인 주소에 대해 신경 쓸 필요 없이 필요할 때 손쉽게 접근할 수 있게 한다.

C++ 프로그램에서의 컴퓨터 메모리는 각 고유한 주소를 가지는 1 바이트 단위의 메모리 층이 연속적으로 이뤄진 형태다. 이런 바이트들을 연속적으로 묶어 1 바이트보다 많은 용량을 차지하는 데이터를 표현하는데 쓰이기도 한다.

변수가 선언될 때, 메모리의 특정 장소에 저장되어야 하는데 일반적으로 이 주소는 C++ 프로그램에서 정확한 주소를 지정하는 것이 아닌 실행되는 환경인 운영체제에서 주로 런타임에 위치를 결정한다.

그리고 포인터는 이런 주소를 런타임에 흭득하는 것이 유용한 경우에 사용된다. (저레벨 수준의 프로그래밍 등)

용어 사전에 남겼던 레퍼런스(또는 Address-of) 연산자 (&)로 변수의 주소를 흭득할 수 있다.

foo = &myvar; // 이렇게 되면 myvar의 값이 아닌 그 주소를 얻게 된다.

역참조 연산자 (*) Dereference operator

참조 연산자 &가 변수를 참조하여 주소를 흭득할 수 있다고 하면 역참조 연산자는 반대로 그 메모리 주소를 참조하여 직접적으로 값을 다룰 수 있게 된다.

baz = *foo;

myvar = 25;
foo = &myvar;

myvar == 25
&myvar == 1776
foo == 1776
*foo == 25

포인터 선언

역참조 연산자로 값을 참고할 때에 다룰 값의 타입을 알아야 저장되는 타입의 형식대로 읽을 수 있다. 그러기 위해선 변수 이름에 역참조 연산자로 썼던 *를 붙이는 것으로 포인터를 선언해야 한다.

int *number;
char *character;
double *decimals;

포인터는 별도의 타입으로 취급되며 보통 타입에 관계 없이 메모리에서 같은 용량을 차지한다. (크기 자체는 플랫폼에서 따라서 다를 수 있다.)
또한, 역참조 연산자와는 처리는 같지만 엄연히 다른 취급이다.

// more pointers
#include <iostream>
using namespace std;

int main ()
{
  int firstvalue = 5, secondvalue = 15;
  int * p1, * p2; // 변수 선언 시에 포인터로 사용할 각 변수에 *를 붙여야 한다.

  p1 = &firstvalue;  // p1 = address of firstvalue
  p2 = &secondvalue; // p2 = address of secondvalue
  *p1 = 10;          // value pointed to by p1 = 10
  *p2 = *p1;         // value pointed to by p2 = value pointed to by p1
  p1 = p2;           // p1 = p2 (value of pointer is copied)
  *p1 = 20;          // value pointed to by p1 = 20
  
  cout << "firstvalue is " << firstvalue << '\n'; // 10
  cout << "secondvalue is " << secondvalue << '\n'; // 20
  return 0;
}

const 선언을 한 포인터의 경우는 주소의 수정과 가르킨 값을 읽을 수 있지만 수정하는 것은 막고 있고 안전을 위해 암시적으로 const 포인터의 const가 없는 포인트로 변환하는 것을 방지한다.
또한 포인터 내의 변수를 const 선언한 경우에는 주소의 수정도 할 수 없게 된다.

int x;
      int *       p1 = &x;  // non-const pointer to non-const int
const int *       p2 = &x;  // non-const pointer to const int
      int * const p3 = &x;  // const pointer to non-const int
const int * const p4 = &x;  // const pointer to const int 

배열과의 관계

사실 배열도 첫 원소의 주소를 가리키는 포인터다. 따라서 포인터에 배열의 주소를 가르키게 하면 배열과 똑같이 다룰 수 있다.

// more pointers
#include <iostream>
using namespace std;

int main ()
{
  int numbers[5];
  int * p;
  p = numbers;  *p = 10; // p를 numbers 주소(이자 첫 번째 원소)를 가르키게 하고 첫번째 원소를 10으로 바꿈
  p++;  *p = 20; // 포인터를 증가시켜 두 번째 원소를 가르키게 하고 20으로 바꿈
  p = &numbers[2];  *p = 30; // 배열은 0부터 시작이니 0, 1, 2 -> 세 번째 원소를 가르키게 하고 30으로 바꿈 
  p = numbers + 3;  *p = 40; // 네 번째 원소를 40으로 바꿈 
  p = numbers;  *(p+4) = 50; // 마지막 원소를 50으로 바꿈
  for (int n=0; n<5; n++)
    // 10, 20, 30, 40, 50
    cout << numbers[n] << ", ";

 // 근데 증가/감소 연산자가 자동으로 해당 자료형의 크기만큼 증가/감소 해주나? 이건 다음 섹션에 소개된다.
  return 0;
}

문자열도 마찬가지로 다룰 수 있다.

포인터의 산술 연산

포인터의 산술 연산은 일반적인 정수와는 조금 다르게 이뤄진다.
우선 덧셈과 뺄셈만을 허용한다.

그리고 포인터에 사용되는 데이터 타입의 바이트 크기에 따라서 연산에 쓰이는 정수를 바이트 크기에 곱한 값을 연산한다.

char *mychar; // 1 바이트 (1000)
short *myshort; // 2 바이트 (2000)
long *mylong; // 4 바이트 (3000)

++mychar; // 1000 + 1
++myshort; // 2000 + (1 * 2)
++mylong; // 3000 + (1 * 4)

*p++ // 이건 주소를 증가하는게 아니라 값을 증가시킨다.

포인터의 포인터의 포인터의…

말 그대로 포인터를 가르키는 포인터다.

char a;
char * b;
char ** c; // *를 가르키는 **, 나아가서 **를 가르키는 ***, 그리고 ***를 가르키는 **** ...
a = 'z';
b = &a;
c = &b;

void 포인터

C++의 특수한 포인터, 타입이 정해지지 않았거나 없는 상태라고 생각하면 된다.
어떤 타입이든 이 포인터로 가르킬 수 있지만 문제는 타입이 정해져있지 않아 읽는 방식이 없어서 다른 포인터로 변환해야만 한다.

// increaser
#include <iostream>
using namespace std;

void increase (void* data, int psize)
{
  // sizeof
  // 동적 타입이라면 그 타입이 사용하는 바이크 크기를 반환한다.
  // 이 외에는 내부에 상수로 적혀있는 사이즈 값을 반환한다.
  if ( psize == sizeof(char) )
  { char* pchar; pchar=(char*)data; ++(*pchar); } // 명시적 형변환
  else if (psize == sizeof(int) )
  { int* pint; pint=(int*)data; ++(*pint); } // 명시적 형변환
}

int main ()
{
  char a = 'x';
  int b = 1602;
  increase (&a,sizeof(a));
  increase (&b,sizeof(b));
  cout << a << ", " << b << '\n';
  return 0;
}

null 포인터

포인터는 항상 유효한 주소만을 가르키지 않는다. 어느 주소던지 가르킬 수 있어 의도치 않은 값을 흭득하거나 에러를 일으킬 수 있다.

따라서 이런 주소 관리는 알아서 해야하지만 포인터에 아무것도 가르키지 않는 의미로 nullptr (또는 0)을 넣어볼 수 있다.(주로 오래된 C 코드에서 보이는 일부 표준 라이브러리의 상수 NULL도 있다.)

함수를 가르키는 포인터

C++에서는 함수를 가르키는 것도 가능하다. 주로 다른 함수에 특정 함수를 매개변수로 주는 경우에 사용된다.
포인터 변수에 괄호를 감싸 그 자체를 함수의 이름으로, 기본적으로 함수의 프로토타입처럼 사용한다.

// pointer to functions
#include <iostream>
using namespace std;

int addition (int a, int b)
{ return (a+b); }

int subtraction (int a, int b)
{ return (a-b); }

int operation (int x, int y, int (*functocall)(int,int))
{
  int g;
  g = (*functocall)(x,y);
  return (g);
}

int main ()
{
  int m,n;
  int (*minus)(int,int) = subtraction;

  m = operation (7, 5, addition);
  n = operation (20, m, minus);
  cout <<n;
  return 0;
}

구조체를 가르키는 포인터

포인터 자체도 가르킬 수 있고 오브젝트의 멤버 역참조 연산자(->)로

struct movies_t {
  string title;
  int year;
};

movies_t amovie;
movies_t * pmovie;

pmovie = &amovie;

	
pmovie->title
// (*pmovie).title와 동일하다. 오브젝트 자체를 가르키고 멤버를 역참조 한다.
// pmovie는 포인터다.

*pmovie.title // *(pmovie.title)와 동일하다. 해당 멤버의 주소 자체를 역참조 한다.

클래스를 가리키는 포인터

선언된 클래스는 엄연한 타입으로 취급되어 포인터로 가르킬 수 있게 된다.

구조체처럼 역참조 연산자를 통해 직접 다룰 수 있다.

// pointer to classes example
#include <iostream>
using namespace std;

class Rectangle {
  int width, height;
public:
  Rectangle(int x, int y) : width(x), height(y) {}
  int area(void) { return width * height; }
};


int main() {
  Rectangle obj (3, 4);
  Rectangle * foo, * bar, * baz;
  foo = &obj;
  bar = new Rectangle (5, 6);
  baz = new Rectangle[2] { {2,5}, {3,6} };
  cout << "obj's area: " << obj.area() << '\n';
  cout << "*foo's area: " << foo->area() << '\n';
  cout << "*bar's area: " << bar->area() << '\n';
  cout << "baz[0]'s area:" << baz[0].area() << '\n';
  cout << "baz[1]'s area:" << baz[1].area() << '\n';       
  delete bar;
  delete[] baz;
  return 0;
}
표현식해석
*xx로 오브젝트를 가르킴
&xx의 주소
x.y오브젝트 x의 멤버 y
x->yx로 가르켜진 오브젝트의 멤버 y
(*x).yx로 가르켜진 오브젝트의 멤버 y (이전과 같음)
x[0]x로 가르켜진 오브젝트의 첫 번째 오브젝트 (변수)
x[1]x로 가르켜진 오브젝트의 두 번째 오브젝트 (변수)
x[n]x로 가르켜진 오브젝트의 n+1 번째 오브젝트 (변수)

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다