본문 바로가기

Studying/Computer Programs

C언어 프로그래밍 기초 : 포인터

여기서는 C언어와 C++가 다른 프로그래밍 언어와 차별화되는 요소 중 하나인 포인터 (pointer)에 대해서 알아봅시다. 포인터의 기본 개념과 함께 일반적인 데이터를 저장하는 변수들과의 상관관계에 대해서도 짚어보겠습니다.

 

반응형

 

포인터에 대해서 간단히 말하자면, 메모리 상에서 변수의 주소를 저장하는 자료형이라고 할 수 있는데요. 따라서 이미 선언된 변수의 주소를 알아낸 뒤, 이를 또 다른 포인터 변수에 저장하는 것이 가능합니다. 포인터를 선언할 때는 변수 이름 앞에 * 기호를 붙이고, 변수의 주소를 얻기 위해서는 이름 앞에 & 기호를 붙이면 되겠습니다.

 

/* 정수형 변수 a를 선언하고
 * 그 값을 3으로 정의 */
int a = 3;
/* 정수형 변수의 주소를 저장하는
 * 포인터 변수 */
int *ptr_a;

/* ptr_a에는
 * a의 주소를 저장 */
ptr_a = &a;

// 정수형 변수 b를 선언
int b;
/* ptr_a에 저장된 주소에 위치한
 * 정수형 변수 값을 b에 저장
 * 따라서 b에 저장된 값은 3 */
b = *ptr_a;

 

반면에 포인터에 저장된 주소에 위치한 변수의 값을 불러와야 할 때도 있는데, 이 때는 포인터 변수의 이름 앞에 * 기호를 붙이면 됩니다. 예를 들면 정수형 변수를 가리키는 ptr_a라는 이름의 포인터가 있을 때, ptr_a에 저장된 주소에 위치한 정수형 데이터의 값을 얻기 위해서 *ptr_a를 사용할 수 있는 것입니다.

 

한가지 짚고 넘어갈 점은 포인터 변수를 선언할 때도 자료형을 지정해줘야 한다는 것인데요. 다시 말해서 포인터에 저장된 주소에 있는 데이터의 형태가 어떤 것인지를 명확히 해야 한다는 것입니다. 이게 중요한 이유는 자료형에 따라 메모리에서 차지하는 용량이 달라지기 때문입니다. 예컨대 문자를 나타내는 char형 변수는 1바이트를 차지하는 반면, 정수를 나타내는 int형 변수를 4바이트를 차지하게 되죠. 이러한 점은 포인터 변수의 연산에서도 중요하게 작용합니다.

 

포인터 변수가 메모리에서 차지하는 용량은 포인터가 가리키는 데이터의 종류와 무관하게 일정한 값을 가집니다. 64비트 CPU가 탑재된 컴퓨터에서 64비트 운영체제를 설치했다면, 포인터 변수의 크기는 항상 8바이트가 됩니다. 이는 1바이트 (byte)가 8비트 (bit)인 것을 고려한 것인데요. 0 또는 1을 64개 나열한 2진수로부터 메모리에 있는 개별 바이트의 주소를 특정한다고 생각하면 이해하기 쉽습니다.

 

결과적으로 64비트 운영체제가 인식가능한 메모리의 이론적인 최대 용량을 바이트 단위로 나타내면 2의 64제곱이 된다고 볼 수 있는 것입니다. 비슷한 논리로 32비트 운영체제에서 인식할 수 있는 메모리의 최대 용량도 계산할 수 있고, 이는 약 4기가바이트가 됩니다.

 

포인터를 사용하면 좋은 점 중 하나는 함수의 매개변수의 값을 바꾸는 것이 가능하다는 것입니다. 변수에 저장된 값을 인자로 받아들이는 함수의 경우, 매개변수에 저장된 값을 복사해서 가져다 쓰는 방식이기 때문에 변경이 불가능한데요. 포인터를 매개변수로 받는 함수라면, 메모리 주소에 위치한 데이터의 값을 바꾸는 연산이 가능하다는 장점을 가지게 됩니다.

 

간단한 예제 프로그램을 통해서 포인터의 연산에 대한 기본적인 내용들을 짚어봅시다.

 

C언어 소스 코드입니다.

 

#include<stdio.h>

/* 포인터를 매개변수로 받고,
 * 해당 메모리 주소에 저장된 정수형 변수의
 * 제곱을 계산한 뒤 그 값을 저장 */
void make_square(int *ptr_var) {
    int var_ini = *ptr_var;
    *ptr_var = var_ini * var_ini;
}

// main 함수
int main(int argc, char *argv[]) {
    int a_set[10];  // 배열
    int *ptr_a;     // 포인터

    for (int i = 0; i < 10; i++) {
        /* 배열의 i-번째 성분의
         * 메모리 주소를
         * 포인터 ptr_a에 저장 */
        ptr_a = &a_set[i];

        /* 배열의 i-번째 성분의 값을
         * i로 지정 */
        a_set[i] = i;
        /* make_square 함수를
         * 호출하여 i-번째 성분의 값을
         * i의 제곱으로 변경 */
        make_square(ptr_a);
    }

    /* 배열의 0-번째 성분의
     * 메모리 주소를
     * 포인터 ptr_a에 저장 */
    ptr_a = &a_set[0];

    printf("\n");
    for (int i = 0; i < 10; i++) {
        /* ptr_a에는 배열의 i-번째
         * 성분의 주소가 저장된 상태 */

        /* 배열 a_set의 i-번째 성분의
         * 값을 출력 */
        printf("a_set[%d] = %d, ",
               i, a_set[i]);
        /* 포인터 ptr_a에 저장된
         * 주소에 위치한 정수형 변수의
         * 값을 출력 */
        printf("*ptr_a = %d\n",
               *ptr_a);

        /* 포인터 ptr_a에 저장된
         * 주소 값을 1만큼 증가시켜
         * 배열의 (i+1)-번째 성분의
         * 주소를 저장 */
        ptr_a += 1;
    }
    printf("\n");

    return 0;
}

 

