05. 포인터

2022. 12. 30. 22:40자료구조, 알고리즘

728x90
반응형

(해당 내용은 자료구조/알고리즘을 공부하기 위해 작성된 것이므로, 내용에 오류가 있을 수 있습니다.)

포인터(Point)란 C/C++ 프로그래밍에서 매우 중요한 개념입니다.

또한, 처음 접할 경우 매우 생소하고 어려운 개념인데요.

저도 이번에 포인터를 접하였을 때 이해가 가지 않았습니다만, 이 글을 작성하면서 공부해 보고자 합니다.

 

변수와 메모리

포인터를 이해하기 위해서는 먼저 컴퓨터 메모리에 변수가 어떻게 저장되어 있는지를 알아야 합니다.

아래와 같이 정수형 변수 num을 선언하고, 이 변수에 4를 대입하는 코드를 실행했다고 가정해 봅시다.

그럼 컴퓨터 메모리에 아래와 같은 동작이 실행될 겁니다.

int main()
{
	int num;
	num = 4;

	return 0;
}

 

변수 num이 정수형(int)로 선언되면, 컴파일러와 운영체제는 num이 들어갈 적절한 메모리 공간을 확보합니다.

여기서 num은 int이므로 4바이트의 메모리가 확보됩니다.

이후 num=4를 실행하게 되면 num이 있는 메모리 주소에 4라는 값이 입력됩니다.

여기서 변수 num은 1004번째 메모리에 저장되며, 이를 변수 num의 주소라고 합니다.

이 메모리 주소는 운영체제가 알아서 정하는 것이므로, 1004는 예시이며 프로그램이 실행될 때마다 메모리 주소는 달라집니다.

위와 같은 방식으로 변수 num이 메모리에 저장됩니다.

여기서 변수의 자료형에 따라 각각 다른 크기의 메모리 공간이 필요합니다.

자료형 메모리 크기
char 1 byte
short 2 byte
int 4 byte
float 4 byte
double 8 byte

 

포인터의 개념

포인터는 메모리의 주소를 저장하기 위해 사용되는 변수를 뜻합니다(일반 변수는 값을 저장).

포인터를 선언하는 방식은 일반 변수와 같으나, 변수명 앞에 "*"를 붙여준다는 차이점이 있습니다.

int *변수명	// 정수형 포인터
char *변수명	// 문자형 포인터
float *변수명	// 실수형 포인터

포인터를 선언하는 방법은 절대 주소값상대 주소값이 있습니다.

절대 주소값은 말 그대로 포인터에 주소값을 입력해 주는 방식입니다.

int *a;			// 포인터 a 선언
a = (int *)1000		// a의 주소값을 1000번지로 지정

반면 상대 주소값포인터를 선언한 후 해당 포인터에 변수의 주소값을 대입하는 것으로, 프로그래머가 주소값을 지정해 주는 방식이 아니라 운영체제가 알아서 메모리 공간을 할당해 주는 방식입니다.

int a = 10;			// 정수형 변수 a를 선언하고, 변수 a를 10으로 초기화
int *p;				// 포인터 * 선언
p = &a;				// 포인터 변수 a는 변수 a의 주소를 저장

여기서 "&"는 주소 연산자로서, 일반 변수의 주소를 표현할 때 사용합니다.

보통 포인터를 선언할 때는 상대 주소값을 사용하여 운영체제가 알아서 메모리를 할당하도록 하며, 절대 주소값은 프로그래머가 메모리 주소를 잘못 지정함으로써 실행 중인 다른 프로그램에 문제가 발생할 수 있으므로 잘 사용하지 않습니다.

위의 코드를 그림으로 표현하면 다음과 같습니다.

 

포인터 사용 시 주의사항

포인터는 C/C++ 프로그래밍에 있어 널리 사용되지만, 잘못 사용해서 오류가 발생하는 경우도 있습니다.

포인터 사용시 주의해야 할 사항으로는 2가지가 있습니다.

1) 포인터 초기화

포인터는 처음 선언 시 아무 정보가 없는 상태가 아니라 무작위의 주소값을 가진 상태로 선언됩니다.

그러므로 포인터를 선언한 직후 반드시 포인터에 저장할 주소값을 저장하거나, 아니면 "NULL"값을 입력해야 합니다.

그렇지 않으면 포인터에 값을 입력할 경우 이미 사용중인 엉뚱한 메모리에 값이 덮어쓰게 되므로,

