포인터(pointer)는 C언어의 가장 중요한 특성 중 하나이다. 그리고 가장 자주 잘못 이해되는 특성이다. 이러한 중요성 때문에, 포인터에 3개의 chapter를 사용할 것이고, 이 chpater에서는 기본에 집중한다. chapter 12와 chapter 17에서 더 고급적인 사용에 대해 알아볼 것이다.
포인터를 이해하는 첫번째 단계는 포인터가 기계 수준(machine level)에서 표현하는 것을 시각화하는 것이다. 현대의 컴퓨터에서, main memory는 8bit의 정보를 저장 가능한 bytes로 나누어진다.

각각의 byte는 메모리에 있는 다른 byte와 구별하기 위해 고유한 주소(address)를 가진다. 만약 n bytes가 메모리에 있다면, 우리는 0부터 n-1까지의 범위의 숫자로써 주소를 생각할 것이다.
실행 가능한 프로그램은 코드(C 프로그램에서 구문에 대응되는 machine instruction)와 데이터(프로그램 내부의 변수)로 구성이 된다. 프로그램 내부의 각각의 변수는 메모리에서 1또는 더 많은 byte를 차지한다.

첫번째 byte의 주소는 변수의 주소라고도 불린다. 아래의 그림에서 변수 i는 주소 2000과 2001을 차지하는 byte인데, 그래서 i의 주소는 2000이다.

비록 주소들이 숫자에 의해 표현되지만, 주소의 범위는 정수의 범위와는 다를 수 있다. 그래서 주소를 반드시 일반적인 정수형 변수로 저장하진 못한다. 그러나 우리는 특별한 pointer variables에는 주소를 저장할 수 있다. 우리가 변수 i의 주소를 포인터 변수 p에 저장할 때, 우리는 p가 i를 "가리킨다"라고 부른다. 다르게 설명하면, 포인터는 주소에 불과할 뿐이고, 포인터 변수는 주소를 저장할 수 있는 단순한 변수일 뿐이다.
이 책에서는, 포인터 변수 p가 변수 i의 주소를 저장하는 것을 나타내기 위해, p의 내용물이 i를 가리키는 화살표를 표시하도록 할 것이다.

포인터 변수는 일반적인 변수와 같은 방법으로 선언된다. 차이점은 포인터 변수의 이름 앞에 반드시 별표(*)가 붙어야 한다는 점이다.
int *p;
이 선언은 p가 int 자료형의 object를 가리킬 수 있는 포인터 변수라는 것을 나타낸다. 변수(variable) 대신 object라는 단어를 사용할 것인데, p는 메모리의 영역을 가리키는 것이지, 변수에 속해있는 것을 가리키는 것이 아니기 때문이다("object"라는 단어는 chapter 19에서 논의할 프로그램 디자인에서와는 다른 의미로 사용했다).
포인터 변수는 다른 변수들과 함께 선언될 수 있다.
int i, j, a[10], b[20], *p, *q;
이 예제에서 i와 j는 일반적인 정수형 변수이고, a와 b는 정수형 배열이며, p와 q는 정수형 object를 가리키는 포인터이다.
C언어는 모든 포인터 변수가 오직 특정한 자료형(referenced type)의 object만 가리키는 것을 허용한다.
int *p; /* points only to integers */
double *q; /* points only to double */
char *r; /* points only to characters */
참조되는 자료형이 무엇인지에 대한 제한은 없다. 사실 포인터 변수는 또다른 포인터도 가리킬 수 있다.
C언어는 포인터와 사용되는 특별한 연산자 한 쌍을 제공한다. 변수의 주소를 찾기 위해, 우리는 & (address) 연산자를 사용한다. 만약 x가 변수라면, &x는 메모리 내부의 x의 주소이다. 포인터가 가리키는 object로 접근하기 위해서는, *(indirection) 연산자를 사용한다. 만약 p가 포인터라면, *p는p가 현재 가리키고 있는 object를 표현한다.
포인터 변수를 선언하면 포인터를 위한 공간이 확보되지만, 이 포인터가 object를 가리키지는 않는다.
int *p /* points nowhere in particular */
p를 사용하기 전에 초기화하는 것은 아주 중요하다. 포인터 변수를 초기화하는 한가지 방법은 & 연산자를 사용하여 어떤 변수의 주소를 할당하거나, 더 일반적으로는 lvalue의 주소를 할당하는 것이다.
int i, *p;
...
p = &i;
변수 p에 i의 주소를 할당하는 것으로, p가 i를 가리키게 된다.

