본문 바로가기

awesome-c Beginner 번역/A tutorial on pointers

<비공식 번역>awesome-c Beginner 번역 : CHAPTER 2 : Pointer tpyes and Arrays

현재위치


<주의!!>

'A TUTORIAL ON POINTERS AND ARRAYS IN C'의 공식적인 번역이 아니며 수를 받은 것 역시 아닙니다!!




CHAPTER 2 : Pointer types and Arrays(챕터2 : 포인터 타입과 배열)


좋아요! 계속 가봅시다. 왜 우리가 포인터가 가리키는 변수의 타입을 구분할 필요가 있는지를 생각해봅시다.


int *ptr;


이렇게 표기하는 이유가 있습니다. 만약 ptr이 뭔가를 "가리킨다"면 이렇게 쓴다:


*ptr = 2;


컴파일러는 얼마나 많은 byte가 ptr이 가리키는 메모리 위치에 복사될 것인지 알 것입니다. 만약 ptr이 integer로 선언되었다면, 4byte가 복사될 것입니다. 유사하게 float과 double도 적절한 수가 복사될 것입니다. 그러나, 포인터 타입 정의하는 것에는 컴파일러가 코드를 해석할 수 많은 수의 흥미로운 방법이 있습니다.(번역자 : "포인터 타입을 정의하는 방법은 여러가지가 있다." 정도로 이해할 수 있겠습니다.) 예를 들어 한 행에 integer가 10개로 구성된 메모리 블록을 가정해봅시다. 메모리 40byte는 10 interger 10개를 잡는데 할당됩니다.


자 이제 이 정수들의 첫번째를 가리키는 우리의 integer 포인터 ptr을 말해봅시다. 또 그 integer의 메모리 위치를 100(10진수로)으로 가정합시다. 어떻게 우리가 써야할까요:


ptr + 1;


컴파일러는 이것이 포인터란 것을 "알기" 때문에 (즉, 이 값은 주소이다) 그것은 integer 배열를 가리키고(이 integer 주소의 현재 위치는 100이다.), ptr에 1대신에 4를 더합니다. 그래서 포인터는 메모리위치 104에 있는 다음 integer를 가리키게 됩니다. 유사하게, short로 선언된 포인터 ptr이라면 1대신 2가 더해질 것입니다. float, double, 구조체같은 사용자 정의 데이타 타입도 같은 방법이 적용됩니다. 이것은 우리가 일반적으로 생각하는 "덧셈"과 같지 않습니다. C에서 위와 같은 덧셈은 "포인터 연산"을 참조합니다. 이는 나중에 다시 언급할 예정입니다.


비슷하게, ++ptrptr++ptr + 1과 같습니다.(비록 프로그램에서 ptr이 증가할 때는 다를 수도 있습니다.), 앞 또는 뒤에 붙어 포인터가 증가에 사용하는 단일 ++ 연산자는 해당 포인터에 저장된 주소를 sizeof(type) 의 양만큼 ("type"은 가리키고 있는 개체의 타입입니다.(즉, integer의 4)) 증가시킵니다.


integer(정수)의 배열 정의에 의해 10 integer의 블록이 메모리에서 서로 인접하게 위치하기때문에, 이것은 배열과 포인터의 재미있는 연관관계를 불러옵니다.


다음을 생각해봅시다:


int my_array[] = {1, 23, 17, 4, -5, 100};


여기서 배열은 6개의 integer를 포함하고 있습니다. 우리는 이 각각의 integer들을 my_array의 이름으로 참조합니다. 즉, my_array[0]부터 my_array[5]를 사용합니다. 그러나 우리는 다른방식으로 포인터를 통해서 이들을 접근할 수 있습니다. 다음을 보세요.


int *ptr;

ptr = &my_array[0]; /* 우리 배열의 첫 integer를 가리키고 있습니다 */


