프로그래밍 언어/C & C++ 정리

포인터

게임 개발 2023. 10. 28. 19:51

 

포인터

포인터란?

 

 포인터는 개체의 메모리 주소를 저장하는 변수이다.

포인터는 세 가지 기본 목적으로 C 및 C++에서 광범위하게 사용된다.

 

  • 힙에 새 개체를 할당하려면
  • 함수를 다른 함수에 전달하려면
  • 배열 또는 기타 데이터 구조의 요소를 반복한다

 

원시포인터

 

원시 포인터란( = raw pointer)?

 

 원시 포인터( = raw pointer )는 스마트 포인터와 같이 캡슐화 개체에 의해 수명이 제어되지 않는 포인터다.

원시 포인터는 즉, 값에 비포인터 변수의 주소를 할당하거나 nullptr을 할당할 수 있다.

값이 할당되지 않은 포인터는 임의 데이터가 포함이 된다.

 

포인터를 역참조하여 포인터가 가리키는 개체의 값을 검색하는 것 또한 가능하다.

멤버 엑세스 연산자는 개체의 멤버에 대한 엑세스를 제공한다.

 

* 역참조 연산자

& 멤버 엑세스 연산자

 

 포인터는 입력된 객체를 가리킬 수도 있고, 빈 공간을 가리킬 수도 있다.

프로그램이 객체를 힙영역에 할당할 때, 해당 객체의 주소를 포인터로부터 받는다.

이러한 포인터를 소유 포인터(owning pointers)라고 한다.

 

힙에서 해당 개체의 할당이 더 이상 필요하지 않을 때, (쓰이지 않을 때)

소유 포인터 ( 혹은 해당 포인터의 복사본 )를 사용하여 힙에서 개체의 할당을 명시적으로 해제해야 한다.

메모리를 해제하지 않으면, 메모리 누수( memory leak ) 현상을 낳게되고,

메모리의 위치가 어디있는지 찾지 못하게 되면서 해당 기기에서 다른 프로그램에서

그 메모리를 사용할 수 없게된다.

 

메모리 할당은 new 연산자를 반드시 사용하고,

메모리 해제는 delete ( 또는 delete[]) 를 사용한다.

 

MyClass* mc = new MyClass();
mc->print();
delete mc;

 

포인터가 const로 선언되지 않은 경우에는 메모리의 다른 위치를 가르키도록

증가시키거나 감소 시킬 수 있다.

= const pointer가 아닌경우 메모리의 위치 변동이 가능하다.

이러한 연산을 포인터 산술 ( = pointer arithmetic ) 이라고 한다.

 

 const 포인터는 다른 메모리 위치를 가리키도록 만들 수 없으며,

그 점에서 참조( = 레퍼런스 'reference' )와 유사하다.

자세한 내용은 const ponter 및 volatile 포인터에서 다루도록 하겠다.

 

64-bit 운영체제에서의 포인터의 크기는 64비트이며,

시스템의 포인터 크기는 시스템이 얼만큼

다룰 수 있는 메모리( = 메모리 크기 addressable memory )를 가지는지 결정한다.

 

포인터의 모든 복사본은 동일한 메모리 위치를 가르킨다.

포인터는(참조 역시) C++에서 함수 간의 객체를 전달하기 위해 광범위하게 사용된다.

객체 전체를 복사하는 것 보다, 객체의 주소를 복사하는 것이 더 효율적인 경우가 많다.

함수를 정의할 때 객체를 수정하려는 의도의 함수가 아니라면

포인터 매개변수를 const로 지정해야 한다.

일반적으로 const reference 값은 nullptr일 가능성이 없는 한 객체를 함수에 전달하는데 선호되는 방법이다.

 

함수에도 포인터를 사용하여 해당 함수를 다른 함수로 전달할 수 있는데,

이러한 방식은 C스타일의 프로그래밍에서 "콜백"을 위해 사용된다.

Modern C++에서는 람다 표현을 사용한다.

 

 다음 예제에서는 원시 포인터를 선언, 초기화 및 사용하는 방법을 보여준다.

힙에서 할당된 개체를 가리키기 위해 new를 사용하여 초기화되며,

이를 명시적으로 삭제해야한다.

이 예제는 원시 포인터와 관련된 몇 가지 위험성도 보여주고있다.

또한 해당 예제는 C 스타일 프로그래밍이지, 모던 C++ 방식은 아니라는 것을 명심하자.

 

 

초기화 및 멤버 엑세스 

 

 다음 예제에서는 원시 포인터를 선언 및 초기화, 사용하는 방법을 보여준다.

힙에 할당된 객체를 가리키기 위해서는, new를 사용하여 초기화해야하며,

이는 delete로 해제해줘야한다.

 

해당 예제에서는 원시 포인터와 관련된 몇 가지 위험도 보여주며,

이 예제는 C스타일 프로그래밍으로 모던 C++가 아님을 감안하자.

 

#include <iostream>
#include <string>

class MyClass
{
public:
    int num;
    std::string name;
    void print() { std::cout << name << ":" << num << std::endl; }
};

