27. 포인터(Pointer)
1. 키워드
- 포인터(Pointer)
- 주소 연산자
- 포인터 변수
- 역참조(Dereference) 연산자
void
타입 포인터- 단일 포인터와 다중 포인터
2. 포인터 사용하기
- 지금까지 값을 저장할 때 변수를 사용했다.
- 이렇게 사용된 변수는 컴퓨터의 메모리에 생성된다.
- 즉, 메모리에 일정한 공간을 확보해 두고 원하는 값을 저장하거나 가져오는 방식이다.
- 보통 변수는
num1
과 같이 이름으로 사용하지만, 메모리의 특정 장소에 위치하고 있으므로 메모리 주소로도 표현할 수 있다. - 일상 생활에서 집을 구분할 때 주소를 사용하는 것과 같은 원리이다.
- 다음은 변수의 메모리 주소를 구한다.
#include <stdio.h>
int main()
{
int num1 = 10;
printf("%p\n", &num1); // num1의 메모리 주소를 출력
// 컴퓨터마다, 실행할 때마다 달라짐
return 0;
}
// 0x3041272c8
- 변수의 메모리 주소를 구할 때는 변수 앞에
&
(주소 연산자)를 붙이면 된다. - 여기서는
&num1
과 같이num1
앞에&
를 붙여서 변수num1
의 메모리 주소를 구했다. - 메모리 주소는
0x3041272c8
과 같이 16진수 형태이며printf
에서 서식 지정자&p
(Pointer의 약어)를 사용하여 출력한다. - 물론 16진수로 출력하는
%x
,%X
를 사용해도 된다. - 이때 메모리 주소는 고정된 것이 아니라 컴퓨터마다, 실행할 때마다 달라진다.
32비트, 64비트와 메모리 주소
- 다음과 같이 시스템이 32비트인지 64비트인지에 따라 메모리 주소의 범위도 달라진다.
1] 32비트
- 16진수 8자리
0x00000000
~0xFFFFFFFF
2] 64비트
- 16진수 16자리
0x0000000000000000
~0xFFFFFFFFFFFFFFFF
- 64비트 메모리 주소는
0x00000000
00000000`처럼 8자리씩 끊어서 ```를 붙이기도 한다.
리눅스, OSX에서 %p
*
- 리눅스, OSX에서 서식 지정자
%p
를 사용하면 메모리 주소008AF7FC
는0x8af7fc
와 같이 앞에0x
가 붙고,A
~F
는 소문자로 출력된다. - 또한 높은 자릿수의
0
은 생략된다.
3. 포인터 변수 선언하기
- C에서 메모리 주소는 포인터 변수에 저장한다.
- 다음과 같이 포인터 변수는
*
를 사용하여 선언한다. - 포인터 변수는 줄여서 포인터라고 부르기도 한다.
#include <stdio.h>
int main()
{
int *numPtr; // 포인터 변수 선언
int num1 = 10; // int 타입 변수를 선언하고 10 저장
numPtr = &num1; // num1의 메모리 주소를 포인터 변수에 저장
printf("%p\n", numPtr); // 포인터 변수 numPtr의 값 출력
// 컴퓨터마다, 실행할 때마다 달라짐
printf("%p\n", &num1); // 변수 num1의 메모리 주소 출력
// 컴퓨터마다, 실행할 때마다 달라짐
return 0;
}
// 0x3041272bc
// 0x3041272bc
- 포인터 변수를 선언할 때는 자료형 뒤에
*
(애스터리스크)를 붙인다. *
의 위치에 따른 차이는 없으며 모두 같은 뜻이다.
- 포인터 변수를 선언했으면 다음과 같이
&
로 변수의 주소를 구해서 포인터 변수에 저장한다.
- 이제
printf
로 포인터numPtr
의 값을 출력해 보면 변수num1
의 메모리 주소가 나온다. - 즉, 포인터와 메모리 주소는 같은 의미이다.
printf("%p\n", numPtr); // 포인터 변수 numPtr의 값 출력
// 컴퓨터마다, 실행할 때마다 달라짐
printf("%p\n", &num1); // 변수 num1의 메모리 주소 출력
// 컴퓨터마다, 실행할 때마다 달라짐
- 포인터 변수를 선언할 때는 자료형을 알려주고
*
를 붙이는 방식을 사용한다. - 만약 변수가
int
타입이면 이 변수의 메모리 주소를 저장하는 포인터는int *
이어야 한다. - 여기서
int *
는 영어로pointer to int
라고 읽는데int
타입 공간을 가리키는 포인터라는 뜻이다. - 간단하게
int
타입 포인터라고도 부른다.
- 즉, 다음과 같이 포인터는 메모리의 특정 위치를 가리킬 때 사용한다.
- 이제 다음 코드를 그림으로 표현해 보자.
numPtr
은10
이 저장된 메모리 공간을 가리킨다.- 즉, 변수
num1
이 있는 공간을 가리키게 되는 것이다.
32비트와 64비트와 포인터 크기
- 다음과 같이 시스템이 32비트인지 64비트인지에 따라 포인터의 크기가 달라진다.
1] 32비트
- 16진수 8자리
0x00000000
~0xFFFFFFFF
2] 64비트
- 16진수 16자리
0x0000000000000000
~0xFFFFFFFFFFFFFFFF
sizeof
로 포인터의 크기를 구해 보면 32비트에서는 4바이트, 64비트에서는 8바이트가 나온다.
4. 역참조 연산자 사용하기
- 포인터 변수에는 메모리 주소가 저장되어 있다.
- 이때 메모리 주소가 있는 곳으로 이동해서 값을 가져오고 싶다면 역참조 연산자
*
를 사용한다.
#include <stdio.h>
int main()
{
int *numPtr; // 포인터 변수 선언
int num1 = 10; // 정수형 변수를 선언하고 10 저장
numPtr = &num1; // num1의 메모리 주소를 포인터 변수에 저장
printf("%d\n", *numPtr); // 역참조 연산자로 num1의 메모리 주소에 접근하여 값을 가져옴
return 0;
}
// 10
- 다음과 같이
numPtr
앞에*
를 붙이면numPtr
에 저장된 메모리 주소로 가서 값을 가져온다. - 여기서는
numPtr
이num1
의 메모리 주소를 저장하고 있으므로num1
의 값인10
이 출력된다.
- 즉, 포인터는 변수의 주소만 가리키며, 역참조는 주소에 접근하여 값을 가져온다.
포인터 선언과 역참조
- 포인터를 선언할 때도
*
를 사용하고 역참조를 할 때도*
를 사용한다. - 같은
*
기호를 사용해서 헷갈리기 쉽지만 선언과 사용을 구분해서 생각하면 된다. - 즉, 포인터를 선언할 때
*
는 "이 변수는 포인터이다"라고 알려주는 역할이고, 포인터에 사용할 때*
는 "포인터의 메모리 주소를 역참조한다"라는 뜻이다.
- 이번에는 포인터 변수에 역참조 연산자를 사용한 뒤 값을 저장(할당)해 보자.
#include <stdio.h>
int main()
{
int *numPtr; // 포인터 변수 선언
int num1 = 10; // 정수형 변수를 선언하고 10 저장
numPtr = &num1; // num1의 메모리 주소를 포인터 변수에 저장
*numPtr = 20; // 역참조 연산자로 메모리 주소에 접근하여 20을 저장
printf("%d\n", *numPtr); // 역참조 연산자로 메모리 주소에 접근하여 값을 가져옴
printf("%d\n", num1); // 실제 num1의 값도 바뀜
return 0;
}
// 20
// 20
- 역참조 연산자는 값을 가져올 수도 있고 값을 저장할 수도 있다.
- 여기서는
*numPtr = 20;
과 같이numPtr
에 저장된 메모리 주소에 접근하여20
을 저장했다. - 따라서
printf
로*numPtr
을 출력해 보면20
이 나온다. - 또 한 가지 중요한 점은
*numPtr = 20;
으로20
을 저장한 뒤printf
로 변수num1
의 값을 출력해 보면20
이 나온다는 것이다. - 왜냐하면
numPtr
에는num1
의 메모리 주소가 저장되어 있으므로 역참조 연산자로 값을 저장하면 결국num1
에 저장하게 된다.
- 역참조 연산자는 자료형을 바꾸는 효과를 낸다.
- 즉,
int *numPtr;
에서*numPtr
처럼 역참조하면pointer to int
에서pointer to
를 제거하여 그냥int
로 만든다.
- 만약 포인터
numPtr
에 변수num1
을 할당한다면 역참조 연산자로 자료형을 맞춰주면 된다.
int *numPtr;
int num1 = 10;
numPtr = num1; // 컴파일 경고. numPtr은 int 타입 포인터 타입이고, num1은 int 타입이라 자료형이 일치하지 않음
*numPtr = num1; // *numPtr은 int 타입이고, num1도 int 타입이라 자료형이 일치함
- 물론 주소 연산자
&
도 자료형을 맞춰주는 역할을 한다.
int *numPtr;
int num1;
numPtr = &num1; // numPtr은 int 타입 포인터 타입이고, &num1은 int 타입 변수의 주소이므로 자료형이 일치함
// numPtr은 pointer to int, &num1은 address of int이므로 자료형이 일치함
- 즉,
pointer to int
와address of int
는 자료형이 같다.
- 이제 변수, 주소 연산자, 역참조 연산자, 포인터의 차이를 정리해 보자.
1] 변수
- 메모리 주소를 몰라도 값을 가져오거나 저장할 수 있다.
- 그냥 변수에 값을 할당하거나 그대로 출력하면 된다.
2] 주소 연산자 &
- 변수의 메모리 주소를 구한다.
- 위의 그림에서
10
을 감싸고 있는 상자는 메모리 공간을 뜻하는데, 주소 연산자는 메모리 공간이 어디에 있는지 위치만 알아낼 수 있다.
3] 역참조 연산자 *
- 위의 그림에서 보면 상자 안까지 들어가서 값을 가져오거나 저장한다.
- 즉, 메모리 주소를 알고 있으므로 메모리 주소를 거쳐서 그 안에 있는 값을 가져오거나 저장한다.
4] 포인터
- 변수의 메모리 주소만 가리킨다.
- 따라서 포인터는 메모리 공간이 어디에 있는지 위치만 알고 있다.
5. 다양한 자료형의 포인터 선언하기
- 이번에는 다양한 자료형의 포인터를 선언해 보자.
#include <stdio.h>
int main()
{
long long *numPtr1; // long long 타입 포인터 선언
float *numPtr2; // float 타입 포인터 선언
char *cPtr1; // char 타입 포인터 선언
long long num1 = 10;
float num2 = 3.5f;
char c1 = 'a';
numPtr1 = &num1; // num1의 메모리 주소 저장
numPtr2 = &num2; // num2의 메모리 주소 저장
cPtr1 = &c1; // c1의 메모리 주소 저장
printf("%lld\n", *numPtr1);
printf("%f\n", *numPtr2);
printf("%c\n", *cPtr1);
}
// 10
// 3.500000
// a
- C에서는 사용할 수 있는 모든 자료형은 포인터로 만들 수 있다.
- 포인터에 저장되는 메모리 주솟값은 정수형으로 동일하지만 선언하는 자료형에 따라 메모리에 접근하는 방법이 달라진다.
- 즉, 다음과 같이 포인터를 역참조하면 선언한 자료형의 크기에 맞춰서 값을 가져오거나 저장하게 된다.
- 즉,
long long
타입 포인터는 8바이트 크기만큼 값을 가져오거나 저장하고,char
타입 포인터는 1바이트 크기만큼 값을 가져오거나 저장한다.
상수와 포인터
- 포인터에도
const
키워드를 붙일 수 있는데,const
의 위치에 따라 특성이 달라진다.
- 먼저 상수를 가리키는 포인터(
pointer to constant
)이다.
const int num1 = 10; // int 타입 상수
const int *numPtr; // int 타입 상수를 가리키는 포인터. int const *numPtr도 같음
numPtr = &num1;
*numPtr = 20; // 컴파일 에러. num1이 상수이므로 역참조로 값을 변경할 수 없음
- 먼저
num1
이const int
이므로 이 변수의 주소를 넣을 수 있는 포인터는const int *
로 선언해야 한다. - 그리고
num1
의 주소를numPtr
에 넣은 뒤 역참조로 값을 변경하려고 해도num1
은 상수이므로 컴파일 에러가 발생한다. - 즉,
pointer to constant
는 메모리 주소에 저장된 값을 변경할 수 없다는 뜻이다.
- 이번에는 포인터 자체가 상수인 상황(
constant pointer
)이다. - 이때는
*
뒤에const
를 붙인다.
int num1 = 10; // int 타입 변수
int num2 = 20; // int 타입 변수
int *const numPtr = &num1; // int 타입 포인터 상수
numPtr = &num2; // 컴파일 에러. 포인터(메모리 주소)를 변경할 수 없음
numPtr
에num1
의 주소가 들어가 있는 상태에서 다시num2
의 주소를 넣으려고 하면 컴파일 에러가 발생한다.numPtr
은 포인터 자체가 상수이므로 다른 포인터(메모리 주소)를 할당할 수 없다.- 즉,
constant pointer
는 메모리 주소를 변경할 수 없다는 뜻이다.
- 마지막으로 포인터가 상수이면서 상수를 가리키는 상황(
constant pointer to constant
)이다. - 이때는 포인터를 선언하는 자료형에도
const
를 붙이고*
뒤에도const
를 붙인다.
const int num1 = 10; // int 타입 상수
const int num2 = 20; // int 타입 상수
const int *const numPtr = &num1; // int 타입 상수를 가리키는 포인터 상수
// int const * const numPtr도 같음
*numPtr = 30; // 컴파일 에러. num1이 상수이므로 역참조로 값을 변경할 수 없음
numPtr = &num2; // 컴파일 에러. 포인터(메모리 주소)를 변경할 수 없음
- 여기서는
numPtr
을 역참조한 뒤 값을 변경하려고 해도num1
은 상수이므로 컴파일 에러가 발생한다. - 그리고
numPtr
자체도 상수이므로num2
의 주소를 넣으려고 하면 컴파일 에러가 발생한다. - 즉,
constant pointer to constant
는 메모리 주소도 변경할 수 없고 메모리 주소에 저장된 값도 변경할 수 없다는 뜻이다.
6. void
타입 포인터 선언하기
long long *numPtr1;
이나float *numPtr2;
는 자료형이 정해진 포인터이다.- 하지만 C에서는 자료형이 정해지지 않은 포인터도 있다.
void
타입 포인터라는 포인터인데 다음과 같이void
키워드와*
로 선언한다.
#include <stdio.h>
int main()
{
int num1 = 10;
char c1 = 'a';
int *numPtr1 = &num1;
char *cPtr1 = &c1;
void *ptr; // void 타입 포인터 선언
// 포인터 자료형이 달라도 컴파일 경고가 발생하지 않음
ptr = numPtr1; // void 타입 포인터에 int 타입 포인터 저장
ptr = cPtr1; // void 타입 포인터에 char 타입 포인터 저장
// 포인터 자료형이 달라도 컴파일 경고가 발생하지 않음
numPtr1 = ptr; // int 타입 포인터에 void 타입 포인터 저장
cPtr1 = ptr; // char 타입 포인터에 void 타입 포인터 저장
return 0;
}
- 기본적으로 C는 자료형이 다른 포인터끼리 메모리 주소를 저장하면 컴파일 경고가 발생한다.
- 하지만
void
타입 포인터는 자료형이 정해지지 않은 특성 때문에 어떤 자료형으로 된 포인터든 모두 저장할 수 있다. - 반대로 다양한 자료형으로 된 포인터에도
void
타입 포인터를 저장할 수도 있다. - 이런 특성 때문에
void
타입 포인터는 범용 포인터라고 한다. - 즉, 직접 자료형을 변환하지 않아도 암시적으로 자료형이 변환되는 방식이다.
// 포인터 자료형이 달라도 컴파일 경고가 발생하지 않음
ptr = numPtr1; // void 타입 포인터에 int 타입 포인터 저장
ptr = cPtr1; // void 타입 포인터에 char 타입 포인터 저장
// 포인터 자료형이 달라도 컴파일 경고가 발생하지 않음
numPtr1 = ptr; // int 타입 포인터에 void 타입 포인터 저장
cPtr1 = ptr; // char 타입 포인터에 void 타입 포인터 저장
- 단,
void
타입 포인터는 자료형이 정해지지 않았으므로 값을 가져오거나 저장할 크기도 정해지지 않는다. - 따라서
void
타입 포인터는 역참조를 할 수 없다.
ptr = numPtr1; // void 타입 포인터에 int 타입 포인터 저장
printf("%d", *ptr); // 컴파일 에러. void 타입 포인터는 역참조할 수 없음
ptr = cPtr1; // void 타입 포인터에 char 타입 포인터 저장
printf("%c", *ptr); // 컴파일 에러. void 타입 포인터는 역참조할 수 없음
void
키워드로는 변수를 선언할 수도 없다.
void
타입 포인터는 되는 게 별로 없어 보이지만 실제로 C에서는 다양한 형태로 사용되고 있다.- 예를 들자면 함수에서 다양한 자료형을 받아들일 때, 함수의 반환 포인터를 다양한 자료형으로 된 포인터에 저장할 때, 자료형을 숨기고 싶을 때 사용한다.
7. 다중 포인터 사용하기
- 이번에는 포인터의 메모리 주소를 저장하는 포인터의 포인터(이중 포인터)를 선언해 보자.
#include <stdio.h>
int main()
{
int *numPtr1; // 단일 포인터 선언
int **numPtr2; // 이중 포인터 선언
int num1 = 10;
numPtr1 = &num1; // num1의 메모리 주소 저장
numPtr2 = &numPtr1; // numPtr1의 메모리 주소 저장
printf("%d\n", **numPtr2); // 포인터를 두 번 역참조하여 num1의 메모리 주소에 접근
return 0;
}
// 10
- 포인터도 실제로는 변수이기 때문에 메모리 주소를 구할 수 있다.
- 하지만 포인터의 메모리 주소는 일반 포인터에 저장할 수 없고,
int **numPtr2;
처럼 이중 포인터에 저장해야 한다. int **numPtr2;
를 영어로 읽으면pointer to pointer to int
가 된다.- 여기서 이중 포인터
numPtr2
를 끝까지 따라가서 실제 값을 가져오려면**numPtr2
처럼 변수 앞에 역참조 연산자를 두 번 사용하면 된다. - 즉, 역참조를 두 번 하므로
numPtr2
←numPtr1
←num1
과 같은 모양이 되고num1
의 값을 가져올 수 있다.
- 포인터를 선언할 때
*
의 개수에 따라서 삼중 포인터, 사중 포인터 그 이상도 만들 수 있다. - 마찬가지로 역참조를 할 때도
*
를 세 번, 네 번 또는 그 이상 사용할 수 있다.
8. 잘못된 포인터 사용
- 포인터는 메모리 주소를 저장하는 용도이므로 다음과 같이 값을 직접 저장하면 안 된다.
#include <stdio.h>
int main()
{
int *numPtr = 0x100; // 포인터에 0x100을 직접 저장
printf("%d\n", *numPtr); // 0x100은 잘못된 메모리 주소이므로 실행 에러
return 0;
}
- 소스 코드를 컴파일해서 실행해 보면 제대로 실행이 되지 않는다.
- 왜냐하면
int *numPtr = 0x100;
과 같이 포인터에 직접0x100
을 저장했을 때 메모리에서0x100
은 잘못된 주솟값이기 때문이다.
- 이 상태에서
numPtr
을 역참조하여 메모리 주소에 접근해 봐야 에러만 발생한다.
- 만약 실제로 존재하는 메모리 주소라면 포인터에 직접 저장할 수 있다.
- 보통 임베디드 시스템이나 마이크로 프로세서에서 제공하는 메모리 주소를 사용할 때 포인터에 직접 저장하기도 한다.
References
- https://dojang.io/mod/page/view.php?id=274
- https://dojang.io/mod/page/view.php?id=275
- https://dojang.io/mod/page/view.php?id=276
- https://dojang.io/mod/page/view.php?id=277
- https://dojang.io/mod/page/view.php?id=278
- https://dojang.io/mod/page/view.php?id=279
- https://dojang.io/mod/page/view.php?id=280