[C] 저장해주는 대신 알아야 할 게 있다.

장세민·2022년 8월 30일
0

📝 TIL

목록 보기
16/40

포인터는 주소를 저장하는 일정한 크기의 메모리 공간이다.
즉, 포인터는 언제든지 다른 주소를 저장하거나 포인터끼리 대입할 수 있는 변수라는 것이다.
그러나, 일반 변수와는 달리 대입 연산에 엄격한 기준이 적용된다.

딥하게 배워보자!


📌 주소와 포인터

주소와 포인터의 차이

다음 코드가 실행될 때

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

변수 a의 주소는 100이고, b의 주소는 200으로 프로그램 실행 중에는 값이 바뀌지 않는다.
그러나, 포인터 p는 a, b 중 어떤 주소를 대입하느냐에 따라 변수가 바뀐다.

즉, 주소는 상수, 포인터는 변수라는 것이다.

따라서 두 포인터가 같은 주소를 저장하는 일, 하나의 변수를 동시에 가리키는 일도 가능하다.

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

이런 식으로 말이다.
이 경우 a값을 바꾸거나 연산하는 데 pa와 pb를 모두 사용할 수 있다. ok?


주소와 포인터의 크기

포인터도 저장 공간이므로 크기가 있고,
저장할 주소의 크기가 클수록넓은 범위의 메모리를 사용할 수 있다.
컴파일러에 따라 다를 수 있으나 모든 주소와 포인터는 그 크기가 같다.

어떻게 그걸 단정짓냐?
주소와 포인터의 크기를 알려주는 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;
}

'ch', 'in', 'db'는 각각 변수 자체의 크기는 다르지만 그 시작 주소 값의 크기는 모두 같다.

따라서 포인터도 가리키는 자료형과 상관없이 모두 크기가 같다.
물론 포인터에 간접 참조 연산자를 사용하여 가리키는 변수의 크기를 출력하면 각각 다른 결과가 나온다.

우리가 기억해야 할 건 딱 하나.

모든 주소와 포인터는 가리키는 자료형에 관계없이 크기가 같다.


📌 포인터의 대입 규칙

포인터는 크기가 모두 같으므로 대입 연산을 쉽게 생각할 수 있지만,
세 가지 규칙에 따라 제한적으로 사용해야 한다.

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

  1. # include <stdio.h>
  2.  
  3. int main(void)
  4. {
  5. int a = 10; // 변수 선언과 초기화
  6. int *p = &a; // 포인터 선언과 동시에 a를 가리키도록 초기화
  7. double *pd; // double형 변수를 가리키는 포인터
  8.  
  9. pd = p; // 포인터 p값을 포인터 pd에 대입
  10. printf("%lf\n", *pd); // pd가 가리키는 변수의 값 출력
  11.  
  12. return 0;
  13. }

6행의 변수 'p'와 7행의 'pd'는 모두 포인터지만 컴파일러는 p에 저장된 값을 int형 변수로,
pd에 저장된 값을 double형 변수의 주소로 생각한다.

따라서 'a'에 할당된 메모리 영역을 넘어 'pd'를 통해 double형 변수로 생각하고
8바이트 안에 있는 값을 실수 값으로 해석하여 알 수 없는 결과를 출력한다.

이처럼 가리키는 자료형이 일치하지 않는 포인터의 대입을 시도하면 컴파일러의 대답은?

형식 호환 에러 메세지를 내보낸다.


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

가리키는 자료형이 일치하지 않으면 포인터 대입은 안되는 걸까?
이전에 배웠던 느낌이 나는 바로 그 방법을 사용하면 된다.

double a = 3.4;		// double형 변수 선언
double *pd = &a;	// pd가 double형 변수 a를 가리키도록 초기화
int *pi;		// int형 변수를 가리키는 포인터
pi = (int*) pd;		// pd값을 형 변환하여 pi에 대입
 

형 변환!

여기서 pi에 간접 참조 연산을 수행하면 변수 a의 일부를 int형 변수처럼 사용할 수 있다.
만약

*pi = 10;
 

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


여기서 잠깐

형 변환 연산자를 사용하면 컴파일 경고나 에러 없이 원하는 정수 값을 포인터에 대입하여 사용할 수 있다.
그렇다면 포인터에 100번지를 직접 대입하는 것은 가능할까?

int *p;
p = (int*) 100;		// 100을 int형 변수의 주소로 형 변환하여 p에 대입
*p = 10;		// 100번지부터 103번지까지 4바이트의 공간에 10 대입

이 문장들은 컴파일 과정에서 전혀 문제될 것이 없지만,
메모리 100번지부터 103번지까지가 어떤 용도로 사용되는 영역인지 알 수 없어서
프로그램을 실행할 때 문제를 일으킬 가능성이 크다.

따라서, 포인터는 항상 정상적으로 할당받은 메모리 공간의 주소를 저장해서 사용해야 한다.
같은 이유로 포인터를 왜 항상 초기화 해주는 지 이해하면 될 것이다.


📌 포인터를 사용하는 이유

포인터를 사용하려면 추가적인 변수 선언을 하고 주소 연산, 간접 참조 연산 등 각종 연산을 해야하는
번거로움이 따른다. 또, 배우는 과정에서 매우 헷갈리기 쉽다.

근데 왜 쓰는거냐!
임베디드 프로그래밍을 할 때 메모리에 직접 접근하는 경우나 동적 할당한 메모리를 사용하는 경우에는 포인터가 반드시 필요하다.

profile
분석하는 남자 💻

0개의 댓글