끝나지 않는 프로그래밍 일기


1. 주소 값의 이해와 표현


이 강좌에서 배우게 될 포인터는 필자도 어렵게 생각하는 부분이며 C언어에서 가장 어렵고도 핵심인 구간입니다. 포인터에 들어와서 바로 포인터를 다루게 된다면 혼란이 생길 수 있으므로, 우선 알아야 할것부터 알아보도록 합시다. 간단한 사항부터 알아보도록 하고, 바로 포인터라는 녀석을 사용하여 어떤 녀석인지 대충 짐작을 하도록 합시다.


포인터(Pointer)란 메모리의 주소 값을 담고있는 변수 혹은 상수입니다. 비슷하게는 데이터의 위치를 가리키는 녀석이라고 할 수도 있습니다. 의외로 간단해 보일지도 모르겠지만 주소 값과 관련이 있어 메모리의 주소체계를 이해하지 못하면 포인터를 정확히 이해할 수 없습니다. 여기서 주소란 그 메모리의 저장장소의 위치를 나타내는 값으로 하나의 주소값은 1바이트 크기의 메모리 공간을 표현합니다.

<한 블럭(주소)당 1바이트의 메모리 공간 차지, 한개의 주소는 8개의 비트가 묶임>


그렇다면 16비트의 주소 값의 범위는 어디까지 일까요? 2의 16은 65,535이므로 16비트로는 총 65536개의 바이트에 주소를 부여할수 있게 됩니다. 만약 주소값의 시작이 0번지 부터라면 65535번지까지 주소를 부여할 수 있습니다. 32비트 같은 경우에는 2의 32승, 최대 4,294,967,296(4GB) 바이트까지 주소를 부여할수 있게 되는것입니다. (64 비트는 2의 64승, 18,446,744,073,709,551,616 바이트, 18EB (1EB = 1000TB))

2. 포인터 변수 선언


포인터 변수란 메모리 주소를 저장하는 변수이며 데이터 타입과 식별자(=변수명) 사이에 그저 * 하나만 넣어주면 포인터 변수가 됩니다. 기본적인 데이터 타입과 구조체, 배열, 공용체에 대해서도 포인터형을 만들수가 있습니다. 그리고 & 연산자를 변수명 앞에다 가져오면 그 변수의 주소값을 반환하게 됩니다. 아래 예제를 한번 보도록 합시다.

#include <stdio.h>

int main()
{
    int num, num1, num2;
    
    num=50;
    num1=72;
    num2=94;
    printf("num의 저장 위치: %#x\n", &num);
    printf("num1의 저장 위치: %#x\n", &num1);
    printf("num2의 저장 위치: %#x\n", &num2);
    return 0;
}

결과:

num의 저장 위치: 0x22ff44
num1의 저장 위치: 0x22ff40
num2의 저장 위치: 0x22ff3c
계속하려면 아무 키나 누르십시오 . . .


10, 11, 12행에서 변수명 앞에 & 연산자를 붙여주어 그 변수의 주소값이 반환되고 16진수의 형태로 출력한다는 의미입니다. %x는 16진수로 출력시킬때 사용하는 출력 포맷이고, 그 가운데에 들어간 #를 제외하면 앞에 0x가 붙지 않습니다. & 연산자는 '어떤 변수의 주소를 알아내는 역할' 도 하는 연산자이며 상수는 메모리에 위치하지 않으므로 주소가 없으며 & 연산자를 사용할 수 없습니다.

포인터 변수를 이용하면 프로그램이 간결하고 효율적이며 그 포인터가 가리키는 변수의 자료형에 따라 타입을 맞추어 선언해야 합니다. 다음은 포인터 변수에 관한 예제입니다.

#include <stdio.h>

int main()
{
    int Number;
    int *pNumber;
    
    Number=50;
    pNumber=&Number;
    
    printf("변수 Number의 값: %d\n", Number);
    printf("변수 Number의 주소값: %#x\n\n", pNumber);
    
    *pNumber=60;
    printf("변수 Number의 값: %d\n", Number);
    return 0; 
}

결과:

