본문 바로가기
C

포인터

by oncerun 2023. 4. 29.
반응형

 

C언어의 장벽으로 포인터를 많이 꼽는다고 들었다. 오늘은 포인터에 대해 알아보자.

 

목록

  • 포인터의 개념
  • 포인터변수의 선언방법
  • 포인터변수의 참조방법
  • 포인터의 기억공간 표현
  • 포인터 연산

 

 

개념

 

 

포인터는 변수이다. 변수는 특정 데이터 값을 가지고 있다. 그렇다면 포인터는 무슨 데이터 값을 가지고 있을까?

 

바로 특정 데이터가 저장된 기억장소의 주소값을 가지고 있다는 것이다. 

 

그렇다 우리는 기억공간을 변수명으로 접근하려는 것이 아니라 실제 주소로 접근하기 위한 방법이다.

 

일반적인 변수를 표현하는 방법은 변수를 선언하고 이를 통해 기억공간이 할당된다. 그리고 할당된 기억공간에 변수에 대입된 데이터가 저장되게 된다. 그리고 할당된 기억공간은 주소가 부여되어 있다. 

 

즉 일반적인 변수는 그 자체가 기억공간 주소에 할당된 값을 가리키도록 설계가 되어있다는 것이다. 

 

그런데 포인터는 그렇지 않고 그 주소를 가르킨다는 것이다. 

 

왜? 이러한 차이점을 주었을까? 왜 간접적으로 참조를 하도록 하였는지 궁금하다. 

 

C언어는 오래전 과거에 만들어졌다. 1972년정도로 알고 있는데, 이 당시에 컴퓨터의 메모리 용량은 매우 제한적이었을 것이다. 변수를 직접 값으로 접근하도록 하면 해당 변수는 이미 메모리 공간을 차지하게 되어 메모리 용량이 낭비될 수 있었기 때문일 것이다. 이에 대한 대안이 필요했는데, 바로 포인터 같다. 변수의 메모리 주소를 저장하는 포인터를 도입함으로써

변수에 간접적으로 접근하는 방법을 제공했는데, 이를 통해 변수의 값을 바로 복사하는 대신, 변수의 주소를 전달함으로써 더 효율적으로 메모리를 사용할 수 있게 된 것이다. 

 

 

예를 들어보자. 

 

    int var;
    var = 200;

 

var라는 이름으로 4바이트의 기억공간을 차지하도록 선언한다. 

 

확보가 된 다음 기억공간에 200이라는 값을 저장한다. 

 

변수를 통해 기억공간에 접근하는 방법이 있다. 이 변수는 특정 주소를 가지고 있고 그 공간에 값을 치환할 수 있다. 

하지만 변수는 그 값을 가리키지 기억공간의 주소를 표현하지는 않는다. 

 

반대로 생각하면 기억공간의 주소를 알고있다면 그 주소에 해당되는 값을 다룰 수도 있을 것이다. 

 

그러면 주소 값을 다룰 수 있는 변수가 필요하게 되는데, 이를 포인터 변수라하고 변수의 주소값을 갖는 특별한 변수라고 설명이 된다.

 


int main() {
   int days = 365;
   int months = 12;
   int weeks = 52;
   int hours = 8760;

   int arr[5] = {1,2,3,4,5};

    printf(" days의 주소 : %x\n", &days);
    printf(" months의 주소 : %x\n", &months);
    printf(" weeks의 주소 : %x\n", &weeks);
    printf(" hours의 주소 : %x\n", &hours);
    printf(" arr의 주소 : %x\n", &arr);
    printf(" arr의 주소 : %x\n", arr);


    return 0;
}

 

 

지금까지는 변수의 이름으로 기억공간을 확보되고 그안에 어떤 데이터가 들어가는지에 대해 관심을 가졌는데,

그 기억공간을 나타내는 번지가 이미할당이 되어있다는 것이고 그 할당된 번지를 확인하고 싶은 경우 앰퍼센트 &를 활용하면 확인할 수 있다는 것이다.

 


포인터 변수와 선언과 참조 표현

 

