환영합니다, Rolling Ress의 카루입니다.
C언어를 배우다보면 흔히 이런 말을 한 번쯤은 들어 보셨을 겁니다.
배열을 선언할 때, 크기를 정하는 인덱스에는 변수를 사용할 수 없다.
int len = 5;
int array[len];
쉽게 말해서 위와 같은 코드가 불가능하다는 뜻이죠. 일반적으로 상수값만 가능하다고 배우니까. 그런데 이 말은 틀렸습니다. C99부터 VLA라는 문법을 지원하기 때문이에요. C11에서 선택구현으로 바뀌어서 이게 사라졌다고 아시는 분들도 있는데, C23에서 다시 부활했습니다. 아마 C언어는 이 문법을 계속 가지고 갈 것 같아요.
* 참고로 이 코드는 gcc에서 C99 이상의 문법으로 컴파일했을 때만 작동합니다. MSVC는 지원하지 않고, C++도 지원하지 않아요. (gcc의 경우 확장 기능으로 C++에서 VLA를 지원하지만, 표준은 아닙니다)
int n;
scanf("%d", &n);
int array[n];
누가봐도 malloc / calloc을 써서 동적할당해야 할 것 같은 코드이지만, 이것도 정상적으로 동작합니다. 위에서 말씀드렸지만 Visual Studio에서는 동작하지 않습니다. 이건 Visual Studio가 표준을 지키지 않아서 발생하는 일이에요. GCC 기준으로는 잘 됩니다.
문법 얘기는 여기서 그만 하겠습니다. 그냥 배열 선언할 때 크기에 변수를 넣으면 끝이니까요. 이 글에서는 이게 왜 되는지, 기존의 배열과는 어떤 점이 다른지를 짚어보고자 합니다.
우선 아래 사이트를 참고해볼까요. C/C++ 표준안을 읽기 쉽게 위키로 정리해둔 사이트입니다. 저도 중학교 때부터 애용하고 있던 사이트입니다.
하단에 보시면 예제 코드가 있습니다. 처음에 이걸 봤을 때는 머리가 띵했습니다. 어떻게 저게 되지 싶으실 거예요. 이건 VLA의 동작 방식이 다른 변수들과 많이 달라서 그렇습니다. 일단 중요한 포인트 몇 가지 먼저 짚고 가겠습니다.
1. VLA의 할당 위치
- VLA는 "주로" 스택 영역에 할당됩니다. GCC는 스택에 할당되지만, C 표준은 VLA의 할당 위치를 정하지 않았습니다. 컴파일러에 따라 힙 영역에 할당하고, 스택에 할당된 것 '처럼' (자동 free) 작동하게 할 수도 있습니다.
2. VLA의 라이프타임
- VLA의 lifetime은 스택에 선언된 배열과 거의 같으나, 그 시작과 끝이 조금 다릅니다. 글로 표현하기에는 한계가 있어 아래에서 설명을 계속하도록 하죠. 1:1로 비교하자면 스택에 선언된 일반 배열보다 lifetime이 조금 더 짧습니다. 할당 매커니즘도 다르고요.
출력 결과를 봅시다. 변수의 주소와 값을 출력한 결과입니다. 변수는 크게 다음 세 가지가 있습니다.
- 스택에 선언된 배열 stack_array
- VLA 배열 vla_array
- 힙에 선언된 배열 heap_array
이 외에 테스트용으로 스택에 선언된 두 변수 stack_variable, stack_variable2가 있습니다.
스택은 큰 주소에서 낮은 주소로 자라죠. 즉 메모리에는 변수가 다음과 같이 존재합니다.
0x5FFE34: stack_array[2]
0x5FFE30: stack_array[1]
0x5FFE2C: stack_array[0]
0x5FFE28: stack_variable
0x5FFE24: stack_variable2
참고로 변수들의 선언 순서는 다음과 같습니다.
int stack_array[3] = {1, 2, 3};
int stack_variable;
int vla_array[stack_variable];
int stack_variable2 = 0;
일치하죠. 어? 그런데 뭔가 이상합니다. vla_array가 없습니다. 혼자만 쏙 빠졌네요. 어디에 있나요?
0x5FFE18: vla_array[2]
0x5FFE14: vla_array[1]
0x5FFE10: vla_array[0]
한참 아래에 있네요. 뭔가 이상합니다. 혹시 스택이 아니라 힙에 할당된 건 아닐까요?
0x7C64D8: heap_array[2]
0x7C64D4: heap_array[1]
0x7C64D0: heap_array[0]
그건 아니네요. 힙은 저 멀찌감치 떨어져 있습니다. 이걸 통해서 VLA는 일단 스택에 할당됨을 확인할 수 있습니다. 그런데 일반적인 stack과는 다르죠. 약간 떨어져 있으니까요.
여기서 C99의 특징을 다시 한 번 봅시다. C언에에서 변수는 무조건 블럭의 최상단에 위치해야 하고, 일반문이 실행된 다음엔 선언문이 올 수 없다고 배우신 분들도 계실 거예요. 근데 그것도 C89/90 시절의 옛날 얘기입니다. C99부터 선언문 위치에 제약이 없어집니다. (애초에 제약이 있었다면 VLA도 절대 나오지 못했을 문법입니다.)
auto int a; // 기본
register int a; // 레지스터 변수
static int a; // 정적
extern int a; // 어딘가에
일반적으로 변수를 선언할 때, 앞에 아무것도 붙이지 않으면 auto로 선언됩니다. (C++의 auto와는 다릅니다. 그건 타입 추론이에요) auto 변수는 자동으로 할당되고, 자동으로 해제됩니다. 프로그램이 특정 블럭에 진입할 때, 그 블럭 안에 있는 지역변수를 한꺼번에 stack에 생성합니다. 그렇기 때문에 선언문 위치가 상관이 없는 겁니다.
그런데 VLA는 달라요. 블럭에 진입하자마자 생성되는 게 아니라, VLA 선언문이 실행되고 나서야 메모리 할당이 이루어집니다. 이 때문에 다른 stack 지역변수들과 분리된 공간에서 메모리가 할당된다고 볼 수 있겠죠.
printf("####### 2. Scope를 벗어난 후 stack, vla, heap array 비교 #######\n\n");
int *ptr;
goto FIRST_STACK;
{
SECOND_STACK:
printf("[GOTO] stack_array: %p\n", ptr);
printf("[GOTO] stack_array[0]: %d\n", *ptr); // 정상 작동 (메모리 해제 전)
goto EXIT_STACK;
FIRST_STACK:
int stack_array[3] = {1, 2, 3};
ptr = stack_array;
goto SECOND_STACK;
} // block scope를 벗어나면서 자동 해제
EXIT_STACK:
printf("Address of stack_array: %p\n", ptr);
printf("Value of stack_array[0]: %d\n\n", *ptr); // 메모리가 해제되었으나, 우연히 정상작동
int size = 3;
goto FIRST_VLA;
{
SECOND_VLA: // goto 문을 이용해 선언 위로 올라오기만 해도 메모리 해제
printf("[GOTO] vla_array: %p\n", ptr);
printf("[GOTO] vla_array[0]: %d\n", *ptr); // ptr is dangling pointer
goto EXIT_VLA;
FIRST_VLA:
int vla_array[size];
for (int i = 0; i < 3; i++) vla_array[i] = i+1;
ptr = vla_array;
goto SECOND_VLA;
} // 마찬가지로 auto 변수이기에, scope를 벗어나면서 자동 해제
EXIT_VLA:
printf("Address of vla_array: %p\n", ptr);
printf("Value of vla_array[0]: %d\n\n", *ptr);
{
int *heap_array = (int *)calloc(3, sizeof(int));
for (int i = 0; i < 3; i++) heap_array[i] = i+1;
ptr = heap_array;
}
printf("Address of heap_array: %p\n", ptr);
printf("Value of heap_array[0]: %d\n\n", *ptr);
free(ptr);
두번째는 결과가 너무 난해해서 코드를 들고 왔습니다. 아까는 VLA가 언제 할당되는지 알아봤다면, 이번에는 언제 해제되는지 보겠습니다. 표준에서는 "블럭을 나갈 때가 아닌, 선언이 스코프를 벗어날 때" 해제가 된다고 합니다. 사실 그 말이 그 말 같죠. 어쨌든 블럭을 나가면 스코프를 벗어나는 꼴이 되니까요. goto를 쓴다면 얘기가 다릅니다. 아까 auto 변수는 블럭을 들어갈 때 할당된다고 했죠? 그러면 일단 블럭을 진입한 순간, 그 블럭 안에서 위로 가든 아래로 가든 선언된 변수는 그대로 있습니다.
VLA의 경우, 블럭 내에서 VLA가 선언된 문장 위로 올라가면 메모리가 해제되어 버립니다. 그래서 GOTO문으로 올라가기만 했는데 쓰레기 값이 출력된 걸 보셨죠. 똑같이 auto 변수지만 VLA는 추가적인 예외가 붙습니다. 할당과 해제, 그 둘이 가장 큰 차이예요.
그 외에 자잘한 차이점을 말씀드리자면 sizeof 연산자로 요소의 개수를 셀 수 있습니다. VLA는 어쨌든 배열이고, malloc으로 할당한 힙 배열은 포인터로 사용하기 때문에 차이가 발생합니다. 힙 배열을 가리키는 포인터에 아무리 sizeof 써봤자 4바이트(8바이트)만 뜨죠.
근데 VLA를 적극적으로 쓰는 코드를 거의 못 봤습니다. 일단 어디까지나 스택을 쓰는 거기 때문에, 힙보다 속도는 빠를지언정 스택 오버플로우의 위험이 따라요. 조그만 변수들 몇 개 정도라면 모르겠습니다만. 그리고 MSVC에서는 지원을 아예 안 하고 있고... 역시 이런 기능이 있다는 것만 알고, 필요에 따라 사용하는 게 최선일 듯 합니다.
'C언어 > C언어 심화 & 특수' 카테고리의 다른 글
[C11] C언어 표준 스레드 실행 (0) | 2020.03.21 |
---|---|
[C99] Compound Literals, C에서의 이름없는 임시 객체 (0) | 2020.03.21 |
[C99] 포인터 최적화를 위한 restrict 키워드 (0) | 2020.03.21 |
[C11] 컴파일 타임 오류내기, static_assert (0) | 2020.03.21 |
[C99] C언어에서 표준 bool 타입 사용하기 (0) | 2020.03.21 |