그리고 우리는 우리 배열에 대해서 배열표기법을 사용하거나 포인터를 통한 접근을 사용함으로써 출력할 수 있습니다.

-----------  Program 2.1  -----------------------------------


/* Program 2.1 from PTRTUT10.HTM   6/13/97 */


#include <stdio.h>


int my_array[] = {1,23,17,4,-5,100};

int *ptr;


int main(void)

{

    int i;

    ptr = &my_array[0];     /* point our pointer to the first

                                      element of the array */

    printf("\n\n");

    for (i = 0; i < 6; i++)

    {

      printf("my_array[%d] = %d   ",i,my_array[i]);   /*<-- A */

      printf("ptr + %d = %d\n",i, *(ptr + i));        /*<-- B */

    }

    return 0;

}


위의 프로그램을 컴파일하고 실행합니다. 그리고 라인 A와 B 그리고 각각의 케이스에서 같은 값이 나오는 프로그램의 출력을 차근차근히 메모해보세요. 또한 라인 B에서 포인터가 주소에 접근하여 데이터를 출력하는지 관찰해 봅니다. 즉, 우리는 처음 i를 추가한 다음에 새 포인터를 역참조했습니다. 바뀐 라인 B를 읽어봅시다.


printf("ptr + %d = %d\n", i, *ptr++);


다시 실행합니다. 그리고 다음과 같이 바꿉니다.


printf("ptr + %d %d\n", i, *(++ptr));


그리고 한번 더 실행해봅니다. 매번 실행하고 결과를 예측합니다 그리고 신중하게 실제결과를 살펴봅니다.


C에서 표준상태일 때 우리가 &var_name[0]를 사용한다면 var_name으로 대체할 수 있습니다. 따라서 우리의 코드는 이렇게 작성될 수도 있습니다:


ptr = &my_array[0];


를 이렇게 쓸 수 있습니다.


ptr = my_arrray;


두 표현은 같은 결과를 만들어냅니다.


이는 [배열의 이름은 포인터]라는 상태로 많은 텍스트를 이끕니다. 나는 오히려 "배열의 이름은 배열의 첫번째 요소를 가리키는 주소이다" 라고 생각하는 걸 좋아합니다. 많은 초보자들이 (학습했었던 제 자신도 포함하여) 배열의 이름을 포인터로 생각하여 혼돈하는 경우가 있습니다. 예를 들어 우리가 이렇게 쓸 수 있겠죠:


ptr = my_array;


그러나 이렇게는 쓸 수 없습니다.


my_array = ptr;


그 이유는 ptr은 변수지만 my_array는 불변하는 상수(constant)이기 때문입니다. 그것은 my_array의 첫 요소의 위치는 저장 되었고 이는 my_array[]로 선언되어 바뀔 수 없습니다.


일찍이 "lvalue"에 대해서 논의할 때 나는 K&R-2를 인용하였습니다:


"An object is a named region of storage; an lvalue is an expression referring to an object".

"객체는 스토리지 구역에 명명된다. 좌변의 표현은 객체를 참조한다."


이는 흥미로운 문제를 제기합니다. my_array는 스토리지 구역에 명명된 이후로, 왜 my_array는 위의 할당문에서 lvalue가 될 수 없을까요? 이 문제를 해결하기 위해 일부는 my_array를 변경 불가능한 lvalue로 참조합니다.


위의 예시를 수정해봅시다.


ptr = &my_array[0];


에서


ptr = my_array;


(번역자 : 예시가 두번 반복되는 느낌입니다. 그러나 핵심은 동일합니다 my_array는 배열의 첫번째 요소를 가리키는 주소이기 때문에 ptr에 저장할 수 있습니다.)


그리고 그 결과가 동일한지 확인하기 위해 다시 실행합니다.


