본문 바로가기
Development/C

(C언어 기초) 14. 포인터의 이해

by eoieiie 2023. 11. 5.

포인터를 완전히 이해하지 못하면 C언어를 사용할 수 없습니다. 아주 단호하죠? 그만큼 포인터는 정말 중요합니다. 앞으로도 계속 마주하게 될 코드들과 친해지기 위해서는 포인터를 완벽하게 이해하는 것이 필요합니다. 

포인터는 언제든지 다른 주소를 저장하거나, 서로간의 대입이 가능합니다. 그러나 일반 변수와는 달리 대입 연산에 엄격한 기준이 적용됩니다. 위 3가지 특징을 잘 기억하면서 포인터에 대해서 더 깊이 알아보도록 하겠습니다. 

 

주소와 포인터의 차이


 

주소는 변수에 할당된 메모리 저장 공간의 시작 주소 값 자체이고, 포인터는 그 값을 저장하는 또 다른 메모리 공간입니다. 

변수의 주소 값은 바뀔 수 없지만, 포인터는 다른 주소를 대입하여서 그 값을 바꿀 수 있습니다. 다음 예시들을 살펴봅시다.

int a, b; //일반 변수 선언
int *p; //포인터 선언
p = &a; //포인터가 변수 a의 주소를 가리키도록 설정
p = &b; //b의 주소를 가리키도록 바꿈

 

위 내용을 요약하면 주소는 값이 변하지 않는 '상수'이고, 포인터는 값이 변할 수 있는 '변수' 라는 것이 되겠습니다. (주소는 상수이기에, %a = %b처럼 대입 연산자의 왼쪽에 올 수 없습니다. a의 주소를 b의 주소로 바꾸는 것이 불가능하다는 뜻입니다. ) 이는 다음과 같이 두 포인터가 같은 주소를 저장하는 일을 가능하게 해 줍니다..

 

int a; //일반 변수 선언
int *pa, *pb; //가리키는 자료형이 같은 두 포인터
pa = pb = &a; // pa와 pb에 모두 a의 주소를 저장한다.

 

이 경우, pa와 pb 모두 a의 값을 바꾸거나 연산하는 데 사용할 수 있습니다. 

 

*pa = 10; //pa를 활용하여 변수 a에 10 대입
printf("%d", *pb); // pb가 가리키는 변수 a값 10 출력

  

위 예제에서 p는 포인터이며, *p는 p가 가리키는 주소의 변수의 값을 나타냅니다.

주소와 포인터의 크기


 

포인터도 저장공간이기에 그 크기가 존재합니다. 저장할 주소의 크기에 따라서 결정되며, 크기가 클수록 더 넓은 범위의 메모리를 사용가능합니다. 그러나 모든 주소와 포인터는 가리키는 자료형과 상관없이 그 크기가 동일합니다. sizeof 연산자로 주소와 포인터의 크기를 확인해보도록 하겠습니다. 

 

#include <stdio.h>

int main(void)
{
    char ch;
    int in;
    double db;

    char *pc = &ch;
    int *pi = &in;
    double *pd = &db;

    printf("char 형 변수의 주소 크기 : %d\n", sizeof(&ch));
    printf("int 형 변수의 주소 크기 : %d\n", sizeof(&in));
    printf("double 형 변수의 주소 크기 : %d\n", sizeof(&db));
   
    printf("char * 포인터의 크기 : %d\n", sizeof(pc));
    printf("int *포인터의 크기 : %d\n", sizeof(pi));
    printf("double  *포인터의 크기 : %d\n", sizeof(pd));
   
    printf("char * 포인터가 가리키는 변수의 크기 : %d\n", sizeof(*pc));
    printf("int *포인터가 가리키는 변수의 크기 : %d\n", sizeof(*pi));
    printf("double  *포인터가 가리키는 변수의 크기 : %d\n", sizeof(*pd));
    
    return 0;

}

// char 형 변수의 주소 크기 : 8
// int 형 변수의 주소 크기 : 8
// double 형 변수의 주소 크기 : 8
// char * 포인터의 크기 : 8
// int *포인터의 크기 : 8
// double  *포인터의 크기 : 8
// char * 포인터가 가리키는 변수의 크기 : 1
// int *포인터가 가리키는 변수의 크기 : 4
// double  *포인터가 가리키는 변수의 크기 : 8

 

