본문 바로가기

Data Structure [C]/문돌이도 할 수 있는 [C언어 자료구조]

#10 [C 자료구조] 포인터가 뭐죠?: 자료의 저장 방법

파이썬과 자바스크립트와 같은 언어에서는 발견되지 않지만 C언어에서는 반드시 발견되는 것이 있다.

바로 포인터!

포인터는 특정한 데이터가 저장된 주소 값을 보관하는 변수이다.

예를 들어 내가 abc라는 데이터에 5라는 int를 부여했다고 하면

abc는 메모리 상의 어딘가에 저장이 됩니다. 그래야 나중에 변수를 호출해서 쓸 수 있으니까!

따라서 abc는 반드시 그 '어딘가'의 '주소 값' 또한 갖게 된다.

나중에 되면 헷갈리는데 머리에 박아두자.

 

포인터에서 자료구조를 포기하는 사람들이 많다. 정신줄 잡고 레-고!!

 

[포인터 = 변수의 주소값]


 

 

배열(Array)에 대하여

배열은 포인터를 배우기 이전에 값들이 어떻게 저장되는지 알기 위해서 짚어주고 넘어가야 하는 부분이다.

배열의 특징에 대해서 알아보자!

왼쪽이 인덱스 / 오른쪽이 값

1) 배열은 <인덱스(주소), 값> 쌍으로 이루어진다. 따라서 배열 형태에서 주소든 값이든 둘 중에 하나만 알면 바로 특정 위치를 잡아낼 수 있다.

2) 동일한 타입의 데이터야 한다. int, float, double, char, 다른 array 이든 뭐든

3) 인덱스, 즉 주소는 정수 값이고 이를 통해 값에 접근 가능하다.

4) 여러 값을 하나의 이름으로 처리 가능 (정의만 해주면 됨)

int Score[10] //원소 개수 10으로 설정
score[3] = 5;
score[7] = 3

위와 같은 방법으로 10개의 1차원 배열을 만들었다고 생각해보자.

그렇다면 다음과 같이 나타낼 수 있을 것이다.

주소에 대입된 숫자들

여기서 하나의 칸(<인덱스, 값>)은 실제 메모리에서 4byte를 차지한다! 이는 32bit 운영체제에서 한 번에 인식 및 처리할 수 있는 것이 32bit라는 뜻인데, 32bit = 4byte 이기 때문이다. 우리가 흔히 하는 대부분의 컴파일러 및 개발 환경은 32bit를 기준으로 한다.

따라서 score[0]의 주소가 10000000이라면 score[1]의 주소는 10000004가 된다. 배열은 첫 원소의 다음부터 순차적으로 메모리에 저장된다.


 

 

포인터(Pointer)에 대하여

포인터는 말 그대로 변수의 주소가 저장된 변수다.

포인터를 표현하는 두 가지 방법만 알면 다음의 식을 유추해낼 수 있다.

 

  1. abc: 변수 abc
  2. *abc: 포인터 abc가 가리키는 변수 -> abc는 포인터
  3. &abc: 변수 abc를 가르키는 포인터 -> abc는 변수

(*과 &는 각각 변수와 포인터를 가르키는 간접 참조 연산자)

int main(void)
{
    int abc1;
    int *abc2;
    abc1 = 10;
    abc2 = &abc1;
    
    printf("%d", abc1);
    printf("&d", &abc1);
    printf("&d", abc2);
    printf("&d", *abc2);
    
    return 0;
}

여기서 abc1의 주소 값이 5468423이라고 할 때,

값은 차례대로 10, 5468423, 5468423, 10 나오게 된다.

abc2가 abc1의 주소 값이기 때문!!


 

 

 

포인터의 선언 법

int, char, float, double에 관계없이 포인터를 선언할 수 있다.

char c = 'A';
float f = 46.1;
double d = 2.73

이렇게 변수를 지정하고 이제 포인터를 지정해보자!!

char *pc = &c;
float *pf = &f;
double *pd = &d;

해석해보자면 'pc는 포인터이고(*pc) pc는 c의 주소 값(&c)이야.'라는 뜻이 된다.

'*pc 가 &c 인 게 아닌가?' 즉, 'pc 주소 값이 가리키는 변수 *pc가 다시 c의 주소값이 된다!' 라고 잠시 헷갈렸지만 아니다. 앞의 *pc는 그저 pc가 주소값임! 이라는 것만 알려주는 것이다. 그리고 주소값은 언제나 정수이므로 앞의 char, float, double은 그들이 가르키는 변수의 특성을 정의하는 것이다.

 

