Unit 34. 포인터 사용하기
변수는 컴퓨터의 메모리에 생성된다. 메모리에 일정한 공간을 확보해두고 원하는 값을 저장하거나 가져오는 방식이다. 보통 변수는 메모리의 특정 장소에 위치하고 있으므로 메모리 주소로도 표현할 수 있다.
변수의 메모리 주소를 구할 때는 변수 앞에 & (주소 연산자)를 붙이면 된다.
#include <stdio.h>
int main()
{
int num1 = 10;
printf("%p\n", &num1); // 008AF7FC: num1의 메모리 주소를 출력
// 컴퓨터마다, 실행할 때마다 달라짐
return 0;
}
메모리 주소는 008AF7FC과 같이 16진수 형태이며 printf에서 서식 지정자 %p를 사용하여 출력한다.
물론 16진수로 출력하는 %x, %X를 사용해도 된다.
34.1 포인터 변수 선언하기
C 언어에서 메모리 주소는 포인터(pointer) 변수에 저장한다.
포인터 변수는 *를 사용하여 선언한다.
- 자료형 *포인터이름;
- 포인터 = &변수;
#include <stdio.h>
int main()
{
int *numPtr; // 포인터 변수 선언
int num1 = 10; // int형 변수를 선언하고 10 저장
numPtr = &num1; // num1의 메모리 주소를 포인터 변수에 저장
printf("%p\n", numPtr); // 0055FC24: 포인터 변수 numPtr의 값 출력
// 컴퓨터마다, 실행할 때마다 달라짐
printf("%p\n", &num1); // 0055FC24: 변수 num1의 메모리 주소 출력
// 컴퓨터마다, 실행할 때마다 달라짐
return 0;
}
포인터 변수를 선언할 때는 자료형 뒤에 * (Asterisk, 애스터리스크)를 붙인다. 포인터 변수를 선언했으면 위와 같이 &로 변수의 주소를 구해서 포인터 변수에 저장한다.
포인터 변수를 선언할 때는 자료형을 알려주고 *를 붙이는 방식을 사용한다. 이때 자료형은 변수의 자료형을 따른다.
즉, int *는 영어로 pointer to int라고 읽는데 int형 공간을 가리키는 포인터라는 뜻이다.
즉, 다음과 같이 포인터는 메모리의 특정 위치를 가리킬 때 사용한다.
34.2 역참조 연산자 사용하기
포인터 변수에는 메모리 주소가 저장되어 있다.
이때 메모리 주소가 있는 곳으로 이동해서 값을 가져오고 싶다면 역참조(dereference) 연산자 *를 사용한다.
- *포인터
#include <stdio.h>
int main()
{
int *numPtr; // 포인터 변수 선언
int num1 = 10; // 정수형 변수를 선언하고 10 저장
numPtr = &num1; // num1의 메모리 주소를 포인터 변수에 저장
printf("%d\n", *numPtr); // 10: 역참조 연산자로 num1의 메모리 주소에 접근하여 값을 가져옴
return 0;
}
역참조 연산자 *는 포인터 앞에 붙인다. 다음과 같이 numPtr 앞에 *를 붙이면 numPtr에 저장된 메모리 주소로 가서 값을 가져온다.
* 포인터 선언과 역참조
포인터를 선언할 때도 *를 사용하고 역참조를 할 때도 *를 사용한다.
포인터를 선언할 때 *는 "이 변수가 포인터다"라고 알려주는 역할이고, 포인터에 사용할 때 *는 "포인터의 메모리 주소를 역참조하겠다"라는 뜻이다.
포인터 변수에 역참조 연산자를 사용한 뒤 값을 저장(할당)할 수 도 있다.
- *포인터 = 값;
#include <stdio.h>
int main()
{
int *numPtr; // 포인터 변수 선언
int num1 = 10; // 정수형 변수를 선언하고 10 저장
numPtr = &num1; // num1의 메모리 주소를 포인터 변수에 저장
*numPtr = 20; // 역참조 연산자로 메모리 주소에 접근하여 20을 저장
printf("%d\n", *numPtr); // 20: 역참조 연산자로 메모리 주소에 접근하여 값을 가져옴
printf("%d\n", num1); // 20: 실제 num1의 값도 바뀜
return 0;
}
이때, 역참조 연산자는 자료형을 바꾸는 효과를 낸다. 즉, int *numPtr;에서 *numPtr처럼 역참조하면 pointer to int에서 pointer to를 제거하여 그냥 int로 만든다.(int 포인터 → int).
34.3 디버거에서 포인터 확인하기
디버거를 사용하면 변수의 메모리 주소, 포인터, 역참조를 손쉽게 확인할 수 있다.
내용이 0a 00 00 00라고 나오는데 메모리의 내용은 보통 16진수로 표현하므로 0a를 10진수로 변환하면 10이다. 즉, 이 메모리 공간이 변수 num1의 위치이다.
특히 num1은 int 자료형이므로 4바이트이다. 따라서 0a 00 00 00과 같이 숫자 4개를 차지한다. 또한, 우리가 사용하는 x86(x86-64) 계열 CPU는 리틀 엔디언 방식이라 값이 거꾸로 저장된다. 그래서 0a 00 00 00은 원래 00 00 00 0a이며 0a 값을 갖는다는 뜻이다.
16진수 14는 10진수로 20이므로 역참조 후 20을 할당하는 코드 *numPtr = 20;이 실행되어 메모리의 내용이 바뀐 것을 확인할 수 있다.
34.4 다양한 자료형의 포인터 선언하기
#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); // 10
printf("%f\n", *numPtr2); // 3.500000
printf("%c\n", *cPtr1); // a
return 0;
}
다음은 다양한 자료형의 포인터를 선언한 것이다.
C 언어에서 사용할 수 있는 모든 자료형은 포인터로 만들 수 있다. 그런데 왜 자료형마다 포인터를 선언하도록 만들었을까?
포인터에 저장되는 메모리 주솟값은 정수형으로 동일하지만 선언하는 자료형에 따라 메모리에 접근하는 방법이 달라지기 때문이다. 다음과 같이 포인터를 역참조 시 선언한 자료형의 크기에 맞춰서 값을 가져오거나 저장하게 된다.
즉, long long 포인터는 8바이트 크기만큼 값을 가져오거나 저장하고, char 포인터는 1바이트 크기만큼 값을 가져오거나 저장한다.
34.5 void 포인터 선언하기
C 언어에서는 자료형이 정해지지 않은 포인터도 있다. void 포인터라는 것인데 다음과 같이 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 언어는 자료형이 다른 포인터끼리 메모리 주소를 저장하면 컴파일 경고(warning)가 발생한다. 하지만 void 포인터는 자료형이 정해지지 않은 특성 때문에 어떤 자료형으로 된 포인터든 모두 저장할 수 있다.
반대로 다양한 자료형으로 된 포인터에도 void 포인터를 저장할 수도 있다.
이런 특성 때문에 void 포인터는 범용 포인터라고 한다.
직접 자료형을 변환하지 않아도 암시적으로 자료형이 변환되는 방식이다.
단, void 포인터는 자료형이 정해지지 않았으므로 값을 가져오거나 저장할 크기도 정해지지 않았다. 따라서 void 포인터는 역참조를 할 수 없다.
ptr = numPtr1; // void 포인터에 int 포인터 저장
printf("%d", *ptr); // void 포인터는 역참조할 수 없음. 컴파일 에러
ptr = cPtr1; // void 포인터에 char 포인터 저장
printf("%c", *ptr); // void 포인터는 역참조할 수 없음. 컴파일 에러
-> void 포인터는 실제로 C 언어에서 다양한 형태로 사용되고 있다. 예를 들자면 함수에서 다양한 자료형을 받아들일 때, 함수의 반환 포인터를 다양한 자료형으로 된 포인터에 저장할 때, 자료형을 숨기고 싶을 때 사용한다.
34.6 이중 포인터 사용하기
포인터를 선언할 때 *를 두 번 사용하면 포인터의 포인터(이중 포인터)를 선언한다.
- 자료형 **포인터이름;
#include <stdio.h>
int main()
{
int *numPtr1; // 단일 포인터 선언
int **numPtr2; // 이중 포인터 선언
int num1 = 10;
numPtr1 = &num1; // num1의 메모리 주소 저장
numPtr2 = &numPtr1; // numPtr1의 메모리 주소 저장
printf("%d\n", **numPtr2); // 20: 포인터를 두 번 역참조하여 num1의 메모리 주소에 접근
return 0;
}
포인터의 메모리 주소는 일반 포인터에 저장할 수 없고, int **numPtr2;처럼 이중 포인터에 저장해야 한다.
int **numPtr2;를 영어로 읽으면 pointer to pointer to int 이다(numPtr2 → numPtr1 → num1).
여기서 이중 포인터 numPtr2를 끝까지 따라가서 실제 값을 가져오려면 **numPtr2처럼 변수 앞에 역참조 연산자를 두 번 사용하면 된다. 즉, 역참조를 두 번 하므로 numPtr2 ← numPtr1 ← num1과 같은 모양이 된다.
'Programming Languages > C' 카테고리의 다른 글
[P4C] C언어 코딩 도장 : UNIT 36 (0) | 2021.03.03 |
---|---|
[P4C] C언어 코딩 도장 : UNIT 35 (0) | 2021.03.03 |
[P4C] CodeUp 1099 : [기초-2차원배열] 성실한 개미 (0) | 2021.02.25 |
[P4C] CodeUp 1098 : [기초-2차원배열] 설탕과자 뽑기 (0) | 2021.02.25 |
[P4C] CodeUp 1097 : [기초-2차원배열] 바둑알 십자 뒤집기 (0) | 2021.02.25 |