ch, in, db은 각각 변수 자체의 크기는 다르지만, 시작 주소의 값은 메모리의 박스 한 칸을 가리키므로 모두 동일합니다. 따라서 해당 주소의 위치를 가리키는 포인터 역시 동일한 크기를 가집니다. 물론 포인터에 애스터리스크를 붙여 가리키는 변수의 크기를 출력하면 원래 우리가 알던 값들이 출력될겁니다. 

 

포인터의 대입규칙


포인터에는 굉장히 깐깐한 대입 연산의 규칙이 존재합니다.

 

규칙 1.) 포인터는 가리키는 변수의 형태가 같을 때만 대입해야 한다. 

포인터끼리 대입 연산을 수행하면 여러 개의 포인터로 같은 데이터를 다루는 것이 가능합니다. 그러나 규칙이 지켜지지 않으면 그 결과를 예상할 수 없게 되어 버립니다. 아래는 가리키는 자료형이 일치하지 않는 포인터를 다른 포인터에 대입하려고 했을 때 발생하는 오류입니다.

 

#include <stdio.h>

int main(void)
{
    int a = 10; //int형 변수 선언과 초기화
    int *p = &a; //포인터 선언, a를 가리키도록 
    double *pd; //double 형 포인터 pd선언

    pd = p; //double형 포인터에 포인터 p의 주소값을 대입
    printf("%lf\n", *pd); //pd가 가리키는 변수의 값을 출력

    return 0;

}



//warning: assignment to 'double *' from incompatible pointer type 'int *'

 

규칙 2.)형 변환을 사용한 포인터의 대입은 가능하다. 

포인터가 가리키는 자료형이 다른 경우라도 형 변환 연산자를 사용하면 경고 메시지 없이 대입할 수 있습니다. 

 

#include <stdio.h>

int main(void)
{
    double a = 3.4; //double형 변수 선언
    double *pd = &a; //double 형 포인터 pd선언 후 a의 주소를 가리키도록 할당
    int *pi; //int형 포인터 pi선언

    pi = (int *)pd; //int형 포인터 pi에 double형 포인터 pd를 int로 형변환 후 pi에 대입

    return 0;

}

 

만약 *pi와 같이 a의 일부분에 정수를 저장하면 정수와 실수의 데이터 크기와 저장 방식이 다르므로 a에 저장한 실수 값은 사용할 수 없습니다.

 

*출력지정형식 f일 경우와 d일 경우가 다른 이유 살펴보기 

포인터를 사용하는 이유


 

사실 포인터는 복잡하고 어렵습니다. 지금까지 변수를 잘 사용해왔는데 갑자기 이상한 아이가 튀어나와서 별로 마음에 안 드시겠지만, 포인터는 임베디드 프로그래밍, 동적 할당 메모리를 사용하는 경우에 꼭 필요합니다.  어려우니까 그냥 예제로 확인해봅시다. 

 

#include <stdio.h>

void swap(int *pa, int *pb); 

int main(void)
{
    int a = 10, b = 20; //변수 선언과 초기화

    swap(&a, &b); // a와 b의 주소를 인수로 함수를 호출
    printf("a : %d, b : %f\n", a, b);

    return 0;

}


void swap(int *pa, int *pb)
{
    int temp; //int형 변수 temp 선언

    temp = *pa; //temp에 pa가 가리키는 주소에 있는 변수를 저장. pa는 포인터, 
                //*pa는 그걸 이용해서 직접 변수를 가져오는 형태이다.

    *pa = *pb; //pa가 가리키는 변수에 pb가 가리키는 변수의 값을 대입
    *pb = temp; //pb가 가리키는 변수에 temp 값 저장

}

 

예제는 두 변수의 값을 swap 함수 호출을 통해 바꿉니다. 교환 작업은 swap 함수 안에서 포인터를 통해서 진행되지만, 실제로 바뀌는 값은 main함수의 변수 a와 b가 됩니다. 결국 swap함수는 포인터를 통해 main 함수의 변수 a, b를 공유하므로 언제든지 직접 바꾸는 일이 가능해집니다. 

 

그러면 포인터 없이 두 변수의 값을 바꾸는 함수는 불가능할까요?

 

우선 예제를 통해 swap 함수에서 main 함수의 a, b를 이름으로 직접 사용하는 방법을 생각해 보겠습니다. 

 

#include <stdio.h>

void swap(void);

int main(void)
{
    int a = 10, b = 20;

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

    return 0;

}

void swap(void)
{
    int temp;

    temp = a;
    a = b;
    b = temp;

}

 

