2024.04.13(토) TIL - 메모리 해제, 할당, 변수 선언과 접근, 포인터

메모리 해제


free(cur);

메모리 해제란 포인터가 가리키는 메모리를 놓아주는 것이다. 포인터는 특정 메모리 주소를 가리킨다. 메모리를 놓아주게 되면 해당 메모리는 프로그램의 입장에서는 사용할 권한이 없어진다. 즉, 다른 프로그램이 해당 메모리를 이제 사용할 수 있다.

dangling pointer

메모리를 해제하고 포인터를 별도로 처리하지 않으면 해당 포인터는 여전히 해제 된 메모리 주소를 가리키게 된다. 다른 프로그램이 해당 메모리를 건드리고 본래의 프로그램이 포인터로 그 메모리에 접근하게 되면 심각한 문제가 발생할 수 있다. 해제 된 메모리 주소를 가리키는 포인터를 dangling 포인터라고 하는데 NULL pointer로 만들거나 다른 메모리주소를 가리키게 해야한다.

함수 내에서 메모리 해제

int remove(LinkedList *ll, int index)
{
    ListNode *pre, *cur;

    // 사용자 입력을 받아들일 수 없는 경우
    // 1) 비어있는 리스트, 2) 인덱스가 0 미만 3) 인덱스가 리스트크기 이상
    if (ll == NULL || index < 0 || index > ll->size)
    {
        return -1;
    }

    // 첫번째 노드를 변경하는 경우, 헤드 노드를 바꿔야함
    if (index == 0)
    {
        cur = ll->head->next;
        free(ll->head); // 헤드 포인터가 가리키는 메모리 해제
        ll->head = cur; // 헤드 포인터를 cur 포인터 값을 가리키게 함
        ll->size--;

        return 0;
    }

    // 두번째 노드 이상의 노드를 변경하는 경우
    if ((pre = findNode(ll, index - 1)) != NULL) // 노드 이전의 노드가 비어 있지 않은 경우
    {
        if (pre->next == NULL) // index 부분이 없는 경우 예외처리
        {
            return -1;
        }
        cur = pre->next; // 
        pre->next = cur->next;
        free(cur);
        ll->size--;
        return 0;
    }
    return -1;
}

연결리스트에서 노드를 삭제하는 함수다. 위의 코드에서 노드를 삭제를 한다는 것은 프로그램에게 노드로 주어진 메모리를 해제한다는 것이다. 메모리가 해제되면 해당 메모리는 어느 프로그램에서든 접근이 가능하며 메모리를 가리키는 포인터cur는 무의미해진다. 비록 함수 안이지만 NULL 포인터로 초기화하는 습관을 들이는 것이 좋다.

메모리 할당


연결리스트에 노드 추가

int insertNode(LinkedList *ll, int index, int value)
{
    ListNode *pre, *cur; // 포인터 노드 2개 선언

    // 사용자가 잘못 입력한 경우
    // 1) 리스트가 빈 경우 2) 인덱스아 0 미만 3) 인덱스가 리스트 크기 넘음
    if (ll == NULL || index < 0 || index > ll->size + 1)
    {
        return -1;
    }

    // 리스트는 있지만 비어 있는 경우
    if (ll->head == NULL || index == 0)
    {
        cur = ll->head;                      // 포인터로 헤드를 가리킴
        ll->head = malloc(sizeof(ListNode)); // 헤드에 노드 연결
        ll->head->item = value;              // 노드에 값 추가
        ll->head->next = cur;                // 포인터로 다음 노드 가리킴
        ll->size++;                          // 리스트의 크기 값 증가
        return 0;
    }

    // 삽입하려는 위치가 존재하는 경우
    if ((pre = findNode(ll, index - 1)) != NULL)
    {
        cur = pre->next;
        pre->next = malloc(sizeof(ListNode));
        pre->next->item = value;
        pre->next->next = cur;
        ll->size++;
        return 0;
    }
    return -1;
}

