C 언어: 배열 초기화와 포인터 선언의 심층 분석

C 언어에서 배열과 포인터는 프로그램의 근간을 이루는 중요한 개념이지만, 그 선언 방식과 의미는 미묘한 차이로 인해 많은 개발자에게 혼란을 야기합니다. 예를 들어, int intArray[3] = {11, 22, 33};와 같이 정수형 배열을 선언하고 초기화하는 방법은 매우 일반적입니다. 또한, int *ptr = intArray;와 같이 배열의 시작 주소를 가리키는 포인터를 정의하는 것도 가능합니다.

여기서 한 가지 궁금증이 생길 수 있습니다. "배열 이름 자체가 배열을 가리키는 포인터 역할을 한다면, int *pointerArray[3] = {11, 22, 33};와 같이 선언하여 int intArray[3]와 동일한 정수형 배열을 정의하고 초기화할 수 있을까?"

결론부터 말씀드리자면, 그렇지 않습니다. 이 두 선언은 근본적으로 다른 의미를 가지며, 후자의 방식은 대부분의 경우 컴파일 오류를 유발합니다.

1. int *pointerArray[3]의 의미와 오류 발생 원인

C 언어에서 [] 연산자는 * 연산자보다 높은 우선순위를 가집니다. 따라서 int *pointerArray[3] 구문은 다음과 같이 해석됩니다:

  • pointerArray는 3개의 요소를 가진 배열입니다.
  • 이 배열의 각 요소는 int형 데이터를 가리키는 포인터입니다.

즉, int *pointerArray[3]포인터 배열(Array of Pointers)입니다. 이는 3개의 정수형 값을 직접 저장하는 정수형 배열(Array of Integers)int intArray[3]와는 완전히 다른 자료형입니다.

int *pointerArray[3] = {11, 22, 33};가 오류를 발생하는 이유는 명확합니다. 컴파일러는 이 코드를 다음과 같이 해석하려고 시도합니다:

// int *pointerArray[3] = {11, 22, 33}; 선언 시 컴파일러의 해석 시도:
pointerArray[0] = (int *)11; // 정수 11을 int* 타입으로 형변환하여 포인터에 할당
pointerArray[1] = (int *)22; // 정수 22를 int* 타입으로 형변환하여 포인터에 할당
pointerArray[2] = (int *)33; // 정수 33을 int* 타입으로 형변환하여 포인터에 할당

여기서 11, 22, 33은 단순한 정수 리터럴이지 유효한 메모리 주소가 아닙니다. 포인터 변수는 메모리 주소를 저장해야 하는데, 정수값을 직접 할당하려고 시도하면 컴파일러는 경고 또는 오류를 발생시킵니다. 만약 컴파일러가 이를 허용하더라도, 런타임 시 실제 메모리 주소 11, 22, 33에 접근하려 할 때 "유효하지 않은 메모리 접근" 오류로 프로그램이 충돌할 것입니다.

포인터 배열의 올바른 사용 예시:

int valueA = 10;
int valueB = 20;
int valueC = 30;

int *pointerArray[3]; // int형 데이터를 가리키는 포인터 3개를 저장할 배열 선언
pointerArray[0] = &valueA; // valueA의 주소를 저장
pointerArray[1] = &valueB; // valueB의 주소를 저장
pointerArray[2] = &valueC; // valueC의 주소를 저장

printf("pointerArray[0]이 가리키는 값: %d\n", *pointerArray[0]); // 출력: 10

"배열 이름은 포인터이다"라는 오해 수정

"배열 이름은 포인터이다"라는 설명은 C 언어 학습 초기 단계에서 종종 사용되는 단순화된 표현이지만, 엄밀히 말하면 정확하지 않습니다.

  • 본질: 배열 이름은 메모리 상의 연속적인 공간을 나타내는 상수 심볼입니다.
  • "붕괴(Decay)": 배열 이름은 대부분의 표현식(예: 포인터에 할당되거나 함수 인자로 전달될 때)에서 해당 배열의 첫 번째 요소의 주소로 "붕괴"됩니다.
  • 주요 차이점:
    • sizeof(intArray): 전체 배열의 크기를 바이트 단위로 반환합니다 (예: int 4바이트 * 3개 = 12바이트).
    • sizeof(ptr) (ptr이 포인터 변수인 경우): 포인터 변수 자체의 크기를 바이트 단위로 반환합니다 (일반적으로 4바이트 또는 8바이트).

