C++ 배열 vs vector | "어느 게 나을까?" 성능과 안전성 비교

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가지 배열 타입 비교
  2. 성능 벤치마크
  3. 메모리 안전성
  4. 상황별 선택 가이드
  5. 정리

1. 3가지 배열 타입 비교

비교표

항목C 배열std::arraystd::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.1ms1.0x (기준)
std::array2.1ms1.0x (동일)
std::vector2.1ms1.0x (동일)

분석: 접근 속도는 동일 (-O2 이상).

테스트 2: 생성/소멸

// 100만 번 생성/소멸
void benchCreation() {
    for (int i = 0; i < 1000000; ++i) {
        // 테스트 대상
    }
}

결과:

타입시간상대 속도
C 배열 (스택)5ms1.0x (기준)
std::array (스택)5ms1.0x (동일)
std::vector (힙)850ms170x (매우 느림)

분석: 빈번한 생성/소멸은 스택 할당이 유리.

테스트 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의 선택크기 고정 여부안전성 요구사항에 달려 있습니다.

핵심 요약

  1. 3가지 배열 타입

    • C 배열: 스택, 고정 크기, 안전성 낮음
    • std::array: 스택, 고정 크기, 안전
    • std::vector: 힙, 동적 크기, 안전
  2. 선택 기준

    • 기본: vector (안전, 동적 크기)
    • 크기 고정 + 작음: std::array
    • C API 연동: C 배열 (불가피)
  3. 성능

    • 접근 속도: 동일 (최적화 빌드)
    • 생성/소멸: std::array >>> vector
    • 안전성: vector ≈ std::array >>> C 배열
  4. 주의사항

    • 큰 배열은 스택 오버플로우 주의
    • 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++ 스택 오버플로우

참고 자료

한 줄 정리: 대부분의 경우 vector를 사용하고, 크기가 고정되고 작으면 std::array를 고려하며, C 배열은 피한다.