# 4.2 포인터

Kal·2021년 7월 21일
0

C언어

목록 보기
9/17

1. 포인터란?

C언어에서 직접 주소 지정 방식은 변수 문법이라고 했다. 그렇다면 간접 주소 지정 방식은 C언어에서 어떻게 표현할까?

간접 주소 지정 방식이란 값을 저장할 ' 주소 ' 를 메모리에 저장하는 것이다.

4바이트 정수형으로 addr 변수를 선언하고 addr 변수에 0x0000006C 값을 대입해 보자.

int addr = 0x0000006C; 주소는 당연하게도 음수가 나올 수 없다, 따라서 unsigned int 자료형을 사용해야 한다.

이렇게 선언한 addr 변수에 주소를 저장할 수는 있지만, 일반 변수이기 때문에 실제로 해당하는 주소의 메모리에 가서 값을 읽거나 저장할 수 있는 기능이 없다.

C언어의 일반 변수는 자신이 위치한 메모리에서만 값을 읽거나 쓸 수 있는 직접 주소 지정 방식으로 동작한다.

이러한 이유로 C언어는 간접 주소 지정 방식으로 동작하는 특별한 변수를 선언하기 위해 포인터(pointer) 문법을 추가로 제공한다.

포인터 변수는 자신이 사용하고 싶은 메모리의 '주소'를 저장하고 있는 메모리이다.

**short *ptr : 
자료형 *(포인터 변수임을 나타내는 기호) ptr(포인터 변수 이름)**

ptr은 포인터 변수의 이름이고 이 변수가 포인터라는 것을 번역기에게 알려주기 위해 ptr 앞에 *을 사용했다. 그리고 자료형을 적는 위치에 short를 사용했다.

그럼 포인터 변수의 크기는 2바이트일까?

포인터 변수는 자료형을 선언하지 않아도 무조건 크기가 4바이트로 정해져 있기 때문에, 포인터 변수의 크기를 적을 필요가 없다.

즉, short는 ptr 변수에 저장된 주소에 저장될 값의 자료형을 의미한다.

1.1 변수가 저장된 메모리 공간의 주소 얻기

프로그램은 실행될 떄마다 사용할 메모리 공간의 주소가 달라진다. 따라서 프로그램에 선언된 변수의 주소는 프로그램이 실행될 떄마다 다른 주소에 할당된다.

따라서 주소를 직접 입력하는 것보다 프로그램 안에 선언한 다른 변수의 주소를 받아와서 사용하는 것이 안전하다. 변수는 해당 프로그램의 메모리 영역에 만들어지기 때문에 다른 프로그램의 메모리 영역에 잘못 접근할 일이 없다.

변수의 주소는 변수 앞에 ' &연산자'를 사용하여 구할 수 있다.

**short birthday; *short형 변수 birthday 선언*
short *ptr; / *포인터 변수 선언* 
ptr = &birthday; *birthday 변수의 주소를 ptr 변수에 대입함***

- 다른 변수의 주소를 사용하여 포인터로 값 대입하기

#include <stdio.h>
void main()
{
   short birthday;
   short *ptr;
   ptr = &birthday;
   *ptr = 0x0412;
   printf ("birthday = %d (0x04%X)\n", birthday, birthday);
}

위 birthday 변수는 선언 후 직접 사용된 적이 없지만, ptr 포인터 변수에 의해 간접적으로 값 0x0412가 대입되었다.

1.2 'ptr='과 '*ptr='의 차이점

'ptr=' 형태로 사용하면 포인터 변수의 값(가리키는 대상의 주소)가 변경되고, 연산자를 붙여서 'ptr=' 형태로 사용하면 ' 포인터가 가리키는 대상'의 값이 변경된다.

'ptr=' 형태는 포인터 변수에 주소를 저장한다.

포인터 변수도 일반 변수처럼 자신을 위한 메모리 공간이 있다. 항상 4바이트로 고정되며 포인터 변수에 저장된 주소는 '포인터가 가리키는 대상 메모리'의 시작 주소를 의미한다.

short *ptr;
ptr = (short 별) 0x000006C; 포인터 변수 ptr에 주소를 직접 대입한다. 

'*ptr=' 형태는 포인터가 가리키는 대상에 값을 저장한다.

short *ptr;
ptr = (short *)0x0000006C; _주소가 변경됨_
*ptr = 0x0412; _포인터가 가리키는 대상에 값이 지정됨_

1.3 다른 함수에 선언된 지역 변수 사용하기

