C 언어 | 포인터 | 포인터

포인터의 기본 개념과 응용에 대해 코드를 사용하여 설명한다. 포인터는 주소 연산자에서 얻은 주소를 저장하고 간접 연산자를 사용하여 원래의 기억 영역에 액세스한다.

메모리 참조

C 언어 초보자가 좌절하기 쉬운 난관 중 하나가 포인터라고 알려져 있다. 하지만 포인터를 이해하기 위해 수학적인 지식이 필요하지 않으며, 특별히 어려운 이론을 학습해야 하는 일도 없다. 원리만 이해하고 나면 포인터는 어려운 것이 아니다. 중요한 순서대로 조금씩 확실하게 이해 가도록 노력하는 것이다.

그럼, 본문에 들어가겠다. 포인터는 메모리 영역의 위치를 가리키는 변수의 일종이다. 지금까지 우리는 변수를 사용해 왔다. 변수에 문자열을 사용할 수 있게 되었다. 포인터도 변수이다. 어려운 것은 없다.

문제는 포인터에 무엇을 저장하는가 하는 것이다. 보통의 변수는 특정 메모리 영역에 어떤 값을 저장하기 위해 사용하고 왔지만, 포인터는 값의 저장을 위해 사용하는 것은 아니다. 포인터는 메모리 주소를 저장하는 변수이다.

변수는 값을 메모리의 어딘가에 일시적으로 저장하는 것이다. 메모리는 1바이트 단위로 어드레스(주소)가 할당되어 있고, 컴퓨터는 이 주소를 사용하여 메모리에 저장되어 있는 값에 액세스한다. 실제로 순수 기계어에는 변수 따위는 존재하지 않고 값을 저장하고 검색하려면 주소를 사용하고 있다. 주소는 처음부터 순서대로 수치로 표현된다.

그림1 - 메모리 구성

메모리 구성

그림1은 메모리를 그림으로 쉽게 설명하고 있다. 메모리 값을 저장할 수 있다. 문자나 실수도 결국은 값(그림에서는 알기 쉽게 10진수로 표현하고 있지만, 사실은 2진수로 저장되어 있다)으로 기록되어 있는 것이다. 값이 어떤 형태로 저장되거나 컴퓨터 아키텍처에 따라 다르지만 메모리 주소는 바이트 단위로 할당 순서대로 단순 증가하고 있다. 메모리 주소가 몇 바이트로 표시되는 것인가 하는 문제도 컴퓨터에 따라 다르다.

포인터는 메모리에 메모리 주소를 나타내는 값을 저장하여 특정 변수(메모리에 저장되어 있는 값)의 위치 정보를 교환하는 수단으로 사용되는 것이다. “메모리에 메모리 주소를 저장"이라는 개념이 까다롭지만, 이는 곧 나중에 C 언어으로 구현하고 확인해 보자.

그럼 변수가 메모리의 어디에 저장되어 있는지를 실제로 살펴보자. 변수의 주소를 얻으려면 변수 이름 앞에 앰퍼샌드 “&“을 붙인다. 이는 주소 연산자라고 한다.

