python으로 코딩을 처음 배운 내게 c언어를 배울 때 가장 머리를 아프게 했던 개념!

포인터는 뭘까?

위키백과에서 정의하는 포인터는 아래와 같다.

포인터(pointer)는 프로그래밍 언어에서 다른 변수, 혹은 그 변수의 메모리 공간주소를 가리키는 변수

 

변수는 대충 값을 저장하는 곳 같은데 메모리 공간 주소는 뭘까?

간단한 c언어 표준 입출력 코드로 알아보자

메모리 공간 주소란?

#include <stdio.h>

int main()
{
    int a; // 변수 a를 정수형(int)으로 선언

    printf("상수 하나를 입력해주세요: ");
    scanf("%d", &a); // 사용자가 입력한 정수를 변수 a에 저장 (a의 주소에 값을 저장)
    printf("입력된 값: %d\n", a); // 변수 a에 저장된 값을 출력

    return 0;
}

변수를 선언하게 되면 컴파일러는 해당 변수의 자료형에 맞는 메모리 공간의 크기를 계산해주고, 운영체제는 해당 코드를 실행할 경우 계산된 메모리 크기에 맞는 메모리 공간을 확보해준다. 그래서 메모리 주소는 그때 그때 달라진다.


쉽게 말하면 선언한 변수에 해당하는만큼 메모리 공간이 확보가 된다!

 

int 자료형의 크기는 4 bytes (환경마다 다를 수는 있겠지만 대부분의 환경에서)
자료형을 간단히 설명하면 컴퓨터는 0과 1로 데이터를 저장하게 되는데 이를 정보의 최소 단위인 bit라고 한다.
따라서 1bit가 나타낼 수 있는 경우의 수는 2가지이다.
1byte는 8bit로 구성되어 있으므로 1byte가 나타낼수 있는 경우의 수는 2 ^ 8인 64가지이다.
따라서 int 자료형인 4 bytes는 2^32가지 경우의 수를 나타낼 수 있는데,

int는 -범위부터 정수를 나타내므로

int가 저장할 수 있는 수의 범위는 (-2^31) ~ (2^31 - 1)까지 범위의 수를 나타낼 수 있다! (양수에 -1이 된 이유는 0을 포함해서 1개가 빠짐)


쉽게 말하면 int 자료형은 4칸만큼의 메모리 공간이 확보가 된다!

 

&연산자는 단항연산자로 사용될 경우 주소 연산자 (Address-of Operator)로 해당 변수의 주소를 가르키게 된다.

 

scanf("%d", &a); 따라서 해당 코드는 a의 주소에 %d (십진수)의 형태로 입력을 받겠다는 코드이다.

 

해당 코드를 실행하고 1를 입력해보자

변수 a에 운영체제와 컴파일러가 0x00000000AC08FF694 라는 주소값을 할당해줬고,

자료형이 int라서 4칸 (4 byte)를 확보한 것을 확인할 수 있다.

 

그렇다면 이론상으로 int 자료형이 저장할 수 있는 범위인 (2^31 - 1) 인 2147483647을 대입해보자.

 

먼저 2147483647를 16진수로 표현해보자.

계산기로 확인해본 결과 2147483647는 16진수로 7FFF FFFF이다.

ff ff ff 7f 형태로 저장되어 있는 것을 확인할 수 있다.

이는 메모리에 리틀 엔디안 방식을 저장하고 있음을 확인할 수 있다.

리틀 엔디안: 바이트 단위로 가장 작은 바이트(LSB)를 먼저 저장하는 방식

 

메모리 공간 주소를  저장하는 포인터

자 그러면 본격적으로 포인터를 사용해보자.

아까 & 연산자는 a의 주소를 나타내는데 사용하였다.

포인터를 사용하여 직접 a의 주소를 포인터에 할당해보자

#include <stdio.h>

int main()
{
    int a; // 변수 a를 정수형(int)으로 선언
    int* p = &a; // 포인터 

    printf("상수 하나를 입력해주세요: ");
    scanf("%d", p); // 사용자가 입력한 정수를 변수 a에 저장 (a의 주소에 값을 저장)
    printf("입력된 값: %d\n", a); // 변수 a에 저장된 값을 출력
    printf("입력된 값: %d\n", *p); // 포인터 p가 가리키고 있는 값을 출력

    printf("코드 끝\n");
    return 0;
}

1을 입력한 결과

포인터인 p가 a의 주소를 가리켜서 a에 값을 저장한다.

*는 포인터 역참조 연산자( Dereference Operator )로 포인터가 가리키고 있는 값을 정상적으로 가리키고 있는 것을 확인할 수 있다. 역참조할 때는 어떤 타입의 포인터인지가 중요하다. 뒤에 설명하겠다.

 

포인터에 타입이 있는 이유

그러면 int의 배열로 선언해보자!

자 이제부터 머리가 슬슬 아프다.

#include <stdio.h>

int main()
{
    int a[3]; // int 3개 크기의 배열 선언
    int* p = &a; // 포인터 

    printf("첫번째 숫자를 입력해주세요: ");
    scanf("%d", p); // 사용자가 입력한 정수를 변수 a에 저장 (a의 주소에 값을 저장)
    printf("두번째 숫자를 입력해주세요: ");
    scanf("%d", p+1); // 사용자가 입력한 정수를 변수 a에 저장 (a의 주소에 값을 저장)
    printf("입력된 값: %d\n", *p); // 변수 a에 저장된 값을 출력
    printf("입력된 값: %d\n", *(p+1)); // 포인터 p가 가리키고 있는 값을 출력

    printf("코드 끝\n");
    return 0;
}

