C++ 고급 문법 (15) — new 연산자(new Expression)

new Widget(7)은 메모리를 할당하는 동시에 생성자를 호출한다. 이 한 줄 안에는 메모리를 얻는 operator new 함수 호출과 그 위에서 객체를 만드는 생성자 호출이 순서대로 들어 있다. 두 단계를 분리해서 보면 new[]와 delete를 섞으면 안 되는 이유, 할당 실패를 처리하는 방법, 이미 확보한 메모리에 객체만 만드는 placement new가 자연스럽게 이해된다. 이 글에서는 g++ 13.3과 AddressSanitizer로 new 표현식의 동작을 단계별로 확인한다. 소유권 관리는 스마트 포인터 편에서 다뤘다.

이 시리즈의 다른 글

new 표현식의 두 단계

#include <cstdio>
#include <cstdlib>
#include <new>

struct Widget {
    int id;
    Widget(int i) : id(i) { std::printf("  2. Widget(%d) constructor\n", id); }
    ~Widget()             { std::printf("  1. ~Widget(%d) destructor\n", id); }

    // 클래스 전용 할당 함수: new/delete 표현식이 이것을 호출한다
    static void* operator new(std::size_t n)
    {
        std::printf("  1. operator new(%zu)\n", n);
        return std::malloc(n);
    }
    static void operator delete(void* p)
    {
        std::printf("  2. operator delete\n");
        std::free(p);
    }
};

int main()
{
    std::puts("new Widget(7):");
    Widget* w = new Widget(7);       // 메모리 할당 -> 생성자
    std::puts("delete w:");
    delete w;                        // 소멸자 -> 메모리 해제
}
$ g++ -std=c++20 -Wall -o st steps.cpp
$ ./st
new Widget(7):
  1. operator new(4)
  2. Widget(7) constructor
delete w:
  1. ~Widget(7) destructor
  2. operator delete

new는 할당 → 생성, delete는 소멸 → 해제 순서다. 클래스에 operator new/operator delete를 정의하면 할당 단계만 바꿀 수 있어 메모리 풀이나 할당 통계에 쓴다.

구분하는 일바꿀 수 있나
new T(args) 표현식operator new 호출 + 생성자 호출불가
operator new(size) 함수원시 메모리 할당만전역·클래스별 교체 가능
delete p 표현식소멸자 호출 + operator delete 호출불가
placement new (ptr) T(args)할당 없이 생성자만–

초기화 형태

#include <cstdio>

int main()
{
    delete[] new int[4]{7, 7, 7, 7};   // 해제된 블록을 하나 만들어 둔다

    int* a = new int[4];      // 기본 초기화: 값이 정해지지 않음 (읽는 것 자체가 미정의 동작, 확인용)
    int* b = new int[4]();    // 값 초기화: 0
    int* c = new int[4]{1};   // 집합 초기화: {1, 0, 0, 0}
    std::printf("a: %x %x %x %x\n", a[0], a[1], a[2], a[3]);
    std::printf("b: %d %d %d %d\n", b[0], b[1], b[2], b[3]);
    std::printf("c: %d %d %d %d\n", c[0], c[1], c[2], c[3]);
    delete[] a; delete[] b; delete[] c;
}
$ g++ -std=c++20 -Wall -o in init.cpp
$ ./in
a: 26cc0318 6 0 0
b: 0 0 0 0
c: 1 0 0 0

괄호가 없으면 스칼라 원소는 초기화되지 않아 해제됐던 블록에 할당자가 남긴 값이 그대로 보인다. ()나 {}를 붙이면 0으로 채워진다.

new[]와 delete[]

#include <cstdio>
#include <cstdlib>
#include <new>

struct Item {
    int v = 0;
    ~Item() {}                         // 소멸자가 있으면 원소 개수를 따로 저장해야 한다
    static void* operator new[](std::size_t n)
    {
        std::printf("operator new[](%zu) for 4 Items of %zu bytes\n", n, sizeof(Item));
        return std::malloc(n);
    }
    static void operator delete[](void* p) { std::free(p); }
};

int main()
{
    Item* items = new Item[4];
    delete[] items;
}
$ g++ -std=c++20 -Wall -o ck cookie.cpp
$ ./ck
operator new[](24) for 4 Items of 4 bytes

4바이트 원소 4개인데 24바이트를 요청했다. 소멸자가 있는 타입의 배열은 delete[]가 소멸자를 몇 번 부를지 알아야 해서, 앞쪽 8바이트에 원소 개수(array cookie)를 저장한다.

#include <string>

int main()
{
    std::string* names = new std::string[3];
    delete names;                      // delete[] 이어야 한다
}
$ g++ -std=c++20 -Wall -g -fsanitize=address -o mm mismatch.cpp 2>&1 | grep -E 'warning' 
mismatch.cpp:6:12: warning: ‘void operator delete(void*, std::size_t)’ called on pointer returned from a mismatched allocation function [-Wmismatched-new-delete]
$ ./mm    # ASan 보고 중 에러 종류만 발췌
ERROR: AddressSanitizer: attempting free on address which was not malloc()-ed: 0x50b000000048
SUMMARY: AddressSanitizer: bad-free

