52. 포인터 연산
1. 키워드
- 포인터 연산
2. 포인터 연산 사용하기
- 포인터로 선언한 변수에는 메모리 주소가 들어있다.
- 이 메모리 주소에 일정 숫자를 더하거나 빼면 메모리 주소가 증가, 감소한다.
- 즉, 포인터 연산을 하면 다른 메모리 주소에 접근할 수 있으며 메모리 주소를 손쉽게 옮겨 다니기 위해 사용한다.
- 여기서 메모리 주소가 커지는 상황을 순방향으로 이동(Forward), 메모리 주소가 작아지는 상황을 역방향으로 이동(Backward)이라고 한다.
3. 포인터 연산으로 메모리 주소 조작하기
- 포인터 연산은 포인터 변수에
+
,-
연산자를 사용하여 값을 더하거나 뺀다. - 또는,
++
,--
연산자를 사용하여 값을 증가, 감소시킨다. - 단,
*
,/
연산자와 실수 값은 사용할 수 없다.
#include <stdio.h>
int main()
{
int numArr[5] = {11, 22, 33, 44, 55};
int *numPtrA;
int *numPtrB;
int *numPtrC;
numPtrA = numArr; // 배열 첫 번째 요소의 메모리 주소를 포인터에 저장
numPtrB = numPtrA + 1; // 포인터 연산
numPtrC = numPtrA + 2; // 포인터 연산
printf("%p\n", numPtrA); // 메모리 주소. 컴퓨터마다, 실행할 때마다 달라짐
printf("%p\n", numPtrA); // sizeof(int) * 1이므로 numPtrA에서 4가 증가함
printf("%p\n", numPtrA); // sizeof(int) * 2이므로 numPtrA에서 8이 증가함
return 0;
}
// 0x00a3fc00 (메모리 주소. 컴퓨터마다, 실행할 때마다 달라짐)
// 0x00a3fc04
// 0x00a3fc08
- 먼저 정수형 배열을 선언하고 값을 초기화했다.
- 그리고 배열을 포인터에 할당했다.
- 배열 이름 자체는 배열의 첫 번째 요소에 대한 포인터라 할 수 있다.
- 포인터 연산은 포인터 변수에 정수 값을 더하거나 빼면 된다.
numPtrA
에 저장된 메모리 주소0x00a3fc00
에1
을 더하면1
이 증가한0x00a3fc01
이 나와야 할 것 같은데0x00a3fc04
가 나왔다.- 또한
0x00a3fc00
에2
를 더하면2
가 증가한0x00a3fc02
가 나와야 할 것 같지만0x00a3fc08
이 나온다. - 즉, 포인터 연산은 포인터 자료형의 크기만큼 더하거나 뺀다.
printf("%p\n", numPtrA); // 메모리 주소. 컴퓨터마다, 실행할 때마다 달라짐
printf("%p\n", numPtrA); // sizeof(int) * 1이므로 numPtrA에서 4가 증가함
printf("%p\n", numPtrA); // sizeof(int) * 2이므로 numPtrA에서 8이 증가함
- 여기서는
numPtrA
가 4바이트 크기의int
타입이다. - 따라서
numPtrA + 1
은 메모리 주소에서 4바이트만큼1
번 순방향으로 이동한다는 뜻이다. - 즉, 계산식은
sizeof(자료형) * 더하거나 빼는 값
이 된다.
- 이번에는 포인터에서 뺄셈을 해보자.
#include <stdio.h>
int main()
{
int numArr[5] = {11, 22, 33, 44, 55};
int *numPtrA;
int *numPtrB;
int *numPtrC;
numPtrA = &numArr[2]; // 배열 세 번째 요소의 메모리 주소를 포인터에 저장
numPtrB = numPtrA - 1; // 포인터 연산
numPtrC = numPtrA - 2; // 포인터 연산
printf("%p\n", numPtrA); // 메모리 주소. 컴퓨터마다, 실행할 때마다 달라짐
printf("%p\n", numPtrB); // sizeof(int) * -1이므로 numPtrA에서 4가 감소함
printf("%p\n", numPtrC); // sizeof(int) * -2이므로 numPtrA에서 8이 감소함
return 0;
}
// 0x00a3fc08 (메모리 주소. 컴퓨터마다, 실행할 때마다 달라짐)
// 0x00a3fc04
// 0x00a3fc00
numPtrA = &numArr[2];
와 같이 배열에[]
(대괄호)를 사용하여 요소에 접근한 뒤&
(주소 연산자)를 사용하면 해당 요소의 메모리 주소를 구할 수 있다.- 여기서는
numArr
에서 세 번째 요소의 메모리 주소를 구해서numPtrA
에 저장했다.
- 포인터 연산으로
numPtrA
에서1
,2
를 뺀다.
numPtrB
는numPtrA
에서4
가 감소한 값이 나오고,numPtrC
는numPtrA
에서8
이 감소한 값이 나온다.- 즉,
sizeof(int) * -1
이므로 메모리 주소에서 4바이트만큼 역방향으로 이동하고,sizeof(int) * -2
이므로 메모리 주소에서 8바이트만큼 역방향으로 이동한다.
printf("%p\n", numPtrA); // 메모리 주소. 컴퓨터마다, 실행할 때마다 달라짐
printf("%p\n", numPtrB); // sizeof(int) * -1이므로 numPtrA에서 4가 감소함
printf("%p\n", numPtrC); // sizeof(int) * -2이므로 numPtrA에서 8이 감소함
char
, short
, long long
타입 포인터를 연산하면?
- 포인터 연산은
sizeof(자료형) * 더하거나 빼는 값
이므로char
타입은 1바이트,short
타입은 2바이트,long long
타입은 8바이트만큼 메모리 주소에서 순방향, 역방향으로 이동한다.
#include <stdio.h>
int main()
{
char *cPtr1 = NULL;
short *numPtr1 = NULL;
long long *numPtr2 = NULL;
printf("%p\n", cPtr1 + 1); // 0x000000에서 1바이트만큼 순방향으로 이동
printf("%p\n", numPtr1 + 1); // 0x000000에서 2바이트만큼 순방향으로 이동
printf("%p\n", numPtr2 + 1); // 0x000000에서 8바이트만큼 순방향으로 이동
return 0;
}
// 0x00000001
// 0x00000002
// 0x00000008
- 이번에는 증감 연산자를 사용해 보자.
#include <stdio.h>
int main()
{
int numArr[5] = {11, 22, 33, 44, 55};
int *numPtrA;
int *numPtrB;
int *numPtrC;
numPtrA = &numArr[2]; // 배열 세 번째 요소의 주소를 포인터에 저장
numPtrB = numPtrA;
numPtrC = numPtrA;
numPtrB++; // 포인터 연산
numPtrC--; // 포인터 연산
printf("%p\n", numPtrA); // 메모리 주소. 컴퓨터마다, 실행할 때마다 달라짐
printf("%p\n", numPtrB); // sizeof(int) * 1이므로 numPtrA에서 4가 증가함
printf("%p\n", numPtrC); // sizeof(int) * -1이므로 numPtrA에서 4가 감소함
return 0;
}
// 0x00a3fc08
// 0x00a3fc0c
// 0x00a3fc04
- 먼저
numPtrB
와numPtrC
에numPtrA
의 메모리 주소를 저장했다. - 그리고
numPtrB
에는 증가 연산자를 사용하고,numPtrC
에는 감소 연산자를 사용했다. - 증가 연산자는
+ 1
과 같기 때문에sizeof(int) * 1
이 되므로numPtrA
에서4
가 증가한다. - 마찬가지로 감소 연산자는
- 1
과 같기 때문에sizeof(int) * -1
이 되므로numPtrB
에서4
가 감소한다.
numPtrB++; // 포인터 연산
numPtrC--; // 포인터 연산
printf("%p\n", numPtrA); // 메모리 주소. 컴퓨터마다, 실행할 때마다 달라짐
printf("%p\n", numPtrB); // sizeof(int) * 1이므로 numPtrA에서 4가 증가함
printf("%p\n", numPtrC); // sizeof(int) * -1이므로 numPtrA에서 4가 감소함
4. 포인터 연산과 역참조 사용하기
- 포인터 연산으로 조작한 메모리 주소도 역참조 연산을 사용하여 메모리에 접근할 수 있다.
#include <stdio.h>
int main()
{
int numArr[5] = {11, 22, 33, 44, 55};
int *numPtrA;
int *numPtrB;
int *numPtrC;
numPtrA = numArr; // 배열 첫 번째 요소의 주소를 포인터에 저장
numPtrB = numPtrA + 1; // 포인터 연산. numPtrA + 4바이트
numPtrC = numPtrA + 2; // 포인터 연산. numPtrA + 8바이트
printf("%d\n", *numPtrB); // 역참조로 값을 가져옴, numArr[1]과 같음
printf("%d\n", *numPtrC); // 역참조로 값을 가져옴, numArr[2]와 같음
return 0;
}
// 22
// 33
numPtrB
에는numPtrA
에1
을 더해서 4바이트만큼 순방향으로 이동한 메모리 주소를 저장했고,numPtrC
에는numPtrA
에2
를 더해서 8바이트만큼 순방향으로 이동한 메모리 주소를 저장했다.
numPtrB
와numPtrC
도 일반 포인터이므로*
를 사용하여 메모리의 값을 가져올 수 있다.- 여기서 포인터 연산은 결과적으로
numPtrA + 1
과numArr[1]
은 같고,numPtrA + 2
와numArr[2]
는 같다.
printf("%d\n", *numPtrB); // 역참조로 값을 가져옴, numArr[1]과 같음
printf("%d\n", *numPtrC); // 역참조로 값을 가져옴, numArr[2]와 같음
- 이번에는 포인터 연산과 동시에 역참조 연산을 사용하는 방법을 알아보자.
- 다음과 같이 포인터 연산을 한 부분을
()
(괄호)로 묶어준 뒤 맨 앞에*
를 붙이면 된다.
#include <stdio.h>
int main()
{
int numArr[5] = {11, 22, 33, 44, 55};
int *numPtrA;
numPtrA = numArr; // 배열 첫 번째 요소의 주소를 포인터에 저장
printf("%d\n", *(numPtrA + 1)); // numPtrA에서 순방향으로 4바이트만큼 떨어진
// 메모리에 주소에 접근. numArr[1]과 같음
printf("%d\n", *(numPtrA + 2)); // numPtrA에서 순방향으로 8바이트만큼 떨어진
// 메모리에 주소에 접근. numArr[2]와 같음
return 0;
}
// 22
// 33
*(numPtrA + 1)
과 같이numPtrA
에1
을 더한 뒤()
로 묶어준다.- 그리고 맨 앞에
*
를 붙여주면numPtrA
에서 순방향으로 4바이트만큼 떨어진 메모리 주소에서 값을 가져올 수 있다. - 여기서 포인터 연산과 역참조 연산
*(numPtrA + 1)
은 결과적으로numArr[1]
과 같다.
printf("%d\n", *(numPtrA + 1)); // numPtrA에서 순방향으로 4바이트만큼 떨어진
// 메모리에 주소에 접근. numArr[1]과 같음
printf("%d\n", *(numPtrA + 2)); // numPtrA에서 순방향으로 8바이트만큼 떨어진
// 메모리에 주소에 접근. numArr[2]와 같음
- 만약 포인터 연산을
()
로 묶어주지 않으면*
가 먼저 실행되어 값을 가져온 뒤 덧셈(뺄셈) 연산을 하게 된다. - 따라서 다음 코드는
numPtrA
를 역참조하여11
을 가져온 뒤1
을 더하므로12
가 된다.
- 즉, 역참조 연산보다 포인터 연산이 먼저 실행될 수 있도록
()
로 묶어주는 것이다.
- 이번에는 증감 연산과 역참조 연산을 사용해 보자.
#include <stdio.h>
int main()
{
int numArr[5] = {11, 22, 33, 44, 55};
int *numPtrA;
int *numPtrB;
int *numPtrC;
numPtrA = &numArr[2]; // 배열 세 번째 요소의 주소를 포인터에 저장
numPtrB = numPtrA;
numPtrC = numPtrA;
printf("%d\n", *(++numPtrB)); // numPtrA에서 순방향으로 4바이트만큼 떨어진
// 메모리 주소에 접근
printf("%d\n", *(--numPtrC)); // numPtrA에서 역방향으로 4바이트만큼 떨어진
// 메모리 주소에 접근
return 0;
}
// 44
// 22
*(++numPtrB)
와 같이 변수 앞에++
를 사용한 뒤()
로 묶어준다.- 그리고 맨 앞에
*
를 붙여주면numPtrB
에 저장된 메모리 주소가 4바이트만큼 증가하고, 해당 메모리의 값을 가져온다. - 마찬가지로
*(--numPtrC)
처럼--
와*
를 사용하면numPtrC
에 저장된 메모리 주소가 4바이트만큼 감소하고, 해당 메모리의 값을 가져온다.
printf("%d\n", *(++numPtrB)); // numPtrA에서 순방향으로 4바이트만큼 떨어진
// 메모리 주소에 접근
printf("%d\n", *(--numPtrC)); // numPtrA에서 역방향으로 4바이트만큼 떨어진
// 메모리 주소에 접근
- 만약 증감 연산자를 변수 앞에 사용할 때
()
로 묶어주지 않아도 동작은 같다. - 왜냐하면 전위 증감 연산자와
*
는 우선순위가 같고 결합방향이 오른쪽에서 왼쪽이기 때문이다.
- 증감 연산자를 변수 뒤에 붙이고 포인터 연산을 하면 현재 메모리의 값을 가져온 뒤 포인터 연산을 하므로 주의해야 한다.
printf("%d\n", *(numPtrB++)); // 33: numPtrA의 메모리에 접근하여 값을 가져온 뒤 포인터 연산
printf("%d\n", *(numPtrC--)); // 33: numPtrA의 메모리에 접근하여 값을 가져온 뒤 포인터 연산
5. 구조체 포인터로 포인터 연산하기
- 구조체 포인터도 포인터 연산을 할 수 있다.
- 다음과 같이 포인터 연산을 한 부분을
()
로 묶어준 뒤->
를 사용하여 멤버에 접근하면 된다.
#include <stdio.h>
struct Data
{
int num1;
int num2;
};
int main()
{
struct Data d[3] = {{10, 20}, {30, 40}, {50, 60}}; // 구조체 배열 선언과 값 초기화
struct Data *ptr; // 구조체 포인터 선언
ptr = d; // 구조체 배열 첫 번째 요소의 메모리 주소를 포인터에 저장
printf("%d %d\n", (ptr + 1)->num1, (ptr + 1)->num2); // 구조체 배열에서 멤버의 값 출력
// d[1].num1, d[1].num2와 같음
printf("%d %d\n", (ptr + 2)->num1, (ptr + 2)->num2); // 구조체 배열에서 멤버의 값 출력
// d[2].num1, d[2].num2와 같음
return 0;
}
// 30 40
// 50 60
- 구조체 배열
d
를 선언한 뒤 첫 번째 요소의 메모리 주소를ptr
에 저장했다. - 구조체 포인터는
(ptr + 1)->num1
과 같이 포인터 연산을 한 뒤()
로 묶어준다. - 그리고
->
를 사용하여 멤버에 접근할 수 있다. - 결과적으로
(ptr + 1)->num1
은 구조체 배열의 인덱스로 접근하는d[1].num1
과 같다.
- 구조체
Data
의 크기는 4바이트짜리int
타입 멤버가 두 개 들어있으므로 8바이트이다. - 따라서 포인터 연산을 하면 8바이트씩 메모리 주소에서 더하거나 뺀다.
- 이번에는
void
타입 포인터에 구조체3
개 크기만큼 동적 메모리를 할당한 뒤 포인터 연산을 해보자.
#include <stdio.h>
#include <stdlib.h> // malloc, free 함수가 선언된 헤더 파일
#include <string.h> // memcpy 함수가 선언된 헤더 파일
struct Data
{
int num1;
int num2;
};
int main()
{
void *ptr = malloc(sizeof(struct Data) * 3); // 구조체 3개 크기만큼 동적 메모리 할당
struct Data d[3];
((struct Data *)ptr)->num1 = 10; // 포인터 연산으로 메모리에 값 저장
((struct Data *)ptr)->num2 = 20; // 포인터 연산으로 메모리에 값 저장
((struct Data *)ptr + 1)->num1 = 30; // 포인터 연산으로 메모리에 값 저장
((struct Data *)ptr + 1)->num2 = 40; // 포인터 연산으로 메모리에 값 저장
((struct Data *)ptr + 2)->num1 = 50; // 포인터 연산으로 메모리에 값 저장
((struct Data *)ptr + 2)->num2 = 60; // 포인터 연산으로 메모리에 값 저장
memcpy(d, ptr, sizeof(struct Data) * 3); // 동적 메모리가 구조체 배열의 형태와 같은지
// 확인하기 위해 동적 메모리의 내용을 구조체 배열에 복사
printf("%d %d\n", d[1].num1, d[1].num2); // 구조체 배열의 멤버 출력
printf("%d %d\n", ((struct Data *)ptr + 2)->num1, ((struct Data *)ptr + 2)->num2); // 포인터 연산으로 메모리의 값 출력
free(ptr); // 동적 메모리 해제
return 0;
}
((struct Data *)ptr)->num1
은 앞에서 배운 구조체 포인터로 변환하는 방법이다.- 이 상태에서 연산을 하려면
((struct Data *)ptr + 1)->num1
과 같이ptr
을 구조체 포인터로 변환한 뒤 값을 더해주면 된다. ->
를 사용하려면 반드시()
로 묶어준다.
- 이제 포인터 연산을 통해 메모리에 값을 저장한다.
- 만약
(ptr + 1)->num1
처럼ptr
에 포인터 연산을 하더라도ptr
은void
타입 포인터라Data
구조체의 형태를 모르기 때문에 멤버에 접근할 수 없고 컴파일 에러가 발생한다.
((struct Data *)ptr + 1)->num1 = 30; // 포인터 연산으로 메모리에 값 저장
((struct Data *)ptr + 1)->num2 = 40; // 포인터 연산으로 메모리에 값 저장
((struct Data *)ptr + 2)->num1 = 50; // 포인터 연산으로 메모리에 값 저장
((struct Data *)ptr + 2)->num2 = 60; // 포인터 연산으로 메모리에 값 저장
- 그리고 포인터 연산으로 값을 저장한 결과가
Data
구조체 배열의 형태와 같은지 확인하기 위해memcpy(d, ptr, sizeof(struct Data) * 3);
처럼 동적 메모리의 내용을 구조체 배열d
에 복사했다.
- 다음과 같이
printf
로 구조체 배열d
의 멤버를 출력해 보면 포인터 연산을 통해 저장한 값이 출력되는 것을 볼 수 있다. - 즉, 동적 메모리에 저장된 값의 위치가 구조체 배열의 형태와 같고, 동적 메모리 내용을 그대로 복사했기 때문에 같은 값이 나온다.
- 또한, 포인터 연산으로도 동적 메모리의 값을 출력할 수 있다.
printf("%d %d\n", d[1].num1, d[1].num2); // 구조체 배열의 멤버 출력
printf("%d %d\n", ((struct Data *)ptr + 2)->num1, ((struct Data *)ptr + 2)->num2); // 포인터 연산으로 메모리의 값 출력