변수 Number의 값: 50
변수 Number의 주소값: 0x22ff44


변수 Number의 값: 60
계속하려면 아무 키나 누르십시오 . . .


앞에서 말했듯이, 포인터 변수는 주소값만 저장할 수 있습니다. 지금까지 우리가 배운 * 연산자의 기능은 곱셈을 할때 사용하거나, 포인터 변수 선언시에도 사용되었습니다. 그런데 14행에서의 * 연산자는 무슨 기능을 할까요? 이것은 간접 참조 연산자로 단항 연산자로 사용되면 이 포인터가 가리키는 메모리 공간의 접근을 의미합니다. 즉, 14행의 문장에서는 pNumber이 가리키는 변수 Number를 의미하며 이것은 'Number=60'과 동일한 기능을 합니다. 이 간접 참조 연산자는 포인터 변수를 초기화 하고 사용해야 합니다.


주의할 점은 여러개의 포인터 변수를 한번에 선언할때 변수마다 *를 붙여주어야 합니다. 아래와 같이 말입니다.

int *a, *b, *c;

아래와 같이 선언해버리면, b와 c는 정수형 포인터가 아닌 정수형 변수가 되어버립니다.

int *a, b, c;
만약에, *가 두번씩이나 쓰이면 어떻게 될까요?
#include <stdio.h>

int main()
{
    int Num1=50, Num2=100;
    int *pNum1=&Num1;
    int **dpNum1=&pNum1;
    
    printf("정수형 변수 Num1의 값: %d\n", Num1);
    printf("pNum1이 가리키는 변수의 값: %d\n", *pNum1);
    printf("dpNum1이 가리키는 변수의 값: %d\n\n", **dpNum1);
    
    *dpNum1=&Num2; // pNum1=&Num2
    printf("정수형 변수 Num2의 값: %d\n", Num2);
    printf("pNum1이 가리키는 변수의 값: %d\n", *pNum1);
    printf("dpNum1이 가리키는 변수의 값: %d\n\n", **dpNum1);
    
    **dpNum1+=150;
    printf("정수형 변수 Num2의 값: %d\n", Num2);
    printf("pNum1이 가리키는 변수의 값: %d\n", *pNum1);
    printf("dpNum1이 가리키는 변수의 값: %d\n", **dpNum1);
    
    return 0;
} 

결과:

정수형 변수 Num1의 값: 50
pNum1이 가리키는 변수의 값: 50
dpNum1이 가리키는 변수의 값: 50

정수형 변수 Num2의 값: 100
pNum1이 가리키는 변수의 값: 100
dpNum1이 가리키는 변수의 값: 100

정수형 변수 Num2의 값: 250
pNum1이 가리키는 변수의 값: 250
dpNum1이 가리키는 변수의 값: 250
계속하려면 아무 키나 누르십시오 . . .


포인터의 주소값을 저장하기 위해 포인터 형이 int **인 변수에 저장하였습니다. 포인터 변수 dpNum1에서 *를 한번만 사용하면 dpNum1이 가리키는 포인터 pNum1의 주소값을 참고합니다. **를 두번 사용하면 dpNum1이 가리키는 변수를 의미합니다. 이런 녀석을 이중 포인터라고 부르며, 포인터 변수를 가리키는 포인터라고 합니다.


3. 포인터 연산


포인터끼리 더하거나 뺄수 있으며 포인터에 정수를 더하거나 빼거나, 대입마저도 가능합니다. 그렇지만 포인터끼리 더하는건 원칙적으로 허용하지 않고 아무 의미가 없으며, 더하려고 시도하면 에러를 내보냅니다. 이것은 불가능 해서가 아니라, 단순히 프로그래머가 실수해서 그런 코드를 적었을 확률이 높기 때문입니다. 물론 굳이 더하려고 한다면 더할수 있습니다. 다만 포인터와 포인터 끼리의 덧셈은 아무 의미도 없다는 것 뿐입니다.


한번, 포인터와 포인터를 서로 더하고 뺀 후의 결과를 출력하는 예제를 보도록 합시다.

#include <stdio.h>