또 이렇게 구조체를 가리켜 볼 수도 있다.

struct group_A *p

이렇게 한다면 group_A라는 구조체 변수를 가리키는 포인터 p를 만드는 것이다.

 


 

 

 

포인터 연산

포인터의 연산은 약간 특이한데 무조건 외워둬야 C언어를 쓸 때 편리하다.

  • p: 포인터, 주소 값
  • *p: 포인터가 가리키는 값
  • (*p)++: 포인터가 가르키는 값을 1 증가
  • *p++: *(p++) 포인터를 읽고, 포인터를 1 증가
  • *p--: *(p--) 포인터를 읽고, 포인터를 1 감소

이건 뭐 사칙연산처럼 자연스럽게 머릿속에 있어야 한다ㅎㅎ


 

 

 

포인터 호출이 중요한 이유

C언어에서 포인터 호출은 굉장히 중요하다. 파이썬이나 다른 후발 주자 언어들과 다르게, 다른 함수의 결괏값을 직접 호출하는 것이 안되기 때문이다. 아래 정의들을 살펴보고 이 것이 왜 문제인지 살펴보자!

 

프로그래밍 언어는 두 가지 방법에 의해 값을 호출한다.

  • 값에 의한 호출
    • C의 기본적인 방법
    • 인수의 값만이 함수로 복사된다.
    • 복사본이 전달된다고 생각하면 됨
  • 참조에 의한 호출
    • C에서 포인터를 이용한 호출 방법
    • 인수의 주소가 복사된다.
    • 원본이 전달된다고 생각하면 된다.

다시 말해, 참조에 의한 호출을 진행해야 함수로 인해 변한 변수 값들을 직접 전달이 가능하다!! 함수 바깥에서 abc = 22라고 정의가 되었으면, 함수 내에서 abc가 아무리 바뀌었어도 참조에 의한 호출을 하지 않으면 abc는 그대로 22이다.

 

void swap(int a, int b);
int main(void){
    int num1, num2;
    printf("두 수 입력: ");
    scanf("&d &d", &num1, &num2);
    swap(num1, num2);
    printf("결과: %d %d\n", num1, num2);
    return 0;
}
void swap (int a, int b){
    int temp;
    if (a>b){
        temp = a;
        a = b;
        b = temp;
    }
}

여기서 swap 함수는 입력받은 두 수 중 앞에 수가 크다면 앞뒤를 바꾸는 것이란 걸 유추할 수 있다.

그러나 swap 함수에 아무리 a, b를 집어넣어도 결과 값은  처음 입력한 두 수와 똑같다.

그러나 만약 다음과 같이 바꾸면 어떻게 될까?!?!

void swap(int *a, int *b);
int main(void){
    int num1, num2;
    printf("두 수 입력: ");
    scanf("&d &d", &num1, &num2);
    swap(&num1, &num2);
    printf("결과: %d %d\n", num1, num2);
    return 0;
}
void swap (int *a, int *b){
    int temp;
    if (*a>*b){
        temp = *a;
        *a = *b;
        *b = temp;
    }
}

위 식은 swap 식을 전부 포인터 넘겨주고 받게 코딩했고, main함수에서 넘겨줄 때 또한 &num1와 같이 포인터로 넘겨주게 되어있다. 따라서 swap과 main 함수는 둘 다 동일한 변수를 사용한 셈이 된 것이다!!

 

한 번만 더 예시를 더 쉬운 걸로 들어보자

int add_ten(int b);
int main(void){
    int a =10;
    add_ten(a);
    printf("a는 %d\n", a);
    return 0;
}
int add_ten(int b){
    b = b+10;
    return b;
}

위 식은 값만을 호출하는 것으로써, add_ten으로 변수는 이동하거나 교환되지 않는다.

int add_ten(int *b);
int main(void){
    int a =10;
    add_ten(&a);
    printf("a는 %d\n", a);
    return 0;
}
int add_ten(int *b){
    *b = *b+10;
    return *b;
}

이 식은 주소 값을 교환함으로써 변수 자체에 변화를 주고 a=20 결과를 뽑아낼 수 있게 된다.


 

 

좋다! 포인터에 대해서 아주아주아주아주아주대략적으로 알아봤다. 파이썬 같은 경우엔 개떡같이 말해도 찰떡같이 알아먹었는데 C언어 욘석은 너무 골치 아프다ㅜㅜ 하지만 자료구조 마스터를 위해선 해야 한다... C언어...

다음 시간엔 이중 포인터와 실제로 배열 내에서 어떻게 쓰이는지 알아보도록 하자!!