본문 바로가기

Programming/c++

c++ 메모리 할당 new 키워드와 delete 키워드(AI와 개념서적 내용이 섞임)

NEW 키워드에 함수를 붙이는것과 배열을 붙이는것의 차이점?

생성자를 NEW 키워드로

ItemManager* ptr = new ItemManager(); 
new 키워드는 메모리 (힙 영역)에 공간을 만든 후, 그 공간의 내용물을 주는 게 아니라
그 공간이 어디인지 알려주는 주소값을 반환



1. new type

2. new type(초기값)
int* ptr = nullptr;
ptr = new int(12) == *ptr = 12;
3.new type[개수]

int* ptr;
ptr = new int[7] 28byte;


*(ptr+1) , ptr[0]

 

 

결론부터 말씀드리면, 현대 C++에서 new는 메모리 할당에 실패하면 0을 리턴하는 것이 아니라 예외(std::bad_alloc)를 던지는 것이 기본 동작입니다.

질문하신 내용에는 C 언어 시절의 습관과 C++의 발전 과정이 섞여 있는데, 이를 명확히 정리해 드릴게요.


1. 0과 nullptr은 사실상 같습니다

C++11 이전에는 NULL을 사용했고, 이 NULL은 내부적으로 정수 0으로 정의되어 있었습니다.

  • 0 리턴: 과거 방식에서 "주소값이 0이다"라는 말은 "가리킬 메모리가 없다"는 뜻으로 통용되었습니다.
  • nullptr 리턴: 현대 C++에서는 0이 정수인지 포인터인지 헷갈리는 문제를 막기 위해 포인터 전용 상스인 nullptr을 사용합니다.
  • 중요: 시스템이 0을 리턴한다면, 그것은 곧 nullptr을 리턴하는 것과 동일한 의미로 해석됩니다.

2. 하지만 현대 C++의 new는 리턴을 하지 않습니다

일반적인 new 사용 시 메모리가 부족하면, 프로그램은 0이나 nullptr을 리턴하며 조용히 넘어가지 않습니다.

C++
 
ItemManager* ptr = new ItemManager(); // 메모리 부족 시 std::bad_alloc 예외 발생!
  • 예외 발생: 메모리 할당에 실패하면 즉시 **std::bad_alloc**이라는 예외를 던져 프로그램을 중단시키거나 try-catch 문으로 보내버립니다.
  • 이유: 할당 실패를 확인하지 않고 ptr->addItem() 같은 코드를 실행해서 프로그램이 죽는(Crash) 것을 원천 봉쇄하기 위해서입니다.

3. "나는 예외 말고 nullptr을 받고 싶어" (nothrow)

만약 질문하신 것처럼 예외 대신 nullptr을 리턴받아 if문으로 체크하고 싶다면, 특별한 옵션을 주어야 합니다.

C++
 
#include <new> // std::nothrow를 위해 필요

// 메모리 할당 실패 시 예외를 던지지 않고 nullptr을 리턴함
ItemManager* ptr = new(std::nothrow) ItemManager(); 

if (ptr == nullptr) {
    // 메모리 할당 실패 대응 로직
    std::cout << "메모리가 부족합니다!" << std::endl;
}

요약: 왜 0이나 nullptr이 언급될까요?

  1. C언어(malloc)의 영향: C언어의 malloc은 실패 시 항상 NULL(0)을 리턴했기 때문에 그 습관이 남아있는 것입니다.
  2. 구식 컴파일러: 아주 오래된 C++ 컴파일러는 new 실패 시 0을 리턴하기도 했습니다.
  3. 현대 표준: 기본적으로 예외를 던지며, std::nothrow를 쓸 때만 **nullptr**을 리턴합니다.

결론: 0이 리턴된다는 것은 아주 옛날 방식이거나 NULL을 의미하는 것이며, 최신 코드에서는 **nullptr**을 체크하거나 **try-catch**를 사용하는 것이 가장 안전합니다.

지금 공부하시는 환경이 혹시 아주 예전 C++ 교재인가요? 그렇다면 교재에서 0과 NULL을 혼용해서 설명하고 있을 가능성이 큽니다! 다음으로 어떤 부분을 더 알아볼까요?

 

 