맨 먼저 정수형 변수의 제곱을 계산하는 make_square라는 함수가 등장합니다. 이 함수는 리턴값이 없는 대신, int형 변수의 포인터를 매개변수로 받아서 그 주소에 저장된 정수의 제곱을 계산합니다. 메모리 주소를 함수에 알려주면, 그 곳에 위치한 정수형 변수의 값을 제곱으로 바꾸는 것이죠.

 

main 함수에서는 a_set이라는 이름의 정수형 배열이 등장하는데요. 첫번째 반복문에서는 a_seti번째 성분인 a_set[i]의 값을 i의 제곱으로 정의합니다. 다만 이는 두 단계에 따라 이루어지는데, 먼저 a_set[i]의 값을 i로 정의한 다음 make_square 함수를 호출하여 제곱을 취하게 되죠. 그리고 여기서 a_set[i]의 주소를 저장하기 위해 포인터 변수 ptr_a가 사용되고 있습니다.

 

a_set의 성분들의 값을 출력하는 반복문에 포인터의 연산이 포함되어 있는것을 볼 수 있습니다. 반복문에 진입하기 전에 포인터 변수 ptr_aa_set[0]의 주소를 먼저 저장합니다. for문의 맨 마지막에 ptr_a의 값에 1을 더하는 연산문이 있는 것을 볼 수 있는데요. 눈여겨봐야 할 점은 ptr_aint형 변수의 주소를 저장하는 포인터이기 때문에, 그 값에 1을 더하면 int형 변수의 크기만큼 건너뛰게 된다는 것입니다. 따라서 a_set의 다음 성분의 주소를 가리키게 되죠.

 

코드를 컴파일하고 프로그램을 실행시켜보면, a_set의 각 성분에 저장된 값과 ptr_a에 저장된 주소에 위치한 값이 서로 같다는 것을 확인할 수 있겠습니다.

 

screenshot of terminal console, showing the result of an example program to demonstrate pointer and array in C language

 

이번 포스팅에서는 C언어와 C++ 프로그램에서의 포인터에 대한 기본적인 내용들을 짚어보았는데요. 포인터를 사용하면 변수를 위한 공간을 필요할때마다 메모리에서 할당받아서 사용할 수 있습니다. 이를 동적 메모리 할당 (dynamical memory allocation)이라고 하며, C언어에서는 mallocfree 함수를 호출함으로써 가능합니다. 더 자세한 내용은 다음 포스팅에 소개되어 있습니다.

 

 

C언어 동적 메모리 할당 (malloc, free 함수)

일반적으로 C언어에서 변수나 배열을 선언하면, 해당 코드블록이 끝나서 범위를 벗어날때 까지 메모리를 차지하게 됩니다. 하지만 이렇게 되면 메모리 공간의 낭비가 발생할 수 있죠. 필요할 때

swstar.tistory.com

 

C++로 프로그램을 작성하는 경우, newdelete 키워드를 통해 동적 메모리 할당이 가능합니다. 다음 포스팅에 더 자세한 내용이 소개되어 있습니다.

 

 

C++ 동적 메모리 할당 (new, delete 키워드)

여기서는 new 및 delete 키워드를 통해 C++ 프로그램에서 메모리 공간을 동적으로 할당받고 해제하는 방법에 대해서 짚어보겠습니다. 보통 변수나 객체를 소스 코드 내에서 선언하게 되면, 선언한

swstar.tistory.com

 

여기서는 변수를 가리키는 포인터에 대해서 다루었지만, 함수를 가리키는 포인터도 있습니다. 함수 포인터 (function pointer)를 사용하면 변수 대신 함수 자체를 또 다른 함수의 매개변수로 넘겨주는 것도 가능하며, 여기에 대한 더 자세한 내용은 다음 포스팅에 소개되어 있습니다.

 

 

C/C++ 에서 함수를 매개변수로 사용하기

함수 포인터를 이용한 구현 일반적으로 C언어나 C++ 에서 사용하는 함수의 경우, 인자(매개변수) 혹은 파라미터로 변수를 받아갑니다. 이 값들을 가지고 정의된 기능을 수행하게 되죠. 하지만 프

swstar.tistory.com

 


 

C언어나 C++ 소스 코드를 가지고 실행가능한 프로그램을 만드는 과정에 대해서는 다음 포스팅에 더 자세한 내용이 소개되어 있습니다.

 

 

C/C++ 코드가 프로그램이 되는 과정

여기서는 C언어 또는 C++로 작성된 소스 파일, 헤더 파일의 개념과 이들을 빌드하여 하나의 프로그램을 만드는 과정에 대해 간략하게 짚어보겠습니다. 여러 개의 소스, 헤더 파일들로 이루어진

swstar.tistory.com