// Accepts a MyClass pointer
void func_A(MyClass* mc)
{
    // 포인터 mc가 가리키는 개체를 수정한다.
    // 모든 함수내 객체 mc(사실상 복사본)는 포인터이므로 
    // 수정된 객체를 가리킨다.
    mc->num = 3;
}

// Accepts a MyClass object
void func_B(MyClass mc)
{
    // mc 는 포인터가 아닌 일반 객체형태로 받아왔다.
    // 포인터가 아니니까 . 연산자로 멤버변수에 접근한다.
    // mc의 지역메모리의 값만 복사해온 것이다.
    mc.num = 21;
    std::cout << "Local copy of mc:";
    mc.print(); // "Erika, 21"
}


int main()
{
    // * 오퍼레이터로 포인터 유형 선언하고 new로 메모리 할당 및 초기화하기
    // Use new to allocate and initialize memory
    MyClass* pmc = new MyClass{ 108, "Nick" };

    // Prints the memory address. Usually not what you want.
    std:: cout << pmc << std::endl;

    // 포인터를 역참조하여 포인터를 개체에 복사한다.
    // 메모리 위치에서 해당 내용을에 접근 가능함
    // mc 는 스택에 할당된 별개의 객체이나 포인터는 힙에서 할당됨
    MyClass mc = *pmc;

    // 주소 연산자로 mc를 가리키는 포인터 선언
    MyClass* pcopy = &mc;

    // 객체의 맴버변수에 접근
    pmc->print(); // "Nick, 108"

    // 포인터가 주소값을 가지고 있으니 *연산자 떼주고 다른 포인터에 할당 가능
    MyClass* pmc2 = pmc;

    // 복사된 포인터로 당연히 개체 수정가능
    pmc2->name = "Erika";
    pmc->print(); // "Erika, 108"
    pmc2->print(); // "Erika, 108"

    // 함수에 매개변수 전달
    func_A(pmc);
    pmc->print(); // "Erika, 3"
    pmc2->print(); // "Erika, 3"

    // Dereference the pointer and pass a copy
    // of the pointed-to object to a function
    func_B(*pmc);
    pmc->print(); // "Erika, 3" (original not modified by function)

    delete(pmc); // don't forget to give memory back to operating system!
   // delete(pmc2); //crash! memory location was already deleted
}

 

 

포인터 연산과 배열

 

포인터와 배열은 밀접한 관련이 있다.

배열이 값으로 함수에 전달되면 첫 번째 요소에 대한 포인터로 전달된다.

다음 예에서는 포인터와 배열에 대하여, 다음과 같은 중요한 속성을 보여줍니다.

 

  • 연산자 sizeof 는 배열의 전체 크기를 바이트 단위로 반환한다.
  • 요소 수를 결정하려면 총 바이트를 한 요소의 크기로 나눈다.
  • 배열이 함수에 전달되면 포인터 유형으로 분해(= decays )된다.
  • sizeof 연산자가 포인터에 적용되면 포인터 크기가 반환된다.
    (ex : x86에서는 4바이트, x64에서는 8바이트)

 

#include <iostream>

void func(int arr[], int length)
{
    // returns pointer size. not useful here.
    size_t test = sizeof(arr);

    for(int i = 0; i < length; ++i)
    {
        std::cout << arr[i] << " ";
    }
}

int main()
{
    int i[5]{ 1,2,3,4,5 };
    // sizeof(i) = total bytes
    int j = sizeof(i) / sizeof(i[0]);
    func(i,j);
}

 

특정 산술 연산은 non-const pointers에서 사용하여 다른 메모리 위치를 가리키게 할 수 있다.

포인터는 ++, +=, -=, -- 연산자를 사용하여 증가 및 감소된다.

이 기법은 배열에서도 역시 사용될 수 있으며 특히 입력되지 않은 데이터의 버퍼에 유용하게 사용될 수 있다.

 

참고로 고정 크기 배열은 프로그램의 스택 메모리에 만들어지고,

동적 배열은 힙 메모리에 만들어진다.

 

void type pointer의 크기 증가

 

 void * 는 1byte 크기만큼 증가한다. 

입력된 포인터는 가리키는 자료형의 크기만큼 증가한다.

 

 

 다음 예제는
windows에서 bitmap의 개별 픽셀에 접근하는데 포인터 산술을 사용하는 것을 보여준다.

 

 

++ plus ++

Bit Map이란?

디스플레이의 1도트 (dot)가 정보의 최소 단위인 1비트에 대응되는 것,
또는 그 화상 표현 방식이다.

 

< 예 제 >

 

#include <Windows.h>
#include <fstream>

using namespace std;

