프로그래밍 언어/[ C ]

[ C ] 06. 포인터연산

kim.svadoz 2020. 8. 13. 10:10
반응형

[ 포인터연산 ]


포인터로 선언한 변수에는 메모리 주소가 들어있다. 이 포인터 변수에서 연산을 할 수 있다.

마찬가지로 메모리 주소에 일정 숫자를 더하거나 빼면 메모리 주소가 증가,감소 한다. 즉, 포인터 연산을 하면 다른 메모리 주소에 접근할 수 있으며 메모리 주소를 손쉽게 옮겨 다니기 위해서 사용한다.

image-20200805113606211

여기서 메모리 주소가 커지는 상황을 순방향 이동(forward), 메모리 주소가 작아지는 상황을 역방향이동(backward)라 하겠다.

포인터연산으로 메모리 주소 조작하기

#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);        // 00A3FC00 : 메모리주소. 컴퓨터 마다, 실행할 때마다 달라짐
    printf("%p\n", numPtrB);        // 00A3FC04 : sizeof(int)*1 이므로 numPtrA에서 4가 증가함
    printf("%p\n", numPtrC);        // 00A3FC08 : sizeof(int)*2 이므로 numPtrA에서 8이 증가함

    return 0;
}
# 실행 결과
00A3FC00 ( 메모리주소. 컴퓨터 마다, 실행할 때마다 달라짐 )
00A3FC04
00A3FC08
  • 포인터 연산은 특별한 것이 없고 포인터 변수에 정수 값을 더하거나 빼면 된다. 단, 연산하는 값이 메모리 주소이므로 곱하거나 나누는 연산은 의미가 없음
  • 포인터 연산은 포인터 자료형의 크기만큼 더하거나 뺀다.
  • 여기서 numPtrA가 4바이트 크기의 int형이다. 따라서 numPtrA+1은 메모리주소에서 4바이트만큼 1번 순방향 이동한다는 뜻 즉, 계산식은 sizeof(자료형) * 더하거나 빼는 값이 된다.

image-20200805160117998

이번에는 포인터 뺄셈을 해보겠다.

#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);        // 00A3FC00 : 메모리주소. 컴퓨터 마다, 실행할 때마다 달라짐
    printf("%p\n", numPtrB);        // 00A3FC04 : sizeof(int) * -1 이므로 numPtrA에서 4가 감소함
    printf("%p\n", numPtrC);        // 00A3FC08 : sizeof(int) * -2 이므로 numPtrA에서 8이 감소함

    return 0;
}
# 실행 결과
00A3FC08 ( 메모리주소. 컴퓨터 마다, 실행할 때마다 달라짐 )
00A3FC04
00A3FC00
  • numPtrA = &numArr[2];와 같이 배열에 [ ](대괄호)를 사용하여 요소에 접근한뒤 &(주소연산자)를 사용하면서 해당 요소의 메모리 주소를 구할 수 있다.

image-20200805160521339

  • 포인터 연산은 char는 1바이트, short는 2바이트, int는 4바이트, long long은 8바이트만큼 메모리 주소에서 순방향, 역방향으로 이동한다.

포인터연산과 역참조

  1. 포인터 연산으로 조작한 메모리주소도 역참조 연산을 사용하여 메모리에 접근할 수 있다.
#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);        // 22. 역참조로 값을 가져온다. numArr[1]과 같음
    printf("%d\n", *numPtrC);        // 33. 역참조로 값을 가져온다. numArr[2]와 같음

    return 0;
}
# 실행 결과
22
33
  1. 포인터 연산과 동시에 역참조 연산을 할 수 있다. 포인터 연산을 한 부분을 ( )괄호로 묶어 준 뒤 맨 앞에 *(역참조 연산자)를 붙이면 된다.
#include <stdio.h>

int main(){
    int numArr[5] = { 11, 22, 33, 44, 55 };
    int *numPtrA;

    numPtrA = numArr;        // 배열 첫 번째 요소의 주소를 포인터에 저장

    printf("%d\n", *(numPtrA + 1));        // 22. numPtrA에서 순방향으로 4바이트만큼 떨어진 메모리에 주소에 접근. numArr[1]과 같음
    printf("%d\n", *(numPtrA + 2));        // 33. numPtrA에서 순방향으로 8바이트만큼 떨어진 메모리에 주소에 접근. numArr[2]와 같음

    return 0;
}
# 실행 결과
22
33
  • 만약 포인터 연산을 괄호로 묶어주지 않으면 역참조 연산자가 먼저 실행되어 값을 가져 온 뒤 연산을 하게 된다.

    ex. printf("%d\n", *numPtrA + 1); 의 값은 11 + 1이 되어 12가 된다.

