5가지 메모리 관리 방법 비교
#include <array>
#include <vector>
int main()
{
// 1. C 정적 배열
int cArray[5] = {1, 2, 3, 4, 5};
// 스택, 컴파일 타임 크기, 자동 관리
// 2. C++ 동적 배열 (new/delete)
int* cppDynArray = new int[5]{1, 2, 3, 4, 5};
// 힙, 런타임 크기, 수동 할당/해제
// ✅ 타입 안전
// ❌ 메모리 누수 위험
delete[] cppDynArray;
// 3. std::array
std::array<int, 5> stdArray{1, 2, 3, 4, 5};
// 스택, 컴파일 타임 크기, 자동 관리
// ✅ 타입 안전, 크기 정보 유지
// 4. std::vector (권장!)
std::vector<int> vec{1, 2, 3, 4, 5};
// 힙, 런타임 크기, 자동 관리
// ✅ 동적 크기 조정
// ✅ 메모리 자동 해제
// ✅ 타입 안전
return 0;
}
메모리 할당 위치 정리
| 타입 | 메모리 위치 | 크기 결정 | 관리 방식 | 안전성 |
|---|---|---|---|---|
| C 배열 | 스택 | 컴파일 타임 | 자동 | ⚠️ |
| C++ 동적 배열 (new) | 힙 | 런타임 | 수동 | ⚠️ |
| std::array | 스택 | 컴파일 타임 | 자동 | ✅ |
| std::vector | 힙 | 런타임 | 자동 | ✅ |
왜 std::vector를 사용해야 하는가?
// ❌ C++ 동적 배열의 문제점
int* arr2 = new int[5];
// delete[] 깜빡하면 메모리 누수
// 크기 변경 불가
delete[] arr2;
// ✅ std::vector의 장점
std::vector<int> vec(5);
// 자동 메모리 관리
// 크기 정보 포함 (vec.size())
// 동적 크기 조정 (vec.push_back())
// 타입 안전
// RAII (자동 소멸)
std::array란?
std::array의 장점
std::array<int, 5> arr{1, 2, 3, 4, 5};
// 경계 검사
arr.at(10); // std::out_of_range 예외 발생
// 크기 확인
std::cout << arr.size() << std::endl; // 5
// 반복자 사용
for (auto it = arr.begin(); it != arr.end(); ++it)
{
std::cout << *it << " ";
}
// Range-based for
for (const auto& elem : arr)
{
std::cout << elem << " ";
}
std::array vs C 배열
// C 배열
int cArr[5];
// sizeof(cArr) == 20 (5 * 4)
// 함수에 전달 시 포인터로 decay
// 크기 정보 손실
void func(int arr[]) // 실제로는 int*
{
// sizeof(arr) == 8 (포인터 크기)
}
// std::array
std::array<int, 5> stdArr;
// sizeof(stdArr) == 20 (5 * 4)
// 함수에 전달 시 크기 정보 유지
void func(std::array<int, 5>& arr)
{
// sizeof(arr) == 20
// arr.size() == 5
}
메모리 레이아웃 차이
1. 다차원 배열 (스택, 연속된 메모리)
int arr[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// 메모리 레이아웃 (연속적):
// [1][2][3][4][5][6][7][8][9][10][11][12]
// ↑ 스택에 한 번에 할당
2. 다차원 벡터 (힙, 분산된 메모리)
std::vector<std::vector<int>> vec = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// 메모리 레이아웃 (비연속적):
// vec (스택):
// [ptr1][ptr2][ptr3]
// ↓ ↓ ↓
// 힙:
// [1][2][3][4] (첫 번째 행)
// [5][6][7][8] (두 번째 행)
// [9][10][11][12] (세 번째 행)
연속된 메모리를 사용하는 Wrapper
벡터를 사용하되 연속된 메모리를 유지하려면 1차원 벡터로 관리하고 인덱스 변환을 사용:
class Matrix
{
public:
Matrix(size_t rows, size_t cols)
: mRows{rows}, mCols{cols}, mData(rows * cols)
{
}
int& operator()(size_t row, size_t col)
{
return mData[row * mCols + col]; // 2D → 1D 인덱스 변환
}
const int& operator()(size_t row, size_t col) const
{
return mData[row * mCols + col];
}
private:
size_t mRows;
size_t mCols;
std::vector<int> mData; // 1차원 벡터로 연속 메모리 보장
};
int main()
{
Matrix mat(3, 4);
// 사용
mat(0, 0) = 1;
mat(0, 1) = 2;
mat(1, 0) = 5;
return 0;
}
캐시 라인 (Cache Line)
잘못된 루프 (Cache Miss 많음)
const size_t ROWS = 1000;
const size_t COLS = 1000;
int arr[ROWS][COLS];
// ❌ 열 우선 순회 (Column-major)
for (size_t col = 0; col < COLS; col++)
{
for (size_t row = 0; row < ROWS; row++)
{
arr[row][col] = 0; // 메모리 점프 발생!
}
}
메모리 접근 패턴:
arr[0][0] → arr[1][0] → arr[2][0] → ...
↓ ↓ (COLS * 4 bytes 떨어짐)
캐시 미스!
올바른 루프 (Cache Hit 많음)
// ✅ 행 우선 순회 (Row-major)
for (size_t row = 0; row < ROWS; row++)
{
for (size_t col = 0; col < COLS; col++)
{
arr[row][col] = 0; // 연속된 메모리 접근!
}
}
메모리 접근 패턴:
arr[0][0] → arr[0][1] → arr[0][2] → ...
↓ ↓ (4 bytes 떨어짐, 같은 캐시 라인)
캐시 히트!
핵심 규칙: Inner Loop는 Cache Line에 맞춰라
std::deque란?
std::deque vs std::vector
#include <deque>
#include <vector>
std::vector<int> vec;
vec.push_back(1); // 뒤에 추가: O(1) (amortized)
// vec.push_front(0); // ❌ vector는 push_front 없음
std::deque<int> deq;
deq.push_back(1); // 뒤에 추가: O(1)
deq.push_front(0); // ✅ 앞에 추가: O(1)
deque의 메모리 구조 (Chunk Array)
Deque:
┌─────────────────────┐
│ Chunk 포인터들 │ (중앙 배열)
│ [ptr1][ptr2][ptr3] │
└────┬─────┬─────┬────┘
↓ ↓ ↓
청크 청크 청크 (힙)
[▢▢▢▢][■■■■][▢▢▢▢]
↑
데이터
Vector의 재할당 vs Deque의 확장
Vector:
std::vector<int> vec{1, 2, 3, 4};
// 메모리: [1][2][3][4] (capacity: 4)
vec.push_back(5); // capacity 부족!
// → 새 공간 할당 (capacity: 8)
// → 기존 데이터 전체 복사/이동
// → 기존 공간 해제
Deque:
std::deque<int> deq{1, 2, 3, 4};
// Chunk1: [1][2][3][4]
deq.push_back(5); // 현재 청크가 가득 찬 경우
// → 새 청크만 할당
// → 기존 데이터는 그대로!
// Chunk1: [1][2][3][4]
// Chunk2: [5][▢][▢][▢]
Deque의 장점
1. push_front() / push_back() 모두 O(1)
2. 재할당 시 기존 데이터를 복사/이동하지 않음
3. Iterator invalidation이 적음 (중간 삽입 제외)
Deque의 단점
두 번의 포인터 역참조 필요
deq[i];
// 1. 어느 청크인지 계산
// 2. 청크 포인터 배열에서 청크 주소 읽기
// 3. 청크 내에서 오프셋 계산
// 4. 실제 데이터 접근
캐시 미스 가능성
메모리 오버헤드
성능 비교
// 랜덤 접근
vec[1000]; // O(1), 빠름 (한 번의 역참조)
deq[1000]; // O(1), 느림 (두 번의 역참조)
// 앞쪽 삽입
// vec.push_front() // 없음 (구현하려면 O(n))
deq.push_front(x); // O(1)
// 뒤쪽 삽입
vec.push_back(x); // O(1) amortized
deq.push_back(x); // O(1)
언제 Deque를 사용할까?
std::vector가 더 나음기본 차이
struct Student {
std::string name;
int age;
Student(std::string n, int a) : name(n), age(a) {
std::cout << "생성자 호출\n";
}
Student(const Student& other) : name(other.name), age(other.age) {
std::cout << "복사 생성자 호출\n";
}
Student(Student&& other) : name(std::move(other.name)), age(other.age) {
std::cout << "이동 생성자 호출\n";
}
};
std::vector<Student> v;
// push_back: 임시 객체 생성 → 이동 → 소멸
v.push_back(Student("Alice", 20));
// 출력: 생성자 호출 → 이동 생성자 호출
// emplace_back: 컨테이너 내부에서 직접 생성
v.emplace_back("Bob", 21);
// 출력: 생성자 호출 (끝)
언제 emplace_back이 확실히 유리한가?
1. 복사/이동이 비용이 큰 경우
struct BigData {
std::vector<int> data;
BigData(int size) : data(size, 42) {}
};
std::vector<BigData> v;
// push_back: 생성 → 이동 (vector 내부 데이터도 이동)
v.push_back(BigData(10000));
// emplace_back: 바로 생성 (이동 없음)
v.emplace_back(10000); // ✅ 더 효율적
2. 복사 생성자가 delete된 경우
struct NonCopyable {
NonCopyable(int x) {}
NonCopyable(const NonCopyable&) = delete;
NonCopyable(NonCopyable&&) = delete;
};
std::vector<NonCopyable> v;
// v.push_back(NonCopyable(42)); // ❌ 컴파일 에러
v.emplace_back(42); // ✅ 작동
3. 생성자 인자가 여러 개일 때 (코드 간결성)
struct Point3D {
double x, y, z;
Point3D(double x, double y, double z) : x(x), y(y), z(z) {}
};
std::vector<Point3D> points;
// push_back: 임시 객체 명시적 생성
points.push_back(Point3D(1.0, 2.0, 3.0));
// emplace_back: 인자만 전달 (더 간결)
points.emplace_back(1.0, 2.0, 3.0);
C++17 이후의 현실: Copy Elision
std::vector<Student> v;
// C++17 이후: 컴파일러가 임시 객체 생성을 최적화
v.push_back(Student("Alice", 20));
// → 실제로는 vector 내부에 바로 생성될 수 있음 (RVO)
v.emplace_back("Alice", 20);
// → 거의 동일한 코드로 컴파일됨
간단한 타입에서는 차이가 거의 없음:
std::vector<int> v;
v.push_back(42); // 성능 차이 없음
v.emplace_back(42); // 성능 차이 없음
주의사항: explicit 생성자
struct Widget {
explicit Widget(int x) {}
};
std::vector<Widget> v;
// v.push_back(42); // ❌ 컴파일 에러 (explicit 생성자)
v.emplace_back(42); // ✅ 작동 (의도치 않은 변환 가능)
실전 가이드
// ✅ emplace_back 사용 권장
std::vector<ComplexType> v;
v.emplace_back(arg1, arg2, arg3); // 생성자 인자 직접 전달
// ✅ push_back도 괜찮음 (C++17 이후)
std::vector<int> numbers;
numbers.push_back(42); // 간단한 타입은 가독성 우선
// ✅ 이미 생성된 객체는 push_back
Student s("Alice", 20);
v.push_back(std::move(s)); // 명시적 의도
결론
emplace_back 사용push_back도 OKstd::span이란?
기본 사용법
#include <span>
void print(std::span<int> data) // 배열, vector, array 모두 받음
{
for (int val : data)
{
std::cout << val << " ";
}
std::cout << std::endl;
}
int main()
{
// C 배열
int arr[] = {1, 2, 3, 4, 5};
print(arr);
// std::array
std::array<int, 5> stdArr{1, 2, 3, 4, 5};
print(stdArr);
// std::vector
std::vector<int> vec{1, 2, 3, 4, 5};
print(vec);
return 0;
}
std::span의 구조
template<typename T>
class span
{
T* ptr; // 데이터 시작 주소 (8 bytes)
size_t size; // 원소 개수 (8 bytes)
};
// sizeof(std::span<int>) == 16 bytes
메모리 레이아웃
Vector:
┌──────────┐
│ vec │ (스택)
│ ptr ─────┼───────→ [1][2][3][4][5] (힙)
│ size: 5 │
│ cap: 5 │
└──────────┘
Span:
┌──────────┐
│ span │ (스택)
│ ptr ─────┼───────→ [1][2][3][4][5] (vec의 데이터를 참조)
│ size: 5 │
└──────────┘
↑ 소유권 없음!
주의사항: Dangling Span
std::span<int> createSpan()
{
std::vector<int> vec{1, 2, 3, 4, 5};
return std::span{vec}; // ❌ 위험!
} // vec 소멸
int main()
{
auto sp = createSpan();
// sp는 이미 소멸된 vec의 메모리를 가리킴 (Dangling)
std::cout << sp[0] << std::endl; // ❌ Undefined Behavior
return 0;
}
재할당 시 문제
std::vector<int> vec{1, 2, 3};
std::span<int> sp{vec};
std::cout << sp[0] << std::endl; // ✅ 1
vec.push_back(4);
vec.push_back(5); // capacity 부족 → 재할당!
// sp는 여전히 옛날 메모리를 가리킴
std::cout << sp[0] << std::endl; // ❌ Undefined Behavior
안전한 사용법
void process(std::span<int> data)
{
// data 사용
for (int& val : data)
{
val *= 2;
}
} // span 소멸, 하지만 원본 데이터는 유지
int main()
{
std::vector<int> vec{1, 2, 3, 4, 5};
// ✅ 안전: process 내부에서만 사용
process(vec);
// vec는 여전히 유효
for (int val : vec)
{
std::cout << val << " "; // 2 4 6 8 10
}
return 0;
}
std::span의 장점
1. 통일된 인터페이스
효율적
부분 뷰 생성
std::vector<int> vec{1, 2, 3, 4, 5};
std::span<int> all{vec}; // 전체
std::span<int> first3 = all.first(3); // 처음 3개
std::span<int> last2 = all.last(2); // 마지막 2개
std::span<int> middle = all.subspan(1, 3); // 중간 3개
std::span vs const T&
// ❌ 배열은 받을 수 없음
void process(const std::vector<int>& data) { }
int arr[] = {1, 2, 3};
// process(arr); // 컴파일 에러
// ✅ 배열도 받을 수 있음
void process(std::span<int> data) { }
process(arr); // OK
배열과 컨테이너 선택 가이드
다차원 배열 vs 벡터
캐시 최적화
std::deque
push_back vs emplace_back
emplace_back 권장std::span (C++20)
코드없는 프로그래밍
C++ std::array
C++ std::deque
C++ std::span