int main()
{
	BITMAPINFOHEADER header;
	header.biHeight = 100;
	header.biWidth = 100;
	header.biBitCount = 24;
	header.biPlanes = 1;
	header.biCompression = BI_RGB;
	header.biSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);

	constexpr int bufferSize = 30000;
	unsigned char* buffer = new unsigned char[bufferSize];

	BITMAPFILEHEADER bf;
	bf.bfType = 0x4D42;
	bf.bfSize = header.biSize + 14 + bufferSize;
	bf.bfReserved1 = 0;
	bf.bfReserved2 = 0;
	bf.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER); //54

	unsigned char* begin = &buffer[0];
	unsigned char* end = &buffer[0] + bufferSize;
	unsigned char* p = begin;
	constexpr int pixelWidth = 3;
	constexpr int borderWidth = 2;

    while (p < end)
    {
        // Is top or bottom edge?
        if ((p < begin + header.biWidth * pixelWidth * borderWidth)
            || (p > end - header.biWidth * pixelWidth * borderWidth)
            // Is left or right edge?
            || (p - begin) % (header.biWidth * pixelWidth) < (borderWidth * pixelWidth)
            || (p - begin) % (header.biWidth * pixelWidth) > ((header.biWidth - borderWidth) * pixelWidth))
        {
            *p = 0x0; // Black
        }
        else
        {
            *p = 0xC3; // Gray
        }
        p++; // Increment one byte sizeof(unsigned char).
    }

    ofstream wf(R"(box.bmp)", ios::out | ios::binary);

    wf.write(reinterpret_cast<char*>(&bf), sizeof(bf));
    wf.write(reinterpret_cast<char*>(&header), sizeof(header));
    wf.write(reinterpret_cast<char*>(begin), bufferSize);

    delete[] buffer; // Return memory to the OS.
    wf.close();
}

 

void* pointers

 

 void에 대한 포인터는 단순히 원시 메모리 위치를 가리킨다.

C++ 코드와 C 함수 사이를 전달할 때와 같이 void* 포인터를 사용해야 할 때도 있다.

입력된 포인터가 빈 포인터로 캐스트되면, 메모리 위치의 내용은 변경되지 않는다.

그러나 자료형 정보가 손실되므로 증가 또는 감소를 수행할 수 없다.

 

void pointer는 메모리 위치를 캐스트하는 것도 가능하다.

예를 들어, MyClass*에서 void*로, 다시 void에서 MyClass*로가 가능하다.

이러한 작업은 본질적으로 오류가 발생하기 쉬우며, 오류를 방지하기 위해 세심한 주의가 필요하기 때문에,

모던 C++에서는 거의 모든 상황에서 void*의 사용을 막는다.

 

 

//func.c
void func(void* data, int length)
{
    char* c = (char*)(data);

    // fill in the buffer with data
    for (int i = 0; i < length; ++i)
    {
        *c = 0x41;
        ++c;
    }
}

// main.cpp
#include <iostream>

extern "C"
{
    void func(void* data, int length);
}

class MyClass
{
public:
    int num;
    std::string name;
    void print() { std::cout << name << ":" << num << std::endl; }
};

int main()
{
    MyClass* mc = new MyClass{10, "Marian"};
    void* p = static_cast<void*>(mc);
    MyClass* mc2 = static_cast<MyClass*>(p);
    std::cout << mc2->name << std::endl; // "Marian"
    delete(mc);

    // use operator new to allocate untyped memory block
    void* pvoid = operator new(1000);
    char* pchar = static_cast<char*>(pvoid);
    for(char* c = pchar; c < pchar + 1000; ++c)
    {
        *c = 0x00;
    }
    func(pvoid, 1000);
    char ch = static_cast<char*>(pvoid)[0];
    std::cout << ch << std::endl; // 'A'
    operator delete(pvoid);
}

 

 

함수에 대한 포인터  ( = Pointers to function )

 

 C 스타일 프로그래밍에서 함수 포인터는 주로 함수를 다른 함수에 전달하는데 사용된다.

이러한 기법은 호출자에서 동작을 수정하지 않고 사용자 정의할 수 있게 한다.

모던 C++에서 람다에서 동일한 기능을 제공하며, 유형 안정성 및 기타 이점을 제공한다.

 

함수 포인터 선언은 가리키는 함수가 가져아하는 서명( = signature )을 지정한다.

 

// Declare pointer to any function that...

// ...accepts a string and returns a string
string (*g)(string a);

// has no return value and no parameters
void (*x)();

// ...returns an int and takes three parameters
// of the specified types
int (*i)(int i, string s, double d);

 

 

다음 예제에서는 std::string을 허용하고,

std::string을 반환하는 함수를 매개변수로 사용하는 함수 조합을 보여준다.

std::string에 전달된 함수에 따라 combine문자열 앞에 추가한다.

 

 

#include <iostream>
#include <string>

using namespace std;

string base {"hello world"};

string append(string s)
{
    return base.append(" ").append(s);
}

string prepend(string s)
{
    return s.append(" ").append(base);
}

string combine(string s, string(*g)(string a))
{
    return (*g)(s);
}

int main()
{
    cout << combine("from MSVC", append) << "\n";
    cout << combine("Good morning and", prepend) << "\n";
}

 

 

 

 

 

참조 문서

https://learn.microsoft.com/en-us/cpp/cpp/raw-pointers?view=msvc-170