현대 C++에서는 new가 실패했을 때 nullptr을 체크하는 방식보다 더 강력하고 자동화된 두 가지 방식을 주로 사용합니다.

가장 핵심적인 변화는 **"실패를 무시할 수 없게 만드는 것"**입니다.


1. 예외 처리 (Exception Handling)

현대 C++의 new는 메모리 할당에 실패하면 **std::bad_alloc**이라는 예외를 던집니다. 만약 개발자가 아무런 처리를 하지 않으면 프로그램은 그 자리에서 안전하게 종료됩니다. (쓰레기 값으로 동작하다가 나중에 터지는 것보다 훨씬 안전합니다.)

이를 직접 제어하고 싶다면 try-catch 블록을 사용합니다.

C++
 
#include <iostream>
#include <new> // std::bad_alloc을 위해 필요

try {
    // 아주 큰 메모리를 할당하려고 시도
    ItemManager* ptr = new ItemManager[1000000000]; 
} 
catch (const std::bad_alloc& e) {
    // 할당 실패 시 이 블록으로 즉시 점프함
    std::cerr << "메모리 할당 실패: " << e.what() << std::endl;
    // 로그를 남기거나, 불필요한 메모리를 해제하는 등의 복구 작업 수행
}

2. 스마트 포인터 (Smart Pointers)

현대 C++ 개발자들은 new와 delete를 직접 쓰는 것조차 지양합니다. 대신 **std::unique_ptr**나 **std::shared_ptr**를 사용합니다.

C++
 
#include <memory>

// std::make_unique는 내부적으로 new를 호출하고, 실패 시 예외를 던집니다.
auto manager = std::make_unique<ItemManager>();

// 함수가 끝나면 delete를 안 해도 자동으로 메모리가 해제됩니다!
  • 장점: delete를 깜빡할 일이 없고, 예외가 발생하더라도 메모리 누수 없이 깔끔하게 정리됩니다.

3. std::nothrow (고전 방식의 현대적 수용)

만약 임베디드 환경이나 성능상의 이유로 예외를 쓰고 싶지 않다면, 앞서 말씀드린 nothrow를 명시적으로 사용합니다. 이때는 nullptr이 반환되므로 반드시 체크해야 합니다.

C++
 
ItemManager* ptr = new(std::nothrow) ItemManager();
if (!ptr) {
    // nullptr 처리 (고전적 방식)
}

요약: 현대 C++의 처리 흐름

방식 특징 추천 상황
기본 new std::bad_alloc 예외 발생 일반적인 응용 프로그램
스마트 포인터 가장 권장됨. 자동 메모리 관리 포함 현대 C++ 표준 설계
nothrow new nullptr 반환 예외를 쓸 수 없는 특수 환경(커널, 임베디드)

결론적으로, 현대 C++에서는 std::make_unique 같은 스마트 포인터를 써서 메모리 할당과 해제를 시스템에 맡기는 것이 가장 세련된 방법입니다. 혹시 방금 만든 ItemManager를 스마트 포인터로 관리하는 코드를 한번 짜볼까요? 방법이 아주 간단해서 놀라실 거예요!

 

 

 

delete

delete 포인터 변수
int* ptr;
ptr = new int;
delete ptr;

delete [] 포인터 변수
int* ptr;
ptr = new int[12];
delete[] ptr;

 

 

변수 -> 기능 : 값 저장.
상수 , 메모리 주소를 저장하지 갈라짐


배열의 특징
연속된 메모리
같은 고정 개수
같은 고정된 변수 이름
같은 고정된 데이터 타입

int(*ptr)[3] 열이 3개인 배열의 메모리 주소를 저장하는 변수

구조체 포인터는 


이중포인터를 이용하면 행과 열을 다르게 생성가능

int **ptr;
ptr = new int*[3] //곱하기가 아니라 데이터 타입

int **ptr = new int*[3] //2차원 배열에 0,1,2 3행을 할당
int*ptr[3]

이중 포인터는 메모리 해제를 행마다 해주고 있음

delete [] ptr[0]
delete [] ptr[1]
delete [] ptr[2]
delete [] ptr;

 


int*ptr[3]