컴파일러에서는 이를 방지하기 위해 오류를 출력합니다.

NULL값은 "stdio.h" 헤더 파일에서 0으로 선언되어 있으며, 0번지로 지정하면 운영체제에서 자동으로 해결해주기 때문에

선언하고 당장 사용하지 않는 포인터라도 0으로 선언하여 유효하지 않은 상태로 만들어주는 것이 좋습니다.

2) 포인터와 포인터가 지정하는 값의 자료형 일치

포인터를 선언할 때 포인터가 가리키는 일반 변수의 값을 확인하고, 그 변수의 자료형과 맞는 포인터로 선언하여야 합니다.포인터의 자료형과 포인터가 저장할 변수의 자료형이 맞지 않으면 오류가 발생하며, 이는 자료형이 차지하는 메모리 크기와 관련이 있습니다.

예를 들어 아래와 같은 코드를 실행한다고 가정해 봅시다.

// 출처 : C 언어 일취월장(한빛아카데미, 2022)
int a;
double* pd = NULL;

pd = &a;
*pd = 13.8;		// 포인터 *pd가 가리키는 변수 a는 정수형

변수 a의 크기는 4바이트, 포인터 *pd의 크기는 8바이트입니다.

  1. 먼저 a는 정수형이므로 이에 맞게 4바이트가 할당되었고, 이후 *pd가 실수형으로 선언되어 8바이트가 할당됩니다.
  2. 그 다음 pd가 a의 주소를 담게 됨으로써 *pd는 a의 값을 가리키게 되었습니다.
  3. 이 다음에 *pd에 13.8이 대입되면서 *pd가 가리키는 a에 13.8이 대입되어야 합니다만, a는 정수형이라 4바이트만 확보되어 있는데, 여기에 8바이트 자료가 입력되게 되었습니다.
  4. a에 실수형 자료를 담기 위해 인접한 메모리까지 침범하게 되어 결국 메모리 사용에 차질이 생기게 되었습니다.
  5. 이를 방지하기 위해 컴파일러에서는 포인터와 그 포인터가 가리키는 변수의 자료형이 다르면 오류를 출력합니다.

포인터 연산

1) 포인터 연산자 종류와 연산 규칙

포인터도 연산을 할 수 있습니다.

다만 일반적인 데이터를 연산하는 것이 아니라 주소에 대해 연산하기 때문에 사용하는 연산자는 제한되어 있습니다.

포인터 연산자의 종류는 아래와 같습니다.

(출처 : C 언어 일취월장(한빛아카데미, 2021)

유형 종류 활용(p : 포인터, v : 일반 변수)
산술 연산자 +, -, ++, -- *p+1, *p-1, *p--, *++p, ++*p, --*p
주소 참조 연산자 &, * int *p, p=&v
대입 연산자 =, +=, -= p = &v, v += (*p)++
비교 연산자 ==, != p === NULL, p != NULL

포인터 연산은 일반 변수와는 달리 주소값을 이용하므로, 다음과 같은 규칙을 가집니다.

  1. 포인터끼리의 연산은 불가능
  2. 포인터와 정수 연산은 가능하나, 실수와의 연산은 불가능
  3. 대입 연산자 사용 시 자료형이 동일한 포인터끼리 대입 가능

2) 포인터 증감 연산

포인터 증감 연산을 수행하기 위해서는 포인터 자료형별 증감과 증감 연산자의 활용법을 알아야 합니다.

아시다시피 C언어에서는 char, short, int, double 등의 자료형이 있고, 이들이 차지하는 바이트 수 또한 다릅니다.

각각의 자료형에 포인터 증감을 수행하였을 경우 포인터가 가리키는 주소값은 다음과 같이 변합니다.

포인터 연산에서의 증감은 단순히 1을 증감하는 것이 아니라 포인터의 자료형 크기만큼 증감하는 것입니다.

예를 들어 int형은 4바이트이므로 4바이트씩 증감하고,

double형은 8바이트이므로 8바이트씩 증감하는 방식입니다.

 

포인터의 증감 연산자를 어떻게 사용하느냐에 따라 주소값이 변하기도 하고, 포인터가 가리키는 값이 달라지기도 합니다.

# include <stdio.h>

