포인터

신승준·2022년 4월 30일
0
post-custom-banner

1042라는 십진수를 102(십진수)라는 주소에 대입하라.

  1. 1042를 2진수로 보면, 이 값은 8비트 하나에 모두 저장할 수 없다. 따라서 두 개의 8비트에 나눠서 들어가게 된다.

  2. 메모리에 저장된 데이터를 표시할 때 16진수로 하는데, 이는 2진수에 가장 가까운 표현법이다. 1바이트는 8비트, 혹은 4비트 2개라고 할 수 있으며 이러한 4비트는 2의 4승으로 0부터 15까지, 16진수 1개를 나타낼 수 있다.즉 16진수 1개가 나타낼 수 있는 '한 자리수'와 같다. 이때 0x는 그저 10진수와 헷갈리는 것을 방지하기 위해 붙이는 것이다.

  3. 따라서 그림과 같이 0x04(10진수로 4), 0x12(10진수로 18)로 표현된다. 주소 또한 메모리에 할당될 수 있기 때문에 16진수로 표현한다.

  4. 0x0412값을 0x00000066 주소에 2바이트 크기로 대입하라를 어셈블리로 보면

mov word ptr[00000066h], 0412h

어셈블리에서는 16진수의 0x를 뒤의 h(hexadecimal)로 표현한다. 여기서 word는 2바이트 단위로 처리하겠다는 것이다.


  1. 위를 역으로, c언어로 봤을 땐
short birthday; 		// birthday가 메모리 0x00000066에 위치한다고 가정.
birthday = 0x0412; 		// mov word ptr[00000066h], 0412h로 번역(컴파일)된다.

어셈블리와는 다르게 c언어가 메모리 주소 대신 변수라는 개념을 사용하는 이유는, 코드를 더 쉽게 이해할 수 있기 때문이다. 이는 c언어, 혹은 프로그래밍 언어(사람에 가까운 언어)가 개발된 이유라고 볼 수 있을 듯 하다.

C 언어에서 변수 이용의 한계


tips는 main이라는 함수에서 지역 변수로 사용되기 때문에, Test라는 함수 내에서 사용할 수 없다. 메모리에서의 주소가 다름에도 불구하고 참조를 할 수 없는 것이다.





간접 주소 지정 방식

메모리에 주소값이 대입되므로 메모리를 더 사용하게 된다. 직접 주소 지정 방식은 c언어에서 변수를 이용하고, 간접 주소 지정 방식은 포인터를 이용하고 있다.

int addr = 0x0000006C;

일반 변수도 주소를 저장할 수 있으나, 저장된 주소의 메모리에 가서 값을 읽거나 저장할 수 있는 기능은 없다.

포인터

  • 자신이 사용하고 싶은 메모리의 '주소'를 저장하고 있는 '메모리'가 포인터이다.
  • 포인터 변수는 자료형을 선언하지 않아도 주소를 저장하기 때문에 무조건 크기가 4바이트이다.

    이 때 자료형은 포인터가 가르키는 대상의 크기이다.
short birthday;			// 2byte 크기의(short형) 변수 birthday 선언
short *ptr;				// 2byte 크기의 대상을 가르킬 수 있는 포인터 ptr 선언
ptr = &birthday;		// 변수 birthday 주소를 계산해서 ptr에 알려달라. 
						// 컴파일하면 birthday 값이 아니라 birthday의 주소가 ptr에 들어가게 된다.
						// 즉 0x0000006C라고 직접 적지 않는다. 주소가 바뀌면 문제가 될 수 있기 때문이다.
#include <stdio.h>

// 포인터는 메모리를 효율적으로 이용하는 도구이다.
void main() 
{
    short birthday;
    short *ptr;
    ptr = &birthday;
    
    // %p 형식은 메모리 주소를 16진수 형태로 출력한다.
    printf("birthday 변수의 주소는 %p입니다.\n", ptr);
}

ptr에 직접 0x0000006C라고 적어도 되지만, 프로그램이 실행될 때마다 주소는 바뀐다. 따라서 프로그램 실행 시 우연히 0x0000006C에 어떤 값이 저장되어 있을 수 있다. 이러면 문제가 생긴다. 따라서 주소를 직접적으로 적는 것은 좋지 않다.

#include <stdio.h>