함수 안에서 선언된 변수명은 사용 범위가 함수 내부로 제한되므로 main 함수에 있는 변수 a, b는 다른 함수인 swap 함수에서 그 이름을 사용할 수 없습니다. 또 다른 예제를 보겠습니다. 

 

#include <stdio.h>

void swap(int x, int y);

int main(void)
{
    int a = 10, b = 20;

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

    return 0;

}

void swap(int x, int y)
{
    int temp;

    temp = x;
    x = y;
    y = temp;

}

 

위의 예제에서 swap 함수는 main 함수의 변수 a, b값이 복사되어 매개변수 x, y 에 저장됩니다. 결국 swap 함수 안에서는 a, b의 복사본을 바꾸므로 main 함수의 두 변수 a,b의 값에는 변함이 없습니다. swap 함수 안에서 a, b의 값을 수정하더라도 말이죠. 즉 이름이 같아도 함수가 다르면 메모리에 별도로 저장된다는 말입니다. 

swap 함수에서 바꾼 값을 main 함수로 반환하면 되지 않느냐? 라고 생각할 수 있지만 그렇지 않습니다. 함수는 오직 하나의 값만을 반환할 수 있으므로 한 번의 함수 호출을 통해 두 변수의 값을 모두 바꾸는 것은 불가능합니다. 

 

핵심 정리


 

주소와 포인터는 각각 상수와 변수라는 차이가 있다. 

포인터의 크기는 주소의 크기와 같다. 

포인터에 주소를 저장할 대는 가리키는 자료형이 같아야 한다. 

포인터의 주요 기능 중 하나는 함수 간에 효과적으로 데이터를 공유하는 것이다. 

 

포인터 pa의 사용법 예시 

 

*pa = 10; //pa가 가리키는 주소의 값에 10대입
b = *pa; //b에 pa가 가리키는 주소의 값 대입
*pa + 20; //pa가 가리키는 주소의 값에 20추가
printf("%d", *pa); 
scanf("%d", &*pa); 
//=
scanf("%d", pa);

 

int a, b; //int 형 a, b선언
int *p = &a; //int 형 포인터 p선언, a의 주소로 초기화
p = &b; //p가 b의 주소를 가리키도록 초기화
sizeof(p) //포인터의 크기 확인

 

확인 문제


 

다음 코드를 참고해 보기에서 상수와 변수를 구분하세요. 

int a = 10;
int *p = &a;
*p = 20;
더보기

a : 변수
10 : 상수
*p : 변수
&a : 상수

 

주소 값이 크기가 4바이트일 때, sizeof 연산의 결괏값이 가장 큰 것을 고르세요. 

char *pc;
double *pd;

 

1.sizeof(pc)

2.sizeof(pd)

3.sizeof(*pc)

4.sizeof(*pd)

 

다음 코드의 실행결과를 적으세요. 

#include <stdio.h>

int main(void)
{
    int a = 10, b = 20;
    int *pa = &a,  *pb = &b, *pt;
    pt = pa; //pt도 이제 pa가 가리키는 a의 주소를 가리킴
    pa = pb; //pa는 pb가 가리키는 b의 주소를 가리킴
    pb = pt; //pb에 pt가 가리키는 주소 대입

    printf("%d, %d", *pa, *pb);

}
더보기

20, 10

 

키보드로 실수 3개를 입력한 후 큰 숫자부터 작은 숫자로 정렬한 뒤 출력하는 프로그램을 작성합니다. 다음 코드와 출력 결과를 참고해 line_up 함수를 작성하세요. line_up 함수에는 이미 정의된 swap 함수를 호출해 구현하세요. 

 

#include <stdio.h>

void swap(double *pa, double *pb);
void line_up(double *maxp, double *midp, double *minp);


int main(void)
{
    double max, mid, min;
    scanf("%lf%lf%lf", &max, &mid, &min);
    line_up(&max, &mid, &min);
    printf("정렬된 값 출력ㄱ : %.1lf, %.1lf, %.1lf\n", max, mid, min);

    return 0;
}

void swap(double *pa, double *pb)
{
    double temp;

    temp = *pa;

    temp = *pa;
    *pa = *pb;
    *pb = temp;
}

void line_up(double *maxp, double *midp, double *minp)
{
//
//
//
}
더보기

if(*maxp < *midp) swap(maxp, midp); 

if(*maxp < *minp) swap(maxp, minp); 

if(*midp < *minp) swap(midp, minp);

 

댓글