short *ptr;
ptr = &birthday; birthday 변수의 주소를 ptr 변수에 대입
*ptr = 0x0412; _ptr에 저장된 주소에 가서 값을 대입함_

이렇게 포인터를 사용하여 간접 주소 방식으로 값을 대입하는 이유는, 모든 변수가 같은 함수에 선언되는 것이 아니기 때문이다.

포인터는 변수 이름이 아니라 주소로 대상을 찾아가기 때문에 같은 함수가 아니더라도 대상 메모리의 값을 읽거나 쓸 수 있다.

1.4 간접 주소 지정 방식(포인터)으로 다른 함수에 선언한 변수 사용하기

#include <stdio.h> 
void Test(short *ptr)
{
   short soft = 0;
   soft = *ptr;
   *ptr = 3;
}
void main()
{
   short tips = 5;
   Test(&tips); /* ptr = &tips */
}

이렇게 간접 주소 방식을 사용하면 Test 함수를 호출한 main 함수에 선언된 변수의 값만 받아 오는 것이 아니라 해당 변수의 값을 변경할 수도 있다.

1.5 두 변수의 값 서로 바꾸기

프로그래밍을 하다 보면 두 변수의 값을 서로 바꿔야 하는 경우가 생긴다.

- 간접 주소 지정 방식으로 변수 값 교환하기

#include <stdio.h>

void Swap(int* pa, int* pb) /* 각각 start, end 변수의 주소가 지정됨 */

{
   int temp = *pa; /* temp를 이용해서 *pa의 값을 저장 */
   *pa = *pb; 
   *pb = temp;

}

void main()

{
   int start = 70, end = 3;

   printf("before : start = %d, end = %d\n", start, end);
   if (start > end) {
      Swap(&start, &end); /* pa와 pb포인터에 start, end의 주소를 저장함 */
   }
   printf("After : start = %d, end = %d\n", start, end);
}

Swap 함수에서 지역 변수를 사용했다면 잘못된 결과값이 나왔을 것이다. 포인터 변수를 사용하여 이렇게 범함수적 실행을 처리할 수 있다.

return 키워드를 사용해서 반환할 수 있지만, 두 개 이상의 값을 동시에 반환할 수 없기 때문에 포인터를 사용하는 것이 더 효과적이다.

2. 포인터와 const 키워드

포인터를 사용해 값을 사용하거나 대입할 때 *연산자의 사용 여부에 따라 두 가지 표현이 가능하다.

그런데 이 연산자를 빠뜨리거나 잘못 넣은 경우 오류가 발생하게 되는데, 이를 방지하기 위한 키워드가 바로 const 키워드이다.

2.1 const 키워드로 주소 변경 실수 막기

Swap 함수도 마찬가지겠지만 피호출자에서 호풀자로부터 전달받은 주소를 변경하는 경우는 거의 없다.

따라서 주소를 바꾸는 코드가 있다면 대개 프로그래머의 실수이기 때문에 const 키워드를 이용하여 명시적으로 주소가 바뀌는 실수를 막을 수 있다.

void Swap(int *const pa, int *const pb)
{
   int temp = *pa;
   pa = pb; /*pa는 const 변수라서 값을 변경할 수 없기 때문에 오류가 발생*/
   *pb = temp;
}

위 코드와 같이 Swap 함수의 매개변수 형태를 변경하면 둘 다 const 키워드에 의해 번역할 때 오류가 발생한다.

2.2 포인터 변수에서 const 키워드를 사용하는 여러 가지 방법

포인터 변수는 일반 변수와 const 키워드를 사용하는 위치가 다르다.

포인터 변수는 포인터 변수에 저장된 값(주소)를 변경하거나 포인터 변수가 가리키는 대상의 값을 변경하는 두 가지 형태로 사용할 수 있다.

const int * const p; 자료형 앞에 있는 const는 *p로 값을 변경하려면
오류가 발생한다. (대상의 값을 const)
p 앞에 있는 const는 포인터 변수의 값(주소)를 const.

이렇게 const 키워드를 사용할 수 있는 위치가 두 곳이기 때문에, 이 위치를 조합하면 포인터 변수를 세 가지 방법으로 선언할 수 있다.

- int * const p;

p앞에 const 키워드를 사용했다. 따라서 p가 가지고 있는 주소를 변경하면 번역할 떄 오류가 발생한다.

int data = 5, temp = 0;
int *const p = &data;
*p = 3;
p = &temp; /* 오류 발생: 변수 p에 const 속성이 적용되어 p에 저장된
주소를 변경할 수 없다. */

const int *p;

