프로그래밍 C언어

포인터 (pointer)

게임첫걸음 2024. 8. 13. 17:56

 이전 글에서 잠깐 다뤘던 포인터(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 = &ii;
 
    ////////////////////////////////////////////////////////////////////////////////////////////////////////
 
    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에 대한 예문을 가져오겠습니다.

void main()
{
int var = 10; //var라는 정수형 변수를 설정, 10으로 초기화
    int* pVar = &var; //포인터 변수 *pVar에 var의 주소를 저장
    printf("var: %d\n", var); //var: 10
    printf("&var: %p\n"&var); //&var: "var의 주소" (%p로 출력은 주소값을 출력해달라는 뜻)
    printf("pVar: %p\n", pVar); //pVar: "var의 주소" (pVar는 이미 var의 주소를 저장했기 때문에 var의 주소값 출력)
    *pVar = 100; //*pVar의 값을 100으로 초기화;

 

    printf("*pVar: %d\n"*pVar);     //*pVar를 100으로 초기화했기 때문에 100
    printf("var: %d\n", var);             //var과 *pVar의 주소값이 동일하기 때문에 source 코드인 var의 값도 100으로 바뀜.
    printf("&pVar: %p\n"&pVar);  //pVar의 주소를 가져온다. [pVar] 자체의 위치값은 var과 *pVar과 다르다.
    printf("pVar Size: %d byte\n"sizeof(pVar));  //sizeof로 pVar 공간 크기를 표현합니다. 플랫폼에 따라 byte가 다름.

}

 주석으로 설명했지만 추가 설명이 필요한 요점을 정리한 후 진행하겠습니다.

  1. *포인터 변수가 주소를 저장하는 건 이해하겠는데 왜 pVar와 *pVar의 위치값이 다른 것인가?
  2. *포인터의 공간 크기는 왜 플랫폼에 따라 바뀌는가?
  3. *포인터 변수의 초기화가 소스파일에도 영향을 끼치는 것인가?

 순서대로 살펴보겠습니다. *포인터 변수는 메모리 주소 공간을 기억하고 저장하는 역할을 합니다. 이는 그림으로 표현하겠습니다. 

  1. 위의 그림처럼 pVar 자체의 메모리 주소가 있습니다. *포인터로 var의 메모리 주소를 *pVar에 기억, 저장을 하는 메모리 주소가 따로 있어야 하겠죠? 그게 메모리 주소 0x2000이라고 생각하시면 됩니다. 그리고 var의 메모리 주소를 저장한 *pVar를 초기화시켜주면 var의 값에도 적용이 되는 겁니다. 
  2. 포인터는 컴파일러, 하드웨어 등, 환경에 따라 가지고 있는 공간의 크기가 달라집니다. 64비트 운영체제에서는 2E64 범위의 주소를 다룰 수 있습니다. 이에 따라 포인터는 64비트 체제에서 8바이트 크기를 가지는 것입니다. 만약 32비트 운영체제라면 포인터의 공간 크기는 4바이트라고 생각하면 됩니다.
  3. 포인터는 주소값을 기억, 저장합니다. 위의 그림대로 *pVar에는 var의 메모리 주소가 저장됩니다. 여기서 *pVar = 100;은 0x1000의 메모리 주소에 100으로 초기화한다는 의미이기 때문에 소스 변수에도 영향을 미치는 것입니다.
printf("char* Size: %zu byte\n"sizeof(char*)); //%d 가능
printf("short* Size: %zu byte\n"sizeof(short*)); //%d 가능
printf("int* Size: %zu byte\n"sizeof(int*)); //%d 가능
printf("long* Size: %zu byte\n"sizeof(long*)); //%d 가능
printf("long long* Size: %zu byte\n"sizeof(long long*)); //%d 가능
printf("float Size: %zu byte\n"sizeof(float*)); //%d 가능
printf("double* Size: %zu byte\n"sizeof(double*)); //%d 가능
printf("double long* Size: %zu byte\n"sizeof(double long*));  //%d 가능

 

 위의 예제는 모두 2번에 해당하는 코드들입니다. 64비트 체제이기 때문에 출력값은 모두 8 byte로 동일합니다. 여기서는 %zu가 등장하는데, 이건 부호 없는 정수 타입인 size_t를 출력하기 위해 사용하는 포맷지정자입니다. 가장 적절한 방법이지만, int 범위에 맞는 경우라면 %d도 사용가능합니다. 종합적으로 포인터를 사용하는 이유는 다음과 같습니다.

  • int sheet[100] = { 0 }; 이라는 배열이다. 이 배열에 할당된 바이트는 int의 4바이트 x 100으로 총 400바이트를 소비한다.
  • 다른 배열에 sheet 배열의 주소를 입힌다면 800바이트를 소모하는 꼴이다.
  • 이때 포인트를 사용하면 8바이트 만을 사용하여 400바이트의 주소값을 모두 저장하여 사용할 수 있다.

 공간을 효율적으로 사용할 수 있기 때문에 사용합니다.

 

<다중 포인터>

float f = 3.14f;
    float* pF = &f;                 //&f가능, pointer자체가 주소를 저장하는 용도이기 때문에
    //다차원 포인터
    float** ppF = &pF;             //float*의 *, 이중 포인터이기 때문에 float*인 pF에 대한 것만 취급
    **ppF = 1.2345f; //**ppF를 1.2345f로 초기화시켰습니다.
    printf("f: %f\n", f);
    printf("ppF: %p\n", ppF);
 

 위의 예제는 다중 중 이중포인터를 다룹니다. 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);

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로 초기화
 
  • 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문을 활용해 주소 상수화를 진행하면 됩니다.

int i = 123;
    int* const ptrI = &i;     //const가 자료형과 변수 사이에 등장해 주소 상수화를 진행하여 주소 변경을 막아준다.
    *ptrI = 789;                 //위의 const 때문에 i는 123 그대로
    //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의 값은 초기화하지 못하게 됩니다.

 

<배열에 사용되는 포인터>

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(int)의 주소, 두 번째 요소가 된다.
    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의 출력값이 생성

 

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