이전 글에서 잠깐 다뤘던 포인터(pointer)에 대해 더 자세하게 쓰는 글입니다. 밑은 예제 코드.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
|
#include <stdio.h>
void main()
{
// 포인터(Pointer): 메모리 주소 공간 기억
int var = 10;
// *: pointer
// &: 변수의 주소
int* pVar = &var;
printf("var: %d\n", var);
printf("&var: %p\n", &var);
printf("pVar: %p\n", pVar);
*pVar = 100;
printf("*pVar: %d\n", *pVar); //var와 같은 주소 위치이기 때문에 10
printf("var:%d\n", var); //같은 주소의 원본도 값이 바뀜
printf("\n");
printf("&pVar: %p\n", &pVar); //&를 통해 pVar의 주소 크기를 나타냄.
printf("pVar Size: %d byte\n", sizeof(pVar)); //공간의 크기
printf("\n");
double d = 3.14f;
double* pD = &d;
int* pI = &d;
printf("pD: %p\n", pD);
printf("*pD: %lf\n", *pD);
printf("pI: %p\n", pI);
printf("*pI: %lf\n", *(double*)pI);
printf("\n");
printf("char* Size: %zu byte\n", sizeof(char*));
printf("short* Size: %zu byte\n", sizeof(short*));
printf("int* Size: %zu byte\n", sizeof(int*));
printf("long* Size: %zu byte\n", sizeof(long*));
printf("long long* Size: %zu byte\n", sizeof(long long*));
printf("float Size: %zu byte\n", sizeof(float*));
printf("double* Size: %zu byte\n", sizeof(double*));
printf("double long* Size: %zu byte\n", sizeof(double long*));
//포인트는 배열에서 많이 사용하는 이유.
//주소는 어떠한 자료형을 쓰든 8바이트.
//1. int Sheet[200] = {0}; 이라는 배열은 int의 4바이트 x 200을 해서 800바이트를 소비
//2. 만약 다른 배열로 위의 Sheet 배열을 복사 붙여넣기를 할 때 똑같이 800바이트를 소비
//3. 포인터를 이용하면 8바이트만을 사용하여 주소만 알려주는 용도
printf("\n");
float f = 3.14f;
float* pF = &f; //&f가능, pointer자체가 주소를 저장하는 용도이기 때문에
//다차원 포인터
float** ppF = &pF; //float*의 *, 이중 포인터이기 때문에 float*인 pF에 대한 것만 취급
**ppF = 1.2345f;
printf("f: %f\n", f);
printf("ppF: %p\n", ppF);
printf("\n");
/*
long double* pLd = 0xffffffff; //쓰레기값으로 지정되어 알 수 없는 값으로 자동 지정됨.
printf("pLd: %p\n", pLd); //pLd의 주소를 보여달라는거지만 초기화가 되지 않은 pLd는 오류의 원인
*pLd = 3.14L;
printf("*pLd: %lf\n", *pLd);
*/
long double* pLd = NULL; //pointer는 0으로 초기화하는 것이 기본. C에선 주소를 0 숫자 대신 NULL로 표기
{
long double ld = 3.14L; //지역 내에서 ld 변수 선언.
pLd = &ld; //지역 내에서 pLd = ld의 주소값을 가진다.
*pLd = 1.2345L;
pLd = NULL; //사용하기 전에 NULL로 초기화시키는 것을 권장 (pLd 변수는 남아있기 때문에)
}
//*pLd = 1.23L;
printf("pLd: %p\n", pLd); //지역 밖으로 나가면 pLd의 주소값이 NULL이 되어 오류 발생, 지역 내에서 NULL로 초기화
printf("\n");
//원본값의 변경을 원치않을 경우 사용하는 상수화 const
/*
int i = 123;
int* ptrI = &i;
*ptrI = 789; //같은 주소기 때문에 원본 i의 값도 789로 바뀜
*/
int i = 123;
int* const ptrI = &i; //const가 자료형과 변수 사이에 등장해 주소 상수화를 진행하여 주소 변경을 막아준다.
*ptrI = 789; //위의 const 때문에 i는 123 그대로
//const가 앞이면 주소, const가 뒤면 값을 상수화.
int ii = 456;
//ptrI = ⅈ
////////////////////////////////////////////////////////////////////////////////////////////////////////
int arr[3] = { 1,2,3 };
int* pArr = arr; //pArr에 arr배열의 주소값을 가져옴
//메모리 주소 연산
printf("pArr: %p\n", pArr); //pArr의 첫 번째 요소의 주소는 arr
printf("pArr + 1: %p\n", pArr + 1); //pArr의 첫 번째 요소 + 1은 arr의 주소값 + 4byte(자료형 크기만큼)의 주소, 두 번째 요소가 된다.
printf("&pArr[1]: %p\n", &pArr[1]); //위와 같은 출력값을 수행 pArr의 두 번째 요소 주소이기 때문에
printf("*(pArr + 2): %d\n", *(pArr + 2)); //pArr의 세 번째 요소 초기값이 출력되고 주소는 arr의 주소값 + 8byte다.
printf("pArr[2]: %d\n", pArr[2]); //위와 같은 역할
printf("*pArr + 2: %d\n", *pArr + 2); //첫 번째 요소의 초기값 + 2의 출력값이 생성
}
|
cs |
<포인터의 특징>(point)
포인터는 지난 글에서 조금 다뤘지만, 이번엔 조금 더 자세하게 알아보겠습니다.
- 포인터는 메모리 주소 공간 기억을 담당합니다.
- * : pointer로 자료형 뒤 혹은 변수명 앞에 붙여 사용합니다. (ex) int* pVar && int *pVar
- & : 변수에 붙여 사용합니다. 그 변수의 주소를 뜻합니다. (ex) int* pVar = &var; (var은 source)
포인터의 특징들입니다. 포인터는 *로 메모리 주소 공간 기억을 담당합니다. 위의 예시 int* pVar에 대한 예문을 가져오겠습니다.
}
주석으로 설명했지만 추가 설명이 필요한 요점을 정리한 후 진행하겠습니다.
- *포인터 변수가 주소를 저장하는 건 이해하겠는데 왜 pVar와 *pVar의 위치값이 다른 것인가?
- *포인터의 공간 크기는 왜 플랫폼에 따라 바뀌는가?
- *포인터 변수의 초기화가 소스파일에도 영향을 끼치는 것인가?
순서대로 살펴보겠습니다. *포인터 변수는 메모리 주소 공간을 기억하고 저장하는 역할을 합니다. 이는 그림으로 표현하겠습니다.
- 위의 그림처럼 pVar 자체의 메모리 주소가 있습니다. *포인터로 var의 메모리 주소를 *pVar에 기억, 저장을 하는 메모리 주소가 따로 있어야 하겠죠? 그게 메모리 주소 0x2000이라고 생각하시면 됩니다. 그리고 var의 메모리 주소를 저장한 *pVar를 초기화시켜주면 var의 값에도 적용이 되는 겁니다.
- 포인터는 컴파일러, 하드웨어 등, 환경에 따라 가지고 있는 공간의 크기가 달라집니다. 64비트 운영체제에서는 2E64 범위의 주소를 다룰 수 있습니다. 이에 따라 포인터는 64비트 체제에서 8바이트 크기를 가지는 것입니다. 만약 32비트 운영체제라면 포인터의 공간 크기는 4바이트라고 생각하면 됩니다.
- 포인터는 주소값을 기억, 저장합니다. 위의 그림대로 *pVar에는 var의 메모리 주소가 저장됩니다. 여기서 *pVar = 100;은 0x1000의 메모리 주소에 100으로 초기화한다는 의미이기 때문에 소스 변수에도 영향을 미치는 것입니다.
위의 예제는 모두 2번에 해당하는 코드들입니다. 64비트 체제이기 때문에 출력값은 모두 8 byte로 동일합니다. 여기서는 %zu가 등장하는데, 이건 부호 없는 정수 타입인 size_t를 출력하기 위해 사용하는 포맷지정자입니다. 가장 적절한 방법이지만, int 범위에 맞는 경우라면 %d도 사용가능합니다. 종합적으로 포인터를 사용하는 이유는 다음과 같습니다.
- int sheet[100] = { 0 }; 이라는 배열이다. 이 배열에 할당된 바이트는 int의 4바이트 x 100으로 총 400바이트를 소비한다.
- 다른 배열에 sheet 배열의 주소를 입힌다면 800바이트를 소모하는 꼴이다.
- 이때 포인트를 사용하면 8바이트 만을 사용하여 400바이트의 주소값을 모두 저장하여 사용할 수 있다.
공간을 효율적으로 사용할 수 있기 때문에 사용합니다.
<다중 포인터>
위의 예제는 다중 중 이중포인터를 다룹니다. f 변수의 주소를 *pF에 저장했습니다. 그리고 *pF의 주소를 **ppF에 저장했습니다. f값 출력문에서는 **ppF의 초기화 값인 1.2345f가 *pF 주소로 넘겨지고 *pF의 주소는 f의 주소기 때문에 함께 초기화되어 f: 1.234500f가 출력됩니다. ppF의 주소값은 포인터와 상관없이 따로 존재하기 때문에 자신만의 주소가 출력됩니다.
<NULL>(pointer초기화)
long double* pLd;
printf("pLd: %p\n", pLd);
*pLd = 3.14L;
printf("*pLd: %lf\n", *pLd);
- pointer는 0으로 초기화하는 것이 기본입니다. '0'이라는 숫자는 코드에서 그 자체의 기능이 있기 때문에 코드 상 가장 완전한 초기화, 無는 NULL로 표기합니다. 여기서 NULL은 전체 대문자 표기하여 리터럴 상수입니다.
- 위의 long double* pLd의 초기화가 진행되지 않으면 쓰레기값으로 자동 지정됩니다. pLd의 주소를 출력할 때 오류가 발생합니다. C언어 내에서 주소는 0 대신 NULL을 사용합니다.
- 위의 코드 상 { } 중괄호 지역 내에서 ld의 변수를 선언하고 pLd가 ld의 주소값을 가집니다. 포인터 *pLd에 1.2345L을 초기화를 진행합니다. 하지만 지역에서 벗어나버린 출력문에서는 1.2345L의 초기값은 사라지고 기존 초기값인 NULL만이 남게 되어 주소값 0000000000000000이 나오게 됩니다.
- { } 중괄호 지역 내의 pLd = NULL;은 필요없는 것 아닌가? 하실 수 있지만, pLd 변수 자체는 남아있어 디버깅 과정에서 NULL로 초기화하지 않으면 오류가 발생할 수 있습니다. 때문에 포인터를 사용하고 난 후는 NULL로 설정하는 것이 좋습니다.
<주소 상수화>(const)
만약 포인터로 특정 변수의 주소를 저장하고 그 포인터를 초기화할 때 기존의 특정 변수의 주소값에 영향을 끼치고싶지 않다면 const문을 활용해 주소 상수화를 진행하면 됩니다.
위의 예제에서 상수*ptrI = i의 주소값을 가지게 됩니다. *ptrI를 789로 초기화시킵니다. 하지만, const로 *ptrI를 상수화시켰기 때문에 i에는 영향이 가지 않아 출력하면 그대로 i는 123이 출력됩니다. const위치에 따라 상수화하는 것이 다릅니다.
- const int* var -> int, 자료형을 상수화
- int const* var -> *var, 값을 상수화
- int* const var -> var, 주소를 상수화
예제에서는 const의 위치에 따라 주소를 상수화하는 방식입니다. &i로 i의 주소값을 받은 후, 그것을 상수화시켰습니다. 이렇게 되면 *ptrI를 어떤 값으로 초기화하더라도 i의 값은 초기화하지 못하게 됩니다.
<배열에 사용되는 포인터>
pArr: arr의 첫 번째 요소의 주소
pArr+1: arr의 두 번째 요소의 주소
pArr[1]: arr의 두 번째 요소의 주소
(pArr + 2): 3 ( arr의 첫 번째 요소 +2 )
pArr[2]: 3 (arr의 세 번째 요소)
pArr+2: 3 (arr의 초기값 +2)
출력값이 된다.
끝
'프로그래밍 C언어' 카테고리의 다른 글
문자열 (String) (0) | 2024.08.16 |
---|---|
함수 (Function) (0) | 2024.08.14 |
배열 (Array) (0) | 2024.08.12 |
반복문(Loop) (1) | 2024.08.11 |
분기문(Branch)과 조건문(Condition) (0) | 2024.08.09 |