포인터란 간단히 말해 메모리 주소를 저장하는 공간 이다.
int var = 10;
// * : 포인터
// & : 변수의 주소
int* pVar = &var; // int* : 정수형 포인터 선언, 정수형 포인터 변수 pVar에 var의 주소를 담는다
#include<stdio.h>
void main()
{
int var = 10;
int* pVar = &var;
printf("var: %d\n", var);
printf("*(&var): %d\n", *(&var) );
printf("&var: %p\n", &var );
*pVar = 100;
printf("pVar: %p\n", pVar);
printf("*pVar: %d\n", *pVar);
printf("var: %d\n", var);
}
*을 변수 뒤에 붙인다는 것은 해당 주소가 가리키는 값을 표시하겠다는 의미 이다.
printf("&pVar: %p\n", &pVar);
printf("pVar Size: %d byte\n", sizeof(pVar));
포인터로 선언하여 주소를 담고 있는 변수도 본인의 주소가 따로 있으며
포인터의 크기는 8byte이다.
포인터의 크기가 8byte라면
다른 자료형을 선언한 포인터여도
결국 그 값은 메모리 주소 값이니 자료형의 구분이 의미 없는 것은 아닐까?
double d = 3.14;
double* pD = &d;
int* pI = &d;
printf("pD: %p\n", pD);
printf("pI: %p\n", pI);
printf("*pD: %lf\n", *pD);
printf("*pI: %lf\n", *pI);
printf("double형변환 pI: %lf\n", *((double*)pI)); //double형 포인터로 형변환
메모리 공간을 찾아갔을때 그 공간을 어떻게 읽을 것인지 결정하는 것이 앞에 선언한 자료형 이다.
메모리 주소는 같은 주소를 저장 할 수 있어도
그 값을 읽어내기 위해서는 같은 자료형의 선언이 필요함을 알 수 있다.
모든 포인터는 정말로 8byte일까?
printf("char* Size: %d byte\n", sizeof(char*));
printf("short* Size: %d byte\n", sizeof(short*));
printf("int* Size: %d byte\n", sizeof(int*));
printf("long* Size: %d byte\n", sizeof(long*));
printf("long long* Size: %d byte\n", sizeof(long long *));
printf("float* Size: %d byte\n", sizeof(float*));
printf("double* Size: %d byte\n", sizeof(double*));
printf("long double* Size : %d byte\n", sizeof(long double*));
정말로 8byte이다
모든 포인터 자료형 변수는 크기가 8byte로 같다 단순히 주소를 저장하는 공간이기 때문이다.
자료형이 필요한 것은 그 메모리 공간을 읽기 위한 것일 뿐이다.
과거에는 포인터가 모두 4byte였으나(32bit 체계) 지금은 8byte(64bit 체계)를 지니고 있다.
옛날에는 32bit 운영체제에 맞게 컴파일도 x86으로 진행되었기에 포인터 변수의 크기가 4byte로 나왔으나
지금은 64bit 운영체제에 맞게 컴파일을 x64로 진행중이다.
그런데 여기서 의문점이 드는게 하나 있다.
과거 적은 글중에 32bit 운영체제에 맞춰 연산의 기본단위라고 할 수 있는 int형의 크기가 4byte라고 올린 적이 있다.
int는 4byte인데 왜 포인터는 8byte? 64bit운영체제에 맞게 int도 8byte가 되어야 맞지 않을까?
처리할 수 있는 주소 양은 8byte가 맞지만
지금 현재 컴퓨터는 64bit를 사용하더라도 32bit때의 프로그램들이 구동되는 구조라는 것을 기억해둘 필요가 있다.
그렇기에 연산의 기본단위라고 할 수 있는 int는 현재 4byte의 크기를 기본으로 가지는 것이다.
8byte의 크기를 기본으로 가지게 된다면 버그가 날 수 있기 때문이다.
따라서 이 제한을 해제하기 위해서는 __int64 라는 64bit 즉, 8byte의 크기를 가지는 int형을 선언 할 수 있게 만들어 놨다.
본인의 개발환경에 맞춰서 활용할 수 있도록 하자
용량에 대한 이야기를 하니.. 포인터를 사용할때 의문점이 하나 있을 수 있는데
char형은 1byte의 크기를 가지고 있다
그런데 이 char형 하나의 주소를 담아내고자 8byte를 쓴다는 것이다
1byte를 위해 사용하는 8byte?
비효율 아닐까?
그렇지 않다.
지난 시간에 올린 배열이 그 내용의 핵심이다.
만약 char c[100] 이라는 배열을 선언한다면 이 배열의 크기는 100byte이다.
하지만 이 배열을 기억하기 위한 주소는 8byte면 충분하다
왜?
c[0]의 주소 = c의 주소이기고 주소와 자료형을 알 수 있다면 c[0]에서 c[99]까지의 주소 역시 알 수 있기 때문이다.
만일 이렇게 주소를 기억하는 방식이 없었다면 이 100개의 주소를 일일히 기억하기 위해 용량의 낭비가 심했을 것이다.
이중 포인터
float f = 3.14f;
float* pF = &f;
float** ppF = &pF;
//float** ppF = &f;
//두번의 주소 참조를 해야하는데 바로 f의 주소를 넣은경우 1번 참조 후 참조 할 것이 없기에 프로그램이 터짐
**ppF = 1.2345f;
printf("f: %f\n", f);
printf("f: %p\n", &f);
printf("*pF: %f\n", *pF);
printf("pF: %p\n", pF);
printf("&pF: %p\n", &pF);
printf("ppF: %p\n", ppF);
** 포인터를 두번 선언하는 것으로
기존의 포인터가 변수의 주소를 기억하는 것 이라면
이중 포인터는 변수의 주소를 기억중인 포인터의 주소를 기억하는 것이다.
주의 사항으로는 이중 포인터를 사용 했다면
꼭 두번의 주소 참조가 필요하다는 것이다.
한번만 주소를 참조하게 된다면 컴퓨터는 추가적인 주소 참조를 위해 메모리 탐색에 들어가게 되고 거기서 프로그램이 터지게 된다. 꼭 주의하자
포인터 변수 초기화
long double* pLd = 0;
long double* pLd = NULL; //NULL에서 F12를 눌러보자
포인터의 초기화는 0이 기본이다. 이는 잘못된 주소나 초기화를 안하는 것을 방지하기 위해서 하는 것이다.
메모리에서 0이란 주소에서는 무언가를 하지는 않기 때문에 가장 안전한 주소이다.
코드에는 0과 NULL 이 있는데 왠만하면 NULL로 초기화를 하도록 하자
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
다음과 같이 NULL은 0과 같은 의미를 지니고 있지만 용어상 NULL로 초기화를 하는 것이 좋다.
지역 변수에서 포인터 사용시 주의 점
long double* pLd = NULL;
{
long double ld = 3.14L;
pLd = &ld;
*pLd = 1.2345L;
printf("ld: %f\n", ld);
printf("*pLd: %f\n", *pLd);
pLd = NULL; //사용 한 후에도 NULL로 초기화 하는 습관을 들이자
}
*pLd = 1.235L;
printf("pLd: %p\n", pLd);
printf("*pLd: %f\n", *pLd);
터지는 코드다.
변수 ld는 지역변수 이기에
{ }에서 사용 된 후 메모리를 반환한다
그 후 포인트 변수 pLd를 NULL로 초기화 시킨다고 해도
이후 1.235L이라는 값을 담을 변수가 없기에 프로그램이 터지는 것이다.
지역변수에서 활용을 한다면 이점을 꼭 주의하자
포인터 활용시 참고 할 사항
int i = 123;
int* ptrI = &i;
int* const ptrI = &i; //다른 주소가 들어오는 것을 막음
const int* ptrI = &i; // 다른 값으로 바꾸는 것을 막음
const int* const ptrI = &i; // 양쪽 다 막음
주소나 값을 고정 시켜야 할 상황이 올때 참고하고 활용 하자
포인터 활용, 메모리 주소 연산
int arr[3] = { 1,2,3 };
int* pArr = arr;
//메모리 주소 연산
printf("pArr: %p\n", pArr);
printf("pArr + 1 : %p\n", pArr + 1);
printf("*(pArr + 1) : %d\n", *(pArr + 1));
printf("&pArr[1]: %p\n", &pArr[1]);
printf("*(pArr + 2): %d\n", *(pArr+2));
printf("pArr[2]: %d\n", pArr[2]);
printf("*pArr + 2: %d\n", *pArr + 2);
arr[3]은 [0][1][2] 이렇게 3개의 메모리 공간을 가지고 있다.
pArr은 arr의 주소를 담고 있고 그 말은 pArr은 arr[0]의 주소를 가지고 있는 것과 같다
int형 포인터 변수 pArr 에 1을 더하게 되면 int형인 4byte만큼 더해지게 된다 즉 arr[1]의 주소를 나타내는 것이다.
이를 활용해 포인터 변수를 통해서도 원하는 배열의 주소를 탐색할 수 있다.
*(pArr + n) 과 *pArr + n은 다르다
*(pArr+n)은 그 메모리 공간을 탐색하는 것이고
*pArr+n은 arr[0]의 값을 +n 하는 것이니 사용에 있어서 혼동이 있어선 안된다.
'프로그래밍 언어 > C' 카테고리의 다른 글
문자열(String) (0) | 2024.08.14 |
---|---|
함수(Function) (0) | 2024.08.13 |
배열(Array) (0) | 2024.08.12 |
반복문(Loop) (0) | 2024.08.09 |
조건문 (0) | 2024.08.09 |