int main()
{
    char Array[]="Pointer Array";
    char  *pArray1, *pArray2, *pArray3;
    
    pArray1=&Array[0];
    pArray2=&Array[11];
    pArray3=(char *)((unsigned)pArray1+(unsigned)pArray2); // 아무 의미가 없음! 
    printf("Array1의 주소 값: %#x\n", pArray1);
    printf("Array2의 주소 값: %#x\n\n", pArray2); 
    printf("Array1과 Array2의 주소를 더한 값: %#x\n", pArray3);
    printf("Array1(%c)과 Array2(%c)의 거리: %d\n", *pArray1, *pArray2, pArray2-pArray1); 
    return 0; 
}

결과:

Array1의 주소 값: 0x22ff30
Array2의 주소 값: 0x22ff3b

Array1과 Array2의 주소를 더한 값: 0x45fe6b
Array1(P)과 Array2(a)의 거리: 11
계속하려면 아무 키나 누르십시오 . . .


10행의 포인터끼리의 덧셈 연산은 아무 의미도 없습니다. (포인터 끼리의 곱셈 또는 나눗셈도 포함합니다). 굳이 하겠다면 두 포인터를 unsigned로 캐스팅 후 포인터 타입으로 다시 캐스팅 하여 덧셈이 가능하며 명시적인 캐스트 연산자에 대해서는 컴파일러가 에러를 내보내지 않습니다. 그렇지만 뺄셈은 가능합니다. 포인터 끼리의 뺄셈은 두 포인터 간의 거리를 나타냅니다. 14행에서 P와 a의 거리는 11이며, 그 사이에 'o', 'i', 'n', 't', 'e', 'r', ' ', 'A', 'r', 'r'가 존재함을 확인할 수 있습니다.

만약, 포인터에 정수를 더하거나 빼는 문장을 거친다면 어떤 결과가 출력될까요?

#include <stdio.h>

int main()
{
    char *pc;
    int *pi;
    double *pd;
    
    pc=(char *)100;
    pi=(int *)100;
    pd=(double *)100;
    
    printf("pc 증가 전: %d\n", pc);
    printf("pi 증가 전: %d\n", pi); 
    printf("pd 증가 전: %d\n\n", pd); 
    pc++;
    pi++;
    pd++;
    printf("pc 증가 후: %d\n", pc);
    printf("pi 증가 후: %d\n", pi); 
    printf("pd 증가 후: %d\n", pd); 
    return 0;
}

결과:

pc 증가 전: 100
pi 증가 전: 100
pd 증가 전: 100


pc 증가 후: 101
pi 증가 후: 104
pd 증가 후: 108
계속하려면 아무 키나 누르십시오 . . .


결과를 보시면 포인터의 자료형의 크기만큼 증가한다는 것을 알수 있습니다. char는 1, short는 2, int는 4, float는 4, double는 8로 말입니다. 만약에 2 이상의 수를 더한다면 '포인터가 가리키는 변수 데이터 타입의 크기 * 정수' 만큼 증가가 되는걸 보실수 있습니다. 뺄셈도 이와 마찬가지입니다.

포인터에 덧셈과 뺄셈을 하는것 말고도, 포인터끼리의 비교도 가능합니다. 

#include <stdio.h>

int main()
{
    int Num1, Num2;
    int *pNum1, *pNum2;
    
    pNum1=&Num1;
    pNum2=&Num2;
    
    if(pNum1!=NULL) printf("pNum1은 NULL이 아닙니다.\n");
    if(pNum1!=pNum2) printf("pNum1과 pNum2은 다릅니다.\n");
    if(pNum1<pNum2) printf("pNum1은 pNum2보다 앞에 있습니다.\n");
    else printf("pNum1은 pNum2보다 뒤에 있습니다.\n"); 
    return 0;
}

pNum1은 NULL이 아닙니다.
pNum1과 pNum2은 다릅니다.
pNum1은 pNum2보다 뒤에 있습니다.
계속하려면 아무 키나 누르십시오 . . .


참고로, 11행에서의 비교는 pNum1이 널 포인터(NULL Pointer)인지를 확인하기 위해서 입니다. 널 포인터는 아무곳도 가리키지 않는 포인터를 말합니다.