형식

int *p;

 

위와 같은 형식에서 p는 포인터 변수로서 정수형의 자료가 들어있는 주소를 가지고 있다.

 

*p를 합쳐표현하면 이는 해당 주소에 저장된 정수형 자료를 갖고 있다. 

 

조금 헷갈린다. 다음과 같이 예제를 작성해 보자.

 

int main() {


    int a,b; //일반 변수 선언
    int *p; // 포인터 변수 언언

    a = 500; //일반 변수 할당

    p = &a; // 값이 아닌 주소를 할당해 주어야 한다.

    b = *p; // 포인터 변수에 저장된 주소를 이용하여 (a에 접근) 일반 변수 값을 가져옴.

    printf("a = %d, b = %d\n", a, b);
    



    return 0;
}

 

정리가 된다. 실제 포인터 변수는 변수의 주소값을 할당하기 위함이고. 애스터리크(*)가 붙은 포인터 변수는 포인터 변수가 가지고 있는 주소의 실제 값을 표현한다. 

 

사용되는 연산자는 & (주소를 표현), * (포인터 변수를 표현) 이를 기억하자.

 

근데 포인터 변수를 선언하고 *p의 값에 직접 값을 할당하면 어떻게 될까?

 

당연히 문제가 될 것이다. 어느 주소공간의 값을 변경해야 하는지 모르기 때문에  버그가 발생할 것이다. 

 

그러면 여기서 주의할 점 하나를 알게 된다. 포인터 변수를 사용할 때에는 포인터 변수가 가리키는 메모리 주소가 유효한지, 초기화되었는지 등을 항상 확인하고 안전하게 방어코드를 작성해야 한다는 것이다.

 

그럼 기초를 알았다면 C언어를 통해 프로그래밍을 할 때 포인터를 언제 활용하는지 알아볼 필요가 있다.

전부 이해가 되지 않아도 한 번읽어보자.

 

1. 동적 메모리 할당: 동적으로 메모리를 할당할 때 포인터를 사용합니다. 예를 들어, 배열의 크기가 런타임에 결정되는 경우에는 포인터를 사용하여 메모리를 할당할 수 있습니다.

int size;
int *arr;
printf("Enter the size of the array: ");
scanf("%d", &size);
arr = (int*)malloc(size * sizeof(int));

 

* void 형 포인터.

 

주소값을 void 형으로 초기화를 하면  실제 포인터에 주소를 할당할 때 명시적 타입 변환을 통해 여러 자료형의 주소를 가질 수 있다. 

 

즉 위 예시는 arr의 주소를 입력받은 size에 정수형 크기를 곱하고 이를 정수형으로 선언하여 주소를 할당하는 것 같다.

 

2. 함수 호출 시 인수 전달: 함수 호출 시 인수를 전달할 때 포인터를 사용합니다. 포인터를 사용하면 함수 내에서 인수에 대한 변경이 가능해지므로, 함수 내에서 데이터를 변경할 필요가 있는 경우에 사용됩니다.

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int x = 5, y = 10;
swap(&x, &y);
printf("x = %d, y = %d", x, y); // 출력: x = 10, y = 5



3. 데이터 구조 및 연결 리스트: 포인터를 사용하여 데이터 구조 및 연결 리스트를 구현할 수 있습니다. 예를 들어, 연결 리스트의 각 노드는 포인터를 사용하여 다음 노드를 가리키는 방식으로 구현됩니다.

struct node {
    int data;
    struct node* next;
};

struct node* head = NULL;
head = (struct node*)malloc(sizeof(struct node));
head->data = 1;
head->next = NULL;



4. 대규모 데이터 세트: 대규모 데이터 세트를 처리할 때 포인터를 사용하면 메모리를 효율적으로 사용할 수 있습니다. 예를 들어, 이미지나 비디오 데이터를 처리할 때 포인터를 사용하여 메모리 사용량을 최소화할 수 있습니다.

int* image_data = (int*)malloc(width * height * sizeof(int));
for (int i = 0; i < height; i++) {
    for (int j = 0; j < width; j++) {
        int index = i * width + j;
        image_data[index] = get_pixel_value(i, j);
    }
}

 

