포인터
포인터란?
포인터는 개체의 메모리 주소를 저장하는 변수이다.
포인터는 세 가지 기본 목적으로 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
'프로그래밍 언어 > C & C++ 정리' 카테고리의 다른 글
const 및 volatile 포인터 (0) | 2023.10.29 |
---|---|
volatile (0) | 2023.10.29 |
부동소수점 (0) | 2023.06.20 |
포인터 & 함수 포인터 & 깊은 복사 & 얕은 복사 & 생성자 & 오버로딩 & explicit & 형변환 연산자 & R-value , L-value (0) | 2023.06.12 |
템플릿의 특수화 (0) | 2023.05.18 |