4. 포인터 배열


포인터 배열(Pointer Array)란 말 그대로 포인터 변수로 이루어진 배열을 말하는 것이며 포인터 배열의 선언방식은 우리가 알고있는 배열 선언방식과 크게 다르지 않습니다.

 #include <stdio.h>

int main()
{
    int num1, num2, num3;
    int *pNumArray[3]={&num1, &num2, &num3};
    int i;
    
    for(i=0; i<3; i++) 
     scanf("%d", pNumArray[i]);
     
     printf("입력된 숫자는 각각 %d, %d, %d 입니다.\n", *pNumArray[0], *pNumArray[1], *pNumArray[2]);
     return 0;
} 

결과:

4
5
7
입력된 숫자는 각각 4, 5, 7 입니다.
계속하려면 아무 키나 누르십시오 . . . 


6행에서 길이가 3인 포인터 배열 pNumArray를 선언후 각각 num1, num2, num3의 주소값으로 초기화 시켰습니다. 그리고 for문을 이용하여 수를 입력받게 하였는데, 10행에서 주의할 것은 pNumArray[i]은 주소값을 의미하므로 &를 붙이면 안됩니다. 그리고 12행에서 참조하고 있는 변수의 값을 차례대로 출력하였습니다. 간단하죠? 물론 1차원 포인터 배열뿐만 아니라, 2차원 포인터 배열도 만들 수 있습니다.

5. 배열과 포인터


포인터와 배열은 밀접한 관계가 있으며 이제부터 그 관계를 설명하고자 합니다. 배열의 이름은 사실 배열의 시작번지를 갖는 포인터 상수이며, 즉 첫번째 원소의 주소값을 나타냅니다. 아래 예제를 살펴보도록 합시다.

#include <stdio.h>

int main()
{
    int Array[5]={44,77,64,13,42};
    int i;
    
    printf("Array: %#x\n\n", Array);
    for(i=0; i<5; i++)
     printf("Array[%d]: %#x\n", i, &Array[i]);
     
    return 0;
}

결과:

Array: 0x22ff20

Array[0]: 0x22ff20
Array[1]: 0x22ff24
Array[2]: 0x22ff28
Array[3]: 0x22ff2c
Array[4]: 0x22ff30
계속하려면 아무 키나 누르십시오 . . .


결과를 확인해봤더니, int형의 크기인 4바이트씩 값이 증가되는걸 확인할 수 있으며 이것을 이용하여 배열의 접근이 가능합니다.

#include <stdio.h>

int main()
{
    int Array[5]={44,77,64,13,42};
    int *p=&Array[2];
    
    printf("p가 가리키는 배열의 위치: %d\n", *p);
    printf("p가 가리키는 배열의 위치에서 한칸 앞: %d\n", *(p+1));
    printf("p가 가리키는 배열의 위치에서 두칸 앞: %d\n", *(p+2));
    printf("p가 가리키는 배열의 위치에서 한칸 뒤: %d\n", *(p-1));
    printf("p가 가리키는 배열의 위치에서 두칸 뒤: %d\n", *(p-2));
    return 0;
} 
결과:
p가 가리키는 배열의 위치: 64
p가 가리키는 배열의 위치에서 한칸 앞: 13
p가 가리키는 배열의 위치에서 두칸 앞: 42
p가 가리키는 배열의 위치에서 한칸 뒤: 77
p가 가리키는 배열의 위치에서 두칸 뒤: 44
계속하려면 아무 키나 누르십시오 . . .


p가 가리키는 세번째 요소의 주소값에 각각 1, 2를 더한 결과와 뺀 결과를 출력하도록 했습니다. 전에 말씀드렸듯이 '포인터 변수가 가리키는 변수 자료형의 크기 * 정수'만큼 주소값에서 값이 더해져 배열 요소의 그 다음값을 가리키고 있습니다.

만약 길이가 3인 1차원 배열 a를 선언했다고 합시다. 우리는 배열명이 포인터 상수인걸 알고 있으며 이것을 이용하여 'a+i는 &a[i]이며 *(a+i)는 a[i]이다.' 라는 사실을 알 수 있습니다.

