본문 바로가기

무언가 만들기 위한 지식/C,C++,Embedded C

[C,C++] 1-Array and Pointer(일차원 배열과 포인터)

포인터하면 누구나 알고넘어가야할 C언어의 가장 기초이며,
공부하는 사람들이 가장 좌절하고 어려워 하는 부분이다.
이 부분은 주소값과 연관지어져 확실한 개념이 있어야 하부 디자인이 가능해진다.
(비록 자바나 기타 언어는 포인트를 사용하지 않더라도, 확실히 알면 사용하기 유용하다.)

필자의 경우에도 알고 있다고 생각했으나, 이런저런 수업을 추가로 받으며 포인터와 배열의 연관성에 대한 중요성에 대해 다시한번 생각하게 끔되었다.

예전에 Pointer와 Array의 연관성에 대해 생각해보면 "배열의 이름은 배열의 첫번째 주소값이 들어있다" 정도이다. 하지만 정식으로 배우면 생각보다 그렇게 단순하지만은 않다.

일단 포인터 변수 *p에서 p에 주소값이 들어가고 *p는 주소가 가리키는 값을 나타낸다 는 사실을 알고 있는분에게 도움이 될만한 포스팅이 될 듯 싶다.


C코드를 보다보면 int *(*ary[10])(int) 같은 해석하기 난해한 변수들을 접한적이 있을 것이다.
이런 변수의 해석에 대해 먼저 알아보자.

이를 해석하기 위해서는 먼저 연산자 우선순위에 대해 확실히 짚고 넘어가야한다. 

예를 들면,
int *(*ary[3]) 의 경우를 보자.
여기서 변수는 ary이다. C를 조금은 하는 사람은 일단 이게 2차원 배열인지?, 아니면 배열인지 이중 포인터인지 부터 헷갈릴 것이다.

위에서 ary를 기준으로 연산자가 *, [] 두가지가 있다.
(배열을 나타내는 연산자 []도 연산자이다. 연산자라는 사실을 확실히 집고 넘어가야 한다.)
그러면 ary는 일단 배열이다. 그리고 ()에 의하여 그 배열 요소의 값은 *이고, 그 포인터가 가리키는 값은 int *가 된다.
이런식으로 생각해 본후 무엇보다 메모리 구조에 대해 생각해보는 것이 중요하다.
그림으로 그린다면 메모리 구조는 다음과 같다.



다음으로 C에서의 주소값 연산에 대해 알아보자.
[작성환경 : Window XP  작성툴 : Visual Studio 6.0   컴파일러 : Visual Studio 6.0 사용언어 : C]


위 코드는 포인터의 주소를 이용한 주소 연산 이다.
보시다시피 8,9,10 Line은 Error을 유발한다. error message로는 두개의 포인터를 연산할 수 없다고 나온다.
이는 컴파일러에서 주소연산을 아예 막기 때문이다. 주소를 곱하거나 더하거나 나누어 봤자 사용될 곳이 없다고 컴파일러가 판단한 것이다.
(12 Line에서처럼 Unsigned int로 강제 형변환을 하면 어쨋든 연산은 가능하다.)
하지만 11 Line에서 두 주소의 뺄셈은 허용된 것을 볼 수 있다.
그 이유는 일단 Offset계산에 주소의 뺄셈이 이용되기 때문이다.
결과 값을 보면 약간 당황스러운 면을 볼 수 있다.
아래 결과를 보면 뺄셈의 결과 값이 1이 나온 것을 알 수 있다.
정상적이라면 4가 나와야 하는데 왜 1이 나온 것일까?
정답은 일단 4를 int type 즉 4byte로 나뉜 1이다.
이 부분은 컴파일러가 처리해 주는 부분으로 주소계산을 하게 되면 포인터가 가리키는 주소의 절대크기를 가리켜주는 것이 아니라 pointer가 가리키는 자료형의 개수를 알려준다.
즉 byte가 나오는 것이 아니라 해당 자료형의 개수를 알려주는 것이다.
이점은 배열(ex: ary[10])에서 *(ary+1)을 하면 ary[1]의 요소를 가리킨다는 사실에 적용된다.



바로 다음으로는 배열의 특징중의 하나라고 일컬어지는 양면성에 대해 확인해 보자.
[작성환경 : Window XP  작성툴 : Visual Studio 6.0   컴파일러 : Visual Studio 6.0 사용언어 : C]


위 코드는 배열의 특징을 테스트 해본 것이다. 3, 4 Line을 통하여 pAry가 배열 ary를 가리키도록 선언한 후의 테스트다.
여기서 ary, &ary, pAry를 출력해보았다.
ary와 pAry는 당연히 같으나 &ary는 어떤 값이 출력이 될 것인가?
아래 결과를 보면 ary와 &ary, pAry가 같다는 점을 확인할 수 있다.
그런데 6 Line에서 pAry=&ary는 어째서 컴파일 에러를 출력하는 것인가...
error message를 확인해보면 양쪽 자료타입이 다르다는 사실을 발견할 수 있다.
결론부터 명확히 말하자면 ary&ary의 값은 같지만, 다른 것을 나타내고 있다.
그림으로 나타내면 다음과 같다.


위에서처럼 &ary는 배열 전체를 나타내고 있고,
ary는 배열의 요소를 가리키고 있다. 배열이 시작되는 처음 주소를 가리키고 있기 때문에 출력하면 그 값은 같아보이지만 가리키는 대상은 다르다.
이 부분은 9 Line에서 주소값에 +1 을 한 값으로 확인이 가능하다.
ary에 1을 더한 값은 두번째 배열요소인 100을 확인할 수 있고,
&ary에 1을 더한 값은 배열전체 크기 에 1을 더한 곳의 값을 확인할 수 있다. (화면상에는 쓰레기값 1245056이 출력)
여기서 1이란 이전 배열 연산에서 확인했듯이 가리키는 대상의 전체 크기이다.