새로운 노드를 추가할 때 메모리 공간도 추가해야 한다. pre->next = malloc(sizeof(ListNode)) 를 쓰지 않고 lcur1 = lcur1->next; 를 쓰기만 하면 포인터가 조회만 할 뿐, 노드를 추가할 수는 없다.

변수 선언


변수의 존재를 프로그램에게 알리고 메모리 공간을 예약하는 과정을 변수 선언이라고 합니다. 변수 선언 시 저장할 데이터의 종류와 변수의 이름을 지정합니다. 컴파일러는 변수 선언에 따라 필요한 메모리 크기를 정하고 프로그램에서 변수를 사용할 수 있도록 준비합니다.

int age;

변수의 이름은 age 이고 int형 자료형을 담습니다.

변수 접근


선언 된 변수의 값을 읽거나 수정하는 행위를 변수 접근이라고 합니다. 변수가 선언 된 후에는 변수의 이름을 사용해서 값을 조회하거나 변경할 수 있습니다.

age = 30; // 값 30을 변수에 할당
int myAge = age; // 변수의 값을 읽어 myAge 변수에 저장

변수 선언과 접근의 차이


선언은 변수를 처음 소개하고 메모리 공간을 할당하는 단계입니다. 접근은 선언 된 변수를 사용하는 행위입니다.

노드 구조체와 포인터

#include <stdio.h>
#include <stdlib.h>

typedef struct _listnode
{
    int item;
    struct _listnode *next;
} ListNode;

노드 구조체를 선언한 코드다. 노드 구조체는 멤버로 struct _listnode *next 를 갖는다. struct _listnode *next 는 포인터다. 포인터를 활용해서 다른 노드에 연결할 수 있다.

연결리스트 구조체와 포인터

typedef struct _linkedList
{
    int size;
    ListNode *head;
} LinkedList;

연결리스트는 멤버로 ListNode *head를 갖는다. 구조체 대신에 포인터를 멤버로 가지면 노드를 동적으로 생성하고 삭제할 수 있다. 노드를 직접 멤버로 사용하면 미리 메모리를 할당해야해서 메모리 낭비가 될 수 있다.

함수의 매개변수 포인터

void printList(LinkedList *ll)
{

    ListNode *cur;
    if (ll == NULL)
        return;
    cur = ll->head;

    if (cur == NULL)
        printf("Empty");
    while (cur != NULL)
    {
        printf("%d ", cur->item);
        cur = cur->next;
    }
    printf("\n");
}

함수의 매개변수가 void printList(LinkedList *ll) 형태이다. 매개변수에 구조체 포인터를 전달하게 되면 데이터 복사본을 만들지 않고 데이터에 접근할 수 있다. 구조체를 복사하지 않기 때문에 메모리와 시간을 아낄 수 있다. 함수 내부에서 원본 데이터에 접근할 수 있기 때문에 데이터 수정에도 용이하다.

포인터와 구조체의 멤버 접근

LinkedList *ll1; // 연결리스트 구조체 포인터
LinkedList ll2; // 연결리스트 구조체

ListNode *cur1; // 노드 포인터
ListNode cur2; // 노드 구조체

cur = ll1->head; // 포인터로 구조체포인터의 멤버에 접근

연결리스트 합치기

C로 연결리스트를 머지하는 함수를 만드는 예제가 있었다. 필요한 개념을 활용해서 바닥부터 만드려고 했는데 오히려 시간은 더 걸리고 완성도가 떨어졌다. 친구들한테 물어보니 처음부터 구현하기 보다는 문서에 나오는 다른 함수들을 활용해보는게 좋을 것 같다고 조언을 받았다.

함수들을 활용하니 내가 너무 돌아가고 있었다는게 느껴졌다. 무언가가 있다면 그것을 적극적으로 활용해야지 찾아보지도 않고 만드는 것은 돌아오는 결과에 비해 너무 적게 돌아온다. 알고리즘을 짜는 것은 알고리즘 연습 때하고 함수 제작 연습은 이미 만들어진 함수들을 적극적으로 활용하자.