C & C++/디딤돌 C언어

Part 23. 동적 메모리 할당 malloc, calloc, realloc, free

언휴 2024. 1. 17. 15:32

Part 23. 동적 메모리 할당 malloc, calloc, realloc, free

C언어 동적 메모리 할당, malloc, calloc, realloc, free 

82. 동적 메모리 할당 함수과 void *

C언어에서는 데이터를 관리하기 위해 메모리를 할당받는 방법으로 변수 선언을 제공하고 있어요.
그런데 변수 선언으로 메모리를 할당받는 것은 컴파일러 시점에 할당할 메모리 크기를 결정해요.
물론 실제 메모리를 할당하는 시점은 프로그램이 실행 중에 할당하지만 할당할 크기를 결정하는 것은 컴파일 시점이예요.

그런데 프로그래밍하다 보면 할당할 메모리 크기를 컴파일 시점이 아닌 프로그램 실행 중에 결정할 때도 있어요.
학생 관리 프로그램에서 최대 관리할 학생 수를 최종 사용자가 결정할 수 있다면 개발자가 정하는 것보다 훨씬 유연하겠죠.

C언어에서는 표준 라이브러리 함수로 동적 메모리 할당 관련 함수를 제공하고 있어요.

void *malloc(size_t size);
void *calloc(size_t count,size_t size);
void *realloc(void *base,size_t nsize);
void free(void *base);

동적 메모리 할당 관련 함수를 사용하려면 stdlib.h 파일을 추가하세요.
메모리를 동적으로 할당할 때는 목적에 따라 malloc, calloc, realloc 함수를 사용할 수 있어요.
그리고 동적 메모리 할당을 요청하면 힙 메모리에 할당한답니다.
이렇게 동적으로 할당한 메모리는 free 함수로 해제할 수 있어요.

주의할 점은 free 함수는 동적으로 할당한 메모리만 해제할 수 있고 부분적으로 해제할 수는 없어요.
만약 동적으로 할당한 메모리를 해제하지 않으면 메모리 누수가 발생해요.

malloc, calloc, realloc 함수의 반환 형식을 보면 모두 void *죠.
여기에서 void *는 원소 형식을 정하지 않은 포인터를 말해요.

void 포인터 변수를 선언하면 원소 형식은 결정하지 않고 메모리 주소를 값으로 갖는 메모리 4바이트를 할당해요.
그리고 원소 형식에 관계없이 포인터를 받을 수 있어요.
◈ void 포인터 변수에 다른 원소 형식의 포인터 변수를 대입하여 주소를 출력한 예

#include <stdio.h>
#include <stdlib.h>
int main()
{
    int arr[10]={1,2,3,4,5,6,7,8,9,10};
    char name[10+1]="Hello";
    void *vp = 0;
    vp = arr; //int 형을 원소로 하는 포인터를 대입
    printf("vp: %p arr: %p \n", vp, arr);
    vp = name;  //char 형을 원소로 하는 포인터를 대입
    printf("vp: %p name: %p \n", vp, name);
    return 0;
}

◈ 실행 결과

vp: 004DFAC0 arr: 004DFAC0
vp: 004DFAAC name: 004DFAAC

하지만 원소 형식을 결정하지 않았서 간접 연산이나 인덱스 연산으로 원소에 접근하여 사용할 수가 없어요.

int arr[10] = {1,2,3,4,5,6,7,8,9, 10};
void *vp = 0;

vp = arr; //int 형을 원소로 하는 포인터를 대입, 가능한 표현
*vp = 0; //void 포인터를 간접 연산의 피연산자로 사용할 수 없음
vp[0] = 0; //void 포인터를 인덱스 연산의 피연자로 사용할 수 없음

83. malloc 함수

void *malloc(size_t size);

malloc 함수를 사용할 때는 입력 인자로 필요한 형식의 메모리 크기를 전달하세요.
malloc 함수는 요청한 크기의 메모리를 동적으로 할당하여 반환해줘요.
호출한 곳에서는 원하는 형식의 포인터로 형변환하여 할당받은 주소를 기억하세요.
그리고 간접 연산이나 인덱스 연산으로 할당받은 메모리에 원하는 값을 설정하거나 얻어올 수 있어요.

참고로 malloc 함수를 호출하면 메모리만 할당하며 할당한 메모리의 값을 초기화하지 않아요.
따라서 동적으로 할당받은 메모리의 초기값은 쓰레기 값(Garbage Value)이예요.
그리고 동적으로 할당받은 메모리가 더 이상 필요 없으면 free 함수를 호출하여 해제하세요.

◈ malloc 함수 호출로 동적으로 메모리 할당하여 사용하는 예

