티스토리 뷰

반응형

코딩을 하거나 문제를 풀다 보면 우린 배열을 선언하고, 그 안의 초깃값들을 채워 넣어야 할 일들이 많다. 

그럴 때, 다음과 같이 for 반복문을 이용하여 아래와 같은 방법으로 배열 초기화를 한다.

 

1
2
3
for (int i = 0; i < N; i++) {
    arr[i] = 1;
}
cs

 

그러나, 초기화를 매번 해야 하거나, 배열이 2차원, 3차원이 되면 코드가 불필요하게 길어지거나 가독성이 떨어질 수 있다. 이를 방지하기 위하여, C언어 & C++에서는 두 가지 종류의 배열 초기화 함수를 제공한다.

 

N개의 칸을 차지하는 배열 arr의 값을 0으로 초기화한다고 하면 다음 두 가지 방법으로 채울 수 있다.

 

  • memset(arr, 0, sizeof(arr))
  • fill(arr, arr+N, 0)

 

1. memset

(※ https://www.cplusplus.com/reference/cstring/memset/ 에서 함수에 대한 정보를 확인할 수 있다.)

 

C의 메모리 관련 헤더 <memory.h>에서의 memset 함수는 다음과 같은 구조를 가지고 있다.

 

void * memset( void * ptr, int value, size_t num)

 

memset는 3개의 인자를 받는다.

 

  • void *ptr : 배열이나 어떤 container의 자료형의 시작 포인터
  • int value : 배열이나 어떤 container에 채울 자료. 이 값은 unsigned char 로 변환되어 삽입된다.
  • size_t num : 배열이나 어떤 container에서 int value 값을 얼마나 넣을지 정해준다.

 

memset 함수는 다음과 같이 활용할 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <bits/stdc++.h>
using namespace std;
 
int main() {
    
    int arr[5]; int arr2[5][5];
    // for문을 이용하여 arr[5] 0으로 초기화
    for (int i = 0; i < 5; i++) {
        arr[i] = 0;
    }
 
    // memset을 이용하여 arr[5] -1로 초기화
    memset(arr, -1sizeof(arr));
 
    //memset으로 2차원 배열도 초기화 할 수 있다.
    memset(arr2, 0sizeof(arr2));
 
    return 0;
}
cs

 

이 중, 주목해야 할 부분은 int value의 unsigned char 로 변환되는 부분이다.

 

이 말을 해석하자면, 1byte 단위로 해당 메모리를 초기화시킨다는 의미이다.

따라서, int형(4바이트), long long형(8바이트)과 같이 자료형이 1byte가 넘어간다면? 값을 자동으로 4byte형과 8byte형으로 늘려버린다. 이 점이 memset의 말썽을 불러일으키는 부분이다.

 

흔히 다른 블로그나 글에서 memset은 -1, 0로 초기화할 때만 사용 가능하다고 말한다. 아래의 특징으로 인하여 가능한 일이다.

 

-1, 0을 각각 1byte로 나타내면 11111111, 00000000가 된다. 이를 강제로 4byte로 늘릴 때 컴퓨터는 자동으로 1byte의 값을 4번 복사해서 넣는다. 따라서 memset으로 처리하면 각 숫자는 다음과 같이 된다.

 

  • 11111111 => 11111111 / 11111111 / 11111111 / 11111111 = -1 (32bit)
  • 00000000 => 00000000 / 00000000 / 00000000 / 00000000 = 0 (32bit)

 

위를 32비트 기준으로 숫자를 읽으면 각각 -1, 0이 된다. 따라서, 우리가 의도한 숫자가 배열 안에 들어간다.

 

memset에 1이란 값을 넣는다고 하여보자. 1을 1byte로 나타내면 00000001이 된다. 같은 방법으로 4byte로 늘려보자. memset으로 처리하면 다음과 같은 값을 가진다.

 

  • 00000001 => 00000001 / 00000001 / 00000001 / 00000001 = 2^24 + 2^16 + 2^8 + 2^0 = 16,843,009 (32bit)

 

즉, 아예 다른 값이 들어간다고 할 수 있다. 실제로 다음과 같이 코드를 생성하고 실행시켜보자.

 

5개의 int형을 담는 세 배열 arr1, arr2, arr3이 있고, 각각 memset 함수를 이용하여 -1, 0, 1으로 채우려고 시도를 하였다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <bits/stdc++.h>
using namespace std;
 
int arr1[5];
int arr2[5];
int arr3[5];
 
int main() {
    
    memset(arr1, -1sizeof(arr1));
    memset(arr2, 0sizeof(arr2));
    memset(arr3, 1sizeof(arr3));
 
    for (int i = 0; i < 5; i++) {
        cout << arr1[i] << ' ';
    } cout << "\n";
 
    for (int i = 0; i < 5; i++) {
        cout << arr2[i] << ' ';
    } cout << "\n";
 
    for (int i = 0; i < 5; i++) {
        cout << arr3[i] << ' ';
    } cout << "\n";
 
    return 0;
}
cs

 

이에 대한 결과는 다음과 같이 출력된다.

 

1
2
3
-1 -1 -1 -1 -1
0 0 0 0 0
16843009 16843009 16843009 16843009 16843009  
cs

 

의도한 대로 -1, 0으로 초기화한 배열은 각각 -1, 0으로 옳게 초기화되었지만, 1로 초기화된 배열은 16,843,009라는 숫자로 채워진 것을 확인할 수 있다. 만약 숫자가 더 커진다면 오버플로우가 발생할 수 있다.

 

그럼, 4바이트의 int형이 아닌, 1byte형의 char형은 말썽을 일으키지 않을까?

 

이에 대한 코드를 다음과 같이 작성하였다. 이번에는 char형 배열 3가지를 준비하였고, 각각 48, 65, 97이라는 값을 채웠다. 순서대로 ASCII 코드 기준 '0', 'A', 'a'를 나타내는 값이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <bits/stdc++.h>
using namespace std;
 
char arr1[5];
char arr2[5];
char arr3[5];
 
int main() {
    
    memset(arr1, 48sizeof(arr1));
    memset(arr2, 65sizeof(arr2));
    memset(arr3, 97sizeof(arr3));
 
    for (int i = 0; i < 5; i++) {
        cout << arr1[i] << ' ';
    } cout << "\n";
 
    for (int i = 0; i < 5; i++) {
        cout << arr2[i] << ' ';
    } cout << "\n";
 
    for (int i = 0; i < 5; i++) {
        cout << arr3[i] << ' ';
    } cout << "\n";
 
    return 0;
}
cs

 

이에 대한 실행 결과는 다음과 같다.

 

1
2
3
0 0 0 0 0
A A A A A
a a a a a
cs

 

우와! 우리가 의도한 대로 배열 안에 char들이 들어간 것을 확인할 수 있다. 따라서, 1byte형의 크기를 가지는 char형에서는 배열 초기화를 memset으로 해도 문제를 일으키지 않는 것을 확인할 수 있다.

 

여태까지의 내용을 정리하면 다음과 같이 정리할 수 있다.

 

1. int형, long long형 등 1바이트가 넘어가는 자료형은 -1, 0 이외의 수는 원하는 값으로 초기화되지 않는다.

2. char형과 같은 1바이트 자료형은 원하는 값으로 초기화할 수 있다.

 


2. fill

(※https://en.cppreference.com/w/cpp/algorithm/fill에서 함수에 대한 정보를 확인할 수 있다.)

 

C++의 STL 헤더 <algorithm.h>에서의 fill 함수는 다음과 같은 구조를 가지고 있다.

 

void fill (ForwardIt first, ForwardIt last, const T& value)

 

fill 함수는 3개의 인자를 받는다.

 

  • ForwardIt first : 배열이나 어떤 container의 시작 Iterator, 배열의 경우 첫 포인터
  • ForwardIt last : 배열이나 어떤 container의 끝 Iterator, 배열의 경우 마지막 포인터
  • const T& value: 배열이나 어떤 container의 자료형에 넣을 값

 

fill 함수는 다음과 같이 활용할 수 있다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <bits/stdc++.h>
using namespace std;
 
int main() {
    
    int arr[5]; int arr2[5][5];
    // for문을 이용하여 arr[5] 0으로 초기화
    for (int i = 0; i < 5; i++) {
        arr[i] = 0;
    }
 
    // fill을 이용하여 arr[5] -1로 초기화
    fill(arr, arr+5-1);
 
    //fill로 2차원 배열을 초기화 하려면 역참조 연산자(&)를 사용해야한다.
    fill(&arr2[0][0], &arr2[0][0+ 5*50);
 
    return 0;
}
cs

 

fill 함수는 내부적으로 for문과 비슷한 방법으로 채우기에, memset과 다르게 value 자료형에서의 오버플로우가 아니라면 원하는 값을 채울 수 있다.

 

따라서, -1, 0 이외의 int형이나 long long형을 채우기 위해선 fill 함수를 써야 한다. 물론, char형 등 다른 형태도 fill 함수를 통해 원하는 값을 넣을 수 있다.

 

여태까지의 내용을 정리하면 다음과 같이 정리할 수 있다.

 

1. fill 함수는 원하는 값을 채워 넣을 수 있다.


3. memset vs fill

두 함수 모두 배열을 원하는 값으로 채운다는 기능을 하는 함수이다. 그럼, memset과 fill 중 어떤 함수가 더 빠를 것인가? 이를 실험하여보자.

 

다음과 같은 코드를 작성하였다. 1000x1000의 크기를 가지는 int형 배열 arr에 memset, fill을 각각 10,000번씩 수행하고 각 수행 시간을 비교하였다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <bits/stdc++.h>
#include <time.h>
using namespace std;
 
int arr[1000][1000];
 
int main() {
    
    time_t start, end;
    double result_memset, result_fill;
 
    start = clock();
    for (int i = 0; i < 10000; i++) {
        memset(arr, 0sizeof(arr));
    }
    end = clock();
    result_memset = (double)end - start;
 
    start = clock();
    for (int i = 0; i < 10000; i++) {
        fill(&arr[0][0], &arr[0][0]+1000*10000);
    }
    end = clock();
    result_fill = (double)end - start;
 
    cout << "memset : " << result_memset << "ms" << endl;
    cout << "fill : " << result_fill << "ms" << endl;
 
    return 0;
}
cs

 

이에 대한 결과는 다음과 같다.

 

 

1
2
memset : 1063ms
fill : 23380ms
cs

 

  • memset : 1,063ms
  • fill : 23,380ms

 

대략 22~24배 차이 나는 것을 확인할 수 있다. 배열의 크기, 채우는 값이 다르거나 다른 컴파일러를 쓴다면 수행 시간 차이 결과는 다르겠지만 memset이 fill보다 월등히 빠르다는 것을 확인할 수 있다.

 

이러한 차이는 memset과 fill의 구현 방식에서 기인한다. memset는 C언어보다 더 low-level 선에서 처리하기에 더 빠른 실행 속도를 가질 수밖에 없다. fill은 for문을 사용하여 채우는 것과 비슷하게 구현되었기에, memset은 fill보다 더 빠른 실행 속도를 가질 수밖에 없다.

 

4. 3줄 요약

1. memset은 -1, 0으로 채울 거 아니면 쓰지 말자. 아니면 빠르게 채워야 할 때나 쓰자.

2. fill은 느리지만 안전하다.

3. 정말 많은 테스트 케이스가 있는 거 아니면 fill 쓰자.

 

도움이 되었다면 지나가는 길에 하트 하나 눌러주세요, 양질의 글을 쓰는데 하나의 동기부여가 됩니다😍

지적이나 오타 수정 댓글 환영합니다!!

반응형
댓글
Total
Today
Yesterday
공지사항
최근에 올라온 글
최근에 달린 댓글
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함