다양한 자료형의 포인터 선언
const int num = 10; // int형 상수
const int *numptr; // int형 상수를 가리키는 포인터
// int const *numptr과 같은 의미다.
numptr = #
*numptr = 20; // 컴파일 에러 발생. num이 상수이므로 역참조하여 값을 변경할 수 없음.
위 코드들에서는 'int형' 포인터들을 선언하였다.
하지만 포인터에는 그보다 훨신 다양한 자료형들이 있는데, 심지어 'void'형 자료형도 있다.
#include <stdio.h>
int main()
{
long long *numptr1; // long long형 포인터 변수 선언
float *numptr2; // float 형 포인터 변수 선언
char *cptr; // char 형 포인터 변수 선언
long long num1 = 10;
float num2 = 3.14f;
char c = 'A';
numptr1 = &num1; // 포인터 변수 numptr1에 num1의 주솟값 저장
numptr2 = &num2; // 포인터 변수 numptr2에 num2의 주솟값 저장
cptr = &c; // 포인터 변수 cptr에 c의 주솟값 저장
retuen 0;
}
위 코드에서도 보았다 싶이 자료형마다 각각의 포인터 변수들을 선언할 수 있다.
조금 더 확장하자면, C에서 사용할 수 있는 모든 자료형으로 포인터 변수를 선언할 수 있다.
각각의 자료형 포인터의 존재 이유?
하지만 왜 이렇게 굳이 자료형마다 포인터를 선언하게 만들었을까?
근본적인 이유는, 바로 어떤 자료형으로 포인터 변수를 선언하느냐에 따라서 메모리 접근 방법이 달라지기 때문이다.
int num;
가령, int형 변수를 선언하였다고 가정하자.
sizeof(num);
자료형 int는 4byte의 크기를 메모리 상의 공간에 정적할당한다.
int *numptr = #
sizeof(*numptr);
그렇다면 int형 포인터 변수를 사용하여 역참조한다면 어떻게 될까?
printf("%d", sizeof(num));
printf("%d", sizeof(*numptr));
위 두 출력 코드는 동일한 '4'라는 값이 출력된다.
여기서 알 수 있는 것은 포인터 변수들의 자료형은 선언한 자료형의 크기에 맞춰서 값을 가져오거나(참조하거나) 저장하게 된다.
변수 num은 int형으로 선언하였고, int형의 메모리상의 할당 크기는 4byte이다.
마찬가지로 numptr는 int 포인터형으로 선언하였고, 참조 혹은 저장할 때 할당받는 메모리상의 크기가 4byte이다.
또한 역참조를 할 때에도 참조 크기가 4byte임을 위 코드에서 알 수 있다.
void 포인터의 선언
위 포인터들은 모두 자료형이 존재하는 포인터이다.
long long *numptr1 // long long형 포인터
float *numptr2 // float형 포인터
하지만 자료형이 정해지지 않은 포인터도 존재한다.
void *numptr // 자료형이 정해지지 않은(void) 포인터
기본적으로 C와 C++은 자료형이 다른 포인터끼리 메모리 주소를 저장하는 컴파일 경고가 발생한다.
하지만 void 포인터는 자료형이 정해지지 않았다는 특성 덕분에, 어떠한 자료형으로된 포인터들과도 대응될 수 있다.
반대로 다양한 자료형으로 된 포인터에도 void 포인터를 저장할 수 있다.
int *numptr = #
char *cptr = &c;
void *ptr; // void 포인터의 선언 방식
// 저장되는 포인터의 자료형이 달라도 컴파일 경고가 발생하지 않음
ptr = numptr; // void 포인터에 int 포인터 저장
ptr = cptr; // void 포인터에 char 포인터 저장
// 저장하는 포인터의 자료형이 달라도 컴파일 경고가 발생하지 않음
numptr = ptr; // int 포인터에 void 포인터 저장
cptr = ptr; // char 포인터에 void 포인터 저장
이러한 특성 덕분에 'void 포인터' 는 '범용 포인터' 라고도 한다.
즉, 직접 자료형을 변환하지 않더라도 암시적으로 자료형이 변환되는 방식이다.
void 포인터의 역참조
하지만 void 포인터는 자료형이 정해지지 않았으므로 값을 가져오거나 저장할 크기도, 참조할 크기도 정해지지 않는다.
즉, void 포인터는 역참조가 불가능하다.
ptr = numptr; // void 포인터에 int 포인터 저장
printf("%d", *ptr); // void 포인터는 역참조할 수 없음. 컴파일 에러
ptr = cptr; // void 포인터에 char 포인터 저장
printf("%c", *ptr); // void 포인터는 역참조할 수 없음. 컴파일 에러
역참조를 할 때 접근해야할 메모리 크기가 정해지지 않았기 때문이다.
void 포인터의 사용 이유?
그럼 왜 역참조가 불가능한 void 포인터를 사용하는 것일까?
나도 잘 모르겠다
더 공부해야지 히히
상수(const)와 포인터
포인터에도 const 키워드를 사용할 수 있다.
하지만 const 키워드의 위치에 따라 의미가 달라진다.
먼저 상수를 가리키는 포인터이다.
const int num = 10; // int형 상수
const int *numptr; // int형 상수를 가리키는 포인터.
// int const *numptr도 같은 의미임.(const의 위치만 다름)
numptr = #
*numptr = 20; // 컴파일 에러 발생. num1이 상수이므로 역참조로 값을 변경할 수 없음
num이 const int형이므로 이 변수의 주소를 넣을 수 있는 포인터 역시 const int *로 선언해야 한다.
num은 상수이므로 역참조하여 값을 조작하려고 시도한다면 컴파일 에러가 발생한다.
int num1 = 10; // int형 변수
int num2 = 20; // int형 변수
int * const numptr = &num1; // int형 포인터 상수 선언 및 초기화
numptr = &num2; // 컴파일 에러. 포인터(메모리 주소)를 변경할 수 없음
numptr에 num1의 주솟값이 저장된 상태에서 num2의 주솟값으로 초기화를 시도한다면 컴파일 에러가 발생한다.
numptr은 포인터 자체가 상수로 선언되었기 때문에 최초 초기화 값 이외에 다른 주솟값을 대상으로 초기화를 시도할 수 없다.
즉, 위 코드의 numptr(상수 포인터)은 최초 초기화값 이외의 메모리 주소로 변경할 수 없게된다는 것이다.
앞서 보여준 두 개의 코드에서 알 수 있었던 개념을 합친 코드는 다음과 같다.
const int num1 = 10; // int형 상수
const int num2 = 20; // int형 상수
const int * const numptr = &num1; // int형 상수를 가리키는 포인터 상수
// int const * const numPtr도 같음
*numptr = 30; // 컴파일 에러. num1이 상수이므로 역참조로 값을 변경할 수 없음
numptr = &num2; // 컴파일 에러. 포인터(메모리 주소)를 변경할 수 없음
마무리
그럼 간단하게 상수와 포인터를 사용한 예제를 해석해 보며 포스팅을 마치도록 하겠다.
- const int num = 10;
→ 변수 num을 10으로 상수화하겠다. - const int * ptr1 = &val1;
→ 'const int 형' 포인터 ptr1에 val1(상수)의 주솟값으로 초기화 - int * const ptr2 = &val2;
→ 'int 형' 상수 포인터 ptr2를 val2의 주솟값으로 초기화 - const int * const ptr3 = &val3;
→ 'const int 형' 상수 포인터 ptr3에 val3(상수)의 주솟값으로 초기화
참고 및 출처
-
코딩 도장