C++ 배열 vs vector | "어느 게 나을까?" 성능과 안전성 비교
이 글의 핵심
C++ 배열 vs vector에 대한 실전 가이드입니다.
들어가며: “배열을 써야 할까, vector를 써야 할까?"
"배열이 더 빠르다고 들었는데 vector를 쓰라고 하네요”
C++는 C 스타일 배열, std::array, std::vector 세 가지 배열 타입을 제공합니다. 각각 메모리 위치, 크기 변경 가능 여부, 안전성이 다릅니다.
비유로 말씀드리면, C 배열·std::array는 자리 수가 정해진 고정 좌석, vector는 필요하면 줄을 늘리는 가변 좌석에 가깝습니다. 크기가 런타임에 바뀌면 vector 쪽이 자연스럽습니다.
언제 고정 배열(std::array/C 배열)을, 언제 vector를 쓰나요?
| 관점 | 고정 크기(스택·std::array 등) | vector |
|---|---|---|
| 성능 | 스택 할당은 힙보다 가벼울 수 있음(작을 때) | 재할당·용량 관리 비용이 있으나 크기 가변 |
| 사용성 | 크기가 컴파일 타임 상수일 때 단순 | push_back 등으로 동적 확장 |
| 적용 시나리오 | 작은 버퍼, 행렬 크기 고정 | 입력 개수를 모를 때, 컨테이너로서 STL과 연동 |
// C 스타일 배열 (스택, 고정 크기)
int arr1[5] = {1, 2, 3, 4, 5};
// std::array (스택, 고정 크기, 안전)
std::array<int, 5> arr2 = {1, 2, 3, 4, 5};
// std::vector (힙, 동적 크기, 안전)
std::vector<int> vec = {1, 2, 3, 4, 5};
이 글에서 다루는 것:
- 배열, std::array, vector의 차이
- 성능 비교 (벤치마크)
- 메모리 안전성
- 상황별 선택 가이드
목차
1. 3가지 배열 타입 비교
비교표
| 항목 | C 배열 | std::array | std::vector |
|---|---|---|---|
| 메모리 | 스택 | 스택 | 힙 |
| 크기 | 고정 (컴파일 타임) | 고정 (컴파일 타임) | 동적 (런타임) |
| 범위 체크 | 없음 | at() 제공 | at() 제공 |
| 크기 조회 | sizeof/수동 | size() | size() |
| STL 호환 | 부분적 | 완전 | 완전 |
| 함수 전달 | 포인터로 decay | 값 또는 참조 | 참조 |
| 안전성 | 낮음 | 높음 | 높음 |
C 스타일 배열
int arr[5] = {1, 2, 3, 4, 5};
// ❌ 범위 체크 없음
arr[10] = 99; // 미정의 동작
// ❌ 크기 조회 번거로움
size_t size = sizeof(arr) / sizeof(arr[0]);
// ❌ 함수 전달 시 크기 정보 손실
void foo(int arr[]) { // int* 로 decay
// sizeof(arr)는 포인터 크기 (8바이트)
}
std::array (C++11)
#include <array>
std::array<int, 5> arr = {1, 2, 3, 4, 5};
// ✅ 범위 체크
arr.at(10); // 예외 발생: std::out_of_range
// ✅ 크기 조회
size_t size = arr.size(); // 5
// ✅ STL 알고리즘
std::sort(arr.begin(), arr.end());
// ✅ 함수 전달 (크기 정보 유지)
void foo(const std::array<int, 5>& arr) {
std::cout << arr.size() << '\n'; // 5
}
std::vector
#include <vector>
std::vector<int> vec = {1, 2, 3, 4, 5};
// ✅ 동적 크기 변경
vec.push_back(6);
vec.resize(10);
// ✅ 범위 체크
vec.at(10); // 예외 발생
// ✅ 자동 메모리 관리
// 소멸 시 자동 해제
2. 성능 벤치마크
테스트 1: 접근 속도
// 100만 번 접근
template <typename Container>
void benchAccess(Container& c) {
long long sum = 0;
for (int i = 0; i < 1000000; ++i) {
sum += c[i % c.size()];
}
}
결과:
| 타입 | 시간 | 상대 속도 |
|---|---|---|
| C 배열 | 2.1ms | 1.0x (기준) |
| std::array | 2.1ms | 1.0x (동일) |
| std::vector | 2.1ms | 1.0x (동일) |
분석: 접근 속도는 동일 (-O2 이상).
테스트 2: 생성/소멸
// 100만 번 생성/소멸
void benchCreation() {
for (int i = 0; i < 1000000; ++i) {
// 테스트 대상
}
}
결과:
| 타입 | 시간 | 상대 속도 |
|---|---|---|
| C 배열 (스택) | 5ms | 1.0x (기준) |
| std::array (스택) | 5ms | 1.0x (동일) |
| std::vector (힙) | 850ms | 170x (매우 느림) |
분석: 빈번한 생성/소멸은 스택 할당이 유리.
테스트 3: 순회
template <typename Container>
void benchIteration(const Container& c) {
long long sum = 0;
for (const auto& x : c) {
sum += x;
}
}
결과: 모두 동일 (최적화 빌드).
3. 메모리 안전성
범위 체크
// C 배열: 범위 체크 없음
int arr[5] = {1, 2, 3, 4, 5};
int x = arr[10]; // ❌ 미정의 동작 (크래시 또는 쓰레기 값)
// std::array: at()으로 범위 체크
std::array<int, 5> arr2 = {1, 2, 3, 4, 5};
try {
int x = arr2.at(10); // ✅ 예외 발생
} catch (const std::out_of_range& e) {
std::cerr << "Out of range\n";
}
// std::vector: at()으로 범위 체크
std::vector<int> vec = {1, 2, 3, 4, 5};
try {
int x = vec.at(10); // ✅ 예외 발생
} catch (const std::out_of_range& e) {
std::cerr << "Out of range\n";
}
함수 전달 시 크기 정보
// ❌ C 배열: 크기 정보 손실
void foo(int arr[]) { // int* 로 decay
// sizeof(arr)는 8 (포인터 크기)
}
int arr[5] = {1, 2, 3, 4, 5};
foo(arr);
// ✅ std::array: 크기 정보 유지
void foo(const std::array<int, 5>& arr) {
std::cout << arr.size() << '\n'; // 5
}
// ✅ std::vector: 크기 정보 유지
void foo(const std::vector<int>& vec) {
std::cout << vec.size() << '\n'; // 5
}
4. 상황별 선택 가이드
결정 트리
Q1. 크기가 컴파일 타임에 고정되어 있는가?
Yes → Q2
No → vector
Q2. 크기가 작은가? (< 100)
Yes → std::array
No → vector (스택 오버플로우 방지)
상황별 권장
| 상황 | 권장 | 이유 |
|---|---|---|
| 기본 선택 | vector | 안전, 동적 크기 |
| 크기 고정 + 작음 | std::array | 스택 할당, 안전 |
| 빈번한 생성/소멸 | std::array | 힙 할당 비용 없음 |
| 큰 배열 | vector | 스택 오버플로우 방지 |
| 크기 변경 필요 | vector | 동적 크기 |
| C API 연동 | C 배열 | 호환성 |
실전 예제
예제 1: 고정 크기 버퍼
// 요구사항: 크기 고정, 빈번한 생성
// 권장: std::array
std::array<char, 1024> buffer;
readData(buffer.data(), buffer.size());
예제 2: 동적 크기 리스트
// 요구사항: 크기 변경, 안전성
// 권장: vector
std::vector<int> numbers;
numbers.reserve(100); // 재할당 방지
for (int i = 0; i < n; ++i) {
numbers.push_back(i);
}
예제 3: 좌표 (x, y, z)
// 요구사항: 크기 3 고정, 빈번한 생성
// 권장: std::array
struct Position {
std::array<float, 3> coords; // x, y, z
float& x() { return coords[0]; }
float& y() { return coords[1]; }
float& z() { return coords[2]; }
};
실무 사례
사례 1: 게임 엔진 - 파티클 시스템
#include <array>
#include <iostream>
#include <vector>
struct Particle {
std::array<float, 3> position; // x, y, z (고정 크기)
std::array<float, 3> velocity;
float lifetime;
};
class ParticleSystem {
private:
std::vector<Particle> particles_; // 동적 크기
public:
void emit(const Particle& particle) {
particles_.push_back(particle);
}
void update(float deltaTime) {
for (auto& particle : particles_) {
particle.position[0] += particle.velocity[0] * deltaTime;
particle.position[1] += particle.velocity[1] * deltaTime;
particle.position[2] += particle.velocity[2] * deltaTime;
particle.lifetime -= deltaTime;
}
// 수명이 다한 파티클 제거
particles_.erase(
std::remove_if(particles_.begin(), particles_.end(),
[](const Particle& p) { return p.lifetime <= 0; }),
particles_.end()
);
}
size_t count() const {
return particles_.size();
}
};
int main() {
ParticleSystem system;
Particle p = {{0, 0, 0}, {1, 1, 0}, 5.0f};
system.emit(p);
system.update(0.016f); // 60 FPS
std::cout << "파티클 수: " << system.count() << std::endl;
return 0;
}
사례 2: 이미지 처리 - 픽셀 버퍼
#include <array>
#include <iostream>
#include <vector>
struct Pixel {
std::array<uint8_t, 3> rgb; // R, G, B (고정 크기)
};
class Image {
private:
int width_;
int height_;
std::vector<Pixel> pixels_; // 동적 크기
public:
Image(int width, int height) : width_(width), height_(height) {
pixels_.resize(width * height, {{{0, 0, 0}}});
}
Pixel& at(int x, int y) {
return pixels_[y * width_ + x];
}
void fill(const Pixel& color) {
for (auto& pixel : pixels_) {
pixel = color;
}
}
};
int main() {
Image img(800, 600);
img.at(100, 100) = {{{255, 0, 0}}}; // 빨간색
img.fill({{{255, 255, 255}}}); // 흰색으로 채우기
return 0;
}
사례 3: 네트워크 - 패킷 버퍼
#include <array>
#include <iostream>
#include <vector>
constexpr size_t MAX_PACKET_SIZE = 1024;
struct Packet {
std::array<char, MAX_PACKET_SIZE> data; // 고정 크기
size_t length;
};
class PacketQueue {
private:
std::vector<Packet> queue_; // 동적 크기
public:
void enqueue(const Packet& packet) {
queue_.push_back(packet);
}
Packet dequeue() {
Packet packet = queue_.front();
queue_.erase(queue_.begin());
return packet;
}
bool empty() const {
return queue_.empty();
}
};
int main() {
PacketQueue queue;
Packet packet;
packet.length = 5;
std::copy_n("Hello", 5, packet.data.begin());
queue.enqueue(packet);
if (!queue.empty()) {
Packet received = queue.dequeue();
std::cout << "수신: " << std::string(received.data.begin(), received.data.begin() + received.length) << std::endl;
}
return 0;
}
사례 4: 행렬 연산
#include <array>
#include <iostream>
#include <vector>
// 고정 크기 행렬 (3x3)
using Matrix3x3 = std::array<std::array<float, 3>, 3>;
Matrix3x3 multiply(const Matrix3x3& a, const Matrix3x3& b) {
Matrix3x3 result = {{{0, 0, 0}, {0, 0, 0}, {0, 0, 0}}};
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
for (int k = 0; k < 3; ++k) {
result[i][j] += a[i][k] * b[k][j];
}
}
}
return result;
}
// 동적 크기 행렬
class Matrix {
private:
int rows_;
int cols_;
std::vector<float> data_;
public:
Matrix(int rows, int cols) : rows_(rows), cols_(cols) {
data_.resize(rows * cols, 0.0f);
}
float& at(int row, int col) {
return data_[row * cols_ + col];
}
};
int main() {
// 고정 크기 행렬
Matrix3x3 m1 = {{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}};
Matrix3x3 m2 = {{{2, 0, 0}, {0, 2, 0}, {0, 0, 2}}};
Matrix3x3 m3 = multiply(m1, m2);
// 동적 크기 행렬
Matrix m4(10, 10);
m4.at(5, 5) = 42.0f;
return 0;
}
트러블슈팅
문제 1: 스택 오버플로우
증상: 크래시
// ❌ 큰 배열을 스택에 할당
int main() {
int arr[10000000]; // 40MB (스택 오버플로우)
return 0;
}
// ✅ vector로 힙에 할당
int main() {
std::vector<int> vec(10000000); // 힙 할당
return 0;
}
문제 2: 범위 오류
증상: 미정의 동작
// ❌ C 배열: 범위 체크 없음
int arr[5] = {1, 2, 3, 4, 5};
int x = arr[10]; // ❌ 미정의 동작
// ✅ vector: at()으로 범위 체크
std::vector<int> vec = {1, 2, 3, 4, 5};
try {
int x = vec.at(10);
} catch (const std::out_of_range& e) {
std::cerr << "범위 오류" << std::endl;
}
문제 3: 함수 전달 시 크기 손실
증상: 크기 정보 손실
// ❌ C 배열: 크기 정보 손실
void foo(int arr[]) { // int* 로 decay
// sizeof(arr)는 8 (포인터 크기)
}
int arr[5] = {1, 2, 3, 4, 5};
foo(arr);
// ✅ std::array: 크기 정보 유지
void foo(const std::array<int, 5>& arr) {
std::cout << arr.size() << std::endl; // 5
}
std::array<int, 5> arr2 = {1, 2, 3, 4, 5};
foo(arr2);
// ✅ std::vector: 크기 정보 유지
void foo(const std::vector<int>& vec) {
std::cout << vec.size() << std::endl; // 5
}
std::vector<int> vec = {1, 2, 3, 4, 5};
foo(vec);
문제 4: vector 복사 비용
증상: 성능 저하
// ❌ vector 값 전달 (복사)
void process(std::vector<int> vec) { // 복사 발생
// ...
}
std::vector<int> vec(1000000);
process(vec); // 100만 개 복사
// ✅ const 참조 전달
void process(const std::vector<int>& vec) { // 복사 없음
// ...
}
std::vector<int> vec2(1000000);
process(vec2); // 복사 없음
마무리
배열과 vector의 선택은 크기 고정 여부와 안전성 요구사항에 달려 있습니다.
핵심 요약
-
3가지 배열 타입
- C 배열: 스택, 고정 크기, 안전성 낮음
- std::array: 스택, 고정 크기, 안전
- std::vector: 힙, 동적 크기, 안전
-
선택 기준
- 기본: vector (안전, 동적 크기)
- 크기 고정 + 작음: std::array
- C API 연동: C 배열 (불가피)
-
성능
- 접근 속도: 동일 (최적화 빌드)
- 생성/소멸: std::array >>> vector
- 안전성: vector ≈ std::array >>> C 배열
-
주의사항
- 큰 배열은 스택 오버플로우 주의
- C 배열은 범위 체크 없음
- vector는 reserve로 재할당 방지
선택 가이드
| 상황 | 권장 | 이유 |
|---|---|---|
| 기본 선택 | vector | 안전, 동적 크기 |
| 크기 고정 + 작음 | std::array | 스택 할당 |
| 빈번한 생성/소멸 | std::array | 힙 할당 비용 없음 |
| 큰 배열 | vector | 스택 오버플로우 방지 |
| 크기 변경 필요 | vector | 동적 크기 |
| C API 연동 | C 배열 | 호환성 |
코드 예제 치트시트
// C 배열
int arr1[5] = {1, 2, 3, 4, 5};
// std::array
std::array<int, 5> arr2 = {1, 2, 3, 4, 5};
arr2.at(0); // 범위 체크
// std::vector
std::vector<int> vec = {1, 2, 3, 4, 5};
vec.push_back(6); // 동적 크기
vec.at(0); // 범위 체크
// 함수 전달
void foo(const std::vector<int>& vec); // 참조
다음 단계
- vector 기초: C++ vector 완벽 가이드
- 메모리 기초: C++ 메모리 기초
- 스택 오버플로우: C++ 스택 오버플로우
참고 자료
- “Effective STL” - Scott Meyers
- “C++ Primer” - Stanley Lippman
- cppreference: https://en.cppreference.com/w/cpp/container
한 줄 정리: 대부분의 경우 vector를 사용하고, 크기가 고정되고 작으면 std::array를 고려하며, C 배열은 피한다.