void main() {
    short birthday;             // short형 변수 birthday 선언
    short *ptr;                 // 포인터가 가리키는 대상의 크기가 2byte인 포인터 변수 선언
    ptr = &birthday;            // birthday 변수의 '주소'를 자기 자신인 ptr 에 대입
    *ptr = 1042;                // ptr에 저장된 주소(ptr이 가리키는 대상) 에 가서 값 1042를 대입, 즉 birthday = 1042
    
    printf("%d", birthday);
}

ptr 자기 자신한테 값을 대입할 때는 * 없이, ptr이 가리키는 대상에 값을 저장할 때는 *을 붙인다. 이렇듯 포인터는 *를 붙이고, 혹은 붙이지 않고 사용할 수 있다.

#include <stdio.h>

void main() 
{
    short birthday;
    short *ptr;
    ptr = &birthday;
    *ptr = 0x0412;
    
    printf("birthday = %d\n", birthday);    
}

birthday에 직접적으로 값을 입력하지 않았음에도 불구하고, birthday에 1042이라는 10진수가 제대로 들어간 것을 확인할 수 있다.

포인터를 사용하여 간접 주소 방식으로 값을 대입하는 이유

모든 변수가 같은 함수에 선언되어 있는 것이 아니기 때문이다. c언어는 함수와 함수가 단절되어 있다. 서로의 안에 선언된 일반 변수를 서로 참조할 수 없다. 허나 포인터 변수는 다른 함수에 선언된 변수의 값을 읽거나 변경이 가능하다.

start와 end 값 서로 바꾸기

  • 직접 주소 지정 방식 사용 시

end에 start를 대입하는 순간, end와 start 모두 96이 되어버린다. 따라서 start에 5를 대입할 수 없게 된다.

이를 보완하기 위해서는 temp라는 변수(공간)이 더 필요하다.

#include <stdio.h>

void Swap(int a, int b)
{
    int temp = a;
    a = b;
    b = temp;
}

void main()
{
    int start = 96, end = 5;
    printf("before : start = %d, end = %d\n", start, end);
    
    if (start > end) {
        Swap(start, end);
    }
    
    printf("after: start = %d, end %d\n", start, end);
}

하지만 프로그램 실행 시 여전히 after에서 start와 end 값이 변하지 않은 것을 확인할 수 있다. 이는 Swap 함수 안에서만 서로 교환 되었을 뿐이기 때문이다. main 함수에서는 바뀐 start와 end를 참조하지 못하는 것이다.

  • 간접 주소 지정 방식 사용 시
#include <stdio.h>

void Swap(int *pa, int *pb)
{
    int temp = *pa;     // *pa(start) = 96, *pb(end) = 5
    *pa = *pb;          // *pa(start) = 5, *pb(end) = 5
    *pb = temp;         // *pa(start) = 5, *pb(end) = 96
}

void main()
{
    int start = 96, end = 5;
    printf("before : start = %d, end = %d\n", start, end);
    
    if (start > end) {
        Swap(&start, &end);
    }
    
    printf("after : start = %d, end = %d\n", start, end);
}

위와 같이 포인터를 활용하여 swap 기능을 제대로 실현할 수 있다.


포인터를 사용하며 자주 발생하는 실수들

pa = pb;
*pa = pb;

위의 경우 데이터 타입이 다르기 때문에(포인터 or int형) 컴파일 에러가 난다.

int temp = *pa;	// *pa(start) = 96, *pb(end) = 5
pa = pb;		// pb에 저장된 주소가 pa에 복사되어 pa도 end를 가리킴
*pb = temp;		// *pa(end) = 96, *pb(end) = 96

위의 경우 pa, pb 모두 end를 가르키게 된다. 컴파일 에러가 나지 않기 때문에 코드가 복잡해지면 에러를 찾아내기 어렵다.

이러한 실수를 막기 위해 const 키워드를 사용하기도 한다.

void Swap(int * const pa, int * const pb)
{
	int temp = *pa;
    pa = pb;		// pa는 const 변수이기 때문에 변경이 불가능하여 오류 발생
    *pb = temp;
}

pa라는 포인터의 값, 즉 주소는 const로 인해 바뀔 수 없다.


const int * const pa;

위의 경우는 pa(pa의 값으로 주소)와 *pa(pa가 가리키는 대상) 모두 변경될 수 없다.


const int *pa;

*pa(pa가 가리키는 대상)은 변경될 수 없다.


이 모든 경우, 당연히 처음 초기화(선언)될 때에는 오류가 나지 않는다.

