요즘은 C++에서 로우레벨 메모리 연산은 가급적 피하고 컨테이너 또는 스마트 포인터와 같은 최신 기능을 활용하는 추세이다. 하지만, 메모리의 내부 처리 과정에 대해서는 알아둘 필요가 있다.
// ptr 변수를 스택에 생성 -> nullptr로 초기화 -> ptr가 동적으로 생성된 힙 메모리를 가리킴
int * ptr = new int;
주의할 점
- 항상 포인터 변수를 선언하자마자 nullptr나 적절한 포인터 변수로 초기화
- 초기화 하지 않은 상태로 냅두지 않는다.
// 정수 포인터에 대한 포인터 선언
int** handle = nullptr;
// 정수 포인터를 담는데 충분한 크기로 메모리 할당후 그 메모리에 대한 포인터를 handle에 저장
handle = new int*;
// *handle에 정수를 담기 충분한 크기의 힙 메모리를 동적으로 할당
*handle = new int;
// int를 담을 공간만큼 메모리 누수가 발생하는 예
voide leaky() {
new int; // 메모리 누수 발생
cout << "메모리 누수 발생!" endl;
}
// 힙 메모리 해제
int* ptr = new int;
delete ptr;
ptr = nullptr;
주의할 점
- new로 메모리를 할당할 때 스마트 포인터가 아닌 일반 포인터로 저장했다면 반드시 그 메모리를 해제하는 delete 문을 new와 짝을 이루도록 한다.
- 해제한 포인터는 nullptr로 다시 초기화 한다.
// Foo 객체 생성 예시
Foo* myFoo = (Foo*)malloc(sizeof(Foo));
Foo* myOtherFoo = new Foo();
C++ 에서는 malloc() free()를 사용하지말고 new와 delete 를 사용하자!
// 익셉션이 발생하지 않는 new
int* ptr = new(nothrow) int;
int arr[5];
int* myArrPtr = new int[5];
delete [] myArrPtr;
myArrPtr = nullptr;
Document* createDocArray() {
size_t numDocs = askUserForNumberOfDocuments();
Document* doArray = new Document[numDocs];
return docArray;
}
class Simple{
public:
Simple() { cout << "Simple constructor called" << endl; }
~Simple() { cout << "Simple destructor called" << endl; }
};
Simple* mySimpleArr = new Simple[4]; // 위의 생성자 4번 호출
Simple* mySimpleArr = new Simple[4];
// mySimpleArr 사용
delete [] mySimpleArr;
mySimpleArr = nullptr;
const size_t size = 4;
Simple**mySimplePtrArr = new Simple*[size];
// 포인터마다 객체 할당
for (size_t i = 0; i < size; i++) { mySimplePtrArr[i] = new simple(); }
// mySimplePtrArr 사용
// 할당된 객체를 삭제
for (size_t i = 0; i < size; i ++ ) { delete mySimplePtrArr[i]; })
// 배열 삭제
delete [] mySimplePtrArr;
mySimplePtrArr = nullptr;
char board[3][3] = {};
// 테스트 코드
board[0][0] = 'X';
board[2][1] = 'O';
- 다차원 스택 배열
- 다차원 배열의 크기는 각 차원의 크기를 서로 곱한 값에 한 원소의 메모리 크기를 곱한 값과 같다.
- 단, 3차원을 넘어가면 잘 사용하지 않음.
- 다차원 힙 배열
- 차원 수를 실행 시간에 결정하고 십다면 힙 배열로 생성!
- 힙에서는 메모리 공간이 연속적으로 할당되지 않기 때문에 스택 방식의 다차원 배열처럼 메모리를 할당하면 안 된다.
// 이차원 배열을 동적으로 할당하는 예
char** allocateCharacterBoard(size_t xDimension, size_t yDimension){
char** myArr = new char*[xDimension]; // 첫 번째 차원의 배열 할당
for (size_t i = 0; i < xDimension; i++){
myArr[i] = new char[yDimension]; // i번째 하위 배열 할당
}
return myArr;
}
// 다차원 힙 매열을 해제하는 예
void releaseCharacterBoard(char** myAyy, size_t xDimension){
for (size_t i = 0; i < xDimension; i++) {
delete [] my Arr[i]; // i번째 하위 배열을 해제
}
delete [] myArr; // 첫 번째 차원의 배열 해제
}
// 메모리 주소 7에 대한 포인터 생성
char* dangerPtr = (chat*)7;
Document* documentPtr = getDocument();
char* myCharPtr = static_cast<char*>(documentPtr); // 컴파일 에러
// 0으로 초기화한 스택 배열을 만들고 접근하는 예시
int myIntArr[10];
int* myIntPtr = myIntArr;
// 포인터로 배열 접근
myIntPtr[4] = 5;
// 이 함수는 스택 배열을 전달해도 Ok, 힙 배열을 전달해도 Ok
void doubleInts(int* theArr, size_t size) {
for (size_t i = 0; i < size; i++) {
theArr[i] *= 2;
}
}
//
size_t arrSize = 4;
int* heapArr = new int[arrSize]{1, 5, 3, 4};
doubleInts(heapArr, arrSize);
delete [] heapArr;
heapArr = nullptr;
int stackArr[] = {5, 7, 9, 11} ;
arrSize = std::size(stackArr); // <array> 사용
// arrSize = sizeof(stackArr) / sizeof(stackArr[0]) // C++17 이전 방식
doubleInts(stackArr, arrSize);
doubleInts(&stackArr[0], arrSize);
// 포인터가 아닌 배열 매개변수를 받더라도 원본배열 변경
void doubleInts(int theArr[], size_t size){
for (size_t i =0; i < size; i++){
thrArr[i] *= 2;
}
}
// 아래의 세 표현 방식은 모두 같음
void doubleInts(int* theArr, size_t inSize);
void doubleInts(int theArr[], size_t inSize);
void doubleInts(int theArr[2], size_t inSize);
int* ptr = new int;
모든 배열은 포인터로 참조가 가능하지만, 반대로 모든 포인터가 배열은 아니다!
// int 타입의 힙 배열 선언
int* myArr = new int[8];
// index 2에 값 넣기
myArr[2] = 333;
// 포인터 연산으로 표현
*(myArr + 2) = 333;
<memory>
헤더 파일을 인클루드 해야 함.// Simple 객체를 힙에 할당한 뒤, 해제하지 않고 끝내서 메모리 누수 현상 발생
void leaky() {
Simple* mySimplePtr = new Simple(); // 메모리를 해제하지 않음
mySimplePtr->go();
}
// Simple 객체를 동적으로 할당, 사용 후 delete 호출
// 이 경우에도 메모리 누수 발생 가능성 있음
void couldBeLeaky() {
Simple* mySimplePtr = new Simple();
mySimplePtr->go();
delete mySimplePtr;
}
// make_unique() & auto 키워드 적용
void notLeaky(){
auto MySimpleSmartPtr = make_unique<Simple>();
mySimpleSmartPtr->go();
}
unique_ptr를 생성할 때, 항상 make_unique()를 사용하자.
(*mySimpleSmartPtr).go();
// get() 메서드 이용 예시
void processData(Simple*simple) { /* do something*/ }
auto mySimpleSmartPtr = make_unique<Simple>();
processData(mySimpleSmartPtr.get());
// reset()을 사용하면 unique_ptr의 내부 포인터해제
mySimpleSmartPtr.reset(); // 리소스 해제 후 nullptr로 초기화
mySimpleSmartPtr.reset(new simple()); // 리소스 해제 후 새로운 Simple 인스턴스 설정
// release()를 사용하면 unique_ptr와 내부 포인터의 관계를 끊을 수 있음.
Simple* simple = mySimpleSmartPtr.release(); // 소유권 해제
// simple 포인터 사용 ..
delete simple;
simple = nullptr;
auto myVariableSizedArray = make_unique<int[]>(10);
// unique_ptr 방식 변경
int* mallock_int(int value){
int* p = (int*)malloc(sizeof(int));
*p = value;
return p;
}
int main() {
unique_ptr<int, decltype(free*)> myIntSmartPtr(malloc_int(42), free);
return 0;
}
auto mySimpleSmartPtr = make_shared<Simple>();
// shared_ptr로 파일 포인터를 저장하는 예시
void CloseFile(FILE* filePtr){
if (filePtr == nullptr)
return;
fclose(filePtr);
cout << "File closed." << endl;
}
int main() {
FILE* f = fopen("data.txt", "w");
shared_ptr<FILE> filePtr(f, CloseFile);
if (filePtr == nullptr){
cerr << "Error opening file." << endl;
} else {
cout << "File opened." << endl;
// filePtr를 사용 ...
}
return 0;
}
class Foo {
public:
Foo(int value) : mData(value) {}
int mData;
};
auto foo = make_shared<Foo>(42);
auto aliasing = shared_ptr<int>(foo, &foo->mData);
shared_ptr가 가리키는 리소스의 레퍼런스를 관리하는데 사용
리소스를 직접 소유하지 않음 -> shared_ptr가 해당 리소스를 해제하는데 영향을 미치지 않음
삭제될 때, 가리키던 리소스를 삭제하지 않고 shared_ptr가 리소스를 해제했는지 알아낼 수 있음.
생성자는 shared_ptr나 다른 weak_ptr를 인수로 받음
weak_ptr 포인터에 접근하려면 shared_ptr로 변환!
// weak_ptr 사용 예제
void useResource(weak_ptr<Simple>& weakSimple){
auto resource = weakSimple.lock();
if (resource) {
cout << "Resource still alive." << endl;
} else {
cout << "Resource has been freed!" << endl;
}
}
int main() {
auto sharedSimple = make_shared<Simple>();
weak_ptr<Simple> weakSimple(SharedSimple);
// use weak_ptr
useResource(weakSimple);
// shared_ptr 리셋
// Simple 리소스에 대한 shared_ptr는 하나뿐
// weak_ptr가 살아 있더라도 리소스 해제
sharedSimple.reset();
// weak_ptr를 한 번 더 사용.
useResource(weakSimple);
return 0;
}
- Simple constructor called!
- Resource still alive.
- Simple destructor called!
- Resource has been freed!
unique_ptr<Simple> create(){
auto ptr = make_unique<Simple>();
// ptr 사용 ...
return ptr;
}
int main() {
unique_ptr<Simple> mySmartPtr1 = create();
auto mySmartPtr2 = create();
return 0;
}
제공하는 메서드
- shared_from_this() : 객체의 소유권을 공유하는 shared_ptr 리턴
- weak_from_this() : 객체의 소유권을 추적하는 weak_ptr 리턴
class Foo : public enable_shared_from_this<Foo>{
public:
shared_ptr<Foo> getPointer() {
return shared_from_this();
}
};
int main() {
//make_shared()로 Foo 인스턴스를 담은 shared_ptr인 ptr1을 생성
auto ptr1 = make_shared<Foo>();
auto ptr2 = ptr1 -> getPointer();
}
// 과소 할당 예시
char buffer[1024] = {0}; // 버퍼 공간 확보
while (1) {
char* nextChunk = getMoreData();
if (nextChunk == nullptr) {
break;
} else {
strcat(buffer, nextChunk); // 버퍼 공간이 넘칠 수 있음.
delete [] nextChunk;
}
}
// 스트링의 모든 문자를 'm'으로 바꾸는 함수를 호출하면 루프의 종료 조건을 만족하지 못함.
// 스트링에 할당된 공간을 지나도 계속 'm'으로 채움
void fillWithM(char* inStr){
int i = 0;
while (inStr[i] != '\0') {
inStr[i] = 'm';
i++;
}
}
string, vector 와 같이 메모리 관리 기능을 제공하는 C++ 기능을 활용하도록 하자!
class Simple {
public:
Simple() { mIntPtr = new Int(); }
~Simple() { delete mIntPtr; }
void setValue(int value) { *mIntPtr = value; }
private:
int* mIntPtr;
};
void doSomething(Simple*& outSimplePtr){
outSimplePtr = new Simple(); // 원본 객체를 삭제하지 않음!
}
int main() {
Simple* simplePtr = new Simple(); // Simple 객체 할당
doSomething(simplePtr);
delete simplePtr; // 두 번째 객체만 해제
return 0;
}
위의 예제는 메모리 누수 현상을 재현하기 위한 목적으로 작성함. 실전에서는 mIntPtr와 simplePtr를 unique_ptr로 만들고 outSimplePtr를 unique_ptr에 대한 레퍼런스로 만들자.
#define _CRTDBG_MAP_ALLOC
#include <cstdlib>
#include <crtdbg.h>
#ifdef _DEBUG
#ifndef DBG_NEW
#define DBG_NEW new ( _NORMAL_BLOCK, __FILE__, __LINE__ )
#define new DBG_NEW
#endif
#endif // _DEBUG
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
메모리 누수 현상이 발생하지 않도록 최대한 스마트 포인터를 적용하도록 하자.
댕글링 포인터(dangling pointer)
이때, 중복 삭제를 하면 문제가 발생함
한 포인터에 delete를 두 번 적용하면 이미 다른 객체를 할당한 메모리를 해제하기 때문임
메모리 누수 감지 기능을 제공하는 도구는 중복 삭제 문제와 해제된 객체를 계속 사용하는 문제를 감지하는 기능도 함께 제공
동적 메모리에 관련된 문제를 방지하기 위한 두 가지 사항
C 스타일 구문, 함수를 최대한 피하고 C++ 구문을 사용하도록 하자