우선 나도 이해 안 가는 부분이 많아 예시만 남기고 넘어가도록 한다. 

 

 

 

 

포인터 연산

 

 

예시를 들어보자. 

 

int main() {

    int *p;
    int i;
    p = &i;

    printf("%x\n", p);
    printf("%x\n", p+1);
    printf("%x\n", p+2);
    printf("%x\n", p+3);
    printf("%x\n", p+4);
    
    return 0;
}

 

포인터 연산은 포인터 변수가 가리키는 메모리 주소를 변경하거나 값을 읽어오는 작업을 말합니다.

 포인터 연산은 크게 두 가지로 나눌 수 있습니다.

1. 포인터 증감 연산자

포인터 증감 연산자(`++`, `--`)는 포인터 변수가 가리키는 메모리 주소를 증가 또는 감소시킵니다. 이때 증감 연산자가 포인터 변수 앞에 위치하면 값을 증가시키고, 뒤에 위치하면 값을 증가시킨 후 이전 값을 반환합니다.


int arr[5] = {1, 2, 3, 4, 5};
int* ptr = &arr[0]; // 배열의 첫 번째 요소를 가리킴
printf("%d\n", *ptr); // 출력: 1
ptr++; // 포인터를 다음 요소로 이동
printf("%d\n", *ptr); // 출력: 2
ptr--; // 포인터를 이전 요소로 이동
printf("%d\n", *ptr); // 출력: 1



2. 포인터 덧셈과 뺄셈 연산자

포인터 덧셈과 뺄셈 연산자(`+`, `-`)는 포인터 변수가 가리키는 메모리 주소에 값을 더하거나 빼서 새로운 메모리 주소를 생성합니다. 이때 덧셈 연산자는 포인터 변수를 다음 위치로 이동시키고, 뺄셈 연산자는 포인터 변수를 이전 위치로 이동시킵니다.


int arr[5] = {1, 2, 3, 4, 5};
int* ptr = &arr[0]; // 배열의 첫 번째 요소를 가리킴
printf("%d\n", *(ptr + 2)); // 출력: 3번째 요소인 3 출력
printf("%d\n", *(ptr - 1)); // undefined behavior(정의되지 않은 동작)


다만 포인터 변수 간의 주소의 뺄셈도 가능하다. 하지만 덧셈은 지원하지 않는다는 점이다.

 

 

 

생각

 

지금까지 다른 프로그래밍 언어를 공부하면서 이렇게 까지 주소에 깊이 고민하면서 코드를 작성한 적이 없던 것 같다. 

 

확실히 이러한 추상적인 문제를 내부적으로 감춘 언어를 사용하면 이러한 고민을 하지 않아도 돼서 편하긴 하지만 C부터 공부하고 개발해 왔던 개발자들은 감춰지는 것들에 대해 신뢰할 수 있는지 첫 발자국에는 많은 고민과 불신을 가졌을 것 같다는 생각도 한다. 

 

과거를 모르거나 무시하는 것도 잘못된 것 같지만, 이를 고집하는 것도 빠르게 변하는 IT 생태계에 적응하기 위해선 좋은 자세가 아닌 것 같다는 생각도 든다. 

 

C언어를 통해 어떤 프로그램을 만들고자 배우는 것은 아니다. 그냥 궁금했을 뿐이고, shader language가 C언어 문법을 기반으로 따른다길래 공부를 시작한 것도 있는데, 과거를 아는 것도 재밌는 일 중 하나인 것 같다. 

 

다음에는 이중포인터나, 배열형태의 포인터 배열을 공부해보려고 한다. 

 

 

 

 

반응형

'C' 카테고리의 다른 글

구조체  (0) 2023.05.01
포인터와 배열.  (0) 2023.04.29
C언어에서 배열을 어떻게 다룰까?  (0) 2023.04.17
입력 및 출력 프로그램  (0) 2023.04.16
3D Computer Graphics (3)  (1) 2023.04.08

댓글