배열과 포인터
배열과 포인터는 컴퓨터 내부적으로 거의 같은 방법으로 메모리에 접근한다. 그러나 이 둘 사이의 차이를 잘 구분해야 한다.
배열의 이름은 그 배열에 할당된 메모리의 시작주소를 나타내는 상수이다. 컴파일러는 컴파일 과정에서 상수값을 가진 포인터로 처리한다. 그러므로 프로그램이 실행되는 동안 배열이 가리키는 주소는 바뀔 수 없다.
배열명은 첫 번째 배열요소를 가리키는 포인터를 기호화한 것이다.
포인터에 정수값을 더할 때는 포인터가 가리키는 자료형의 크기를 곱해서 더해준다.
예를들면, &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의 출력된 주소값과 동일하다는 것을 확인 할 수 있다.
#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'가 대입되어서 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을 쓰는 것도 고려해볼 수 있을 것이다.
'개발자과정준비 > C' 카테고리의 다른 글
[C] 배열 - 1 (배열의 선언과 초기화, 문자열) (0) | 2020.10.22 |
---|---|
[C] 포인터 - 4 (실수부 포인터) (0) | 2020.10.21 |
[C] 포인터 - 3 (포인터 초기화, 포인터 연산) (0) | 2020.10.20 |
[C] 포인터 - 2 (0) | 2020.10.15 |
[C] 포인터 - 1 (0) | 2020.10.14 |