나만의 작은 도서관

[C++] 가끔 C 배열을 쓰고 싶을 때를 위한 글 본문

C++/문법 및 메소드(STL)

[C++] 가끔 C 배열을 쓰고 싶을 때를 위한 글

pledge24 2026. 1. 12. 17:30

나는 vector가 있는데 왜 불편한 C 배열을 쓰고 싶어 할까?

  • vector라는 좋은 게 있는데 왜 C 배열을 쓰고 싶어지는 이유는 크게 두 가지 정도 있는 것 같다.
  • 첫 번째는 C 배열이 vector보다 가독성이 높다는 점이다. 개인적으로 vector<int>와 같이 적는 것보단 int arr[5]와 같이 적는 게 훨씬 눈에 잘 들어왔다. 그래서 vector <vector<int>>와 같이 꺾쇠 지옥에 시달리다 보면 int arr[5][5]와 같은 문법이 그리워지곤 한다.
  • 두 번째는 고정된 크기임을 명시하고 싶을 때이다. vector는 기본적으로 크기 조절을 막을 수 있는 방법이 없다. 그래서 고정된 크기로 쓸 vector는 계속 머릿속으로 ‘이 vector는 고정 크기다’라는 생각을 하고 있어야 되는데 이게 너무 귀찮았다. 물론 std::array를 쓰면 해결되는 문제이다. 그럼에도 C 배열을 쓰고 싶어 하는 건 std::array도 std::vector와 똑같이 첫 번째 이유에 걸리기 때문이다. 

  • 아래 내용들은 혹여나 C 배열을 쓰게 될 때 참고하기 위해 작성해 둔 내용이다.

배열 초기화

int arr[5];		// -858993460 -858993460 -858993460 -858993460 -858993460
  • std::vector와 달리 C 배열을 초기화를 하지 않았을 때 0이 아닌 쓰레기 값이 들어있으므로(단, 전역 배열과 정적 배열은 제외), 반드시 초기화해줘야 한다.
  • C++에서 C 배열을 초기화하는 방법은 다양하다. C++로 올라오면서 C++ 방식으로 초기화하는 방식이 생겼기 때문. 우선 C 방식부터 정리하자면 아래와 같다.

 

C 방식으로 배열 초기화하기

 

첫 번째 방법. 중괄호 초기화

  • 가장 흔한 방식으로, 넣어준 값으로 배열을 초기화한다.
  • 중괄호 안에 넣은 초기값의 개수에 따라 다르게 동작하는데, 정리하면 아래와 같다.
    • 초기값 0개(안 넣음): C++에서 새로 생긴 방식으로, 전부 0으로 초기화한다.
    • 초기값 1개: 전부 넣은 초기값으로 초기화한다.
    • 초기값 2개 이상: 해당 값들로 초기화. 남는 자리는 0으로 초기화
// 1. 전부 같은 값으로 초기화
int arr[5] = {};  // 0 0 0 0 0

// 2. 초기값이 1개 -> 해당 값으로 전부 초기화
int arr[5] = {0}; // 0 0 0 0 0

// 3. 초기값이 2개 이상 -> 부분 초기화 + 나머지는 0으로
int arr[5] = {1, 2}; // 1 2 0 0 0

// 추가. 딱 하나만 초기값을 넣고 싶다면,
// 어쩔 수 없이 아래와 같이 작성해야한다.
int arr[5] = {};
arr[0] = 3;

 

 

두 번째 방법. memset 함수

  • C에는 memXXX 시리즈가 있다. 이 시리즈 중 memset은 지정한 범위 내의 메모리의 값을 설정하는 함수로, 배열의 선언과 초기화를 따로 해야 하는 경우(즉, 첫 번째 방법을 사용할 수 없는 경우) 사용한다.
  • memset은 “memory.h”, “string.h” 등 여러 헤더 중 하나를 include 하여 사용할 수 있는데 C++ 에선 <cstring> 헤더를 추가하여 사용하는 것을 권장한다고 한다.
#include <cstring>
int arr[5];
memset(arr, 0, sizeof(arr)); // 0 0 0 0 0

C++ 방식으로 초기화하기

  • C++ 방식으로 초기화하고 싶다면, std::fill을 사용하면 된다. std::fill <algorithm> 헤더에 있으며, 사용 방법은 아래와 같다.
int arr[5];

// 전체를 0으로 초기화
std::fill(std::begin(arr), std::end(arr), 0);

// [0, 1) 범위를 7로 초기화
std::fill(std::begin(arr), std::begin(arr) + 1, 7);
  • 위 코드를 보면 std::begin이랑 std::end 함수를 사용하여 배열 포인터를 반복자(iterator)로 바꿔주는 작업이 있는데, 이는 어디까지나 C++ 방식에 맞추기 위한 거고, C 배열의 경우 (연속된 메모리 컨테이너이기 때문에) 반복자로 안 바꿔줘도 똑같이 작동한다.