선언과 동시에 포인터 변수를 초기화하는 것도 가능하다.
int i;
int *p = &i;
i의 선언과 p의 선언을 결합하는 것도 가능한데, i가 먼저 선언되어야 한다.
int i, *p = &i;
포인터 변수가 object를 가리키고 나면, 우리는 *(indirection) 연산자를 사용하여 object에 저장되어 있는 것에 접근할 수 있다. 만약 p가 i를 가리키고 있다면, 우리는 i의 값을 아래와 같이 출력할 수 있다.
printf("%d\n", *p);
printf는 i의 주소가 아닌 값을 출력할 것이다.
수학적인 경향이 있는 독자들은 *이 &의 역이라고 생각할 수도 있다. &를 변수에 적용하는 것은 변수를 가리키는 포인터를 생성한다. *를 포인터에 적용하는 것은 원래의 변수를 가져오도록 한다.
j = *&i; /* same as j = i; */
p가 i를 가리키는 한, *p는 i의 별칭(alias)이다. *p가 i와 같은 값을 가질 뿐만 아니라, *p의 값을 바꾸는 것은 i의 값 또한 바꾸기 때문이다(*p는 lvalue이고, 그래서 *p에 대입을 하는 것도 규칙에 어긋나지 않는다). 아래의 예시는 *p와 i의 동등함을 나타낸다. 아래의 다이어그램은 계산 동안 다양한 지점에서의 p와 i의 값을 보여준다.
p = &i;

i = 1;

printf("%d\n", i); /* prints 1*/
printf("%d\n", *p); /* prints 1*/
*p = 2;

printf("%d\n", i); /* prints 2 */
printf("%d\n", *p); /* prints 2 */
절대로 초기화되지 않은 포인터 변수에 indirection 연산자를 적용하면 안된다. 만약 포인터 변수 p가 초기화되지 않았다면, 어떠한 방법으로든 p의 값을 사용하도록 시도하면 undefined behavior를 일으킬 것이다. 아래의 예제에서, printf의 호출은 쓰레기(garbage)를 출력하고 프로그램이 충돌하도록 할 것이며, 또다른 어떤 효과를 가질 수도 있다.
int *p;
printf("%d", *p); /*** WRONG ***/
*p에 값을 대입하는 것은 특히 위험하다. p가 유효한 주소를 포함하고 있다면, 다음에 나오는 대입(assignment)은 주소에 저장된 데이터를 수정하려고 시도할 것이다.
int *p;
*p = 1; /*** WRONG ***/
만약 이런 대입에 의해 수정된 위치가 프로그램에 있다면, 프로그램이 돌발적인 행동을 할 수 있다. 만약 운영체제(operating system)에 이러한 대입이 있다면, 그 프로그램은 대부분 crash된다. 컴파일러가 p가 초기화되지 않았다고 경고할 것이고, 그렇기 때문에 어떠한 경고 메시지든지 반드시 주의를 기울여야 한다.
C언어는 포인터가 같은 자료형을 가지고 있다면 포인터를 복사하기 위한 대입(assignment) 연산자를 허용한다. i, j, p, p가 아래와 같이 선언되었다고 생각해보자.
int i, j, *p, *q;
아래의 구문은 포인터 대입의 예시이다. i의 주소가 p로 복사된다.
p = &i;
아래는 포인터 대입의 또다른 예시이다.
q = p;
위의 구문은 p의 내용물(i의 주소)을 q로 복사하는데, q가 p와 동일한 지점을 가리키도록 하는 효과를 낸다.