심지어 포인터를 배열처럼 사용할 수도 있습니다.

#include <stdio.h>

int main()
{
    int Array[3]={44,77,64};
    int *p=Array;
    
    printf("Array[0]:%d Array[1]:%d Array[2]:%d\n", Array[0], Array[1], Array[2]);
    printf("p[0]:%d p[1]:%d p[2]:%d\n\n", p[0], p[1], p[2]);
    
    p[0]=64;
    p[2]=44;
    printf("Array[0]:%d Array[1]:%d Array[2]:%d\n", Array[0], Array[1], Array[2]);
    printf("p[0]:%d p[1]:%d p[2]:%d\n", p[0], p[1], p[2]);
    
    return 0;
} 
결과:
Array[0]:44 Array[1]:77 Array[2]:64
p[0]:44 p[1]:77 p[2]:64

Array[0]:64 Array[1]:77 Array[2]:44
p[0]:64 p[1]:77 p[2]:44
계속하려면 아무 키나 누르십시오 . . .


포인터를 사용하면 좋은게 참 많습니다. 포인터를 사용하면 원소의 주소를 계산할 필요가 없어져 인덱스 표기법보다 더 빠른 속도를 낼수 있습니다. 참 편리하지 않습니까?


배열을 인자로 넘길때는 어떻게 넘길 수 있을까요? 다음은 배열을 인자로 넘겨 배열의 원소를 역순으로 출력합니다.

#include <stdio.h>

void ReverseArray(int *array, int len);
int main()
{
    int Array[7]={44,77,64,11,20,467,500};
    
    ReverseArray(Array, sizeof(Array)/sizeof(Array[0])-1);
    return 0;
} 
void ReverseArray(int *array, int len) {
    int i;
    for(i=len; i>=0; i--)
     printf("Array[%d]: %d\n", i, array[i]);
}

결과:

Array[6]: 500
Array[5]: 467
Array[4]: 20
Array[3]: 11
Array[2]: 64
Array[1]: 77
Array[0]: 44
계속하려면 아무 키나 누르십시오 . . .


예제를 보다 8행을 보고 의문을 가지시는 분들도 계실텐데 sizeof 연산자는 '괄호 안의 대상이 메모리를 어느 정도 차지 하는가'를 계산합니다. ReverseArray의 첫번째 전달인자로 배열 Array(첫번째 원소의 주소값이 전달됨)를 전달하고 두번째로 배열의 길이를 전달하는데 배열의 전체크기를 배열 요소의 크기로 나누어 배열요소의 갯수가 되었습니다. 그리고 배열의 역순출력을 위해 1을 빼준 후에 두번째 인자로 넘겼습니다. 그리고 ReverseArray 함수에서 Array[6]에서 Array[0]까지의 수를 출력하였습니다. 참고로 매개변수에서 int *array로 선언하지 않고 int array[]를 쓰셔도 괜찮습니다.

6. Call-By-Value 그리고 Call-By-Reference 


Call-By-Value은 값에 의한 호출을 의미하며, Call-By-Reference는 참조에 의한 호출을 의미합니다. 앞서 말한, 두 호출이 어떻게 다른지 확인해보도록 합시다.

#include <stdio.h>
 
void callByValue(int, int);
void callByReference(int *, int *);
 
int main()
{
    int x=10, y=20;
    printf("x의 값: %d 그리고 y의 값: %d\n", x,y);
 
    printf("\nCall By Value(값에 의한 호출) 함수 호출...\n");
    callByValue(x, y);
    printf("x의 값: %d 그리고 y의 값: %d\n", x,y);
 
    printf("\nCall By Reference(참조에 의한 호출) 함수 호출...\n");
    callByReference(&x, &y);
    printf("x의 값: %d 그리고 y의 값: %d.\n", x,y);
 
    system("pause");
    return 0;
}
 
void callByValue(int x, int y)
{
    int temp;
    temp = x;
    x = y;
    y = temp;
 
    printf("callByValue 함수 내부에서의 x의 값: %d 그리고 y의 값: %d \n", x,y);
}
 
