본문으로 바로가기
반응형

배열과 포인터

배열과 포인터는 컴퓨터 내부적으로 거의 같은 방법으로 메모리에 접근한다. 그러나 이 둘 사이의 차이를 잘 구분해야 한다. 

포인터는 변수, 배열 이름은 상수인것에 주목해보자

 

배열의 이름은 그 배열에 할당된 메모리의 시작주소를 나타내는 상수이다. 컴파일러는 컴파일 과정에서 상수값을 가진 포인터로 처리한다. 그러므로 프로그램이 실행되는 동안 배열이 가리키는 주소는 바뀔 수 없다.

 

배열명은 첫 번째 배열요소를 가리키는 포인터를 기호화한 것이다.

포인터에 정수값을 더할 때는 포인터가 가리키는 자료형의 크기를 곱해서 더해준다.

 

예를들면, &ary[2] 과 &ary[0] + 2은 동일한 주소값 44를 나타낸다. &ary[0] + 2는 컴파일러에 의해 

&ary[0] + (2 * sizeof(int))의 값(여기서는 8로)으로 계산된다.

 

위 그림을 통해 배열의 이름은 배열의 시작주소라고 할 수 있다.

배열명으로 주소값을 계산하여 모든 배열요소를 참조할 수 있으며 의미상 이해하기 쉽게 배열표현을 주로 사용하는 것이다.

 

직접 구문을 작성해서 확인해보자.

#include <stdio.h>

int main() 
{
	int iArray[5] = {100, 101, 102, 103, 104};
	printf("%p\n", &iArray);
	printf("%p\n", iArray);
	printf("%d\n", &iArray);
	printf("%d\n", iArray);
}

 

배열의 이름이 그 배열에 할당된 메모리의 시작주소를 뜻한다고 했는데, &iArray와 iArray가 같은 값이 출력되는 것을 확인할 수 있다. (1703624는 위의 주소값의 10진수이다)

 

실제로 디버그모드로 메모리를 확인해봐도 iArray의 출력된 주소값과 동일하다는 것을 확인 할 수 있다.

 

참고로 100의 16진수는 64이다

 

 

 

#include <stdio.h>
int main() {
	int iArray[5] = {100, 101, 102, 103, 104};
	
	// 주소
	printf("-------------주소-----------\n");
	printf("%d\n", &iArray[0]);
	printf("%p\n", iArray + 0);
	printf("%p\n", iArray + 1);
	printf("%p\n", iArray + 2);
	printf("%p\n", iArray + 3);
	printf("%p\n\n", iArray + 4);

	// 값
	printf("-------------값-------------\n");
	printf("%d\n", iArray[0]);
	printf("%d\n", *(iArray + 0));
	printf("%d\n", *(iArray + 1));
	printf("%d\n", *(iArray + 2));
	printf("%d\n", *(iArray + 3));
	printf("%d\n", *(iArray + 4));
}

 

배열과 포인터 표현

배열의 이름이 배열 첫번째 값의 주소값이 된다

 

배열명을 포인터 변수에 저장하면, 포인터 변수도 배열명처럼 사용할 수 있다.

이때 포인터 변수는 첫번째 배열 요소를 가리킨다.

 

배열의 이름은 주소를 다루는 상수이고, 포인터 변수는 주소를 다루는 변수라고 할 수 있다.

 

 

위의 그림은 ary배열의 값을 표현하는 방법들이다.

ary[0]로 예를들면,

ary[0]은 10이다.

*(ary + 0) 은 *(ary) 이고, ary 주소로 따라갔을때 그 값은 10이다.

*(ap + 0)은 *(ap)이고, ap는 ary로 위에 선언을했기때문에 값이 10이된다.

ap[0]도 마찬가지로 10이 되는 것이다.

 

따라서 주소값에 *를 붙이면 값이되고, 값에 &를 붙이면 주소값이 된다.

 

 

실제로 예제를 통해 확인해보면 전부 iArray[0]값인 100이 출력되는 것을 확인할 수 있다.