자, 위에 사용한 ptrmy_array의 차이점을 조금 더 탐구해봅시다. 몇몇 작가들은 배열의 이름을 상수(constant) 포인터로 참조합니다. 이게 무슨 의미일까요? 그럼 이러한 의미에서 용어 "상수"를 이해하기 위해, 용어 "변수"의 정의로 돌아가보겠습니다. 변수를 선언할 때 우리는 적절한 타입의 값을 넣기 위해서 메모리에 자리를 따로 설정합니다. 그것이 끝나면 변수의 이름은 두가지 중에 하나로 해석될 수 있습니다. 할당 연산자의 왼쪽 측면에 사용할 때, 컴파일러는 할당 연산자의 오른쪽 측면의 평가결과로 이동되는 메모리 위치로 해석합니다. 그러나 오른쪽 측면에 할당연산자를 사용할 경우, 변수의 이름은 그 변수의 값으로서 변수의 값이 저장된 메모리 주소로 해석됩니다.


그 사실을 인지해두고, 지금 간단한 상수를 생각해봅시다:


int i, k;

i = 2;


여기에, i는 가변적이면서 메모리의 데이터 부분에 공간을 점유하며, 2는 상수이고, 데이터 세그먼트 메모리를 따로 설정하는 대신에 이것은 메모리의 코드 세그먼트에 직접 입력되어(imbedded) 있습니다. 즉, k = i; 같이 쓸 때 컴파일러는 &i의 메모리 위치에 있는 값을 k으로 이동시키는 코드를 생성하고, 위에 적혀진 i = 2; 에선 코드의 단순한 입력 2는 데이터 세그먼트를 참조하지 않습니다. 즉, 와 는 객체이고 2는 아닙니다.


비슷하게, 위에서 my_array는 상수이기 때문에. 컴파일러는 배열이 저장될 때, my_array[0]의 주소를 "알고 있습니다".


ptr = my_array;


단순히 코드 세그먼트에서 상수로서 주소를 사용하고 데이터 세그먼트에 대한 참조는 없습니다.


이것은 chapter 1의 1.1에서 사용했던 표현 (void *)을 설명할 수 있습니다. 우리가 봐온 것 처럼 다양한 타입의 포인터를 가질 수 있습니다. 지금까지 우리는 character와 integer 포인터를 토의했습니다. 다음 chapter에서는 구조체를 가리키는 포인터와 포인터를 가리키는 포인터에 대해 배울 것입니다.


또한 우리는 서로 다른 시스템에서는 포인터의 크기가 다를 수 있다는 것을 배웠습니다. 포인터의 크기는 가리키는 객체의 유형에 따라 달리질 수 있음도 밝혀졌습니다. 따라서, short integer 타입 변수에 long integer를 할당을 시도할 때, 다른 타입의 변수 포인터에 다양한 타입의 포인터 값을 할당을 시도할 때 문제가 될 수 있습니다.


이 문제를 최소화하기 위해서 C는 void 포인터를 제공합니다. 우리는 이 포인터를 다음과 같이 쓸 수 있습니다:


void *ptr;


void포인터는 일반 포인터의 종류입니다. 예를 들어, C는 character 타입 포인터와 integer 타입 포인틔 비교를 허락하지 않을 것이지만 이들 각각은 void 포인터와는 비교가 가능하다. 물론 다른 변수와 같이 적당한 상황 하에서 다른 포인터의 타입으로 전환 할 수 있습니다. Chapter 1의 프로그램 1.1에서 integer 포인터는 %p 변환에 적합하게 만들기 위해서 void 포인터로 cast됩니다. 이후 chapter에서는 다른 캐스트가 내부에 정의된 이유에 대해 알아보겠습니다.


자. 소화해야할 기술적인 것들이 많이 있고 초보자 분들이게 한번 읽고 이 모든 것을 이해하리라 기대하진 않습니다. 시간을 들여서 chapter 2를 다시 읽어 보세요. 하지만 지금 포인터와 문자 배열 그리고 문자열의 관계로 이동할 수 있습니다.

 

출처 : https://github.com/aleksandar-todorovic/awesome-c