void callByReference(int *x, int *y)
{
    int temp;
    temp = *x;
    *x = *y;
    *y = temp;
}

결과:

x의 값: 10 그리고 y의 값: 20

Call By Value(값에 의한 호출) 함수 호출...
callByValue 함수 내부에서의 x의 값: 20 그리고 y의 값: 10
x의 값: 10 그리고 y의 값: 20

Call By Reference(참조에 의한 호출) 함수 호출...
x의 값: 20 그리고 y의 값: 10
계속하려면 아무 키나 누르십시오 . . .


이상한건 12행에서 callByValue의 호출을 통해 x와 y의 값을 서로 바꾸었는데 값은 왜 그대로 일까요? 그 이유는 변수 x, y가 전달된게 아닌 변수 x, y의 값이 전달되었고 결국 별개의 변수가 되어버려 아무런 영향을 미칠수 없게 되었습니다. 즉, x와 y의 값이 넘어가고 callByValue의 매개변수 x, y에 전달된 x, y의 값이 들어간 것입니다. 실제로 변수의 값이 바뀐게 아닌, 매개변수의 값만 바뀌고 함수를 빠져나온 것입니다.


이번에는, 16행에서 보시면 callByReference로 변수 x와 y의 주소값을 전달하였습니다. 이는 callByReference 함수 내에서 전달된 주소값을 참조하여 이 주소값들이 가리키는 변수의 값을 서로 바꾸어 실제 변수 x와 y의 값이 바뀌었음을 알 수 있습니다.

7. 동적 메모리 할당


동적 메모리 할당이란 실행 시간동안 사용할 메모리 공간의 할당을 뜻하며 정적 메모리 할당은 우리가 사용하지 않아도 프로그램을 실행할때 프로그램에서 필요한 메모리 공간을 확보합니다. 예를 들어 저장하려는 데이터의 메모리 공간이 얼마인지 알고있을때는 정적 메모리 할당(Static Memory Allocation)을 사용합니다. 우리가 만약 전화번호를 저장한다 한다면 아무리 길어도 15자 이상은 되지 않는다 생각하고 아래와 같이 저장하기 충분한 메모리 공간을 할당합니다.

char PhoneNumber[15];

그런데, 정적 메모리 할당은 고정된 메모리 공간을 할당하므로 메모리 낭비가 있을수 있습니다. 반대로 동적 메모리 할당은 어떨까요?

동적 메모리 할당(Dynamic Memory Allocation)은 실행 중에 우리가 필요한 만큼 메모리를 할당시키는 기법입니다. 만약에 모든 학생의 점수를 받아 평균으로 처리하고 싶은데 어떻게 해야 할까요? 하지만 여기서 문제점은 메모리 필요량을 전혀 예측할 수 없는 경우가 존재합니다.

...
int StudentNum;
scanf("%d", &StudentNum); // 학생 수를 입력받음 
int StudentScore[StudentNum];
..

위와 같은 코드를 정상적으로 컴파일 할수 있을까요? 우리는 배열의 크기에 상수만 올수 있으며 변수가 오지 못한다는것을 알고 있습니다. 그럼 어떻게 해야 우리가 원하는 기능을 만들 수 있을까요? 이것을 동적 메모리 할당이 해결하여 줄 수 있습니다.

#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    int StudentNum, TotalScore=0;
    int *StudentScore;
    int i;
    
    printf("학생 수를 입력하세요: "); 
    scanf("%d", &StudentNum);
    StudentScore=(int *)malloc(sizeof(int)*StudentNum);
    
    for(i=0; i<StudentNum; i++) {
     printf("%d번 학생의 점수: ", i+1);
     scanf("%d", &StudentScore[i]);
     TotalScore+=StudentScore[i];
    }
    
    printf("모든 학생의 평균: %d\n", TotalScore/StudentNum);
    return 0;
}

결과:

학생 수를 입력하세요: 5
1번 학생의 점수: 10
2번 학생의 점수: 20
3번 학생의 점수: 30
4번 학생의 점수: 40
5번 학생의 점수: 50
모든 학생의 평균: 30
계속하려면 아무 키나 누르십시오 . . .


