• 포인터와 배열에 대한 마지막 글이 될 것 같다.
• 이번 포스트에서는 필자가 포인터와 배열의 공부하면서 가장 헷갈리고 애매했던 것에 대해 이야기하려고 한다.
[char a[]와 char *a 차이]
• 결론부터 말씀드리자면, char a[]와 char *a는 전혀 다르다.
• 차이점은
- 배열: 같은 타입의 연속적인 요소들이 미리 할당된 공간, 크기와 위치가 고정되어 있다.
- 포인터: 할당된 메모리 공간을 가리킬 수 있으며, 변경할 수도 있다. 또한 배열을 가리킬 수 있으며, 동적으로 할당한 배열처럼 흉내내어 쓸 수 있다.
• 아마 헷갈리는 이유는 함수의 Formal Parameter에 관한 것이라 생각한다.
(cf. void func(char * a); 라는 함수를 호출할 때 char a[]; func(a); 와 같이 호출한다.)
• 비록 서록 연관이 되어있긴 하지만, 또 비슷하게 쓰이기는 하지만!!
• 배열은 포인터가 아니고 반대로 포인터는 배열이 아니다.
• char a[6]
- 위 선언은 문자 6개를 저장할 수 있는 공간을 요청하고, 그 결과 ‘a’라는 이름이 이 공간을 대표한다
• char *p
- 위 선언은 포인터를 저장할 수 있는 공간을 요구하고, 그 위치는 ‘p’라는 이름이 지어진다.
- 이 포인터는 어떤 곳(문자 혹은 문자로 이루어진 배열 한 요소)도 가리킬 수 있으며, 아무것도 가리키지 않을 수 있다.
• 위 내용을 다음 그림으로 확인해보자.
- char a[]=“hello”;
- char *p = “hello”;
• 어떤 x[3]과 같은 참조가 x가 배열인지 포인터인지에 따라 컴파일러는 서로 다른 코드를 생성한다는 것을 알아야 한다.
• 위 예시를 보면 컴파일러는 a[3]을 보았을 때, ‘a’에서 세 개만큼 지나서 그 위치의 문자를 가져온다.
• 반면 p[3]을 보았을 때, ‘p’가 가리키는 곳에서 세 개만큼 지나서 그 위치의 문자를 가져온다.
• 위 2개 문장을 보면 차이가 없어 보이지만 컴파일러 상에서 분명 다른 코드로 접근한다는 것은 사용성에 있어 큰 차이를 불러일으킨다.
• 정리하자면, char a[]와 char *p는 다른 것이며, a[3]은 ‘a’라고 이름 붙인(named) 곳에서 3만큼 지난 곳을 의미하며, p[3]은 ‘p’가 가리키는(pointed) 곳에서 3만큼 지난 곳을 의미한다.
[포인터와 배열이 같다(equivalent)라는 말은 어디서 온 것일까?]
• 배열과 포인터가 같다는(equivalent) 의미는 서로 같다는(identical) 말도 아니고, 서로 바꿔쓸 수 있다는(interchangable) 뜻도 아니다.
• 다음 원문을 보자!
An lvalue of type array-of-T which appears in an expression decays (with three exceptions) into a pointer to its first element; the type of the resultant pointer is pointer-to-T. (The exceptions are when the array is the operand of a sizeof or an & operator or is a string literal initializer for a character array.)
• T타입의 배열의 ‘lvalue’가 수식에서 나타낼 때에는 배열의 첫 요소를 가리키는 포인터로 변경이 된다. 그리고 변경된 포인터 타입은 T타입을 가리키는 포인터이다. 이 때 예외사항은 첫째, 배열이 sizeof의 피연산자로 쓰일 때, 둘째, &연산자의 피연산자로 쓰일 때, 셋째, 문자 배열에 대해 문자열 초기화가 될 때이다.
• 따라서 위 정의에 따라서, 내부적으로는 배열과 포인터는 매우 다르지만, 배열이나 포인터에 상관없이 []연산자를 쓸 수 있는 것이다.
• 다음 예시를 보자.
- int a[] = {1,2,3};
- int *p = a;
• 이때 “a”이름을 갖은 배열은 lvalue이며, 수식에서 나타났으므로 “a”는 배열의 첫 요소를 가리키는 포인터로 변경된다.
• 또한 void func(char a[]);에서 “a”는 lvalue가 되고 컴파일러에 의해 포인터로 인식되어 void func(char *a);의 형태로 변경된다.
• 하지만, sizeof(a), &a, char a[] = “abc”; 일 때 a는 배열이고 포인터로 변경되지 않는다.
※ “lvalue”는 “대입할 수 있는 것“을 뜻하지 않는다. ”메모리에 어떤 위치를 갖고 있는 것“이 더 나은 표현이다.
[ 5[“abcd”]라는 표현 ]
• []연산자는 교환 법칙이 성립한다. 이것은 이전 포스팅에서 많이 예시를 들어 설명했으므로 더 이상 설명하지 않겠다.
• 다만, 이런 것은 “혼동이 되는 C Contest”이며 특별히 쓸모 있는 표현은 아니다.
[arr과 &arr의 차이?]
• int arr[3]이 있을 때, arr의 출력 값과 &arr의 출력 값은 분명 같은 주소 값을 출력한다.
• 그렇다면 arr과 &arr의 차이는 무엇일까?
• 표준 C언어에서는 &arr는 주소 값 즉, 포인터를 만들어 내며, 이 포인터의 타입은 배열 T전체를 가리키는 포인터이다. 무슨 말인지 감이 안올 것이다. 아래 예시를 보자.
- int a1[10]의 선언이 있다.
- a1에 대한 reference는 “pointer to int”란 타입을 갖으며, &a1에 대한 reference는 “pointer to array of 10 ints”란 타입을 갖는다.
- int a2[2][3]의 선언이 있다.
- a2에 대한 reference는 “pointer to array of 3 ints”이며, &a2에 대한 reference는 “pointer to array of 2 array of 3 ints”이다.
• 다음 예시를 통해 확실하게 이해를 하자. 어떤 값이 나올지 예측해 보자.
#include <iostream> using namespace std; void func1(int (*p)[3]) { printf("%d\n", *p); printf("%d\n", *(p + 1)); } void func2(int(*p)[2][3]) { printf("%d\n", *p); printf("%d\n", *(p + 1)); } int main(void) { int a[2][3] = { 1, 2 ,3, 4, 5, 6 }; func1(a); func2(&a); return 0; }
• 답은 아래와 같다.
• 먼저 func1의 매개변수를 보면 “int (*p)[3]”이고, 호출할 때 func1(a)로 호출한다. 즉, a의 “요소 배열”은 int [3]이며, 이거에 맞는 매개변수는 int형 3개짜리 요소이어야 한다.
• fucn2의 매개변수를 보면 “int (*p)[2][3]”이고, 호출할 때 func2(&a)로 호출한다. 즉 a의 “요소 배열”은 int [2][3]이며, &은 배열 전체를 나타내는 것! 그리고 이거에 맞는 매개변수는 int형 [2][3]짜리 요소이어야 한다.
• 그렇기 때문에–int가 4byte라고 할 때-func1에서 *(p+1)은 3*4byte를 이동하는 것이고, func2에서 *(p+1)은 6*4byte를 이동하는 것이다.
[포인터 배열 심화]
• 이전 포스트에서 설명한 부분이므로 자세한 부분없이 바로 설명하도록 하겠다.
• int (*ap)[N]과 같이 선언할 수 있으며, 이 때 N은 “배열 요소”의 크기이다. 만약 “배열 요소”의 크기를 모른다면 N은 생략될 수 있지만, “크기를 모르는 배열의 포인터”가 되기 때문에 전혀 쓸모가 없다.
• 아래 선언을 보자.
- int a1[3] = {1, 2, 3};
- int a2[2][3] = {1, 2, 3, 4, 5, 6};
- int *p1;
- int (*p2)[3];
• 위 선언처럼 되어있다 하자.
- p1 = a1; 으로 초기화가 되었다.
- cout << *p1 << endl;
- p1++;
- cout << *p1 << endl;
• 출력은 1, 2와 같다.
• 그렇다면 다음으로 초기화가 된 경우를 생각해보자.
- p2 = a1;
- cout << **p2 << endl;
- p2++;
- cout << **p2 << endl;
• 출력은 1이고 다음 출력은 어떻게 동작하는지 알 수 없다. 즉, a2와 같은 배열에 대해서만 의미가 있는 것이다.
- p2 = a2;
- cout << (*p2)[0] << “ ” << (*p2)[1] << endl;
- p2++;
- cout << (*p2)[0] << “ ” << (*p2)[1] << endl;
• 출력은 1, 2, 4, 5 이다.
'Learn business > C++' 카테고리의 다른 글
[C++]포인터와 배열2 (0) | 2016.12.06 |
---|---|
[C++]포인터와 배열1 (1) | 2016.12.06 |
[C++]포인터 (0) | 2016.12.04 |
[C++]new & delete (0) | 2016.11.29 |