int data = 5, temp = 0;
int * const pa = &data;	// 처음 초기화(선언)되는 부분으로 오류가 발생하지 않는다.
*pa = 3;
pa = &temp; 			// 오류 발생




포인터 변수의 주소 연산

사용할 메모리의 범위를 기억하는 방법

시작 주소와 끝 주소를 기억하는 것과 시작 주소와 사용할 크기를 기억하는 것이 있다.

  • 시작 주소와 끝 주소 기억

시작 주소와 끝 주소 2개를 기억하려면 4바이트씩 총 8바이트가 필요하다. 따라서 시작 주소와 사용할 크기를 이거하는 방법으로 포인터를 사용하고 있다.

int data = 0;
int *p = &data;		// 시작 주소인 &data에서 4바이트 크기만큼 사용하겠다.
*p = 5;

포인터 변수의 주소 연산

short data = 0;
short *p = &data;
p = p + 1;

위의 경우, data 주소에 0이라는 값이 대입된다. 그리고 포인터 p가 선언되고 이 p는 data의 시작 주소를 가르키게 된다. 그리고 p가 가르키는 대상에 가서 작업을 할 때 2byte 크기만큼만 쓰게 된다. 즉 p에는 주소값이 대입되고, *p는 data의 시작 주소를 가르키게 된다.

여기서 +1을 하면 산술 연산으로 1이 증가하는 것이 아니라, 포인터가 가르키는 데이터의 다음 데이터를 가르키게 되는 주소 연산을 수행하게 된다.

  • 포인터 주소 연산 : 포인터 변수가 가진 주소를 연산하면 자신이 가리키는 대상의 크기만큼 연산
    여기서는 short이므로 +1을 하면 포인터는 short만큼의 다음 데이터를 가르키게 된다.

주소에 +1을 하면 100 주소에서 101 주소로 가는 것이 아니라 102 주소로 가는 것이다. +2를 하면 104 주소로 가게 된다.

만약 다음과 같이

int *p = &data;

int 형이었다면, +1 주소 연산 시 100 주소에서 102 주소가 아니라 104 주소로 가게 된다.

#include <stdio.h>

int main()
{
    // p1, p2, p3, p4에 강제로 100이라는 주소를 저장
    char *p1 = (char *)100;
    short *p2 = (short *)100;
    int *p3 = (int *)100;
    double *p4 = (double *)100;
    
    p1++; // 101
    p2++; // 102
    p3++; // 104
    p4++; // 108
    
    printf("%p %p %p %p", p1, p2, p3, p4);
}

추가로, 포인터 변수가 가르키는 대상의 자료형과, 포인터 변수 선언 시 적는 자료형을 동일하게 지정하는 것이 일반적이다.

int형 변수에 저장된 값을 1바이트 단위로 출력하기

#include <stdio.h>

void main()
{
    int data = 0x12345678, i;
    char *p = (char*)&data;
    
    // 4바이트 데이터를 바이트 단위로 값 출력 4번 반복
    for (i = 0; i < 4; i++) {
        printf("%x, ", *p);		// p가 가진 주소부터 1바이트 크기만큼 사용
        p++;					// 1바이트 만큼 증가
    }
}

대상의 크기가 정해져 있지 않은 void *형 포인터

  • void 키워드 : 정해져 있지 않다.
  • 포인터 변수가 가르키는 대상의 크기를 모를 때 사용한다.
  • 즉 사용할 메모리의 시작 주소만 알고 끝 주소를 모를 때 사용한다.
int data = 0;
void *p = &data;		// data의 시작 주소를 저장한다. 캐스팅 없이, 경고 없이 다 받을 수 있다.
*p = 5;					// 이 때는 오류가 난다. 대상 메모리의 크기가 정해져 있지 않기 때문

자기가 얼마만큼의 크기로 작업해야 될지 모르기 때문에(끝 주소를 모르는 상황이기 때문에) 오류가 난다.

따라서

int data = 0;
void *p = &data;
*(int *)p = 5;		// 대상의 크기를 4바이트로 정함.

받을 때는 편하나 쓸 때는 캐스팅으로 타입 변환을 해줘야 한다. 받을 때는 편하지만 쓸 때는 불편하다. 해당 프로그램을 쓰는 사람에게는 편리함을 제공한다.

profile
메타몽 닮음 :) email: alohajune22@gmail.com
post-custom-banner

0개의 댓글