int 3개 크기의 배열을 선언하고 a의 주소값을 int형의 포인터 p에 할당했다.

해당 코드를 실행하고 1과 2를 입력해보았다.

위 내용을 그림으로 표현하면 아래와 같다

꼬아보기

자, 그러면 여기서 조금 더 꼬아서 p의 포인터형을 char*로 바꾸면 어떻게 될까?

아래 상황을 미리 설정하고 해당 상황을 코드로 작성해보자.

주소값에 순서대로 23, 34, 45, 56를 입력하려면 어떡해야할까? 리틀 앤디안을 고려해서 16진법으로 56453423에 해당하는 수를 찾아서 입력하면 된다.

계산기로 16진법으로 56453423 에 해당하는 수는 십진법으로 1447375907임을 확인할 수 있다.

scanf 를 받을 때 %x로 입력을 받으면 16진법으로 바로 입력 받을 수 있다.

리틀 앤디안을 고려해서 순서를 고려해야 한다.

 

코드 작성

#include <stdio.h>

int main()
{
    int a[3]; // int 3개 크기의 배열 선언
    char* p = &a; // 포인터 

    printf("16진법으로 56453423에 해당하는 수를 입력해주세요: "); // 1447375907 입력
    scanf("%d", a); // a를 배열로 선언했을 경우 a는 &a[0]을 의미
    printf("32435465를 입력해주세요: ");
    scanf("%x", &a[1]); // 16진법으로 바로 입력
    printf("첫번째 주소에 입력된 값을 char로 출력: %c\n", *p); // 변수 a에 저장된 값을 출력
    printf("두번째 주소에 입력된 값울 char로 출력: %c\n", *(p+1)); // 포인터 p가 가리키고 있는 값을 출력

    printf("코드 끝\n");
    return 0;
}

결과확인

메모리를 확인해보면 계획했던대로 값이 입력된 것을 확인할 수 있다.

16진법으로 23과 34에 해당되는 값을 확인하면

각각 '#' 과 '4' 인 것을 확인할 수 있다.

포인터의 자료형에 따라 포인터가 이동하는 크기가 달라지는 것을 확인할 수 있었다.

포인터를 선언하고 해당 포인터가 가리키는 주소에 값 할당하기

그러면 자료형 변수를 선언하는게 아니라 바로 포인터를 선언하고 해당 포인터가 가르키고 있는 주소에 값을 바로 넣을 수 있지 않을까?

#include <stdio.h>

int main()
{
    int* p; // 포인터 선언

    printf("상수 하나를 입력해주세요: ");
    scanf("%d", p); // 해당 포인터에 값 넣기
    printf("입력된 값: %d\n", *p); // 포인터가 가르키고 있는 값 출력하기

    return 0;
}

해당 코드의 진행을 보면 정상적으로 진행될 것 같다. int형 포인터를 선언하고 해당 포인터가 가르키는 주소에 값을 입력 값고 그 값을 출력할 것 같다. 하지만 결과는?

초기화되지 않은 변수 p를 사용해 run-time check failure가 발생하였다.

 

int a; 처럼 자료형으로 선언할 경우 해당 자료형에 맞는 메모리 공간이 확보되고 유효한 주소를 갖게 된다. (값은 몰론 초기화 x)

int* p 처럼 포인터를 선언할 경우, 해당 포인터를 저장할 수 있는 메모리 공간은 확보되지만 해당 포인터는 쓰레기 주소값을 가리키고 있기 때문에 유효한 주소를 할당해줘야 한다. 이럴 경우에

 

1. 변수를 선언하고 해당 변수의 주소에 할당해 주거나,

2. malloc을 사용하여 동적으로 메모리 주소를 할당해줄 수 있다. (이경우 메모리 누수가 일어나지 않도록 사용하고 해제해줘야 함)

 

1번은 이미해봤으니 2번으로 해보자.

#include <stdio.h>
#include <stdlib.h>

int main()
{
    int* p = malloc(sizeof(int)); // int 크기만큼 메모리 할당

    printf("상수 하나를 입력해주세요: ");
    scanf("%d", p);
    printf("입력된 값: %d\n", *p);

    free(p); // 동적 메모리 해제
    printf("동적 메모리 해제\n");

    return 0;
}

 

 

실행 결과

정상적으로 포인터 p가 가르키고 있는 주소에 값이 할당된 것을 확인할 수 있다

 

포인터 p를 저장하고 있는 공간은 따로 있으며 해당 주소(&p)에서 가리키는 값이 p의 주소임을 확인할 수 있다.

포인터 p의 크기는 8 bytes인데 이를 통해 운영체제가 64bit임을 알 수 있다.

운영체제가 64bit라는 것은 64bit의 (8bytes)의 메모리 주소를 사용할 수 있음을 뜻한다.

참고로 32bit 운영체제는 32bit의 메모리주소를 사용할 수 있어서 메모리 용량을 2^32 bytes = 4gb까지 사용할 수 있다.

 

free(p)로 동적메모리를 해제한 결과를 확인할 수 있다.

+ Recent posts