C언어에서 변수 이름을 수식에 사용하면 그 변수의 내용(저장되어 있는 값)을 반환하지만 주소 연산자를 변수 앞에 지정된 경우 해당 변수의 메모리 주소를 반환한다. 주소 연산자는 scanf("%d”, &iVariable)과 같이 scanf() 함수에서 이미 사용했던 적이 있다. 이 때 & 기호는 변수의 주소를 함수에 전달한다.

주소 연산자 &는 논리 연산자 &와 다르기 때문에 주의하자. 단항 연산자 &가 사용된 경우는 주소 연산자이며, 2항 연산자 &가 이용되면 논리 연산자로 해석된다.

주소의 표현은 처리계에 의존하는 문제이다. 기계어 수준에서 생각하면 주소는 정수형으로 표현되고 있지만, 구현에 의존하는 처리는 피하는 것이 좋다. 주소를 printf() 함수를 사용하여 표시하는 경우 %X와 같은 형식을 사용하여 표시할 수 있지만, 보다 확실하게 표시하려면 %p를 사용하여 처리계에 맞는 표현으로 출력한다.

코드1

#include <stdio.h>

int main() {
  char chVar = 'G';
  int iVar = 10;

  printf("chVar : 내용 = %c, 주소 = %p\n" , chVar , &chVar);
  printf("iVar : 내용 = %d, 주소 = %p\n" , iVar , &iVar);
  return 0;
}

각 변수의 내용은 변수를 초기화할 때 저장한 값이다. 이것은 변수가 가리키는 메모리에 저장되어 있던 정보이다. 이에 대해 주소는 이 변수를 나타내고 있는(즉, 이 값이 저장되어 있는) 메모리 주소이다. printf()의 인수에 주소 연산자 &를 사용하는 것에 주목한다.

표시되는 실제 숫자는 실행하는 컴퓨터와 그 때의 메모리 상황에 따라 달라진다. 메모리 주소는 응용 프로그램을 실행했을 때 시스템이 할당한 때 결정되기 때문에 정적으로 고정할 수 없다. 요즘 대부분의 컴퓨터에서 메모리를 자유롭게 처리할 수 있는 권한이 있는 것은 기본 소프트웨어(운영 체제)뿐이다.

주소 저장

메모리 주소를 얻는 방법은 알았지만, 그것만으로는 아무것도 할 수 없다. 메모리 주소의 수치를 이용하는 것은 컴퓨터이며, 개발자와 이용자에게 메모리 주소는 그다지 의미가 없다.

그래서 포인터가 필요하다. 포인터는 메모리 주소를 저장하는 것을 전문으로하는 특수한 변수이다. 포인터에 주소를 대입하여 포인터 변수는 직접 메모리 주소에 액세스할 수 있게 되는 것이다. 원격에서 변수의 내용을 조작하는 것 같은 이미지이다.

포인터의 선언은 기본적으로 일반 변수와 동일하지만, 변수 이름 앞에 별표 *를 붙입니다.

포인터의 선언

형식 *변수명

이렇게 하면 포인터 변수에 주소를 저장할 수 있다. 예를 들어 int *iPo 선언한 경우 iPo 변수는 int 형 변수에 대한 포인터임을 나타낸다. 메모리 주소는 정수임을 설명했다. 포인터 변수는 주소 저장하기 위한 변수이며 실체로는 정수형인 것을 의미하고 있다.

포인터 변수가 들어있는 주소를 따라 원래의 변수(주소가 가리키는 메모리의 값)을 얻기 위해서는 간접 연산자 *를 사용한다. 이 포인터 변수 앞에 지정하여 포인터 변수에 포함된 주소 값을 검색할 수 있다. 간접 연산자 *는 곱셈 연산자 *는 다르므로 구별해야 한다. 간접 연산자는 단항 연산자이다.

간접 연산자를 사용하여 포인터가 가리키는 메모리의 값을 취득하는 것을 간접 참조라고 한다.

그림2 - 주소에서 간접 참조

주소에서 간접 참조

그림2는 포인터를 저장하는 주소를 사용하여 간접 참조를하는 상황을 나타낸다. 포인터도 변수의 일종이며 메모리의 어딘가에 정수 값으로 주소를 저장한다. 포인터는 이것을 사용하여 원래의 변수에 액세스하는 것이다.

코드2

#include <stdio.h>

int main() {
 int iVar = 100;
 int *iPo = &iVar;

 printf("iPo = %p : &iVar = %p : *iPo = %d\n" , iPo , &iVar , *iPo);
  return 0;
}

iPo 변수는 int 형 변수에 대한 포인터이다. 포인터가 저장하고 있는 메모리 주소이며, iPo 변수의 내용을 printf() 함수로 표시하면 저장된 메모리 주소임을 알 수 있다. 이것은 & iVar와 같은 값인 것을 확인할 수 있다.

포인터가 저장된 주소가 가리키는 메모리의 값을 얻으려면 간접 연산자 *를 사용한다. printf() 함수의 마지막에 *iPo을 지정하여 iPo 변수에 할당된 주소를 사용하여 간접 참조를 하고 있다. 이렇게 함으로써 iPo에 저장되어 있는 주소, 즉 iVar 값을 간접적으로 검색할 수 있다.

간접 연산자를 이용하면 주소를 간접 참조 할뿐만 아니라, 포인터가 나타내는 주소 값을 간접 할당할 수 있다. 즉, 포인터를 통해 변수에 값을 할당한다.

코드3

#include <stdio.h>

int main() {
  int iVar = 0;
 int *iPo = &iVar;

 *iPo = 100;
 printf("iVar = %d\n" , iVar);
  return 0;
}

이 프로그램은 iPo 포인터 변수에 iVar int 형 변수의 주소를 할당한다. 그럼 *iPo = 100;와 같이 포인터를 사용하여 iVar 간접 할당을 하고 있는 것이다. 그 결과 printf() 함수는 iVar 값은 100이 되어있는 것을 확인할 수 있다.

이러한 간접 대입에 무슨 의미가 있는지 이해하기 어렵지도 모른다. 확실히 코드3을 보면 값은 직접 iVar 변수에 할당하면 되는 것만으로 포인터를 사용하는 의의를 느낄수 없다.

포인터는 다른 함수끼리 자동 변수를 공유할 때 위력을 발휘한다. 함수 안에서 선언된 변수는 자동 변수가되고, 변수는 선언된 블록 내부에서만 사용할 수 있다. 함수의 인수로 전달할 수 있는 값의 복사이며, 다른 함수에서 자동 변수를 조작하는 방법은 없다.

그래서 함수의 인수에 포인터를 전달한다. 그러면 다른 함수의 자동 변수를 원격 조작할 수 있다. 포인터를 전달하면 함수는 이를 간접 참조하여 다른 함수의 자동 변수에 값을 대입하는 것이 가능하게 되는 것이다. 따라서 함수 포인터를 전달 간접적으로 정보에 액세스하는 방법을 참조로 전달이라고 단순히 값을 복사하여 전달 방법을 값 전달이라고 한다.

예를 들어, “두 변수의 값을 바꿀 함수를 만드십시오"라는 과제가 주어진 경우 기존의 방법으로는 달성할 수 없다. 함수의 반환 값은 항상 하나의 값 밖에 없으며, 매개 변수의 값을 교체해도 함수의 호출에 아무런 영향도 주지 않는다. 이런 경우에 포인터를 매개 변수로 받는 방법을 생각할 수 있다.

코드4

#include <stdio.h>

void swapInt(int * , int *);

int main() {
  int iVar1 = 100 , iVar2 = 1000;
 swapInt(&iVar1 , &iVar2);

 printf("iVar1 = %d : iVar2 = %d\n" , iVar1 , iVar2);
 return 0;
}

void swapInt(int *iPo1 , int *iPo2) {
 *iPo2 ^= *iPo1 , *iPo1 ^= *iPo2 , *iPo2 ^= *iPo1;
}

코드4의 swapInt() 함수는 두 개의 int 형의 포인터를 받는다. 이 함수는 전달된 포인터에서 간접 참조를 하고 호출자가 지정한 변수의 값을 변경시킬 수 있는 것이다. 프로그램을 실행하면 iVar1 = 1000 : iVar2 = 100이라는 결과를 얻을 수 있을 것이다.

그런데 포인터 변수에 데이터 형이 존재한다. char 형 변수의 주소는 char 형의 포인터를 int 변수의 주소는 int 형 포인터에 할당해야 한다. 하지만 포인터는 메모리 주소를 나타내는 정수를 저장하기 위한 것으로, 포인터 변수가 점유하는 메모리 영역은 항상 같은 크기이다. 포인터는 C 컴파일러가 포인터가 가리키는 변수가 몇 바이트인지를 인식하는데 사용된다. 이제 간접 참조로 할당하고 검색을 갔을 때 몇 바이트 단위로 값을 복사하거나 대입하는 방법이 결정된다. 따라서 특별한 경우를 제외하고 서로 다른 타입의 포인터 변수에 주소를 할당할 수는 없다.

코드5

#include <stdio.h>

int main(){
  int iVar1 = 0xFFF , iVar2;
  unsigned char *chPo = &iVar1;

 iVar2 = *chPo;

  printf("iVar2 = %d\n" , iVar2);
  return 0;
}

이 프로그램은 부호없는 char 형의 포인터 chPo에 int 형 변수 iVar1 주소를 대입한다. 확실히 chPo는 iVar1의 주소를 틀림없이 저장할 수 있지만 iVar2 = *chPo에서 간접 참조했을 때, 컴파일러는 이 포인터 참조는 1바이트(char 타입)으로 판단한다. iVar1에 저장되는 값은 16 진수 0xFFF이지만, char 형태로 간접 참조하면, 이 중 하위 1바이트 밖에 보이지 않기 때문에 iVar2는 16 진수 0xFF 즉 10 진수 255이 대입된다.