p가 주소에 접근할 때 사용하는 크기 앞에 const 키워드를 사용했기 때문에 *pㄹㄹ 사용하여 대상의 값을 변경하면 번역할 때 오류가 발생한다.

int data = 5;
const int *p = &data;
*p = 3; /* 오류 발생: 변수 p가 가리키는 대상의 값에 const 적용 */

const int * const p;

자신과 대상에 모두 const 키워드를 사용했기 때문에 p가 가진 주소를 바꾸거나 *p를 사용하여 대상의 값을 바꾸면 오류가 발생한다.

int data = 5, temp = 0;
const int * const p; = &data;
*p = 3; /* 오류 발생 : 변수 p가 가리키는 대상에 const속성 적용 */
p = &temp; /* 오류 발생 : 변수 p에 const 속성 적용, 주소 변경 불가 */

3. 포인터 변수의 주소 연산

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

자신이 사용할 메모리의 범위를 기억하는 방법은 크게 두 가지이다.

1. 시작 주소와 끝 주소로 메모리 범위 기억하기
2. 시작 주소와 사용할 크기로 메모리 범위 기억하기

각 방법 모두 총 두 가지 정보를 기억해야 한다.

그런데 C 언어 문법은 메모리를 사용할 때 항상 그 메모리의 크기를 먼저 결정하도록 되어 있다.

정수 값 5를 저장하고 싶다면 int data;와 같이 변수를 선언하는데,
이렇게 사용할 메모리의 크기는 이미 int형으로 선언되어 4바이트로 결정된다.
결국 자신이 사용할 메모리의 크기는 명령문에 포함되어있다는 것이다.

사용할 메모리는 이미 명령문에 포함되어 있기 때문에 자신이 사용할 메모리의 시작 주소만 기억하면 된다.

3.2 포인터 변수의 주소 연산

포인터가 자신이 가리킬 대상 메모리의 시작 주소만 기억하면 되기 때문에 갖게 되는 특성이 있다.

short data = 0;
short *p = &data;
p = p +1; /* 포인터 변수에 저장된 주소 값을 1만큼 증가시킴 */

포인터 변수에 저장된 주소도 정수 값이기 때문에 일반 변수로 연산할 수 있다. 그러나 단순히 주소 값에 1을 더한다는 뜻이 아니고 그 다음 데이터의 주소를 의미한다.

위 예시에서 포인터 변수 p가 가리키는 대상의 크기가 2바이트인데 이 포인터로 다음 데이터를 가리키려면 주소 값이 1이 아닌 2가 증가되어야 정상적으로 그 다음 데이터를 가리킬 수 있따.

이처럼 포인터 변수에 +1을 하면 자신이 가리키는 대상의 크기만큼 증가하는데,
이를 '포인터 변수의 주소 연산' 이라고 한다.

4. 포인터와 대상의 크기

int *p라고 포인터 변수를 선언할 떄 int 자료형은 포인터 변수의 크기를 의미하는 것이 아니라 포인터 변수가 가리키는 대상의 크기를 의미한다.

따라서 포인터 변수 p에 일반 변수 data의 주소 값을 저장하고 포인터 변수 p를 사용하여 data 변수의 값을 변경하는 경우에는 두 변수의 자료형을 같게 지정하는 것이 일반적이다.

- 포인터가 가리킬 수 있는 크기와 실제 대상의 크기가 다른 경우

int data = 0;
short *p = (short*)&data; /* 포인터 변수 p는 2바이트 크기의 메모리를
가리킬 수 있는 능력을 가지고 있지만, 4바이트 크기인 data 변수를
가리킬 수도 있다. */

이러한 포인터의 특성을 잘 활용하면 4바이트 크기의 변수에 저장된 값을 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++
  }
}

출력 : 78, 56, 34, 12,

위 예제는 p가 가지고 있는 주소 값을 옮기는 방식으로 작업했는데, p의 주소 값을 변경하지 않고 data 변수 값을 1바이트씩 출력하고 싶다면 반복문 코드만 다음과 같이 변경하면 된다.

for (i=0;i<4;i++) {
     /* 첫 바이트는 *(p+0), 두 번째는 *(p+1), 세 번째는 *(p+2)... */
     printf("%X,", *(p+1));
 }

5. void* 형 포인터

지금까지는 항상 포인터 변수가 가리키는 대상의 크기를 지정해서 사용했다. 하지만 대상의 크기를 모른다면 어떻게 해야 할까?