p와 q는 i를 가리키고, *p나 *q를 사용하여 새로운 값을 i에 대입할 수 있다.
*p = 1;

*q = 2;

몇 개의 포인터 변수든지 같은 object를 가리킬 수 있다.
하지만 q = p와 *q = *p는 다르다. 전자는 포인터 대입이지만, 후자는 포인터 대입이 아니다.
아래의 예시를 보자.
p = &i;
q = &j;
i = 1;

*q = *p;

*q = *p 대입은 p가 가리키는 i의 값을 q가 가리키는 j의 값에 복사한다.
이런 생각이 들 수도 있다. 그래서 포인터가 어디에 좋은거지?
이 질문에 대한 답은 하나가 아니다. 왜냐하면 C언어에서 포인터는 다양한 사용처가 있기 때문이다. 변수를 가리키는 포인터가 어떻게 함수의 argument로써 사용되는지 알아볼 것이다.
우리는 Section 9.3에서 함수 호출을 할 때 argument로써 전달되는 변수가 변하지 않도록 보호된다는 것을 보았다. 왜냐하면 C언어는 argument를 값으로써 전달하기 때문이였다. 만약 우리가 함수에서 변수의 값을 수정할 수 있게 하길 원한다면 이러한 C언어의 특성은 성가실 것이다. 섹션 9.3에서의 변수를 수정하기 위한 decompose 함수를 작성했었고, 이는 실패했었다.
포인터는 이런 문제에 대한 해결책을 제시한다. argument로써 변수 x를 함수에 전달하는 대신에, 우리는 x를 가리키는 포인터인 &x를 전달할 것이다. 대응되는 parameter p를 포인터로 선언해야 한다. 함수가 호출되었을 때, p는 &x의 값을 가질 것이고, 그러므로 *p(p가 가리키는 object)는 x에 대한 별칭이 된다. 함수의 body에서 *p가 나타나면, *p는 x에 대한 indirect reference가 될 것이고, 함수가 x를 읽어 그것을 수정할 수 있다.
이러한 기술을 행동으로 보기 위해 decompose 함수를 수정해보자. int_part와 frac_part parameter를 포인터가 되게끔 선언하면 된다.decompose의 정의는 이제 아래와 같다.
void decompose(double x, long *int_part, double *frac_part)
{
*int_part = (long) x;
*frac_part = x - *int_part;
}
decompose의 prototype은 아래와 같다. 둘 중 하나를 선택하여 사용하면 된다.
void decompose(double x, long *int_part, double *frac_part);
void decompose(double, long *, double *);
우리는 아래와 같은 형태로 decompose를 호출할 것이다.
decompose(3.14159, &i, &d);
i와 d의 앞에 &연산자가 있기 때문에, decompose에 대한 argument는 i와 d에 대한 포인터이고, i와 d의 값이 아니다. decompose가 호출되었을 때, 3.14159의 값은 x에 복사될 것이고, i에 대한 포인터는 int_part에 저장되고, d에 대한 포인터는 frac_part에 복사된다.

decompose의 body 내부의 첫번째 대입은 x의 값을 long 자료형으로 변환시키고, int_part에 의해 가리켜지는 object에 저장된다. int_part가 i를 가리키고 있기 때문에 i에 3을 대입될 것이다.

두번째 대입은 int_part가 가리키는 i의 값을 가져온다. 이 값은 double 자료형으로 변환될 것이고, x로부터 뺄셈이 이루어진다. 그리고 frac_part가 가리키는 object에 .14159가 저장된다.

