new Widget(7)은 메모리를 할당하는 동시에 생성자를 호출한다. 이 한 줄 안에는 메모리를 얻는 operator new 함수 호출과 그 위에서 객체를 만드는 생성자 호출이 순서대로 들어 있다. 두 단계를 분리해서 보면 new[]와 delete를 섞으면 안 되는 이유, 할당 실패를 처리하는 방법, 이미 확보한 메모리에 객체만 만드는 placement new가 자연스럽게 이해된다. 이 글에서는 g++ 13.3과 AddressSanitizer로 new 표현식의 동작을 단계별로 확인한다. 소유권 관리는 스마트 포인터 편에서 다뤘다.
이 시리즈의 다른 글
- C++ 고급 문법 (13) — 스마트 포인터(Smart Pointer)
- C++ 고급 문법 (14) — 형변환(Conversion)
- C++ 고급 문법 (16) — protected 생성자(protected Constructor)
- C++ 고급 문법 (17) — private 상속(private Inheritance)
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는 할당 없이 생성만 하며, 정리는 소멸자 직접 호출로 한다.