소년코딩

07.07 - 포인터 소개 (Introduction to pointer)

'01.02 - 변수, 초기화 및 할당' 포스트에서 변수는 값을 보유하고 있는 메모리 조각의 이름이라는 것을 배웠다. 프로그램이 변수를 인스턴스화 할때 사용 가능한 메모리 주소가 변수에 자동으로 할당되고, 변수에 할당된 값은 이 메모리 주소에 저장된다.

int x;

CPU가 위 문장을 실행하면 RAM의 메모리 조각이 따로 설정된다. 예를 들어 변수 x에 메모리 위치 140이 할당되었다고 가정해보자. 프로그램에서 변수 x를 표현식 또는 명령문으로 접근할 때마다 값을 얻으려면 메모리 위치 140을 찾아야 한다.

변수의 좋은 점은 우리가 어떤 특정한 메모리 주소가 할당되는지 걱정할 필요가 없다는 것이다. 지정된 식별자로 변수를 참조하면 컴파일러에서 이 이름을 할당된 메모리 주소로 변환한다.

하지만 이 접근법에는 몇 가지 제한 사항이 있으며, 이에 대해서는 앞으로 배울 내용에서 살펴보겠다.


주소 연산자 (&) (The address-of operator (&))

주소 연산자 &를 사용하면 변수에 할당된 메모리 주소를 확인할 수 있다.

#include <iostream>

int main()
{
    int x = 5;
    std::cout << x << '\n';  // print the value of variable x
    std::cout << &x << '\n'; // print the memory address of variable x

    return 0;
}
// prints:
// 5
// 0027FEA0

NOTE: 주소 연산자는 비트 AND 연산자와 비슷하게 보이지만, 주소 연산자는 단항이고 비트 AND 연산자는 이항 연산자다.

역참조 연산자 (*) (The dereference operator (*))

변수의 주소를 얻는 것 자체로는 그다지 유용하지 않다.

역참조 연산자(*)를 사용하면 특정 주소에서 값에 접근할 수 있다.

#include <iostream>

int main()
{
    int x = 5;
    std::cout << x << '\n'; // print the value of variable x
    std::cout << &x << '\n'; // print the memory address of variable x
    std::cout << *&x << '\n'; /// print the value at the memory address of variable x

    return 0;
}
// prints:
// 5
// 0027FEA0
// 5

NOTE: 역참조 연산자는 곱셈 연산자처럼 보이지만, 역참조 연산자는 단항이고 곱셈 연산자는 이항 연산자다.


포인터 (Pointer)

주소 연산자(&)와 역참조 연산자(*)와 함께 이제 포인터에 관해 이야기할 수 있다.

포인터는 어떠한 값을 저장하는 게 아닌 메모리 주소를 저장하는 변수다.

포인터(pointer)는 C++ 언어에서 가장 혼란스러운 부분 중 하나로 여겨지지만, 알고 보면 놀랍게도 간단한다.


포인터 선언 (Declaring a pointer)

포인터 변수는 일반 변수처럼 선언되며, 자료형과 변수 이름 사이에 별표(*)가 붙는다.

  • 자료형* 포인터 이름;
int* iPtr;    // int형 포인터
double* dPtr; // double형 포인터

int* iPtr2, *iPtr3; // int형 두 개의 포인터 선언

여러 포인터 변수를 선언하는 경우 별표가 각 변수에 포함되어야 한다.

int* iPtr4, iPtr5; // iPtr6은 int에 대한 포인터이지만 iPtr5는 단순한 int다.

일반 변수와 마찬가지로 포인터는 선언 시 초기화되지 않는다. 초기화되지 않은 값은 쓰레기다.

"int 포인터" 라고 말하면 "int형에 대한 포인터"를 의미한다.


포인터에 값 할당 (Assigning a value to a pointer)

포인터는 메모리 주소만 저장하므로, 포인터에 값을 할당할 때 그 값은 주소여야 한다. 포인터로 하는 가장 흔한 작업은 다른 변수의 주소를 저장하는 것이다.

변수의 주소를 얻으려면 주소 연산자(&)를 사용한다.

  • 포인터 = &변수;
