F.R.I.D.A.Y.

포인터 part1. default 본문

DEV/C C++

포인터 part1. default

F.R.I.D.A.Y. 2020. 1. 7. 22:35
반응형

 잘 배워가던 사람들도 멈추는, 일명 C언어에서 첫 번째 고비라 일컬어지는 포인터입니다. 하다 보면 쉽지만 막상 처음 하면 난생처음 보는 문법에 쓰는 방법도 독특한지라 많이들 힘들어합니다.

 

# 선행으로 함수를 알아야 합니다.


사용자로부터 입력받기

 우리가 사용자로부터 값을 입력받을 때, 표준 입출력 함수를 사용하곤 합니다. 대표적으로 scanf[# scanf_s 또한 표준입니다. scanf_s에 대한 자세한 내용은 https://pang2h.tistory.com/200을 참고하세요.]가 존재하겠네요.

#include <stdio.h>

int main(void){
    int num;
    scanf("%d", &num);
    
    printf("%d\n", num);
    
    return 0;
}

 그렇다면 우리도 scanf 함수처럼 값을 입력받도록 해보면 어떨까요? 함수를 만들어 이용하겠습니다.

void custom_scanf(int v){
    do{
        scanf("%d", &v);
    }while(v < 10);    
}

  값이 10 이상일 때까지 입력받도록 코드를 구성했습니다. 그럼 우리는 이렇게 사용할 수 있겠습니다.

void custom_scanf(int v){
    do{
        scanf("%d", &v);
    }while(v < 10);    
}

int main(void){
    int num;
    custom_scanf(num);
    
    printf("input : %d\n", num);
    
    return 0;
}

 그런데 이렇게 작성하면 예상과는 다르게 동작하게 될 것입니다. 경우에 따라서는 컴파일 오류가 발생해 프로그램도 만들어지지 않겠군요. 분명 잘 작성한 것 같은데 이상합니다.


원인은 언어의 방식

 이 문제는 C언어가 채택한 방식에서 발생합니다. 우리는 정수를 받을 예정이니 int라고 작성했습니다. 그러나 C언어는 다른 타 언어[# Javascript 등 인자의 호출 방식이 기본적으로 CallbyReference인 언어]에 달리 CallByValue. 즉, 값을 복사해 그 값을 넘기는 방식을 채택한 언어입니다. 달리 말하면 함수를 호출하면서 넘긴 변수 자체를 공유하지 않음[# 이 내용은 지금 설명하는 포인터와는 상이한 부분이므로 자세한 내용은 다음 포스트를 참고하세요.]을 의미합니다.

 모든 방식이 장단점이 존재하듯 이 방식은 여러 호출점에서 하나의 공간을 공유하지 않기 때문에 값의 오염[# A라는 곳에서는 a라는 공간에 1이라는 값이 있을 것이라 예측하고 a를 사용합니다. 그러나 A가 예측하고 꺼내 사용하는 과정 중간에 B라는 곳에서 a라는 공간에 4를 집어넣습니다. 그렇게 되면 A가 a의 값을 꺼내기 전에 4가 들어간 게 되므로 결론적으로 A는 4라는 엉뚱한 값을 받게 됩니다. 이 과정을 우리는 값의 오염이라고 부릅니다.]이 발생하지 않습니다. 그러나 역으로 보면 여러 호출점에서 같은 공간을 사용하기 위해 특별한 장치를 사용해야 합니다.


포인터

 그래서 사용하는 것이 바로 포인터(Pointer)입니다. 우리가 어느 한 장소에 모이기 위해서는 그 장소를 설명할 수 있는 무언가가 필요합니다. 우리는 이러한 문제를 해결하기 위해 장소와 주소를 1대 1로 매칭해 사용합니다. 포인터는 변수의 주소[# 정확히는 메모리의 주소]를 보관하는 새로운 형태의 자료형이라고 볼 수 있습니다.

 

 포인터의 기본 골격은 다음과 같습니다.

[type]* [varName];
[type] *[varName];
[type] * [varName];

// 애스터리스크(*) 기호는 타입과 변수명 사이 어디에 들어가던 상관없습니다.
  • [type] : 이미 존재하는 자료형[# int, char, double 등. 혹은 프로그래머가 직접 작성한 구조체나 union, 배열 등]을 작성합니다.
  • [varName] : 포인터 변수의 이름입니다.

 포인터의 핵심은 이 *[# asterisk, 애스터리스크. 속칭 별표]에 존재합니다. 이 문자가 바로 그 변수가 바로 포인터 변수라는 것을 알려주는 나침반 같은 녀석입니다.

 

 변수를 선언했다면 사용도 할 수 있어야 합니다. 값을 넣고 가져오는 법은 아래와 같습니다.

int num;
int* p1 = &num;

printf("p1이 가지고 있는 값 : %p\n" p1);
printf("p1 == &num : %d\n", p1 == &num);

 두 번째 줄의 의미는 int 포인터 타입 변수인 p1에 int 타입 num 변수의 주소를 저장하겠다는 의미입니다.


포인터는 읽는 법이 두 가지

'하나의 변수, 하나의 값'

 

 우리가 변수를 배우고 사용하며 알게 된 특성 한 가지를 꼽으라면 위 문장이 아닐 수 없습니다. 변수는 어떠한 경우에도 변수 하나에는 값 하나만 들어가야 함이 정석입니다. 그러나 이 룰을 깨트리는 몇 가지가 존재[# https://pang2h.tistory.com/264 하나의 예를 설명합니다. 배우는 중이라면 어려울 수 있습니다.]합니다. 그중에 하나가 바로 이번에 소개 중인 포인터입니다.

 

 포인터도 결국은 변수를 사용하는 하나의 자료형에 불과합니다. 따라서 위 문장은 포인터에서도 예외는 아닙니다. 그러나 포인터는 포인터이기에 사용할 수 있는 특별한 기술이 한 가지 있습니다. 이 기술로 하나의 변수에서 여러 값을 불러들일 수 있습니다.

 

int num;
int* p1 = &num;

num = 2;

printf("%p\n", p1);
printf("%d\n", *p1);

 첫 두 줄의 코드는 위에서 설명했습니다. 두 번째 코드까지 실행하면 p1은 num1을 가리키고 있는 것입니다. 세 번째 줄부터는 각 변수에 존재하는 값을 출력하는 코드입니다.

 

그림의 코드를 통해 p1은 num을 가리키도록 한다.

 포인터의 핵심 부분은 앞으로 설명할 이 부분입니다. p1의 자료형은 어떻게 될까요?

char int int* char*

정답: 오른쪽 각주 확인[#정답은 세 번째 int*입니다.]

 

 

 


자료형을 잘 파악하자!

 포인터의 자료형은 [type]*입니다. 처음 포인터를 접하게 되면 기존의 자료형에 * 문자를 찍는다고 배우지만, 정작 *의 의미는 잘 모르고 넘어가곤 합니다. * 문자는 해당 변수가 포인터임을 알려주는 문자로, 연산자가 아닙니다.

 포인터의 자료형을 작성하라 하면 예시로 보여드린 코드에서는 int가 아닌 int *로 말하셔야 합니다. 일부 강의에서 기존의 자료형과 포인터 기호[# *]를 띄어쓰기로 구분 지어 작성합니다. 그러다 보니 청자는 이 기호가 자료형의 일부임을 자각하지 못하는 것 같습니다.

 

 이렇게 띄어쓰기로 구분해 강의하는 경우가 많다 보니 사용할 때도 * 문자 꼭 작성해주어야 한다는 생각을 가지게 됩니다. 이 문제는 포인터의 핵심인 간접 참조를 얼마나 잘 이해했느냐의 문제이기 때문에 작은 실수더라도 큰 문제로 번질 수 있습니다. 저도 포인터를 배울 적에는 난해하다 느낀 편이었기에 시행착오를 겪다가 아래와 같은 표기법을 즐기고 있습니다.

int* p_i;
char* p_c;
short* p_s;

float* p_f;
double* p_d;

 선언하는 모습을 보면 * 문자를 모두 기존 자료형에 붙여서 작성합니다. 띄어서 작성해도 무리는 없지만 붙여서 작성하는 것이 처음 배울 때는 자료형과 변수 이름을 구분 짓는데 도움이 될 것입니다.


간접 참조하기

 포인터를 사용하는 이유, 간접 참조입니다. 간접 참조는 직접 코드에 나와있지 않고 입력받는 값 등을 통해 간접적으로 접근한다고 해서 간접 참조라 불려집니다.

이 코드의 마지막 줄. p1을 간접 참조하고 있다.

 간접 참조는 포인터 변수 앞에 * 문자를 작성해 이용합니다.

더보기

# 포인터를 선언할 때와 간접 참조를 할 때의 *는 다릅니다.

 같은 문자 *라고 생각할 수 있습니다. 그러나 이는 전혀 아닙니다.

 

 "혈액형이 오형입니다."

 라는 문장에서 오형은 어떻게 문자가 이루어져 있을까요?

ㅇㅗ
ㅎㅕㅇ

 이라고 생각할 수 있지만, 아래와 같습니다.

ㅎㅕㅇ

 문자 '오'의 초성에 나오는 'ㅇ'은 모양을 맞춰주기 위해 형식으로 존재하는 이응이라면, '형'의 종성 'ㅇ'은 실제 없으면 소리가 달라지는 문자입니다.

 포인터에 대입해본다면 초성의 'ㅇ'은 선언할 때의 * 문자, 종성의 'ㅇ'은 사용할 때의 * 문자라고 볼 수 있습니다.

 

 실제로 사용할 때 사용하는 * 문자는 단항 연산자로 이름은 간접 참조 연산자라고 불립니다.

 

# 꼭 이해하고 지나가세요!

 우리가 변수를 사용할 때, 앞에 무슨 접두사를 붙이거나 하지는 않습니다. 그냥 변수 이름 그대로 사용합니다. 해당 변수를 그냥 사용하면 그 변수가 가지고 있는 값을 반환하게 됩니다.

 

 그러나 포인터 변수는 간접 참조 연산자를 이용할 수 있습니다. 간접 참조 연산자를 이용하면 호출할 공간이 일시적으로 현재 사용할 변수의 값을 주소로 갖는 공간으로 변경됩니다. 즉, 다른 공간을 잠시 접근할 수 있게 되는 것입니다.

 

포인터 변수가 아니라면 *를 사용할 수 없다

 이 간접 참조 연산자는 절대 일반 변수에는 사용할 수 없습니다. 오로지 포인터 연산자에서만 사용할 수 있는 특혜입니다. 다른 말로 일반 변수는 다른 공간에 접근할 수 없다는 이야기가 되겠네요.


함수 고치기

 이제 원인과 해결 방법을 알았으니 원래대로 custom_scanf 함수를 수정해보겠습니다.

void custom_scanf(int v){
    do{
        scanf("%d", &v);
    }while(v < 10);    
}

 문제의 코드는 위와 같습니다. 우리는 호출할 때 넘기는 변수의 값을 변경해야 합니다. 따라서 다른 공간에 접근할 수 있는 포인터 타입으로 인자를 바꿔 주어야 합니다.

// 인자의 자료형 변경하기
void custom_scanf(int* v){
    do{
        scanf("%d", &v);
    }while(v < 10);    
}

 인자가 바뀌었으나 이제 아래 v 변수를 사용하는 방법도 변경해주어야 합니다.

 scanf 함수는 두 번째부터 입력받는 변수들의 값을 변경할 수 있습니다. 그렇기 때문에 포인터 변수를 사용해야 합니다. 마침 v는 포인터 변수입니다. 그렇다면 굳이 주소 연산자를 이용할 필요가 없겠군요. 주소 연산자를 지워줍니다.

// 주소 연산자 지우기
void custom_scanf(int* v){
    do{
        scanf("%d", v);
    }while(v < 10);    
}
더보기

# 포인터 변수에 주소 연산자를 사용하면 어떻게 되나요?

 [인자의 자료형 변경하기] 코드를 보면, 포인터 변수 v에 주소 연산을 하여 값을 넘긴다고 가정합니다. 그러면 custom_scanf 함수를 호출했던 main 함수 내의 num 변수의 주소가 아니라 custom_scanf 함수의 파라미터 변수 v의 주소가 넘어가게 됩니다. 그렇게 되면 입력으로 받는 값은 v에 저장됩니다. 이는 메모리 접근에 있어 큰 문제를 야기할 수 있습니다[# 이 부분은 따로 설명하겠습니다.].

 

 만일 값 5를 입력으로 받는다고 하겠습니다.

입력 전

custom_scanf 함수의 v 파라미터 변수의 값 100
main 함수의 num 변수의 주소 100

 5를 입력받으면 v 파라미터 변수의 값이 5가 됩니다. 따라서 간접 참조를 통해 메모리에 접근하려고 하면 num 변수에 접근하는 것이 아니라 5를 주소로 가지는 미지의 공간에 접근하게 됩니다.

입력 후

custom_scanf 함수의 v 파라미터 변수의 값 5
main 함수의 num 변수의 주소 100

 그럼 custom_scanf 함수 입장에서는 num 변수의 주소를 잃어버리게 되므로 main 함수에 접근할 수도 없이 쓸 데 없는 작업만 하는 꼴이 됩니다.

// 간접 참조 연산자 사용
void custom_scanf(int* v){
    do{
        scanf("%d", v);
    }while(*v < 10); // here!
}

 scanf 함수의 인수를 수정해주었니 이제 main 함수의 num 변수에 값이 전달됩니다. 함수가 제대로 동작하려면 num 변수의 값이 10 이사이어야 하니 num 변수의 값을 받아와야 합니다. 간접 참조 연산자를 이용해 num 변수 공간에 접근하도록 합니다.

 

 수정된 코드로 컴파일을 시행해보면 정상적으로 10 이상일 때만 입력이 종료되도록 하는 프로그램을 만들었을 것입니다.

void custom_scanf(int* v){
    do{
        scanf("%d", v);
    }while(*v < 10);    
}

int main(void){
    int num;
    custom_scanf(&num); // here!
    
    printf("input : %d\n", num);
    
    return 0;
}

 한가지 더, 함수 인자의 자료형이 변경되었으니 사용할 때도 변경되어야합니다. 포인터 자료형[# 누차 강조하지만 일반적으로 포인터는 메모리의 주소를 저장하는 용도로 사용합니다. 포인터가 존재하는데 굳이 일반 변수에 메모리 주소를 넣어서 보관/사용하려 들지 마세요. 억지로 사용은 가능하겠지만 정말 비효율적입니다.]에 맞게 주소 연산자를 붙여줍니다.


Next.

 포인터는 간단하지만 강력한 기능을 사용할 수 있는 문법이기 때문에 설명해야할 내용이 깁니다. 여기까지만 하더라도 충분히 배웠지만 다차원[# 원서에 차원(dimemsion)이라는 내용은 포인터 문법에서 나오지 않습니다. 옮긴 분들이 옮길만 한 단어를 찾지 못하다 다차원 배열의 차원을 가져다 작성한 것이 굳어진 것입니다.] 포인터, 함수 포인터, 배열과의 관계 등 더 배워야 할 내용들이 있습니다. 그러니 더 공부해보세요. 점차 포인터의 매력에 빠질 것입니다.

 

포인터 part2. 다차원 포인터

지난 시간에 우리는 포인터의 기본적인 내용을 배웠습니다. 포인터(pointer) 잘 배워가던 사람들도 멈추는, 일명 C언어에서 첫 번째 고비라 일컬어지는 포인터입니다. 하다 보면 쉽지만 막상 처음 하면 난생처음..

pang2h.tistory.com

HARD

 

함수 포인터를 배워야 하는 이유 : 코드 간결화

많은 사람들이 C언어를 배우기 시작하다가 중간에 막히는 부분이 있습니다. 대표적으로 포인터가 있는데요, 이번에 배울 것 또한 포인터입니다. 이번에 배울 포인터는 기존의 포인터와는 약간 다른, 함수 포인터입..

pang2h.tistory.com

# index

728x90
반응형

'DEV > C C++' 카테고리의 다른 글

CallByValue vs CallByReference  (0) 2020.01.09
함수 호출 구조  (0) 2020.01.07
배열 변수의 이름이 0번 인덱스의 시작 주소인 이유  (0) 2020.01.06
typedef  (0) 2020.01.04
구조체(struct) part1. default  (0) 2020.01.02
Comments