new[]가 돌려준 주소는 cookie 뒤(할당 시작 +8바이트, 끝자리 48)라서, delete로 해제하면 할당 시작이 아닌 주소를 해제하게 된다. g++는 컴파일 단계에서도 -Wmismatched-new-delete로 경고한다.

할당 실패

#include <cstdio>
#include <new>

int main()
{
    std::size_t huge = std::size_t(1) << 46;          // 64 TiB

    try {
        char* p = new char[huge];                     // 기본: 실패하면 예외
        delete[] p;
    } catch (const std::bad_alloc& e) {
        std::printf("bad_alloc: %s\n", e.what());
    }

    char* q = new (std::nothrow) char[huge];          // nothrow: 실패하면 nullptr
    std::printf("nothrow result: %p\n", static_cast<void*>(q));
}
$ g++ -std=c++20 -Wall -o fl fail.cpp
$ ./fl
bad_alloc: std::bad_alloc
nothrow result: (nil)

기본 new는 실패하면 std::bad_alloc을 던지므로 결과를 nullptr과 비교하는 것은 의미가 없다. 반환값 검사 방식을 원하면 std::nothrow를 쓴다.

placement new

#include <cstdio>
#include <memory>
#include <new>
#include <string>

struct Packet {
    int seq;
    std::string payload;
    Packet(int s, std::string p) : seq(s), payload(std::move(p)) { std::printf("  Packet(%d)\n", seq); }
    ~Packet() { std::printf("  ~Packet(%d)\n", seq); }
};

int main()
{
    alignas(Packet) unsigned char buf[sizeof(Packet)];          // 미리 확보한 저장 공간

    Packet* p = new (buf) Packet(1, "hello");                   // placement new: 할당 없이 생성만
    std::printf("buf=%p p=%p payload=%s\n", static_cast<void*>(buf), static_cast<void*>(p), p->payload.c_str());
    p->~Packet();                                               // 소멸자는 직접 호출 (delete 금지)

    Packet* q = std::construct_at(reinterpret_cast<Packet*>(buf), 2, "world");   // C++20
    std::destroy_at(q);
}
$ g++ -std=c++20 -Wall -o pl placement.cpp
$ ./pl
  Packet(1)
buf=0x7fff3e2a9f00 p=0x7fff3e2a9f00 payload=hello
  ~Packet(1)
  Packet(2)
  ~Packet(2)

메모리는 이미 있으므로 delete가 아니라 소멸자를 직접 호출한다. C++20 std::construct_at/std::destroy_at은 같은 일을 함수 형태로 하고 constexpr에서도 쓸 수 있다.

정렬이 큰 타입의 new

#include <cstdint>
#include <cstdio>

struct alignas(64) CacheLine {        // 캐시 라인 정렬
    long counters[8];
};

int main()
{
    for (int i = 0; i < 3; ++i) {
        auto* c = new CacheLine{};    // C++17: operator new(size, align_val_t) 호출
        std::printf("addr %% 64 = %lu\n", reinterpret_cast<std::uintptr_t>(c) % 64);
        delete c;
        auto* x = new long[3];        // 정렬이 어긋나도록 다른 블록을 끼운다
        (void)x;
    }
}
$ g++ -std=c++20 -Wall -o al aligned.cpp
$ ./al
addr % 64 = 0
addr % 64 = 0
addr % 64 = 0
$ g++ -std=c++14 -Wall -o al14 aligned.cpp 2>&1 | grep warning | head -1
aligned.cpp:11:33: warning: ‘new’ of type ‘CacheLine’ with extended alignment 64 [-Waligned-new=]
$ ./al14
addr % 64 = 48
addr % 64 = 48
addr % 64 = 48

C++17부터 alignas(64) 타입의 new는 정렬을 인수로 받는 operator new를 호출해 64바이트 경계를 보장한다. -std=c++14에서는 경고와 함께 16바이트 정렬만 보장된다.

주의사항

상황문제대응
new[] + delete (또는 반대)잘못된 주소 해제, 소멸자 누락배열은 std::vector나 unique_ptr<T[]>
new 결과를 nullptr 검사실패 시 예외라 검사가 무의미bad_alloc을 잡거나 std::nothrow
new T[n] 원소 미초기화쓰레기 값 사용new T[n]() 또는 std::vector<T>(n)
placement new 객체를 delete할당하지 않은 메모리 해제소멸자 직접 호출 또는 std::destroy_at
placement new 버퍼 정렬 누락정렬 위반, 일부 아키텍처에서 크래시alignas(T) 버퍼
C++14 이하에서 alignas > 16 타입 new정렬이 보장되지 않음C++17 이상으로 빌드

마무리

  • new 표현식은 operator new로 할당한 뒤 생성자를 부르고, delete는 그 역순이다.
  • 소멸자가 있는 배열은 원소 개수를 앞에 저장하므로 new[]/delete[] 짝이 맞아야 한다.
  • placement new는 할당 없이 생성만 하며, 정리는 소멸자 직접 호출로 한다.

참고

답글 남기기