그런데 이상하게 생각해볼 점은 sizeof(ary)와 sizeof(&ary)의 값이 12로 값이 같다는 점이다.



다음은 배열과 포인터의 차이를 알아보자.
[작성환경 : Window XP  작성툴 : Visual Studio 6.0   컴파일러 : Visual Studio 6.0 사용언어 : C]

위 예제는 다시 배열을 포인터와 연동 시킨 것이다.
출력을 보면 모두 같게 나온다는 사실을 알 수 있다. 즉 배열이나 포인터나 같은 방식으로 접근이 가능하다.
ary도 포인터처럼 *(ary+1)방식으로 증감시킬 수 있다.
(맨 위 예제에서 언급하였다시피 1은 주소 1이 아니라 배열의 요소의 개수 이다. 즉 +4 byte가 될 것이다.)
중요한 개념이라고 말했다시피 배열표시 []도 연산자이다. 단항연산자이다.
그렇기 때문에 같은 연산자로 사용이 가능하다.



다음으로는 함수의 인자로 배열을 넘길떄를 보도록 하자.
[작성환경 : Window XP  작성툴 : Visual Studio 6.0   컴파일러 : Visual Studio 6.0 사용언어 : C]


위 코드를 보면 단순히 우리가 사용하는 함수에 배열을 넣은 것이다.
보통 어느정도의 수준의 C 코더라면 8~10 Line에 익숙할 것이다. (call by reference 개념이 확실한분들)
하지만 5~7 Line을 실행해도 아무런 이상이 없다.
필자도 이 부분이 의아했다.
테스트를 통해 int a[3], int *a, int a[] 3가지가 모두 이상없이 call by reference로 돌아간다는 사실을 알 수 있다.
(배열은 절대 함수의 인자로서 복사가 이루어지지 않고 배열의 처음 주소를 전달한다.)
직관적으로 봤을 때는 int *a를 선언시 함수의 인자로 써주는 것이 가장 이상적이라고 본다.

즉, 배열은 함수의 인자로 쓰일때는 항상 주소 값이 전달된다는 사실.

그런데 25번 코드를 보면 재미있는 점이 있다. zzz를 3칸짜리 배열로 선언하고 4번째 index의 요소를 출력해보니
ary[3]의 바뀐 두번째 값인 333이 출력되었다.
여기서 알아두어야 할 것은 일단 배열에서 Index를 벗어나도 컴파일러는 에러를 출력하지 않는다는 점,
(이것이 배열의 단점)과 Stack영역의 메모리 구조이다.

ary[3]는 zzz[3]보다 먼저 선언되었기 때문에 Stack구조상 아래쪽에 있고, zzz[3]은 stack상 가장 상위에 있다. 그렇기 때문에 인덱스-2를 초과하여 4로 출력하면 컴파일 에러는 나지 않고, &zzz[0] - [zzz의 최상의 주소값] 으로부터 4칸 아래층에 있는 ary의 값을 출력하게 된다.

이는 결국 배열이라는 것이 정해진 크기를 제한해주는 한 묶음이 아니라!
주소값으로부터 얼마나 떨어져 있는지를 나타내주는 개념이라는 사실을 알 수 있다.
사실상 zzz[4]는 컴파일러입장에서는 *(zzz+4)로 해석되어 그 주소에 위치한 값을 꺼내올 뿐이다.

배열 []는 사람이 인식하고 코딩하기 편하게 보이는 요소참조연산자일뿐이지,
컴파일러는 주소와 index로 인식한다는 점을 기억하기 바란다.
(그렇기 떄문에 배열에서는 자료형의 크기와 요소의 개수가 가장 중요하다.)



마지막으로 배열이 특수한 구조로 사용되는 모습을 보자.
[작성환경 : Window XP  작성툴 : Visual Studio 6.0   컴파일러 : Visual Studio 6.0 사용언어 : C]


위 코드를 보면 이게 뭐지하고 의아해 할지 모르나, 간단한 배열요소 접근하는 방법이다.
4, 6 Line은 확실히 생소할지모르지만, 에러 없이 잘돌아간다.
그 이유중에 하나가 []연산자가 ary와 2를 연결하는 연산자 역할을 하기 때문에 가능하다.
즉 단순히 프로그램 작성자의 이해를 돕기 위해서 쉽게 ary[i]식으로 나타낼 뿐이지
i[ary]도 가능하다.
마치 a=b+c와 a=c+b가 같은 코드인 것처럼 말이다.




※ 이상 일차원 배열에 대해 간단하게 언급해봤다. 사실 말이 간단하지 위 기본 이론을 더확고히 하고 의해서는
(C Standard에 의해서) 더 공부를 많이 하고 책을 읽어봐야한다. 

그리고  배열자체를 쉽게 이해하기 위해서는 어셈블리에 의한 메모리 구조변화와 Stack의 변화, Stack Point가 어떤식으로 변하는지 알아야지, 진짜 배열을 알 수 있다고 볼 수 있다. Stack이 쌓여가는 구조와 메모리 주소와의 매치가 되어야지 기본적인 ary와 &ary의 차이점, 왜 배열이 연산자이고 index를 통해 접근을 하는지 이해가 갈 것이다.