본문으로 바로가기

memset 함수의 이해

알고리즘 문제풀이를 하다 보면 배열을 생성하고 초기화해야 할 일이 많다. 그리고 많은 사람들이 memset함수를 사용한다. 하지만 memset함수는 배열 원소를 초기화 하는 것이 아니라 메모리 값을 초기화한다는 것을 모르고 사용한다면 전혀 예상치 못한 결과를 얻을 수 있다. 예를 들어 아래와 같이 int타입 배열을 선언하고 원소를 모두 0로 초기화하기 위해 아래와 같이 코드를 작성한 경우를 많이 볼 수 있다.

int nums[10];
memset(nums, 0, sizeof(nums));
for (int num : nums)
    cout << num << " ";

출력 결과

0 0 0 0 0 0 0 0 0

만약 아래와 같이 정수 배열을 -1으로 초기화하고 싶다면 아래와 같이 memset의 두 번째 인자로 -1로 전달할 수 있다.

int nums[10];
memset(nums, -1, sizeof(nums));
for (int num : nums)
    cout << num << " ";

출력 결과

-1 -1 -1 -1 -1 -1 -1 -1 -1 -1

그런데 만약 동일한 방법으로 정수 배열을 1로 초기화 한다면 어떻게 될까?

int nums[10];
memset(nums, 1, sizeof(nums));
for (int num : nums)
    cout << num << " ";

출력 결과

16843009 16843009 16843009 16843009 16843009 16843009 16843009 16843009 16843009 16843009

굉장히 생뚱맞은 결과를 얻게된다.

memset의 정확한 기능

memset 함수는 배열의 원소를 초기화하는 것이 아니라 바이트 단위로 메모리를 초기화하는 함수이다.

memset(nums, 1, sizeof(nums));

즉, 위와 같은 코드는 nums의 주소에서 시작해서 sizeof(nums)바이트 만큼 모든 바이트를 1 초기화하라 라는 명령이다. 그런데 우리가 자주 사용하는 int는 4바이트 정수이지 않은가. 그 결과 우리의 배열 nums의 각 원소들은 16진수로 0x01010101이며 10진수로는 16,843,009인 값으로 초기화된 것이다. 비주얼 스튜디오에서 메모리 맵을 살펴보면 아래의 이미지와 같이 초기화된 것을 확인할 수 있다.

▲ memset의 두 번째 인자로 1을 전달한 경우

그런데 0과 -1에서는 우리가 기대한 결괏값을 얻을 수 있었다. 그 이유는 우연하게 0과 -1을 1바이트 단위로 초기화한 값이 4바이트나 8바이트 정수를 0과 -1로 초기화한 값과 일치하기 때문이다. 16진수로 표현하면 1바이트의 0은 0x00이다. 이를 4개의 바이트에 모두 적용하여 초기화하면 0x00000000인데, 이는 4바이트 0과 동일한 값이다. 마찬가지로 1바이트 -1은 0 xFF이다. 이를 4개의 바이트에 모두 적용하면 0xFFFFFFFF인데 이는 4바이트 -1과 동일한 값이다. 물론 8바이트 정수에 적용해도 동일하다. 이들을 메모리 맵에서 살펴보면 다음과 같다.

▲ memset의 두 번째 인자로 0을 전달한 경우
▲ memset의 두 번째 인자로 -1을 전달한 경우

배열 초기화를 위한 대안

그렇다면 각 원소에 0과 -1이 아닌 다른 정수로 초기화하기 위해서는 어떤 방법을 사용해야 하는가? 가장 쉽게 떠올릴 수 있는 방법은 다음과 같이 원시 배열을 활용하는 방법이 있을 것이다. 그리고 std::arrayfill 함수를 사용하는 방법과 초기화 리스트를사용하는 방법이 있다. 먼저 원시 배열을 활용하는 방법을 살펴보자.

원시 배열 활용

int nums[10];
for (size_t i = 0; i < 10; ++i)
    nums[i] = 10;
for (int num : nums)
    cout << num << " ";

출력 결과

10 10 10 10 10 10 10 10 10 10