#include <algorithm>

int arr[5];
// [0, 1) 범위를 7로 초기화
std::fill(arr, arr + 1, 7);

C 배열은 인자로 전달하면 포인터로 퇴화(decay)한다.

  • 이게 굉장히 골치 아픈 현상이다. std::vector나 std::array처럼 STL 컨테이너들은 함수로 전달할 때 기존과 동일한 데이터를 가지고 있는 반면, C 배열은 넘겨지는 순간 포인터로 퇴화하면서 배열의 크기 정보를 잃어버린다. 즉, 내가 아무리 배열 타입으로 매개변수 타입을 설정해도, 배열의 크기를 함수 내에서 알 수 없다.
void func1(int arr[])
{
    // int arr[]로 타입을 설정했음에도 실제론 int* 포인터라,
    // arr의 크기가 5인지 알 수 없다.
    // sizeof(arr)은 포인터의 크기만 반환한다.
}

void func2(int arr[5])
{
    // 5와 같이 적어줘도 소용없다. 
    // 5는 주석과 같은 역할일 뿐, arr의 크기가
    // 5임을 넘겨주진 않는다.
    // func1과 동일하게 int* 포인터를 받을 뿐이다.
}

int main(void)
{
    int arr[5] = {1, 2, 3, 4, 5};
    func1(arr);
    func2(arr);
}
  • 그래서 C 배열을 함수로 넘겨주는 경우, 배열의 크기에 대한 매개변수를 따로 전달해줘야 한다. 윈도우 라이브러리에서 배열 크기를 따로 보내주는 이유도 이에 기반한다.
void func3(int arr[], int N)
{
}

int main(void)
{
    int arr[5] = {1, 2, 3, 4, 5};
    int N = sizeof(arr) / sizeof(int);
    func3(arr, N);
}

 

 

참조(&)를 사용하면 퇴화하지 않는다.

  • 배열을 참조 방식으로 넘겨주는 경우 포인터로 퇴화하지 않는다. 그래서 조금은 지저분하지만 아래와 같이 참조 형태로 받으면 따로 크기 매개변수를 넘겨주지 않아도 된다.
void func(int (&arr)[5]) {
    // 이제 sizeof(arr)는 20(4*5)바이트가 나온다.
    std::cout << "참조 사용 시 sizeof(arr): " << sizeof(arr) << std::endl;
}

 

 

템플릿 함수의 경우 배열 크기가 자동 추론된다.

  • 템플릿 함수의 경우 배열을 넘겨주면 배열의 크기가 자동으로 추론되기 때문에 배열 크기 정보가 사라진다는 걱정에서 완전히 자유로워진다.
template <std::size_t N>
void printArray(int (&arr)[N]) { // N이 자동으로 결정됨
    // N이 5로 자동 추론됨.
    for(int i = 0; i < N; ++i) {
        std::cout << arr[i] << " ";
    }
}

int arr[] = {1, 2, 3, 4, 5};
printArray(arr);

 

 


C 배열은 복사 대입이 안된다.

  • C++에서 해왔던 대로 vector <int> v1 = v2; 와 같은 복사 대임을 C 배열에도 적용하면 컴파일 에러가 난다. 이유는 명확하게도 C 배열에는 복사 대입 기능을 제공하지 않기 때문. 그래서 아래와 같은 코드는 오류가 발생한다.
int arr1[] = {1, 2, 3, 4, 5};
int arr2[] = arr; // 복사를 기대했지만 돌아온건 컴파일 에러다.
  • C 배열 초기화 시 반드시 중괄호가 우변에 와야 하며, 만약 초기화가 끝난 시점에서 arr2 = arr1;과 같이 작성한다 해도 복사가 아닌 포인터 대입으로 취급되고, 이렇게 취급된 포인터 대입 시도마저 메모리상 고정된 위치를 가리켜야 할 C 배열 변수에 주소를 변경하려는 구문이 되므로 컴파일 에러가 발생한다.
int arr1[5] = {1, 2, 3, 4, 5};
int arr2[5] = {0, 0, 0, 0, 0};

arr2 = arr1; // 컴파일 에러: "array type 'int [5]' is not assignable"

 

 

그럼 C 배열 복사는 어떻게 해야 할까?

  • memcpy를 사용하면 된다. memcpy는 이름 그대로 메모리를 복사하는 함수로, 아래와 같이 사용하면 된다.
#include <cstring>
// arr1의 내용을 arr2에 복사.
std::memcpy(arr2, arr1, sizeof(arr1));
  • C++ 방식으로 처리하고 싶다면 std::copy를 사용해도 된다.
#include <algorithm>
// arr1의 내용을 arr2에 복사.
std::copy(std::begin(arr1), std::end(arr1), std::begin(arr2));