[C] 포인터의 타입과 상수 포인터

Gyuwon Lee·2022년 6월 15일
0

42 Seoul 7기

목록 보기
5/6
post-thumbnail

썸네일 사진은 한 스택오버플로우 게시물에서 찾았는데, 읽어보면 좋을 법한 내용이다.

📢 42서울 7기 라피신을 대비하여 모두의 코드 '씹어먹는 C 언어' 강좌의 내용을 정리한 게시물로, 이전 블로그로부터 옮겨 온 글입니다. (원문)

1. 포인터의 타입

앞에서 우리는 포인터가 메모리 상에서 특정 데이터가 위치해있는 주소를 담고 있다는 사실을 알아보았다.

컴퓨터는 데이터를 메모리에 저장할 때 바이트 단위로 나눠서 저장하는데, 각 자료형은 char 를 제외하고 대부분 2바이트 이상이다. int 자료형이 4바이트인 것처럼 말이다.

이처럼 데이터란 메모리에 연속된 바이트 형태로 저장될텐데, 포인터는 어느 바이트를 가리키고 있는 것이며, 어디까지 한 개의 데이터로 인식할 수 있는 것일까?

여기서 포인터의 타입과 관련된 문제가 발생한다. 각 자료형이 차지할 수 있는 바이트의 크기를 명시하여 포인터에도 동일한 타입을 부여해주지 않으면, 메모리에서 얼마만큼을 읽어들여야 할 지 알 수 없게 된다.

왜냐하면 포인터에게 전달되는 주소값은 해당 변수(데이터)가 메모리에서 차지하는 모든 주소들의 위치가 들어있는 것이 아니라 시작 주소만 들어가 있기 때문이다.


1) 포인터와 변수

포인터도 변수 라는 사실을 잘 기억하자. 즉, 포인터에 들어간 주소값은 바뀔 수 있다. 하나의 포인터가 항상 같은 위치를 가리킬 필요는 없다는 뜻이다.

/* 포인터도 변수다 */
#include <stdio.h>
int main() {
  int a;
  int b;
  int *p;

  p = &a;
  *p = 2;
  p = &b;
  *p = 4;

  printf("a : %d \n", a);
  printf("b : %d \n", b);
  return 0;
}

2) 상수 포인터

바로 위의 내용과는 반대로, 한 번 가리킨 주소값을 절대 바꾸지 않는 포인터 즉 상수 포인터를 만들 수도 있다.


📌 상수?

상수 포인터 이전에, 상수 즉 const 그 자체는 프로그래밍에서 아주 중요한 역할을 한다는 것부터 이해하면 좋을 것이다.

이는 프로그래밍 상에서 프로그래머들의 실수를 줄여주고, 실수가 발생했을 시 이를 잡아내는 데 아주 중요한 역할을 하게 된다. 왜냐하면 상수는 최초의 선언(초기화) 이후 그 어떤 경우에도 대입 연산자(=) 또는 포인터에 의한 값 변경 (ex. *{포인터}) 이 불가능하기 때문이다.

const int a = 3;
a = 3;

심지어는 이와 같은 대입조차 불가능하다. 그래서 중요한 값이며 절대로 바뀌지 않을 것 같은 값에는 무조건 const 키워드를 붙여주는 습관이 중요하다.

/* 상수 포인터? */
#include <stdio.h>
int main() {
 int a;
 int b;
 const int *pa = &a;

 *pa = 3;  // 올바르지 않은 문장
 pa = &b;  // 올바른 문장
 return 0;
}

위 코드에서 포인터 pa 의 선언에 const 키워드를 더해 줌으로써 상수 포인터를 만든 처럼 보인다.

그런데, 이 const 키워드는 아래와 같이 경우를 나누어 위치할 수 있다:

const int *pa = &a; // const int 형 변수를 가리키는 포인터 pa
int const *pa = &a; // int 형 변수를 가리키는 const 포인터 pa

📌 상수를 가리키는 포인터