#include <stdio.h>
int main() {
	int iArray[5] = {100, 101, 102, 103, 104};
	int* ap = iArray;
	
	printf("%d\n", iArray[0]);
	printf("%d\n", *(iArray + 0));
	printf("%d\n", *(ap + 0));
	printf("%d\n", ap[0]);

}

 

그렇다면 값을 표현하는 방법이 여러가지인데 어떤 차이가 있을까?

디버그모드에서 디스어셈블리어로 확인해보자.

디버그모드에서 디스어셈블리로 확인 

 

이걸 보기쉽게 긁어오면 아래와 같다.

	printf("%d\n", iArray[0]);
0041188B  mov         eax,4  
00411890  imul        ecx,eax,0  
00411893  mov         edx,dword ptr iArray[ecx]  
00411897  push        edx  
00411898  push        offset string "%d\n" (0417B60h)  
0041189D  call        _printf (0411046h)  
004118A2  add         esp,8  
	printf("%d\n", *(iArray + 0));
004118A5  mov         eax,dword ptr [iArray]  
004118A8  push        eax  
004118A9  push        offset string "%d\n" (0417B60h)  
004118AE  call        _printf (0411046h)  
004118B3  add         esp,8  
	printf("%d\n", *(ap + 0));
004118B6  mov         eax,dword ptr [ap]  
004118B9  mov         ecx,dword ptr [eax]  
004118BB  push        ecx  
004118BC  push        offset string "%d\n" (0417B60h)  
004118C1  call        _printf (0411046h)  
004118C6  add         esp,8  
	printf("%d\n", ap[0]);
004118C9  mov         eax,4  
004118CE  imul        ecx,eax,0  
004118D1  mov         edx,dword ptr [ap]  
004118D4  mov         eax,dword ptr [edx+ecx]  
004118D7  push        eax  
004118D8  push        offset string "%d\n" (0417B60h)  
004118DD  call        _printf (0411046h)  
004118E2  add         esp,8  

 

이때 같은 기능을하는 똑같은 코드를 지우면 아래와 같아진다.

	printf("%d\n", iArray[0]);
0041188B  mov         eax,4  
00411890  imul        ecx,eax,0  
00411893  mov         edx,dword ptr iArray[ecx]  
00411897  push        edx  

	printf("%d\n", *(iArray + 0));
004118A5  mov         eax,dword ptr [iArray]  
004118A8  push        eax  

	printf("%d\n", *(ap + 0));
004118B6  mov         eax,dword ptr [ap]  
004118B9  mov         ecx,dword ptr [eax]  
004118BB  push        ecx  

	printf("%d\n", ap[0]);
004118C9  mov         eax,4  
004118CE  imul        ecx,eax,0  
004118D1  mov         edx,dword ptr [ap]  
004118D4  mov         eax,dword ptr [edx+ecx]  
004118D7  push        eax  

어셈블리어로 확인해보면 포인터로 출력하는 printf문은 2~3줄인것에 비해, 배열로 출력하는 printf문은 4~5줄인 것을 확인할 수 있다. 특히 배열에는 imul이라는 곱셈도 연산을 해야하기때문에 배열로 출력하는것보다는 포인터로 출력하는 것이 좀 더 효율적이라는 것을 알 수 있다.

 

 

배열과 문자열

문자열 상수는 메모리 공간에 저장되면, 그 순간에 문자열 상수의 주소 값이 반환된다.

 

문자열 상수의 주소는 문자열의 첫 번째 문자의 주소이므로, char형 포인터 타입이다.

str은 문자열이 아니고 문자를 가리키는 주소이다.

즉, char* str = "ABCDEFG"; 를 선언하면 스택영역에 str라는 공간을 만들어서 ABCDEFG라는 문자열이 저장되는 것이아니고, 코드영역에 ABCDEFG를 만들고, 스택영역에 str를 만들어서 str가 ABCDEFG를 가리키는 형태로 저장되는 것이다.

 

 

 