#include <stdio.h>
#include <malloc.h>
int main()
{
    int *pi = (int *)malloc(sizeof(int)); //원하는 형식 포인터로 형변환
    printf("초기: %d \n",*pi);
    *pi= 20; //간접 연산으로 사용
    printf("간접 연산을 수행한 후: %d\n",*pi);
    free(pi); //더 이상 필요없을 때 해제
    return 0;
}

◈ 실행 결과

초기: -842150451
간접 연산을 수행한 후: 20 

동적 메모리 할당을 이용하면 컴파일 시점에 메모리 크기를 결정해야 하는 배열의 한계를 극복할 수 있어요.
예를 들어 관리해야 할 학생 수를 최종 사용자가 결정한다면 컴파일 시점에 배열을 선언하는 것은 한계가 있겠죠.
물론 동적으로 메모리를 할당받아 사용할 때도 할당한 메모리를 벗어나지 않게 주의해서 사용하세요.

◈ 학생 수를 입력 받아 동적으로 메모리를 할당받아 사용하는 예

#include <stdio.h>
#include <stdlib.h>
  
void InputScores(int *base,int asize); //asize 명의 성적을 입력받는 함수
void ViewScores(int *base,int asize); //asize 명의 성적을 출력하는 함수
int InputScore(int num); //num 번의 학생 성적을 입력받는 함수
int main()
{
    int *base = 0;  //동적으로 할당받아 학생들의 성적을 관리할 메모리의 시작 주소
    int max_stu= 0;   //관리할 학생 수
    printf("최대 관리할 학생 수를 입력하세요.\n");
    scanf_s("%d",&max_stu);
    base = (int *)malloc(sizeof(int) * max_stu); //성적을 보관할 메모리를 할당
    InputScores(base,max_stu); //학생들의 성적 입력 요청
    ViewScores(base,max_stu); //학생들의 성적 출력
    free(base); //동적으로 할당한 메모리 해제
    return 0;
}
  
void InputScores(int *base,int asize)
{
    int i = 0;
    for(i = 0;  i<asize; i++)
    {
        base[i] = InputScore(i+1); //i+1 번의 학생 성적을 입력받아 base[i]에 대입
    }
}
void ViewScores(int *base,int asize)
{
    int i = 0;
    for(i = 0;  i<asize; i++)
    {
        printf("%d 번: %d \n",i+1, base[i]); //i+1 번의 성적 출력
    }
}
int InputScore(int num)
{
    int score;
    printf("%d 번의 성적을 입력하세요.\n",num);
    scanf_s("%d",&score);
    return   score;
}

84. calloc 함수

void *calloc(size_t count,size_t size);

calloc 함수는 같은 형식 여러 개를 동적으로 할당하기 쉽게 만들어졌어요.
그리고 할당한 메모리는 0으로 설정한 상태로 반환해 줘요.

calloc 함수는 메모리를 할당하고 초기화까지 해 주지만 실제 프로그래머들은 malloc을 더 많이 사용해요.

◈ calloc 함수 호출로 동적 메모리 할당

#include <stdio.h>
#include <stdlib.h>
int main()
{
    int *pi = (int *)calloc(1, sizeof(int)); //원하는 형식 포인터로 형변환
    printf("초기: %d \n",*pi);
    *pi= 20; //간접 연산으로 사용
    printf("간접 연산을 수행한 후: %d\n",*pi);
    free(pi); //더 이상 필요없을 때 해제
    return 0;
}

◈ 실행 결과

초기: 0
간접 연산을 수행한 후: 20

85. realloc 함수

void *realloc(void *base,size_t nsize);

C언어에서 동적으로 할당한 메모리는 free 함수를 이용하여 부분적으로 해제할 수 없어요.
대신 realloc 함수를 이용하여 할당한 메모리의 크기를 조절할 수 있어요.

첫 번째 입력 인자에는 이전에 동적으로 할당한 메모리 주소를 전달하세요.
두 번째 입력 인자에는 새로 할당할 메모리 크기를 전달해요.
realloc 함수를 이용하면 기존의 메모리의 내용은 유지하면서 메모리 크기를 변경할 수 있어요.
새롭게 늘어난 부분의 값은 쓰레기 값이예요.
그리고 realloc 함수를 호출할 때 첫 번째 입력 인자로 0을 전달해서 동적 메모리를 할당받을 수도 있어요.

대부분 realloc 함수를 사용할 때 동적으로 할당한 메모리를 기억할 포인터 변수를 첫 번째 입력 인자로 전달해서 사용해요.
그리고 realloc 함수가 반환한 메모리 주소를 해당 포인터 변수에 대입하죠.

int *base = 0;
int asize = 0;
int usage = 0;
void Resize()
{
    if(asize == 0)
    {
        asize = 1;
    }
    else
    {
        asize = asize * 2;
    }
    base = (int *)realloc(base,sizeof(int)*asize);
}

◈ realloc 함수를 이용하여 동적으로 할당한 메모리의 크기를 확장하는 예

