Q & A

sizeof는 함수인가요?

sizeof는 괄호를 사용해서 함수 모양을 하고 있지만 실제로는 연산자입니다. 따라서 함수와는 큰 차이점이 있는데 함수는 실행 시점(run-time)에 사용(호출)되지만 sizeof는 컴파일 시점(compile-time)에 사용(연산)됩니다.

함수가 아니므로 괄호를 써도 되고 쓰지 않아도 됩니다.

sizeof (자료형)
sizeof 표현식
sizeof (배열) / sizeof(자료형)
(sizeof 배열) / (sizeof 배열[0])

배열을 int numArr[100000000000000];처럼 크게 생성해도 되나요?

안 됩니다. 배열은 메모리의 스택에 생성되기 때문에 스택의 크기를 넘어서면 스택 오버플로우(stack overflow)가 발생하여 프로그램이 실행되지 않습니다. 따라서 배열은 플랫폼에서 정한 스택의 크기보다 작게 선언해야 하며 더 큰 크기를 원한다면 malloc 함수로 동적 메모리를 할당해서 사용해야 합니다.

Windows의 스택 크기는 1메가바이트이며 부족할 때마다 계속 늘어나지만 int numArr[100000000000000];과 같이 매우 큰 크기는 사용할 수 없습니다. 1메가바이트라면 int 배열일 때 대략 int numArr[1048576];까지 만들 수 있습니다.

리눅스에서는 ulimit -a 명령으로 스택 크기를 알아낼 수 있습니다.

만약 컴파일러에서 스택 크기를 조절하고 싶다면 컴파일 옵션을 사용하면 됩니다.

  • Visual Studio: 프로젝트(P) > 속성(P) > 링커 > 시스템 > 스택 예약 크기
  • GCC: gcc -Wl,--stack=스택크기

최대 스택 크기는 운영체제나 플랫폼에 따라 달라집니다.

왜 메모리를 할당할 때 int *numPtr = malloc(sizeof(int) * 10)처럼 복잡하게 쓰나요?

malloc(40)과 같이 int의 크기 4에 10을 곱하여 40을 직접 지정해도 상관은 없습니다(Windows, 리눅스 32/64비트 기준).하지만 별다른 설명 없이 40이라는 숫자의 숨은 뜻을 찾으려면 쉽지가 않습니다. 따라서 코드의 의도를 명확하게 표현하기 위해 sizeof(int) * 10과 같이 사용합니다. 즉, int 크기 10개를 명확하게 나타내는 것이죠. 또한, 4 * 10과 같이 표현해도 4가 의미하는 뜻이 모호하므로 반드시 sizeof를 사용해줍니다.

여기서 사실 int의 크기도 플랫폼에 따라 달라질 수 있으므로 stdint.h를 포함하여 sizeof(int32_t) * 10과 같이 int의 크기도 명확하게 표현하는 것이 좋습니다.

포인터를 2차원 배열처럼 사용할 때 int **m = malloc(sizeof(int *) * 세로크기);에서 sizeof(int)로 써도 동작하는데 왜 sizeof(int *)로 써야 하나요?

32비트 시스템에서는 sizeof(int)sizeof(int *)는 4바이트로 크기가 같습니다. 그래서 의도하지는 않았지만 크기가 같기 때문에 정상적으로 동작한 것이죠. 하지만 프로그램이 정상적으로 동작하더라도 잘못된 코드이므로 변수의 크기인지 포인터의 크기인지 명확하게 표현해주어야 합니다.

특히 64비트 시스템에서는 sizeof(int)가 4바이트, sizeof(int *)가 8바이트이므로 sizeof(int *)대신 sizeof(int)로 메모리를 할당해버리면 메모리가 절반 크기만 할당됩니다. 이때는 프로그램이 정상적으로 동작하지 않으므로 주의해야 합니다.

배열의 이름은 포인터인가요?

아닙니다. 배열은 배열이고 포인터는 포인터입니다. 배열은 배열 형식으로 선언한 변수를 뜻하며 일정한 공간을 가지고 있습니다. 하지만 포인터는 단지 메모리 주소를 담는 변수일 뿐입니다.

int numArr[10] = { 0, 10, 20, 30, 40, 50, 60, 70, 80, 90 };    // 정수 10개를 저장할 수 있는 공간
int *numPtr;    // 메모리 주소를 저장할 수 있는 공간

int numArr[10]은 정수 10개를 저장할 수 있는 공간이며 배열 이름 numArr이 이 공간을 대표합니다. 하지만 포인터 int *numPtr은 메모리 주소를 저장할 수 있는 공간이며 특정 메모리의 위치를 numPtr로 이름을 지은 것입니다.

단, 배열은 수식에서 사용될 때 포인터로 변환됩니다. 정확하게는 배열이 포인터로 퇴화(decay)한다고 표현합니다.

numArr[3] = 0;    // [ ]를 사용하면 numArr은 배열의 첫번째 요소를 가리키는 포인터로 변환됨

마찬가지로 배열 이름을 포인터에 할당하는 것도 수식이므로 배열 이름이 포인터로 변환됩니다.

numPtr = numArr;    // 수식에서 numArr은 배열의 첫번째 요소를 가리키는 포인터로 변환됨

하지만 sizeof 연산자로 배열의 크기를 구할 때, & 연산자로 주소를 구할 때, 문자 배열을 선언하면서 문자열로 초기화 할 때는 배열이 포인터로 변환되지 않고 배열 그 자체로 사용됩니다.

printf("%d\n", sizeof(numArr));    // 40: 배열이 포인터로 변환되었다면 4 또는 8이 나왔겠지만
                                   // sizeof에서는 배열 차체로 사용되므로 40이 나옴

numPtr = &(numArr[0]);    // 배열 첫 번째 요소의 주소를 구함. &에서는 배열로 사용됨
                          // numPtr = &numArr[0];과 같음

char s1[10] = "Hello";    // "Hello" 문자열이 배열에 복사됨

3[numArr]도 되는 이유는 무엇인가요?

배열 첨자 연산자 [ ]는 교환 법칙이 성립하기 때문입니다. [ ]는 포인터 연산과 역참조로 표현할 수 있습니다.

int numArr[10] = { 0, 10, 20, 30, 40, 50, 60, 70, 80, 90 };

printf("%d\n", 3[numArr]);        // 30
printf("%d\n", *(numArr + 3));    // 30

printf("%d\n", 3[numArr]);        // 30
printf("%d\n", *(numArr + 3));    // 30

먼저 numArr[3]*(numArr + 3)이고, 교환 법칙이 성립하므로 *(3 + numArr)과 같습니다. 따라서 3[numArr]도 쓸 수 있는 문법입니다.

  1. a[e]일 때(a는 배열 e는 수식)
  2. *((a) + (e))
  3. *((e) + (a)) 덧셈 교환 법칙 성립
  4. e[a]

이 부분은 초보자에게는 어려운 내용이므로 이런 것도 된다 정도로 넘어가면 됩니다.