구조체포인터로 포인터 연산

  1. 구조체 포인터로 포인터 연산을 해보자
#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);    // 30 40. 구조체 배열에서 멤버의 값 출력
                                                    // d[1].num1, d[1].num2와 같음
    printf("%d %d\n", (ptr + 2)->num2, (ptr + 2)->num2);    // 50 60. 구조체 배열에서 멤버의 값 출력
                                                    // d[2].num1, d[2].num2와 같음
    return 0;
}
# 실행 결과
30 40
50 60
  • 구조체 포인터는 (ptr+1)->num1과 같이 포인터 연산을 한 뒤 갈호로 묶어준다. 그리고 화살표 연산자를 사용하여 멤버에 접근할 수 있다.
  • 구조체 Data의 크기는 4바이트짜리 int형 멤버가 두 개 들어있으므로 8바이트이다. 따라서 포인트연산을 하면 8바이트씩 메모리 주소에서 연산을 한다.
  • 만약 구조체가 커져서 int형 멤버가 10개가 된다면 40바이트씩 더하거나 빼게 된다.
  1. 이번에는 void 포인터에 구조체 3개 크기만큼 동적 메모리를 할당한 뒤 포인터 연산을 해보자

    ((struct 구조체이름 *)포인터 + 값) -> 멤버

    ((struct 구조체이름 *)포인터 - 값) -> 멤버

#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) -> num1 = 60;        // 포인터 연산으로 메모리에 값 저장

    memcpy(d, ptr, sizeof(struct Data) * 3);    // 동적 메모리가 구조체 배열의 형태와 같은지 확인하기 위해 동적 메모리의 내용을 구조체 배열에 복사

    printf("%d %d\n", d[1].num1, d[1].num2);    // 30 40. 구조체 배열의 멤버 출력
    printf("%d %d\n", ((struct Data *)ptr + 2)->num1, ((struct Data *)ptr +2)-> num2);    // 50. 60. 포인터 연산으로 메모리의 값 출력

    free(ptr);    // 동적 메모리 해제
    return 0;

}
# 실행 결과
30 40
50 60
  • 문법이 복잡해보이지만 어렵지 않아요 ((struct Data *)ptr->num1)은 앞에서 배운 구조체 포인터로 변환하는 방법이다. 이 상태에서 포인터 연산을 하려면 ((struct Data *)ptr + 1)->num1과 같이 ptr을 구조체 포인터로 변환한 뒤 값을 더해주면 된다. (->(화살표연산자)를 사용하려면 반드시 괄호로 묶어준다)
  • 이제 포인터 연산을 통해 메모리에 값을 저장한다. 만약 (ptr + 1)->num1처럼 ptr에 포인터 연산을 하더라도 ptr은 void 포인터라 Data 구조체의 형태를 모르기 때문에 멤버에 접근할 수 없고 컴파일 에러가 발생한다.
  • 그리고 포인터 연산으로 값을 저장한 결과가 Data 구조체 배열의 형태와 같은지 확인하기 위해 memcpy(d, ptr, sizeof(struct Data)*3);처럼 동적 메모리의 내용을 구조체 배열 d에 복사했다.
  • 즉, 동적 메모리에 저장된 값의 위치가 구조체 배열의 형태와 같고, 동적 메모리 내용을 그대로 복사했기 때문에 같은 값이 나온다. 또한, 포인터 연산으로도 동적메모리의 값을 출력할 수 있다.

image-20200805163545310

반응형

'프로그래밍 언어 > [ C ]' 카테고리의 다른 글

[ C ] 08. 구조체  (0) 2020.08.13
[ C ] 07. 열거형  (0) 2020.08.13
[ C ] 05. 메모리와 포인터의 사용  (0) 2020.08.13
[ C ] 04. 포인터의 형변환  (0) 2020.08.13
[ C ] 03. 포인터와 역참조 연산자  (0) 2020.08.13