2. int (*arrayPointer)[3]의 의미와 올바른 사용법

앞서 []* 연산자의 우선순위를 고려하여, 괄호를 사용해 결합 방식을 변경하면 배열과 유사하게 작동하지 않을까 하는 생각이 들 수 있습니다. 예를 들어, int (*arrayPointer)[3]와 같이 선언하면 어떨까요? 하지만 이 역시 int intArray[3]처럼 배열을 직접 초기화하는 용도로는 사용할 수 없습니다.

int (*arrayPointer)[3] 구문은 "3개의 int형 요소를 가진 배열 전체를 가리키는 포인터"를 의미합니다. 즉, arrayPointer는 배열 자체가 아니라 배열을 가리키는 배열 포인터(Pointer to Array)입니다.

int (*arrayPointer)[3] = {11, 22, 33};가 오류를 발생하는 이유:

  • 좌변: arrayPointer는 메모리 주소를 저장해야 하는 포인터 변수입니다.
  • 우변: {11, 22, 33}은 실제 데이터를 나타내는 초기화 리스트입니다.

포인터 변수에 실제 데이터를 직접 저장할 수는 없습니다. 마치 "창고 주소를 적는 메모지"에 "실제 창고에 있는 물건들"을 직접 붙이려는 시도와 같습니다.

배열 포인터의 올바른 사용 예시:

int dataCollection[3] = {100, 200, 300}; // 먼저 실제 정수형 배열을 선언하고 초기화합니다.
int (*arrayPointer)[3] = &dataCollection;  // 배열 포인터가 dataCollection 배열 전체의 주소를 가리키도록 합니다.

// arrayPointer를 통해 배열 요소 접근
printf("첫 번째 요소: %d\n", (*arrayPointer)[0]); // 출력: 100
printf("두 번째 요소: %d\n", (*arrayPointer)[1]); // 출력: 200
printf("세 번째 요소: %d\n", (*arrayPointer)[2]); // 출력: 300

위 코드에서 (*arrayPointer)[0]와 같이 괄호를 사용하여 포인터를 먼저 역참조하고, 그 결과로 얻은 배열에 인덱스 [0]를 적용합니다.

3. C 언어의 세 가지 주요 선언 방식 비교

혼란을 방지하기 위해 이 세 가지 유사한 선언 방식을 하나의 표로 정리하여 비교해 봅시다.

선언 방식 명칭 의미 (간략) 메모리 점유 (예시, 64비트 시스템)
int numericArray[3] 정수형 배열 3개의 int형 값을 직접 저장하는 연속된 메모리 공간 12 바이트 (int 4바이트 * 3)
int *ptrArray[3] 포인터 배열 3개의 int형 포인터를 저장하는 연속된 메모리 공간 24 바이트 (포인터 8바이트 * 3)
int (*arrayPtr)[3] 배열 포인터 3개의 int형 요소를 가진 배열 전체를 가리키는 포인터 8 바이트 (포인터 자체의 크기)

핵심 결론

C 언어에서 어떤 포인터 정의 방식도 배열의 초기화 역할을 완전히 대체할 수 없습니다.

  • 배열은 데이터를 저장할 실제 메모리 공간을 할당하고 초기화하는 주체입니다.
  • 포인터는 할당된 메모리 공간의 '주소'를 저장하여 해당 공간을 참조하는 주체입니다.

따라서 int numericArray[3]와 같은 형태로 먼저 메모리 공간을 요청하지 않는다면, 11, 22, 33과 같은 실제 데이터를 저장할 곳이 존재하지 않습니다. 포인터는 항상 이미 존재하는 메모리 공간을 가리킬 때 사용됩니다.

태그: C 포인터 배열 포인터배열 배열포인터

6월 8일 22:50에 게시됨