int value = 5;
int *ptr = &value; // 변수값의 주소로 ptr 초기화

개념적으로, 위 코드를 다음과 같이 나타낼 수 있다.

0707_pointer

ptr은 값으로 value 변수 값의 주소를 가지고 있다. 그러므로 ptrvalue 변수를 '가리키는' 값이라고 할 수 있다.

코드를 사용해서 쉽게 확인할 수 있다.

#include <iostream>

int main()
{
    int value = 5;
    int *ptr = &value; // 변수 값의 주소로 ptr 초기화

    std::cout << &value << '\n'; // value 변수의 주소 출력
    std::cout << ptr << '\n';    // ptr 변수 값 출력

    return 0;
}

// 0012FF7C
// 0012FF7C

포인터 변수의 자료형은 가리키는 변수의 자료형과 같아야 한다.

int iValue = 5;
double dValue = 7.0;

int *iPtr = &iValue; // ok
double *dPtr = &dValue; // ok
iPtr = &dValue; // wrong -- int 포인터는 double 변수의 주소를 가리킬 수 없다.
dPtr = &iValue; // wrong -- double 포인터는 int 변수의 주소를 가리킬 수 없다.

다음 사항도 올바르지 않다.

int *ptr = 5;

포인터가 주소만 보유할 수 있고 정수 리터럴 5에는 메모리 주소가 없기 때문이다. 위 코드를 시도하면 컴파일러는 정수를 정수 포인터로 변환할 수 없으므로 오류가 발생한다.

C++에서는 포인터에 리터럴 메모리 주소를 직접 할당할 수 없다.

double *dPtr = 0x0012FF7C; // not okay (정수 리터럴을 할당하는 것으로 취급된다.)

The address-of operator returns a pointer

주소 연산자 &는 피연산자의 주소를 리터럴로 반환하지 않는다. 대신 피연산자의 주소가 들어있는 포인터를 반환한다.

이 사실은 다음 예제에서 확인할 수 있다.

#include <iostream>
#include <typeinfo>

int main()
{
    int x(4);
    std::cout << typeid(&x).name(); // typeid는 인수의 자료형을 반환한다.

    return 0;
}

// On Visual Studio 2013, this printed:
// int*

Dereferencing pointers

어떤 것을 가리키는 포인터 변수가 있다면, 역참조 연산자 *를 통해 포인터가 가리키는 주소의 값을 알 수 있다.

int value = 5;
std::cout << &value; // value의 주소를 출력한다.
std::cout << value;  // value의 값을 출력한다.

int *ptr = &value; // ptr은 value를 가리킨다.
std::cout << ptr;  // ptr이 가리키는 주소를 출력한다. (=&value)
std::cout << *ptr; // ptr을 역참조한다. (ptr이 가리키는 주소의 값을 출력한다. =value)

// 0012FF7C
// 5
// 0012FF7C
// 5

이러한 이유로 포인터가 반드시 자료형을 가져야 한다. 자료형이 없으면 포인터는 역참조 시 가리키는 내용을 해석하는 방법을 알지 못한다. 또한, 포인터의 자료형과 할당되는 변수 자료형이 일치해야 하는 이유이기도 한다. 그렇지 않으면 역참조 될 때 비트를 다른 자료형으로 잘못 해석한다.

할당한 후에 포인터 값을 다른 값으로 재할당할 수 있다.

int value1 = 5;
int value2 = 7;

int *ptr;

ptr = &value1; // ptr points to value1
std::cout << *ptr; // prints 5

ptr = &value2; // ptr now points to value2
std::cout << *ptr; // prints 7

일반적으로 다음은 true다:

  • ptr은 &value 값과 같다.
  • *ptr은 value 값과 같다.

*ptr은 value와 같게 취급되므로 마치 변수값인 것처럼 갓을 할당할 수 있다.

int value = 5;
int *ptr = &value;  // ptr points to value

*ptr = 7;           // *ptr is the same as value, which is assigned 7
std::cout << value; // prints 7

A warning about dereferencing invalid pointers