int main()
{

	double d;
	double* pd;
	pd = &d;

	printf("1. 포인터 증감 연산자 사용 전 주소값\n");
	printf("double형 포인터 주소값 : %d\n", pd);

	pd++;
	printf("\n2. 변수 pd++ 연산자 수행 후 주소값\n");
	printf("double형 포인터 주소값 : %d\n", pd);

	pd--;
	*pd++;
	printf("\n3. 포인터 *pd++ 연산자 수행 후 주소값\n");
	printf("double형 포인터 주소값 : %d\n", pd);

	*pd--;
	(* pd)++;
	printf("\n4. 포인터 (* pd)++ 연산자 수행 후 주소값\n");
	printf("double형 포인터 주소값 : %d\n", pd);

	(* pd)--;
	*++pd;
	printf("\n5. 포인터 *++pd 연산자 수행 후 주소값\n");
	printf("double형 포인터 주소값 : %d\n", pd);

	*--pd;
	++* pd;
	printf("\n6. 포인터 ++* pd 연산자 수행 후 주소값\n");
	printf("double형 포인터 주소값 : %d\n", pd);

	return 0;
}

  1. double형 포인터 pd의 주소값은 -1491141384으로 설정되었습니다.
  2. pd는 d의 주소값이므로, pd++를 실행할 경우 주소값이 8바이트 증가하여 -1491141376이 되었습니다.
  3. ++는 *보다 우선순위는 높지만, *가 pd 앞에 존재하면 *pd부터 먼저 처리한 후에야 pd++를 처리합니다. 그러므로 결과적으로는 pd++처럼 주소값을 8바이트 증가시키게 됩니다.
  4. *pd++와는 달리 (*pd)++의 (*pd)에는 괄호가 있습니다. 여기서 먼저 *pd가 가리키는 값을 가지고 온 후 이 값에 ++ 연산을 수행하여 데이터값을 1 증가시킵니다. 결과적으로는 주소값은 변하지 않고 그 주소에 있는 데이터값이 변하는 결과를 얻었습니다.
  5. 포인터 계산은 우측에서 좌측으로 이동하여 계산됩니다. 그러므로 ++pd를 먼저 수행하여 주소값을 변경시킨 후에 변경된 주소가 가지고 있는 값을 출력한다고 보시면 되겠습니다.
  6. 여기서는 반대로 *pd를 먼저 실행하여 포인터가 가리키는 값을 가져온 후, 여기에 ++를 수행하여 데이터값을 변경하는 방식입니다. 그러므로 여기서는 주소값은 변하지 않고, 데이터값만 변하게 되는 것입니다.

예제 11-08 double형 포인터에 괄호와 증감 연산자 사용(출처 : C 언어 일취월장(한빛아카데미, 2021))

# include <stdio.h>

int main()
{
	double d = 3.3058;
	double* pd;

	pd = &d;

	printf("1. 포인터 pd의 증감 연산 전 주소값과 데이터값\n");
	printf("포인터 pd의 현재 주소값 : %u\n", pd);
	printf("포인터 pd의 현재 데이터값 : %f\n", *pd);

	(*pd)++;					// 포인터가 가리키는 주소의 데이터값을 1 증가

	printf("\n2. 포인터 (*pd)++ 증감 연산 후 주소값과 데이터값\n");
	printf("포인터 pd의 증감 연산 후 주소값 : %u\n", pd);
	printf("포인터 pd의 증감 연산 후 데이터값 : %f\n", *pd);

	(*pd)--;					// 포인터가 가리키는 주소의 데이터값을 1 감소
	++*pd;						// 포인터가 가리키는 주소의 데이터값을 1 증가

	printf("\n3. 포인터 ++*pd 증감 연산 후 주소값과 데이터값\n");
	printf("포인터 pd의 증감 연산 후 주소값 : %u\n", pd);
	printf("포인터 pd의 증감 연산 후 데이터값 : %f\n", *pd);

	return 0;
}

포인터 연산 (*pd)++와 ++*pd 결과입니다.

역시 주소값은 변하지 않고 데이터값만 변하는 결과를 얻었습니다.

 

참고자료

1.  신윤환(2021), C 언어 일취월장, 한빛아카데미

2. Stephan Prata(윤성일, 이선민, 조혜란 옮김) (2017), C 기초 플러스 6판, 성안당

728x90
반응형

'자료구조, 알고리즘' 카테고리의 다른 글

06. 동적 메모리  (7) 2023.01.06
04. 클래스  (4) 2022.12.22
03. 배열  (2) 2022.12.03
02. 알고리즘 시간 복잡도 함수(2)  (1) 2022.11.24
02. 알고리즘 시간 복잡도 함수(1)  (6) 2022.11.16