두번째 시험 기간을 맞이하면서, 이해가 잘 되는 것 같다가도 어렵게 느껴지는 포인터에 대해서 다시 한 번 정리해 보기로 했다. 여태까지의 C언어는 그다지 어렵지는 않았었지만, 포인터를 다시 한 번 마주한 이후에는, 헷갈리는 부분이 많아졌다고 생각한다.
C언어에서는 메모리 주소를 저장하는 변수인 포인터라는 자료형이 존재한다. 이런 포인터의 역할은 주소 저장을 통해 간접 참조를 하는 것이 특징이다. 예를 들자면,
int a = 10;
int *p = &a;
*p = 11;
printf("%d %p", *p, p); // 11, 메모리 주소값 출력
이러한 코드에서, int *p는 포인터 변수이고, 변수 a의 메모리 주소를 저장하고 있다.
*p=11;이라는 코드에서는 p라는 포인터 변수가 변수 a의 메모리 주소를 가리키고 있기 때문에 p자체의 값이 아닌 변수 a의 값이 변하게 된다.
&: 주소 연산자, 포인터에 주소값을 지정할 때나 scanf와 같은 입력을 받을 때에도 사용한다.*: 선언할 때의 *기호는 해당 변수가 포인터라는 것을 말해주고, 그 이후 변수를 사용하는 도중의 기호는 해당 포인터가 참조하는 주소의 값을 가리킬 때 사용한다.%p: 포인터의 주소값을 나타낼 때 사용하는 서식 문자이다.포인터는 메모리 주소 값을 저장하고 있기 때문에, 포인터가 가리키는 변수의 자료형과 무관하게 항상 4바이트의 크기를 가지고 있다. (32bit 컴퓨터 기준)
포인터 변수를 선언 한 이후에, 값을 할당하지 않으면 쓰레기 값이 들어가지만 안전하게 선언하기 위해서는 int *p=NULL와 같은 NULL값으로 초기화를 시켜줘야한다. 이런 NULL값은 공백문자('\0')와 몇 가지의 차이점이 있다.
우선 공백문자는 자료형이 char이며, 문자열의 마지막에 존재한다. 또한 널 포인터 상수와 달리 실제값이 존재한다.
NULL은 널 포인터 상수라고 불리며, 자료형은 void이다. 실제값이 존재하지 않고<stdio.h>와 같은 헤더파일이 존재해야 사용할 수 있다.
c언어에서 배열의 이름은 배열의 시작 주소를 가리키는 상수 포인터이다.
#include <stdio.h>
int main(void)
{
int a[5]={1, 3, 5};
if(a==&a[0])
{
printf("True");
} else
{
printf("False");
}
return 0;
}
다음과 같은 코드를 실행해보면 결과값은 True가 나오며, 배열과 배열의 첫번째 값의 주소는 같다는 점을 알 수 있다.
포인터와 배열의 차이점은 포인터는 +/-, ++/--와 같은 증감연산자까지 사용이 가능하지만, 배열은 증감연산자를 사용하지 못해서 +/-만 사용할 수 있다. 증감 연산자를 사용할 시에는 원본의 값까지 변하기 때문에 배열에서는 사용할 수 없다.
c언어에서 배열은 row-major방식을 사용하기 때문에, 배열은 서로 이웃해 있다. 이를 이용하여 포인터에 값을 더하면 옆 칸의 값을 참조할 수 있다.

위처럼 int배열에서는 각 값의 주소가 4byte씩 차이가 나는데,Arr배열의 첫번째 값의 주소를 가지고 있는 포인터 변수에서 1을 더하면, 1001이 아닌 1003이 된다. 하지만 이는 배열의 자료형에 따라 유동적으로 바뀐다. (만일 char형 배열이였다면 p+1 => 1001)
포인터 배열과 배열 포인터, 이름부터 서로 비슷하지만 자세히 알아보면 많은 차이점을 알 수 있다.
우선 포인터 배열은 포인터를 모아놓은 배열, 주소값을 모아둔 배열이다. ex) int* num[]={1, 2, 3};
char *ptr[3]={"dog", "cat", "lion"};
printf("%s\n", *(ptr+2)); // lion
printf("%c\n", *(*(ptr+1)+1); // a
여기서 ptr은 3개의 문자열의 주소를 가리키는 포인터를 담는 배열이다. -> ptr[0]은 dog의 주소, ptr[1]은 cat의 주소... 이런 식으로 주소를 가리키게 된다.
*(ptr+2)는 배열의 시작인 dog의 주소에서 2를 더해, 3번째 값인 lion이 출력된다.*(*(ptr+1)+1)은 안쪽 괄호에서부터, *(ptr+1)은 cat의 값을 불러오고 그 다음 +1을 하면, cat의 첫번째의 +1한 값이므로 cat의 두번째 글자인 a를 불러온다.이러한 포인터 배열의 장점은 2차원 배열과 달리 필요한 부분만 사용할 수 있어서, 공간 낭비가 줄어든다.
반면에 배열 포인터는 하나의 배열을 가리키는 포인터라고 할 수 있다.
int arr[3] = {10, 20, 30};
int (*p)[3];
p = &arr;
printf("(*p)[0] = %d\n", (*p)[0]); // 10
printf("(*p)[1] = %d\n", (*p)[1]); // 20
printf("(*p)[2] = %d\n", (*p)[2]); // 30
포인터 p는 arr의 전체 배열을 가리키고 있다.(*p)[0]과 같은 형식을 사용하면 각 배열의 값을 가져올 수 있고, p[0][i]은 (*p)[i]와 같다.
C언어의 핵심이라고 할 수 있는 포인터에 대해서 정리해보았지만, 포인터는 공부하면 공부할 수록 어렵기도 하고 재미있기도 한 것 같다. 그렇기 때문에 오늘도 C언어에 대해서 한 걸음 더 나아갈 수 있었던 것 같다.