decompose가 반환될 때, i와 d는 우리가 원했던 것처럼 각각 3과 .14159의 값을 가질 것이다.
함수에 대한 argument로써 포인터를 사용하는 것은 실질적으로는 새로운 것이 아니다. 우리는 scanf의 호출을 chapter 2에서 보았었다. 아래의 예제를 생각해보자.
int i;
...
scanf("%d, &i);
우리는 & 연산자를 i앞에 붙였고, scanf에 i에 대한 포인터를 전달했다. 이 포인터는 scanf가 읽은 값을 어디에 넣을 지 말해주는 포인터이다. &가 없다면 scanf는 i의 값을 전달받게 된다.
scanf의 argument는 반드시 포인터가 되어야 한다고 해도, 모든 argument가 &연산자를 항상 필요로 하지는 않는다. 아래의 예제에서는, scanf가 포인터 변수를 전달받는다.
int i, *p;
...
p = &i;
scanf("%d", p);
p는 i의 주소를 포함하고 있기 때문에, scanf는 정수를 읽어 i에 저장할 것이다.
&연산자를 아래와 같은 호출에 사용하는 것은 잘못되었다.
scanf("%d", &p); /*** WRONG ***/
scanf는 i가 아닌 p에 정수를 넣을 것이다.
함수에게 포인터를 전달하는 것을 실패하는 것은 비참한 결과를 초래할 수 있다. i와 d앞에 &연산자가 없는 상태로 decompose를 호출했다고 생각해보자.
decompose(3.14159, i, d);
decompose는 두번째와 세번째 argument로 포인터를 예상하고 있지만, i와 d의 값이 대신 주어졌다. decompose는 그 차이에 대해 알려줄 수 없기 때문에, i와 d의 값을 마치 포인터인 것처럼 사용할 것이다. decompose가 *int_part와 *frac_part 안에 값을 저장했을 때, 우리는 i와 d를 수정하는 대신에 알 수 없는 메모리 위치를 변경시키려고 시도할 것이다.
만약 우리가 decompose에 대한 prototype을 제공했다면(당연히 항상 해야하는 것이다), 컴파일러는 우리가 잘못된 자료형의 argument를 전달하려고 시도한다는 것을 우리에게 알려줄 것이다. 그러나 scanf의 경우에는 포인터 전달의 실패가 컴파일러에게 감지되지 않을 때가 종종 있다. 그래서 scanf를 특히 에러를 잘 발생시키도록 한다.
const to Protect Arguments함수를 호출하고 변수에 대한 포인터를 전달했을 때, 우리는 일반적으로 함수가 변수를 수정할 것이라고 생각할 것이다(그렇지 않다면 함수가 왜 포인터를 필요로 하겠는가?). 예를 들어 아래의 구문이 프로그램 안에 있다면 우리는 f가 x를 변화시키는 것이라고 예상할 것이다.
f(&x);
그러나 f가 값의 변화가 아닌 단지 x의 값을 검사하기 위해서 필요로 할 수도 있다. 포인터의 이유는 효율성일 수 있다. 변수가 공간의 총량을 많이 필요로 한다면 변수의 값을 전달하는 것은 시간과 공간의 낭비일 수 있다.(Section 12.3에서 여기에 대해 더 자세히 다룰 것이다)
만약 주소가 함수로 저날된 object를 변화시키길 원하지 않을 수도 있는데, 이 때 우리는 단어 const를 사용하여 이를 보여줄 수 있다. const는 parameter의 선언에 들어가는데, 자료형의 명시가 나오기 전에 사용한다.
void f(const int *p)
{
*p = 0; /*** WRONG ***/
}
이러한 const의 사용은 p가 "constant integer"에 대한 포인터라는 것을 나타낸다. *p에 대한 수정은 에러이고, 컴파일러가 이를 감지할 것이다.
함수에 포인터를 전달할 뿐만 아니라 포인터를 반환하는 함수 또한 작성할 수 있다. 이러한 함수는 상대적으로 일반적이다. 우리는 다양한 종류를 chapter 13에서 볼 것이다.
아래의 함수는, 2개의 정수에 대한 포인터를 받고, 어떠한 정수가 더 큰지 포인터로 반환하는 함수이다.
int *max(int *a, *b);
{
if (*a > *b)
return a;
else
return b;
}
우리가 max를 호출하였을 때, 우리는 두 개의 int형 변수를 포인터로 전달하고, 포인터 변수에 결과를 저장할 것이다.
int *p, i, j;
...
p = max(&i, &j);
max의 호출동안, *a는 i의 별칭이 되고, *b는 j의 별칭이 된다. i가 j보다 더 큰 값을 가졌다면 max는 i의 주소를 반환할 것이고, 반대의 경우라면 j의 주소를 반환할 것이다. 호출 이후에 p는 i나 j 중 하나를 가리킬 것이다.
max함수가 arguments로써 전달된 포인터 중 하나를 반환하지만, 이것만 가능한 것은 아니다. 함수는 external variable이나 static으로 선언된 지역 변수(local variable) 또한 포인터로 반환이 가능하다.
절대로 automatic 지역 변수를 포인터로 반환하면 안된다.
int *f(void)
{
int i;
...
return &i;
}
변수 i는 f가 반환되면 존재하지 않으며, 그렇기 때문에 i를 가리키는 포인터는 유효하지 않을 것이다. 어떤 컴파일러는 "functions returns address of local variable"과 같은 오류를 발생시키기도 한다.
포인터는 일반적인 변수 말고도 배열의 요소도 가리킬 수 있다. 만약 a가 배열이라면, &a[i]는 a의 요소 i에 대한 포인터이다. 함수가 배열 argument를 가졌을 때, 배열 안의 하나의 요소에 대한 포인터를 반환하는 함수는 종종 도움이 된다. 예를 들어 아래의 함수는 배열 a의 중간 요소에 대한 포인터를 반환하는 함수이다. a가 n개의 요소를 가지고 있다고 생각해보자.
int *find_middle(int a[], int n)
{
return &a[n/2];
}
chapter 12에서 포인터와 배열에 대해 더 자세한 세부적인 관계성에 대해 알아볼 것이다.
포인터는 항상 같은 주소를 나타내는가?
보통은 그렇지만 항상 그런 것은 아니다. byte가 아닌 word로 나누어진 main memory를 가진 컴퓨터를 생각해보자. word는 36비트, 60비트, 또는 다른 비트를 포함할 수 있다. 만약 36bit word라고 가정했을 때, 메모리는 아래와 같은 모습을 보일 것이다.

메모리가 word로 나누어졌을 때, 각각의 word는 주소를 가진다. 하나의 정수는 보통 하나의 word를 차지하는데, 그렇기에 하나의 정수에 대한 포인터는 주소가 될 수 있다. 그러나 워드가 하나의 문자보다 더 많이 저장할 수 있다. 예를 들어 36bit word는 6개의 6bit 문자들을 저장할 수 있다.

또는 9bit 문자를 4개 저장할 수 있다.

이러한 이유로, 문자에 대한 포인터는 다른 포인터에 비해 다른 형태로 저장될 필요가 있다. 문자에 대한 포인터는 주소(문자가 저장된 word)에 작은 정수(word 안에서 문자가 저장된 곳)를 더한 것으로 이루어져 있다.
어떤 컴퓨터에서는, 포인터는 완성된 주소보다는 오히려 "오프셋(offset)"이다. 예를 들어 Intel x86 family 내부의 CPU들은 다양한 모드로 프로그램을 실행할 수 있다. 이 중 시간을 거슬러 가장 오래된 1978년의 프로세서인 8086은 real mode라고 불린다. 이 모드에서는 주소들이 단일 16bit 숫자(offset)에 의해 표현되기도 하고, 두 개의 16bit 숫자(a segment:offset pair)에 의해 표현되기도 한다. offset은 진짜 메모리 주소가 아니다. CPU는 특별한 레지스터에 저장된 segment value를 결합시켜야 한다. real mode를 지원하기 위해서, 오래된 C 컴파일러는 종종 두 종류의 포인터를 제공한다. near pointers(16-bit offsets)와 far pointer(32-bit segment:offset pairs)이다. 이러한 컴파일러들은 보통 단어 near과 far을 비표준 키워드로 예약해놓는다. 그리고 이는 포인터 변수의 선언에 사용될 수 있다.
만약 포인터가 프로그램 내부의 데이터를 가리킬 수 있다면, 포인터가 프로그램의 코드를 가리키는 것이 가능한가?*
가능하다. 함수를 가리키는 포인터는 Section 17.7에서 알아볼 것이다.
int *p = &i;
p = &i;
전자의 구문과 후자의 구문의 선언 사이에 어떠한 불일치가 있는 것처럼 보이는데, 왜 p 앞에는 * 기호가 들어가지 않는데 선언할 때에는 들어가는가?
C언어에서 *는 사용되는 맥락에 따라서 다양한 의미를 가지고 있기 때문에, 오해의 소지가 있다.
int *p = &i;
위의 구문에서 *는 indirection operator가 아니다. 대신에, 이는 p의 자료형을 명시하는 것에 도움을 주어 컴파일러에게 p가 int에 대한 포인터라는 것을 알려준다. 만약 구문에서 *이 나타난다면, *는 indirection(단일 연산자로써 사용됨)을 수행한다.
*p = &i; /*** WRONG ***/
위의 구문은 잘못됐는데, p 자체에 대입한 것이 아니고 p가 가리키는 object에 i의 주소를 대입했기 때문이다.
변수의 주소를 출력할 방법이 있는가?
변수의 주소를 포함하고 있는 어떤 포인터든지 %p 변환 명시자를 사용하는 것으로 printf의 호출을 통해 출력할 수 있다. Section 22.3에서 세부사항을 볼 것이다.
아래의 선언은 혼란을 주는 것 같다. 이는 f가 p를 수정하지 못한다는 의미인가?
아니다. f가 p가 가리키는 정수를 변경시킬 수 없다는 의미이다. f가 p 자체를 바꾸는 것을 막을 수는 없다.
void f(const int *p)
{
int j;
*p = 0; /*** WRONG ***/
p = &j; /* legal */
}
argument는 값으로써 전달되기 때문에 p에 새로운 값을 대입하는 것(다른 어딘가를 가리키도록 만드는 것)은 함수 바깥에는 어떠한 영향도 미치지 않는다.
포인터 자료형의 parameter를 선언할 때, 아래의 예제처럼 const를 parameter의 이름 앞에 놓는 것도 규칙에 맞는가?
void f(int * const p);
규칙에 맞긴 하지만, p의 자료형 앞에 붙은 const와는 다른 효과를 가지진 않는다. 우리는 Section 11.4에서 const를 p의 자료형 이전에 넣는 것은 p가 가리키는 object를 보호한다고 했었다. 그러나 const를 p의 자료형 이후에 넣는 것은 p자체를 보호한다.
void f(int * const p)
{
int j;
*p = 0; /* legal */
p = &j; /*** WRONG ***/
}
이 특징은 아주 자주 사용되지는 않는다. p가 단지 다른 포인터의 복사본(함수가 호출되었을 때의 argument)이기 때문에 이것을 보호할 어떠한 이유는 흔치 않다.
p와 p가 가리키는 object를 둘다 보호해야할 아주 흔치 않은 필요가 있다면 const를 p 자료형의 앞 뒤에 모두 넣으면 된다.
void f(const int * const p)
{
int j;
*p = 0; /*** WRONG ***/
p = &j; /*** WRONG ***/
}