Learn business/C++

[C++]포인터와 배열3


포인터와 배열에 대한 마지막 글이 될 것 같다.

이번 포스트에서는 필자가 포인터와 배열의 공부하면서 가장 헷갈리고 애매했던 것에 대해 이야기하려고 한다.

[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에 대한 referencepointer 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]이며, 이거에 맞는 매개변수는 int3개짜리 요소이어야 한다.

fucn2의 매개변수를 보면 “int (*p)[2][3]”이고, 호출할 때 func2(&a)로 호출한다. a요소 배열int [2][3]이며, &은 배열 전체를 나타내는 것! 그리고 이거에 맞는 매개변수는 int[2][3]짜리 요소이어야 한다.

그렇기 때문에int4byte라고 할 때-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