코드 중 처음보는 녀석이 있죠? 이 malloc 라는 녀석은 힙 영역에 메모리 공간을 할당할 수 있게 도와주는 함수입니다. 이 함수는 stdlib.h에 정의되어 있기때문에 사용하려면 이 헤더를 선언해야만 합니다.

void * malloc(size_t size) // 성공 시 할당된 메모리의 주소 값, 실패시 NULL 반환

이 malloc란 함수는 0보다 큰 숫자를 입력받고 이 숫자의 크기만큼 바이트 단위로 힙 영역에 메모리 공간을 할당합니다. 그리고 이 할당된 메모리 공간의 주소값을 반환합니다. 만약에 우리가 500 바이트가 필요하면 malloc(500);을 호출하고 1000바이트가 필요하다면 malloc(1000);를 호출하여 해결할 수 있습니다(상수가 아닌 변수의 사용도 가능함). 주의할 것은 리턴 타입이 void *형이므로 프로그래머가 직접 포인터의 형을 결정해야 합니다. 또한 malloc 함수로 메모리 공간을 할당했으면 free라는 함수로 반드시 직접 해제해야 합니다. 그렇지 않으면 메모리 공간의 낭비가 발생합니다. 다쓴 공간을 쓸 필요가 없기에 해제하고 다른곳에 쓰는게 더 효율적이기 때문이죠.

void free(void * ptr)

free 함수의 전달인자는 malloc 함수를 호출할 때 반환된 값을 인자로 전달하며 이 함수는 malloc 함수 호출시 할당되었던 메모리 공간을 전부 해제할수 있습니다. 다시 코드로 돌아와 핵심 부분을 살펴볼까요?

StudentScore=(int *)malloc(sizeof(int)*StudentNum);

malloc 함수를 사용하여 반환된 할당된 메모리 값의 주소 값을 포인터 StudentScore에 저장하고 있습니다. 정수형 배열을 생성할 것이므로 int형의 크기와 입력받은 학생 수를 곱합니다. 만약 5라고 입력했다면 20 바이트 만큼의 메모리 공간을 할당하는 것입니다. 예제를 하나 더 보여드리겠습니다.

/* www.cplusplus.com/reference/clibrary/cstdlib/malloc/ */
#include <stdio.h>
#include <stdlib.h>

int main ()
{
  int i,n;
  char * buffer;

  printf ("얼마나 긴 문자열을 입력하고 싶은가요? ");
  scanf ("%d", &i);

  buffer = (char*) malloc (i+1);
  if (buffer==NULL) exit (1);

  for (n=0; n<i; n++)
    buffer[n]=rand()%26+'a';
  buffer[i]='\0';

  printf ("랜덤 문자열: %s\n",buffer);
  free (buffer);

  return 0;
}

결과:

얼마나 긴 문자열을 입력하고 싶은가요? 10
랜덤 문자열: phqghumeay
계속하려면 아무 키나 누르십시오 . . .


여기서 문자형 포인터 buffer를 선언 후 사용자로부터 길이를 입력받았습니다. 13행의 i+1는 char형의 크기는 1바이트라는걸 이미 알고 계시고 여기서 NULL 문자까지 고려하여 1을 더한듯 합니다. 만약 10을 입력했다면 11바이트 크기의 메모리 공간을 힙 영역에 할당하고 그 주소값을 반환하였습니다. 14행은 malloc 호출시 실패하면 NULL를 반환하는데 실패시 프로그램을 종료하는 부분입니다. 16~17행을 보시면 26 내의 난수를 생성하여 문자 'a'를 더하고 있는데 랜덤 문자를 buffer[n]에 저장시키는 부분입니다. 그 후 맨 마지막에 \0(NULL)을 저장하고 그 문자열을 출력시키고 free 함수로 메모리 공간을 해제하고 있습니다

void *calloc(size_t num, size_t size) // 성공 시 할당된 메모리의 주소 값, 실패시 NULL 반환