for(int i =0; i < (sizeof(ptr)/sizeof(int*);i++)
{
delete[] ptr[i];

}

 

sizeof(int(*Ptr)[3]) 8바이트


Gemini의 응답
결론부터 말씀드리면, 대부분의 경우 개발자가 따로 호출할 필요가 없습니다. C++의 핵심 철학 중 하나인 RAII(Resource Acquisition Is Initialization) 덕분에 객체의 생명주기가 끝나면 소멸자(Destructor)는 자동으로 호출됩니다.

하지만 "언제" 호출되는지는 객체가 어디에 선언되었느냐에 따라 다릅니다.

1. 지역 객체 (Stack 영역)
가장 일반적인 경우입니다. { } (중괄호) 블록 안에 선언된 객체는 그 블록을 벗어나는 순간 소멸자가 즉시 실행됩니다.

C++
int main() {
    {
        ItemManager manager; // 생성자 호출
        // ... 작업 수행 ...
    } // <--- 여기서 manager의 소멸자가 자동으로 호출됨! (블록 끝)

    std::cout << "아직 main 함수는 종료되지 않았습니다.\n";
    return 0; 
} // 만약 위 블록이 없다면 여기서 호출됨




2. 동적 할당 객체 (Heap 영역) - 주의!
new 키워드를 사용하여 직접 메모리를 할당한 경우에는 이야기가 다릅니다. 이들은 직접 delete를 해줄 때만 소멸자가 호출됩니다.

C++
int main() {
    ItemManager* ptr = new ItemManager(); // 생성자 호출

    // ... 작업을 마친 후 ...
    delete ptr; // <--- 이 코드를 써야만 소멸자가 호출됨! (수동 호출)

    return 0;
}
위험: 만약 delete를 깜빡하고 main 함수가 종료되면, 소멸자는 영원히 호출되지 않고 메모리만 차지하는 메모리 누수가 발생합니다.

거기서 헷갈리면 안 돼! new는 std::bad_alloc을 '리턴'하는 게 아니라 '던지는(Throw)' 거야. 이 둘은 프로그래밍에서 완전히 다른 개념이거든.

머릿속에 쏙쏙 박히도록 '리턴'과 '예외 던지기'의 차이를 확실히 뽀개줄게!


1. 리턴(Return) vs 던지기(Throw)

택배를 예로 들어볼까?

  • 리턴 (Return): 택배 기사님이 벨을 누르고 ", 여기 물건(주소값)이에요" 하고 전해주는 거야. 물건이 없으면 "미안, 품절(nullptr)이야"라고 말해주지. 그 대답을 듣고 if문으로 판단하면 돼.
  • 던지기 (Throw): 기사님이 오다가 사고가 났어. 그럼 물건을 전해주는 게 아니라 **"비상사태!!(Exception)"**라고 소리를 지르면서 하던 일을 다 멈춰버리는 거야. 미리 try-catch라는 '그물'을 쳐놓지 않으면, 프로그램은 그 비명소리에 놀라서 그냥 죽어버려.

2. 왜 std::bad_alloc은 리턴값이 아닐까?

new 연산자가 성공하면 **할당된 메모리의 주소(포인터)**를 줘야 하잖아?

  • 성공 시 리턴 타입: void* (또는 해당 객체 포인터)
  • 실패 시 던지는 타입: std::bad_alloc (객체 그 자체)

만약 new가 std::bad_alloc을 리턴한다고 치면, 코드에서 int* ptr = new int;라고 썼을 때 ptr에 주소가 들어올지 에러 객체가 들어올지 타입이 안 맞아서 컴파일조차 안 될걸? 그래서 현대 C++은 성공하면 주소를 주고, 실패하면 아예 다른 통로(예외)로 에러를 던지는 방식을 택한 거야.


3. 한눈에 비교하는 malloc vs new

구분 malloc (C 스타일) new (C++ 스타일)
실패 시 행동 nullptr를 리턴 std::bad_alloc 예외를 던짐
체크 방법 if (ptr == NULL) try { ... } catch (std::bad_alloc& e)
장점 처리가 단순하고 빠름 생성자를 호출해주고, 에러 처리가 더 강력함