void 키워드는 ' 정해져 있지 않다 ' 는 의미를 가지고 있다. 
void *p; /* 포인터 변수 p에 주소 값을 저장할 수는 있지만 해당 변수의
크기는 정해져 있지 않다. */
즉, 사용할 메모리의 시작 주소만 알고 끝 주소를 모를 때 사용하는 포인터 형식
int data = 0;
void *p = &data; /* data의 시작 주소를 저장함 */
*p = 5; /* 오류 발생: 대상 메모리의 크기가 지정되지 않음 */

void* 는 포인터가 가리킬 대상의 크기를 정한 것이 아니라서 말 그대로 어떤 크기의 메모리가 오든지 상관이 없다.

5.1 형 변환 문법 사용하기

포인터 변수 p를 사용하여 data 변수에 값 5를 대입하고 싶은 경우에 형 변환 문법을 사용하여 ' 사용할 크기'를 표기해 주면 일반 포인터처럼 사용할 수 있다.

int data = 0;
void *p = &data;
*(int *) p = 5; /* 형 변환 문법을 사용하여 대상의 크기를 4바이트로 
지정하므로, data 변수에 5가 저장됨 */

5.2 void *형 포인터 활용하기

void*는 포인터의 기능을 사용할 수 없는 것이 아니라 자신이 사용할 대상의 크기 지정을 잠시 미룰 수 있다는 장점을 가지고 있다.

다음은 main 함수에 선언한 지역 변수의 주소 값을 매개변수로 받아서
그 주소에 해당하는 메모리에 값 1을 대입하는 MyFunc 함수이다. 
그런데 MyFunc으로 전달되는 주소의 형식이 char*, short* int*중 하나이고 
이 형식은 사용할 때마다 달라질 수 있따면 어떻게 해야 할까?
void MyFunc(char *p_char, short*p_short, int *p_int)
{
   if(p_char !=NULL) *p_char = 1; /* 포인터 변수에 NULL이 저장되어
   있다면 아직 주소를 저장하지 않고 초기화된 상태이다. */
   else if (p_short != NULL) *p_short = 1;
   else *p_int = 1;
}
void main()
{
   short data = 5;
   /* data 변수는 short형이기 때문에 short*를 사용하는 
   두 번째 매개변수에 주소를 넘겨줌 */
   MyFunc(NULL,&data,NULL);
}

어떤 형식의 주소 값이 전달될지 모르기 때문에 위 예시처럼 세 개의 포인터에만 변수의 주소 값을 전달하고 나머지는 사용하지 않겠다는 뜻으로 NULL을 적는다.

주소의 형식이 더 많아진다면 이 방식은 매우 불편하다.

void*를 사용하여 고쳐보면

void MyFunc(void *p, char flag)
{
   if(flag == 0) *(char*)p = 1; /* flag가 0이면 char* 형*/
   else if(flag ==1) *(short*)p = 1; /* 1이면 short*형 */
   else *(int *)p = 1; /* o과 1이 아니면 int* 형 */
   }
void main()
{
   short data = 5;
   /* data 변수는 short형이기 떄문에 short*를 의미하는 1을 전달 */
   MyFunc(&data,1);
}
void*형 포인터를 매개 변수로 사용하면 어떤 형식의 주소이든 저장할 수 있지만 
3가지 주소 형식 중에서 무엇을 사용했는지 알 수 없다. 
따라서 char*는 0, short*는 1, int*는 2를 의미하는 값을 
매개변수로 함께 전달해야한다. (위 소스코드에서는 flag라고 지었다.)

- void*를 사용하여 대상 메모리의 크기 조절하기

#include <stdio.h>
int GetData(void* p_data, char type)
{
   int result = 0;
   /* type 변수에 저장된 값을 기준으로 형 변환 다르게 함 */
   if (type == 1) result = *(char*)p_data; /* 0x78 저장 */
   else if (type == 2) result = *(short*)p_data; /* 0x5678 */
   else result= *(int*)p_data; /* 0x12345678 */
   return result;
}
void main()
{
   int data = 0x12345678;
   printf("%X\n", GetData(&data, 2));}
   /* data 변수에서 2바이트 크기만 출력함 */
                                           * 출력 결과 : 5678
포인터 변수에서 형 변환 문법은 모든 자료형에서 사용할 수 있다.
int data = 0x12345678;
char *p = (char*)&data; /* 자료형 맞지 않아서 (char*)로 형 변환 */
*p = 5; /* data 변수의 시작 1바이트에 값 5를 넣으면 data는 
0x12345605로 변경됨 */
*(short*)p = 0; /* data 변수의 시작 2바이트에 값 0을 넣으면 data는
0x12340000으로 변경됨 */
profile
프로그래밍 독학

0개의 댓글