이 calloc란 함수는 뭘까요? malloc 함수와는 달리 두개의 인자를 받으며 첫번째 인자는 할당할 요소의 개수를 의미하여 두번째 인자는 요소의 크기를 의미합니다. 아래의 두개는 같은 동일한 기능을 합니다.
-> StudentNum이 5라고 가정합니다.

StudentScore=(int *)malloc(sizeof(int)*StudentNum); // 20 바이트를 메모리 공간에 할당해줘!
StudentScore=(int *)calloc(StudentNum,sizeof(int)); // 4 바이트씩 5개를 메모리 공간에 할당해줘!

또하나 다른점은 malloc로 메모리 공간을 할당하면 할당된 메모리에 NULL(쓰레기값)이 들어있으나 calloc로 메모리 공간을 할당하면 전부 0으로 초기화 합니다. 이러한 특징때문에 calloc 함수가 자주 사용되기도 합니다.

8. const 포인터


const 키워드를 사용하여 포인터 상수화를 할수도 있으며 변수를 상수화 할수도 있습니다. 상수는 변수와 달리 변하지 않는 값이며, 아래는 일반적인 const의 사용 예를 보여주고 있습니다.

const int num=50; // 변수의 상수화
const int *num; // 상수지시 포인터
int* const num; // 포인터 상수
const int* const num; // 포인터 num과 num이 가리키는 값의 상수화

첫번째 같은 경우는 변수의 상수화가 이루어지고 있으며 한번 초기화한 num의 값은 더이상 변경할 수가 없습니다. 두번째 같은 경우는 상수지시 포인터로 num이 가리키는 값을 변경할 수 없습니다. 세번째 같은 경우는 포인터 상수로 포인터의 주소값을 바꾸는 행위를 허락하지 않습니다. 다만 포인터가 가리키는 값을 바꾸는건 허용합니다. 네번째 같은 경우는 둘다 상수화가 이루어진 것으로 한번 초기화가 되면 가리키는 값도 바꾸지 못하고 주소값도 바꾸지 못하게 됩니다. 아래 코드를 컴파일 해 볼까요?

#include <stdio.h> 

int main(void)
{
    char ch = 'c';
    char c = 'a'; 

    char *const ptr = &ch;
    ptr = &c; // 에러 발생!

    return 0;
}

컴파일을 시도했으나 아래와 같은 에러가 발생했습니다. 무슨 문제일까요?


9: 읽기 전용 변수 ptr의 할당! (assignment of read-only variable `ptr')


이는 위에서 말한 그대로 포인터 변수 ptr의 상수화가 이루어져있으며, 즉 포인터 변수 ptr에 저장된 주소값을 변경하지 못합니다. 아래 코드도 한번 컴파일 해볼까요?

#include <stdio.h>

int main()
{
    int num[2] = {10,20};
    const int *ptr=&num[0];
    *ptr=50; // 에러 발생

    printf("%d",*ptr);
    return 0;
}

이것도 역시 컴파일을 시도했으나 위와 같은 에러가 발생했습니다.


7: 읽기 전용 공간에 할당! (assignment of read-only location)


6행에서 선언한 const 선언은 포인터를 이용한 값의 변경을 허용하지 않겠다는 소리입니다. 그렇지만 num[0]을 통한 값의 변경은 가능합니다. 아래의 경우는 또 어떨까요?

#include <stdio.h>

int main()
{
    int num1=100;
    int num2=200;
    const int *const ptr=&num1;
    
    *ptr=150; // 에러 발생!
    ptr=&num2; // 에러 발생!
    
    printf("%d",*ptr);
    return 0;
}

이것마저 에러가 발생했습니다. 무슨 이유에서일까요?


9: 읽기 전용 공간에 할당! (assignment of read-only location)
10: 읽기 전용 변수 ptr의 할당! (assignment of read-only variable `ptr')


포인터 ptr와 ptr가 가리키는 값까지 상수화가 되어버렸습니다. 이렇게 선언된 포인터 변수 ptr는 주소값을 더이상 변경하지 못하며 가리키는 값까지 바꿀수 없게 되어버렸습니다. 어느정도 이해가 가셨나요?