참고로 C#에서도 string T = "TTT"; 를 선언하면

스택에 T라는 영역에 TTT를 저장하는게아니라 Code영역에 TTT를 저장하고 T가 TTT를 가리키는 것이다. 즉, 참조는 포인터인것이다.

 

 

char형과 char []의 차이점을 알아보자. 배열과 포인터의 차이라고 볼 수 있는데, 상수와 변수의 차이점을 생각하면 된다. 예제를 살펴보자.

 

#include <stdio.h>
int main() 
{
	char* str = "ABCDEFG";
	char arr[] = "ABCDEFG";  // 똑같은 문자열이면 컴파일러가 1개만 만든다.
	//char arr[] = "ABCDEFG1";   // 근데 이렇게 1을 붙여주면 ABCDEG1을 또 만들어서 메모리 공간낭비가 된다.
	
	printf("%c\n", str[0]); 
	printf("%c\n\n", arr[0]);  
}

char* str와 char arr[]에 같은 "ABCDEFG"으로 초기화하고 첫번째 인자 값을 출력하면 둘다 동일한 A가 출력된다.

 

이때 *str와 arr[]에 A대신 Z를 대입하면 어떻게 될까?

int main() 
{
	char* str = "ABCDEFG";
	char arr[] = "ABCDEFG";  
    
	printf("%c\n", str[0]); 
	printf("%c\n\n", arr[0]);  
	
	//str[0] = 'Z';  // 프로그램이 팅김
	arr[0] = 'Z';

	printf("str[%s]\n", str);
	printf("arr[%s]\n", arr);
	return 0;
}

str[0] = 'Z';을 주석처리했을때(좌)                                               arr[0] = 'Z';을 주석처리했을때(우)

str[0] = 'Z'; 를 주석처리했을때는 arr[0]에 'Z'가 대입되어서 arr을 출력하면 ZBCDEFG가 출력된다.

str[0] = 'Z'; 의 주석을 해제하고 arr[0]='Z'; 을 주석처리하고 실행해보면 AA가 출력되고 다음 출력문이 출력되지 않는다.

 

이 같은 결과는 ABCDEFG는 메모리 구조에서 Code영역에 저장되어있고,

str은 Stack 영역에 저장되면서 포인터이므로 Code영역의 ABCDEFG를 가리킨다.

arr은 Stack 영역에 7바이트로 ABCDEFG를 배열로 저장되어있다.

이때 str[0] = 'Z'; 를 선언해주면, Read-Only인 Code 영역의 값을 수정하려고했기때문에 프로그램이 오류가 날 것을 대비해 컴파일러가 미리 오류로 잡고 실행시키지 않기때문에 출력문이 제대로 출력되지 않았다.

 

arr[0] = 'Z'; 를 선언하면 Stack 영역에서 값을 수정하는 것이므로 A자리에 Z가 대입되어서 그대로 출력이 되는 것을 볼 수 있다.

 

상수와 변수의 차이로써, 상수는 변하지않고 변수는 변하는 수라는 특징을 알고있으면 이해하기 쉬울 것이다.

따라서, 프로그래머가 상황에 따라 사용하면 되는데 지금 같은 경우에서 메모리의 사용을 따져보자면,

만약 char* str를 사용한다고 하면 ABCDEFG를 가리키는 str 포인터 4바이트, ABCDEFG를 저장할 7바이트인 총 11바이트가 필요하다.

char arr[]를 사용한다고하면 코드영역의 ABCDEFG를 생성하고, 스택에도 ABCDEFG를 생성하므로 총 14바이트가 필요하다.

 

str은 읽기 전용이므로 좀 더 안전하게 사용하려면 읽기와 쓰기가 동시에되는 배열을 사용하는 것이 좀 더 좋을수도있지만, 메모리의 효율을 생각하면 사용 용도에따라 str을 쓰는 것도 고려해볼 수 있을 것이다.

반응형