C++의 포인터는 본질적으로 안전하지 않으므로 포인터를 부적절하게 사용하면 응용 프로그램을 손상시키기 쉽다.

포인터를 역참조하면 응용 프로그램은 포인터에 저장된 메모리 위치로 이동하여 메모리 내용을 검색한다. 보안상의 이유로 최신 운영체제의 샌드박스 응용 프로그램은 다른 응용 프로그램과의 부적절한 상호 작용을 방지하고 운영체제 자체의 안정성을 보호한다. 응용 프로그램이 운영 체제에 의해 할당되지 않은 메모리 위치에 접근하려고 하면 운영 체제가 응용 프로그램을 종료할 수 있다.

다음 프로그램을 이를 보여 주며, 실행하면 충돌이 발생한다.

#include <iostream>

void foo(int *&p)
{
    // p is a reference to a pointer.  We'll cover references (and references to pointers) later in this chapter.
    // We're using this to trick the compiler into thinking p could be modified, so it won't complain about p being uninitialized.
    // This isn't something you'll ever want to do intentionally.
}

int main()
{
    int* p; // Create an uninitialized pointer (that points to garbage)
    foo(p); // Trick compiler into thinking we're going to assign this a valid value

    std::cout << *p; // Dereference the garbage pointer

    return 0;
}

포인터의 크기 (The size of pointers)

포인터이 크기는 실행 파일이 컴파일된 아키텍처에 따라 달라진다. 32 비트 실행 파일은 32비트 메모리 주소를 사용하므로 포인터의 크기는 32 비트(4 바이트)다. 64 비트 실행 파일에서 포인터의 크기는 64 비트(8 바이트)다.

char *chPtr; // chars are 1 byte
int *iPtr;   // ints are usually 4 bytes
struct Something
{
    int nX, nY, nZ;
};
Something *somethingPtr; // Something is probably 12 bytes

std::cout << sizeof(chPtr) << '\n'; // prints 4
std::cout << sizeof(iPtr) << '\n'; // prints 4
std::cout << sizeof(somethingPtr) << '\n'; // prints 4

보시다시피 포인터의 크기는 항상 같다. 이는 포인터가 메모리 주소일 뿐이며 주어진 시스템에서 메모리 주소에 접근하는 데 필요한 비트 수는 항상 일정하기 때문이다.


What good are pointers?

이 시점에서, 포인터는 약간 바보 같고, 학문적이거나, 둔하게 보일 수 있다. 일반 변수를 사용할 수 있는데 포인터를 사용하는 이유는 뭘까?

포인터는 여러 가지 경우에서 유용하다.

  • 배열은 포인터를 사용하여 구현된다. 포인터는 배열을 반복할 때 사용할 수 있다. (배열 인덱스 대신 사용 가능)
  • C++에서 동적으로 메모리를 할당할 수 있는 유일한 방법이다. (가장 흔한 사용 사례)
  • 데이터를 복사하지 않고도 많은 양의 데이터를 함수에 전달할 수 있다.
  • 함수를 매개 변수로 다른 함수에 전달하는 데 사용할 수 있다.
  • 상속을 다룰 때 다형성을 달성하기 위해 사용한다.
  • 하나의 구조체/클래스 포인터를 다른 구조체/클래스에 두어 체인을 형성하는 데 사용할 수 있다. 이는 연결리스트 및 트리와 같은 고급 자료구조에서 유용하다.

지금까지 포인터에 대한 기본적인 수준을 이해했으므로, 다음 포스트부터는 다양한 사용법을 살펴볼 예정이다.


요약 (Summary)

포인터는 메모리 주소를 저장하는 변수다. 역참조 연산자 *를 사용해서 저장한 메모리 주소의 값을 알 수 있다. 가비지 포인터를 역참조하면 응용 프로그램이 중단될 수 있다.


cpp 번역: 이 포스트의 원문은 http://www.learncpp.com/cpp-tutorial/67-introduction-to-pointers/ 입니다.

댓글 로드 중…

블로그 정보

소년코딩 - 소년코딩

소년코딩, 자바스크립트, C++, 물리, 게임 코딩 이야기

최근에 게시된 이야기