#include <stdlib.h>
#include <stdio.h>
 
int *base = 0; //저장소의 위치 정보
int asize = 0; //현재 저장소의 용량
int usage = 0; //저장소에 보관한 요소 개수
 
void Input(int num); //저장소에 보관하기
void Resize();//저장소의 용량 변경하기
void View();//저장소의 정보 보기
 
int main()
{
    Input(3);
    View();
 
    Input(5);
    View();
 
    Input(7);
    View();
 
    Input(9);
    View();
    return 0;
}
    
void Input(int num)
{
    if(asize == usage)
    {
        Resize();
    }
    base[usage] = num;
    usage++;
}
 
void Resize()
{
    if(asize == 0)
    {
        asize = 1;
    }
    else
    {
        asize = asize * 2;
    }
    base = (int *)realloc(base,sizeof(int)*asize);
}
void View()
{
    int i = 0;
    printf("저장 용량:%d 사용량:%d\n",asize, usage);
    for(i=0;i<usage;i++)
    {
        printf("%d\n",base[i]);
    }
}

◈실행 결과

저장 용량:1 사용량:1
3
저장 용량:2 사용량:2
3
5
저장 용량:4 사용량:3
3
5
7
저장 용량:4 사용량:4
3
5
7
9

86. 학생 구조체 동적 메모리 할당 실습

C언어 학생 구조체 동적 메모리 할당

이번에는 사용자 정의 형식의 데이터를 동적으로 생성하는 기능과 소멸하는 기능을 작성하는 실습이예요.

사용자  정의 형식의 데이터를 동적으로 생성하는 기능은 New_[형식 이름]으로 명명하기로 해요.
소멸하는 기능은 Delete_[형식 이름]으로 정의할게요.
그리고 생성하는 과정에서 초기화하는 기능을 생성자 소멸하는 과정에서 해제화하는 기능을 소멸자라 부를게요.

사용자 정의 형식 실습에 사용한 프로젝트에 필요한 기능을 추가하는 형태로 작성하는 실습이예요.
먼저 Student.h 파일에 필요한 기능을 선언하세요.
여기에서는 동적으로 개체를 생성하는 함수와 소멸하는 함수가 필요하겠죠.

Student *New_Student(const char *name);//동적으로 개체 생성
void Delete_Student(Student *stu);//동적으로 생성한 개체 소멸

그리고 Student.c 파일에서 구체적으로 두 개의 함수를 정의하세요.

Student *New_Student(const char *name)
{
    Student *stu = 0;
 동적으로 개체를 만드는 함수에서는 생성할 형식의 메모리를 할당하세요.
    stu = (Student *)malloc(sizeof(Student));
 그리고 생성자(생성한 개체의 메모리를 초기화) 함수를 호출하세요.
    Student_Student(stu,name);
 생성 작업을 마쳤으면 동적으로 할당한 메모리를 반환해야겠죠.
    return stu;
}

void Delete_Student(Student *stu)
{
 개체 내부에서 동적으로 생성한 메모리가 있으면 해제해야 하는데 Student 형식 내부에서는 없으니 자신만 해제하세요.
    free(stu);
}

이제 Program.c 파일의 main 함수를 변경하세요.

먼저 TestStudent 함수의 입력 매개변수 형식을 변경해야겠죠.
앞에서는 학생 데이터를 관리할 주소와 학생 이름을 입력받아 생성자(초기화) 과정부터 출발했었요.
그런데 동적으로 생성하는 함수에서 생성자(초기화)를 호출하므로 이 부분은 TestStudent 함수를 호출하기 전에 수행해야겠죠.

void TestStudent(Student *stu);
int main()
{
    Student stu1;
    Student *stu2;

사용자 정의 형식을 변수로 선언할 때는 생성자(초기화) 함수를 호출한 후에 테스트를 수행하세요.
    printf("--------------%s 학생 테스트 시작--------------\n","홍길동");
    Student_Student(&stu1,"홍길동");
    TestStudent(&stu1);

이번에 작성한 동적으로 생성하여 테스트하는 함수도 호출하세요.
    printf("--------------%s 학생 테스트 시작--------------\n","강감찬");
    stu2 = New_Student("강감찬");
    TestStudent(stu2);
 동적으로 생성하여 사용할 때는 소멸의 책임은 개발자에게 있어요.
    Delete_Student(stu2);
    return 0;
}
void TestStudent(Student *stu)
{
    Student_View(stu);
    Student_Study(stu);
    Student_View(stu);
 ...중략...
    printf("--------------%s 학생 테스트 종료--------------\n",name);
}

 

87. 정리하기 (동적 메모리 할당 82~86)

 

void *malloc(size_t size);
void *calloc(size_t count,size_t size);
void *realloc(void *base,size_t nsize);
void free(void *base);