먼저 첫 번째 const int* pa = &a; 의 경우, 우리가 여기서 말하고 싶은 상수 포인터는 아니다. 상수를 가리키는 포인터가 맞다. 즉, 포인터에 담기는 주소값이 변하면 안되는 것이 아니라, 포인터가 가리키는 변수가 값이 바뀔 수 없는 상수값인 경우다.

/* 상수 포인터? */
#include <stdio.h>
int main() {
 int a;
 int b;
 const int *pa = &a;

 *pa = 3;  // 올바르지 않은 문장
 pa = &b;  // 올바른 문장
 return 0;
}

여기서 중요한 것은 위의 이 코드에서 int a 는 초기화 시 상수가 아니었다는 점이다. 변수 a 의 자료형은 그저 int 였지만 포인터 pa 를 통해서 간접적으로 가리킬 때에는 컴퓨터 입장에서 'const인 변수를 가리키고 있구나' 라고 생각하게 되어 값을 바꿀 수 없게 된다.

그래서 *pa = 3 과 같이 pa 가 가리키는 변수(a)의 값은 바꿀 수 없지만, pa = &b 와 같이 pa 에 담기는 값(주소값) 자체는 바꿀 수 있다.


📌 상수 포인터

/* 상수 포인터? */
#include <stdio.h>
int main() {
  int a;
  int b;
  int const *pa = &a;

  *pa = 3;  // 올바른 문장
  pa = &b;  // 올바르지 않은 문장

  return 0;
}

그럼 동일한 원리로, 이처럼 int const *pa = &a;const가 나중에 적힌 포인터의 경우 const * 가 핵심이 되어 pa에 담기는 값 자체가 변경될 수 없다는 의미임을 알 수 있다.

그래서 pa = &b; 처럼 pa가 가리키는 대상을 변경할 수는 없지만, *pa = 3;와 같이 pa가 가리키는 대상의 '값'은 변경할 수 있게 된다.


3) 포인터의 덧뺄셈

#include <stdio.h>

int main() {
  int a;
  int* pa = &a;

  printf("포인터 pa의 값: %p\n", pa);
  printf("(포인터 pa) + 1의 값: %p\n", pa + 1);

  return 0;
}

위 코드를 컴파일해보면, 16진수이지만 어쨌든 papa + 1 의 값은 4만큼 차이난다는 것을 확인할 수 있다.

1만큼 차이날 것이라고 예상했다면, 포인터가 가지는 값인 주소는 그냥 숫자의 나열이 아니라 해당 주소에 들어가 있는 데이터를 위한 값이라는 점을 상기하자.

int 자료형은 4바이트의 메모리 공간을 필요로 하므로, int형 변수 a의 주소를 가리키는 포인터 pa 는 4바이트 중 첫 번째 바이트의 주소를 담고 있다.

예를 들어 이를 0x12345678 이라고 하자. 이 때 변수 am는 이 주소부터 시작해서 총 4바이트만큼, 즉 0x12345678 ~ 0x1234567B 까지의 메모리 공간을 차지하고 있는 것이다. 그러므로 pa + 1a에게 할당된 4바이트만큼의 공간 다음으로 연속된 주소, 즉 원래 주소값 + 4 만큼의 주소값을 반환한다.

achar였다면 1바이트 뒤, double이었다면 8바이트 뒤의 주소값을 반환했을 것이다.

다만, 포인터 + 상수 가 아닌 포인터 간의 덧셈은 C에서 허용되지 않는다. 사실, 포인터끼리의 덧셈은 아무런 의미가 없을 뿐더러 필요 하지도 않다. 두 변수의 메모리 주소를 더해서 나오는 값은 이전에 포인터들이 가리키던 두 개의 변수와 아무런 관련이 없는 메모리 속의 임의의 지점일 뿐이다.

그런데, 포인터끼리의 뺄셈은 가능하다. 왜 그런지는 다음 글에서 자세히 다루고자 한다.

profile
하루가 모여 역사가 된다

0개의 댓글