for문을 범위 기반으로 수정한 다음과 같은 방법을 사용하면 조금 더 안전하게 초기화 할 수 있다. 대신 범위기반 for문에서 값을 입력하기 위해서는 반드시 참조 타입으로 선언해야 함을 잊지 말아야 할 것이다.

int nums[10];
for (int& num : nums)
    num = 10;
for (int num : nums)
    cout << num << " ";

출력 결과

10 10 10 10 10 10 10 10 10 10

fill 함수 사용

C++ 표준 라이브러리에는 원시 배열보다 안전한 std::array 컨테이너를 제공하고 있다. array컨테이너는 다음과 같은 장점이 있기 때문에 배열이 필요한 경우 원시 배열보다 std::array 컨테이너를 사용하는 것이 좋다.

  • at()함수를 통해서 원소에 안전한 접근이 가능하다.
  • size()함수가 있기 때문에 함수의 매개변수로 배열을 전달할 때 배열의 크기를 알려주지 않아도 된다.

이 외에도 std::array에는 fill()이라는 멤버 함수로 아래와 같이 간결하게 배열의 원소를 초기화할 수 있다.

array<int,10> nums;
nums.fill(10);
for (int num : nums)
  cout << num << " ";  

 

출력 결과

10 10 10 10 10 10 10 10 10 10

초기화 리스트 활용

배열의 정수 원소를 0으로 초기화하고 싶다면 초기화 리스트를 사용하는 것도 좋은 대안이 될 수 있다. 초기화 리스트는 memset()이나 fill() 함수들과 다르게 생성할 당시에 값을 초기화하기 때문에 더 빠르기도 하다.(물론 코딩 테스트 기준에서 체감하기 어려운 차이이다.) 초기화 리스트는 원시 배열이나 std::array 혹은 단일 변수에서도 사용할 수 있다. 사용법은 동일하니 std::array기준으로 예시를 보이면 다음과 같다.

array<int, 10> nums{ 10, 10, 10, 10, 10, 10, 10, 10, 10, 10};
for (int num : nums)
    cout << num << " ";

출력 결과

10 10 10 10 10 10 10 10 10 10

초기화 리스트의 문제는 원하는 값으로 배열을 초기화하기 위해 값을 직접 입력해야 한다. 만약 원소가 100 개라면 이는 현실적으로 불가능한 방법이다.  때문에 원소가 많다면 fill() 함수를 사용하는 것이 현명하다.

 

그래도 초기화 리스트는 좋은 초기화 방법 중 하나이다. 초기화 리스트에 아무런 값을 입력하지 않거나 10개의 원소중 2개의 원소의 값만 입력할 경우 입력이 없는 원소들에 대해서는 해당 타입의 기본값으로 초기화된다. 해당 타입의 기본값이란, 정수형 변수일 경우 0, bool 타입 변수일 경우 false, 포인터 변수일 경우 nullptr이 된다. 아래의 예제 코드에서 초기화한 배열과 초기화하지 않은 배열의 차이를 살펴보자.

    array<int, 10> intArr1;
    array<int, 10> intArr2{ 10, 10, 10 };
    array<int, 10> intArr3{};

    for (int num : intArr1)
        cout << num << " ";
    cout << endl;

    for (int num : intArr2)
        cout << num << " ";
    cout << endl;

    for (int num : intArr3)
        cout << num << " ";
    cout << endl;

출력 결과

-858993460 -858993460 -858993460 -858993460 -858993460 -858993460 -858993460 -858993460 -858993460 -858993460
10 10 10 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0

초기화 리스트를 활용하면 {} 연산자 하나만으로 간단하게 배열의 모든 원소를 0으로 초기화할 수 있다. 실전에 적용한 코드는 여기서 확인할 수 있다.

정리

정리하면 변수를 사용하기 전에는 반드시 초기화를 하는 것이 좋다. 그리고 memset은 원소 단위로 초기화하는 함수가 아니라 바이트 단위로 메모리를 초기화함을 알아야 한다. 배열을 초기화하기 위해서는 초기화 리스트를 사용하거나, 반복문을 사용하거나, fill() 함수를 사용할 수 있다. 초기화 리스트를 활용하면 각 원소의 디폴트 값 bool 타입은 false, 정수 타입은 0, 포인터 타입은 nullptr로 초기화됨을 기억하자.