C 언어에서 동적 메모리 할당과 링크드 리스트는 프로그램이 실행 중에 필요한 메모리를 효율적으로 관리하고, 데이터의 삽입과 삭제를 유연하게 처리할 수 있도록 돕는 필수 기술입니다. 본 기사에서는 동적 메모리 관리의 개념부터 링크드 리스트의 기본 동작, 그리고 실전 응용까지 단계적으로 설명합니다. 이 글을 통해 C 언어의 핵심 기능을 이해하고 실무에 활용할 수 있는 기술을 익혀보세요.
동적 메모리 할당의 개념과 필요성
동적 메모리 할당은 프로그램이 실행 중에 필요에 따라 메모리를 요청하고 반환할 수 있는 기능을 제공합니다. 이를 통해 정적 메모리 할당의 한계를 극복하고, 메모리를 보다 유연하고 효율적으로 사용할 수 있습니다.
동적 메모리 할당의 필요성
- 예측 불가능한 데이터 크기: 실행 전에 데이터 크기를 알 수 없는 경우, 동적 메모리를 통해 필요한 만큼 할당할 수 있습니다.
- 효율적인 메모리 사용: 불필요한 메모리 낭비를 방지하고, 실제 사용량에 따라 메모리를 최적화할 수 있습니다.
- 데이터 구조의 유연성: 링크드 리스트, 트리, 그래프 같은 동적 데이터 구조를 구현할 수 있습니다.
동적 메모리 할당의 기본 동작
동적 메모리 관리는 런타임 중에 이루어지며, 이를 위해 힙 영역을 사용합니다. 힙 영역은 프로그램이 필요에 따라 메모리를 할당하거나 해제할 수 있는 영역으로, 프로세스의 메모리 구조에서 중요한 부분을 차지합니다.
다음은 동적 메모리 할당의 기본 흐름입니다.
- 메모리 요청:
malloc
,calloc
함수로 메모리를 할당합니다. - 메모리 사용: 포인터를 통해 할당된 메모리를 접근하고 데이터를 저장합니다.
- 메모리 반환:
free
함수로 사용하지 않는 메모리를 반환하여 메모리 누수를 방지합니다.
이 개념은 링크드 리스트와 같은 데이터 구조 구현의 기초가 되며, 효율적인 메모리 관리를 가능하게 합니다.
malloc과 free 함수의 사용법
malloc 함수
malloc
(memory allocation)은 지정한 크기의 메모리를 힙에서 동적으로 할당하고, 해당 메모리의 시작 주소를 포인터로 반환합니다.
사용법
#include <stdlib.h>
int *ptr = (int *)malloc(sizeof(int) * 5); // 정수 5개 크기의 메모리 할당
if (ptr == NULL) {
// 메모리 할당 실패 시 처리
}
주의사항
- 메모리 할당 실패 시
NULL
을 반환하므로 항상 반환 값을 확인해야 합니다. - 할당된 메모리는 초기화되지 않으므로 필요시 명시적으로 값을 초기화해야 합니다.
free 함수
free
는 malloc
이나 calloc
으로 할당된 메모리를 반환하여 메모리 누수를 방지합니다.
사용법
free(ptr); // ptr이 가리키는 메모리 반환
ptr = NULL; // 사용 후 포인터를 NULL로 초기화
주의사항
- 이미 반환된 메모리를 다시 반환하면 정의되지 않은 동작이 발생할 수 있습니다.
- 메모리를 반환한 후, 포인터를 초기화하지 않으면 댕글링 포인터 문제가 발생할 수 있습니다.
malloc과 free를 활용한 예제
#include <stdio.h>
#include <stdlib.h>
int main() {
int n, *arr;
printf("배열 크기를 입력하세요: ");
scanf("%d", &n);
arr = (int *)malloc(sizeof(int) * n);
if (arr == NULL) {
printf("메모리 할당 실패\n");
return 1;
}
for (int i = 0; i < n; i++) {
arr[i] = i + 1;
}
printf("할당된 배열: ");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
free(arr);
return 0;
}
이 코드에서 사용자는 런타임에 배열 크기를 입력하고 동적으로 메모리를 할당하여 데이터를 저장한 뒤 반환합니다. 이러한 방식은 동적 메모리 관리의 기초를 보여줍니다.
링크드 리스트의 기본 개념
링크드 리스트란?
링크드 리스트(Linked List)는 노드(Node)들이 서로 연결된 형태로 구성된 데이터 구조입니다. 각 노드는 데이터를 저장하는 필드와 다음 노드의 주소를 가리키는 포인터로 이루어져 있습니다.
링크드 리스트의 구조
- 헤드(Head): 리스트의 시작점을 나타냅니다.
- 노드(Node): 데이터 필드와 포인터 필드로 구성됩니다.
- NULL 포인터: 마지막 노드의 포인터는 리스트의 끝을 표시하기 위해
NULL
값을 가집니다.
링크드 리스트의 장점
- 동적 크기: 배열과 달리 크기가 고정되지 않고, 필요에 따라 동적으로 노드를 추가하거나 제거할 수 있습니다.
- 효율적인 삽입/삭제: 노드의 추가 및 제거가 배열보다 빠르고 간단합니다.
링크드 리스트의 단점
- 랜덤 접근 불가: 특정 인덱스에 직접 접근할 수 없으며, 순차적으로 탐색해야 합니다.
- 추가 메모리 사용: 각 노드에 데이터 외에 포인터를 저장해야 하므로 메모리 소비가 더 큽니다.
링크드 리스트의 기본 구조체
C 언어에서 링크드 리스트를 구현하려면 노드를 정의하는 구조체가 필요합니다.
#include <stdio.h>
#include <stdlib.h>
// 노드 정의
typedef struct Node {
int data; // 데이터 필드
struct Node *next; // 다음 노드를 가리키는 포인터
} Node;
단순 링크드 리스트의 예제
노드를 생성하고, 데이터를 추가하고, 출력하는 간단한 예제를 통해 링크드 리스트의 동작 원리를 이해할 수 있습니다.
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int data;
struct Node *next;
} Node;
// 새로운 노드 생성
Node* createNode(int data) {
Node *newNode = (Node *)malloc(sizeof(Node));
if (newNode == NULL) {
printf("메모리 할당 실패\n");
exit(1);
}
newNode->data = data;
newNode->next = NULL;
return newNode;
}
// 리스트 출력
void printList(Node *head) {
Node *temp = head;
while (temp != NULL) {
printf("%d -> ", temp->data);
temp = temp->next;
}
printf("NULL\n");
}
int main() {
Node *head = createNode(1); // 첫 번째 노드 생성
head->next = createNode(2); // 두 번째 노드 연결
head->next->next = createNode(3); // 세 번째 노드 연결
printList(head); // 리스트 출력
// 메모리 해제
Node *temp;
while (head != NULL) {
temp = head;
head = head->next;
free(temp);
}
return 0;
}
이 예제에서는 노드를 생성하고 연결하며, 링크드 리스트의 기본 동작을 시각적으로 확인할 수 있습니다.
노드 삽입 방법
링크드 리스트에서 노드 삽입
노드를 삽입하는 것은 링크드 리스트에서 매우 중요한 작업입니다. 삽입 위치에 따라 다음과 같은 세 가지 주요 경우로 나눌 수 있습니다.
1. 리스트의 맨 앞에 삽입
새로운 노드를 생성한 후, 기존 헤드 노드를 새로운 노드의 next
에 연결하고, 헤드를 새로운 노드로 갱신합니다.
void insertAtHead(Node **head, int data) {
Node *newNode = createNode(data);
newNode->next = *head; // 기존 헤드를 새로운 노드의 next에 연결
*head = newNode; // 새로운 노드를 헤드로 설정
}
2. 리스트 중간에 삽입
지정된 위치에 노드를 삽입하려면 삽입 위치 직전 노드를 탐색한 후, 새로운 노드를 연결합니다.
void insertAtPosition(Node **head, int position, int data) {
Node *newNode = createNode(data);
if (position == 0) {
insertAtHead(head, data);
return;
}
Node *temp = *head;
for (int i = 0; i < position - 1 && temp != NULL; i++) {
temp = temp->next;
}
if (temp == NULL) {
printf("유효하지 않은 위치\n");
free(newNode);
return;
}
newNode->next = temp->next; // 다음 노드 연결
temp->next = newNode; // 현재 노드와 연결
}
3. 리스트의 맨 끝에 삽입
리스트 끝까지 이동한 후, 새로운 노드를 생성하여 마지막 노드의 next
에 연결합니다.
void insertAtTail(Node **head, int data) {
Node *newNode = createNode(data);
if (*head == NULL) {
*head = newNode;
return;
}
Node *temp = *head;
while (temp->next != NULL) {
temp = temp->next;
}
temp->next = newNode;
}
노드 삽입 구현 예제
다양한 위치에 노드를 삽입하는 전체 구현입니다.
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int data;
struct Node *next;
} Node;
Node* createNode(int data) {
Node *newNode = (Node *)malloc(sizeof(Node));
if (newNode == NULL) {
printf("메모리 할당 실패\n");
exit(1);
}
newNode->data = data;
newNode->next = NULL;
return newNode;
}
void insertAtHead(Node **head, int data) {
Node *newNode = createNode(data);
newNode->next = *head;
*head = newNode;
}
void insertAtTail(Node **head, int data) {
Node *newNode = createNode(data);
if (*head == NULL) {
*head = newNode;
return;
}
Node *temp = *head;
while (temp->next != NULL) {
temp = temp->next;
}
temp->next = newNode;
}
void insertAtPosition(Node **head, int position, int data) {
Node *newNode = createNode(data);
if (position == 0) {
insertAtHead(head, data);
return;
}
Node *temp = *head;
for (int i = 0; i < position - 1 && temp != NULL; i++) {
temp = temp->next;
}
if (temp == NULL) {
printf("유효하지 않은 위치\n");
free(newNode);
return;
}
newNode->next = temp->next;
temp->next = newNode;
}
void printList(Node *head) {
Node *temp = head;
while (temp != NULL) {
printf("%d -> ", temp->data);
temp = temp->next;
}
printf("NULL\n");
}
int main() {
Node *head = NULL;
insertAtHead(&head, 10);
insertAtTail(&head, 20);
insertAtPosition(&head, 1, 15); // 1번 위치에 삽입
printList(head);
// 메모리 해제
Node *temp;
while (head != NULL) {
temp = head;
head = head->next;
free(temp);
}
return 0;
}
이 코드를 통해 리스트의 다양한 위치에 노드를 삽입하는 방법을 명확히 이해할 수 있습니다.
노드 삭제 방법
링크드 리스트에서 노드 삭제
링크드 리스트에서 노드를 삭제하는 방법은 삭제 위치에 따라 다릅니다. 삭제 작업은 반드시 메모리를 안전하게 반환하는 작업과 함께 이루어져야 합니다.
1. 리스트의 첫 번째 노드 삭제
첫 번째 노드를 삭제하려면, 헤드를 다음 노드로 이동시키고 기존 헤드 노드를 메모리에서 해제합니다.
void deleteHead(Node **head) {
if (*head == NULL) {
printf("리스트가 비어 있습니다.\n");
return;
}
Node *temp = *head;
*head = (*head)->next;
free(temp);
}
2. 리스트 중간의 노드 삭제
삭제할 노드의 이전 노드를 찾아 연결을 재설정하고, 삭제된 노드를 메모리에서 해제합니다.
void deleteAtPosition(Node **head, int position) {
if (*head == NULL) {
printf("리스트가 비어 있습니다.\n");
return;
}
if (position == 0) {
deleteHead(head);
return;
}
Node *temp = *head;
for (int i = 0; i < position - 1 && temp->next != NULL; i++) {
temp = temp->next;
}
if (temp->next == NULL) {
printf("유효하지 않은 위치입니다.\n");
return;
}
Node *toDelete = temp->next;
temp->next = toDelete->next;
free(toDelete);
}
3. 리스트의 마지막 노드 삭제
마지막 노드를 삭제하려면, 마지막에서 두 번째 노드를 찾아 next
를 NULL
로 설정하고 마지막 노드를 메모리에서 해제합니다.
void deleteTail(Node **head) {
if (*head == NULL) {
printf("리스트가 비어 있습니다.\n");
return;
}
if ((*head)->next == NULL) {
free(*head);
*head = NULL;
return;
}
Node *temp = *head;
while (temp->next->next != NULL) {
temp = temp->next;
}
free(temp->next);
temp->next = NULL;
}
노드 삭제 구현 예제
아래 코드는 리스트의 다양한 위치에서 노드를 삭제하는 방법을 보여줍니다.
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int data;
struct Node *next;
} Node;
Node* createNode(int data) {
Node *newNode = (Node *)malloc(sizeof(Node));
if (newNode == NULL) {
printf("메모리 할당 실패\n");
exit(1);
}
newNode->data = data;
newNode->next = NULL;
return newNode;
}
void deleteHead(Node **head) {
if (*head == NULL) {
printf("리스트가 비어 있습니다.\n");
return;
}
Node *temp = *head;
*head = (*head)->next;
free(temp);
}
void deleteAtPosition(Node **head, int position) {
if (*head == NULL) {
printf("리스트가 비어 있습니다.\n");
return;
}
if (position == 0) {
deleteHead(head);
return;
}
Node *temp = *head;
for (int i = 0; i < position - 1 && temp->next != NULL; i++) {
temp = temp->next;
}
if (temp->next == NULL) {
printf("유효하지 않은 위치입니다.\n");
return;
}
Node *toDelete = temp->next;
temp->next = toDelete->next;
free(toDelete);
}
void deleteTail(Node **head) {
if (*head == NULL) {
printf("리스트가 비어 있습니다.\n");
return;
}
if ((*head)->next == NULL) {
free(*head);
*head = NULL;
return;
}
Node *temp = *head;
while (temp->next->next != NULL) {
temp = temp->next;
}
free(temp->next);
temp->next = NULL;
}
void printList(Node *head) {
Node *temp = head;
while (temp != NULL) {
printf("%d -> ", temp->data);
temp = temp->next;
}
printf("NULL\n");
}
int main() {
Node *head = NULL;
head = createNode(10);
head->next = createNode(20);
head->next->next = createNode(30);
printf("초기 리스트: ");
printList(head);
deleteHead(&head);
printf("첫 번째 노드 삭제 후: ");
printList(head);
deleteTail(&head);
printf("마지막 노드 삭제 후: ");
printList(head);
deleteAtPosition(&head, 0);
printf("남은 노드 삭제 후: ");
printList(head);
return 0;
}
이 예제는 리스트의 노드를 삭제하면서 메모리 누수를 방지하는 안전한 삭제 방법을 보여줍니다.
복잡한 링크드 리스트 구현
이중 연결 리스트
이중 연결 리스트(Doubly Linked List)는 각 노드가 이전 노드와 다음 노드의 주소를 모두 저장하는 링크드 리스트입니다.
이중 연결 리스트의 구조
typedef struct DoublyNode {
int data;
struct DoublyNode *prev; // 이전 노드
struct DoublyNode *next; // 다음 노드
} DoublyNode;
장점
- 양방향 탐색 가능
- 삽입과 삭제가 단순 연결 리스트보다 효율적
단점
- 각 노드에 추가 포인터(
prev
)가 필요하므로 메모리 사용량이 증가
이중 연결 리스트 삽입 예제
void insertAtHeadDoubly(DoublyNode **head, int data) {
DoublyNode *newNode = (DoublyNode *)malloc(sizeof(DoublyNode));
if (newNode == NULL) {
printf("메모리 할당 실패\n");
exit(1);
}
newNode->data = data;
newNode->prev = NULL;
newNode->next = *head;
if (*head != NULL) {
(*head)->prev = newNode;
}
*head = newNode;
}
원형 연결 리스트
원형 연결 리스트(Circular Linked List)는 마지막 노드가 첫 번째 노드를 가리켜 리스트가 순환 구조를 가지는 형태입니다.
원형 연결 리스트의 구조
단일 연결 리스트나 이중 연결 리스트의 next
포인터를 이용하여 순환을 만듭니다.
장점
- 리스트의 모든 노드를 순환 탐색 가능
- 마지막 노드에서 첫 번째 노드로 빠르게 이동
단점
- 무한 루프에 주의해야 함
원형 연결 리스트 삽입 예제
void insertAtTailCircular(Node **head, int data) {
Node *newNode = (Node *)malloc(sizeof(Node));
if (newNode == NULL) {
printf("메모리 할당 실패\n");
exit(1);
}
newNode->data = data;
if (*head == NULL) {
*head = newNode;
newNode->next = newNode; // 자기 자신을 가리킴
return;
}
Node *temp = *head;
while (temp->next != *head) {
temp = temp->next;
}
temp->next = newNode;
newNode->next = *head;
}
응용 사례
- 이중 연결 리스트
- 브라우저의 뒤로/앞으로 탐색
- 음악 플레이어의 이전/다음 곡 선택
- 원형 연결 리스트
- 프로세스 스케줄링
- 게임의 턴 기반 로직
실전 구현 예제
다음 코드는 이중 연결 리스트와 원형 연결 리스트의 기본 동작을 보여줍니다.
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int data;
struct Node *next;
struct Node *prev; // 이중 연결 리스트용
} Node;
void insertAtHeadDoubly(Node **head, int data) {
Node *newNode = (Node *)malloc(sizeof(Node));
if (newNode == NULL) {
printf("메모리 할당 실패\n");
exit(1);
}
newNode->data = data;
newNode->prev = NULL;
newNode->next = *head;
if (*head != NULL) {
(*head)->prev = newNode;
}
*head = newNode;
}
void insertAtTailCircular(Node **head, int data) {
Node *newNode = (Node *)malloc(sizeof(Node));
if (newNode == NULL) {
printf("메모리 할당 실패\n");
exit(1);
}
newNode->data = data;
if (*head == NULL) {
*head = newNode;
newNode->next = newNode;
return;
}
Node *temp = *head;
while (temp->next != *head) {
temp = temp->next;
}
temp->next = newNode;
newNode->next = *head;
}
void printCircularList(Node *head) {
if (head == NULL) {
printf("리스트가 비어 있습니다.\n");
return;
}
Node *temp = head;
do {
printf("%d -> ", temp->data);
temp = temp->next;
} while (temp != head);
printf("(Head)\n");
}
int main() {
Node *doublyHead = NULL;
Node *circularHead = NULL;
insertAtHeadDoubly(&doublyHead, 10);
insertAtHeadDoubly(&doublyHead, 20);
insertAtTailCircular(&circularHead, 30);
insertAtTailCircular(&circularHead, 40);
printf("이중 연결 리스트: ");
Node *temp = doublyHead;
while (temp != NULL) {
printf("%d <-> ", temp->data);
temp = temp->next;
}
printf("NULL\n");
printf("원형 연결 리스트: ");
printCircularList(circularHead);
return 0;
}
이 코드는 복잡한 링크드 리스트를 이해하고 활용할 수 있는 기초를 제공합니다.
동적 메모리 관리에서 흔한 오류
1. 메모리 누수(Memory Leak)
메모리 누수는 동적으로 할당된 메모리를 적절히 반환하지 않아 사용할 수 없는 상태로 남아있는 문제를 의미합니다. 이는 장기적으로 메모리 부족 문제를 초래할 수 있습니다.
원인
- 할당된 메모리를 반환하지 않음
- 포인터를 다른 메모리 주소로 재할당
예시
int *ptr = (int *)malloc(sizeof(int));
*ptr = 100;
// 메모리를 반환하지 않음 -> 메모리 누수 발생
예방 방법
- 항상
free
를 호출하여 사용한 메모리를 반환 - 메모리를 반환한 후 포인터를
NULL
로 설정
2. 잘못된 포인터 접근(Invalid Pointer Access)
잘못된 포인터 접근은 이미 해제된 메모리를 참조하거나 초기화되지 않은 포인터를 사용했을 때 발생합니다.
원인
- 초기화되지 않은 포인터 사용
free
된 포인터를 다시 참조
예시
int *ptr = (int *)malloc(sizeof(int));
free(ptr);
// ptr을 다시 사용 -> 정의되지 않은 동작 발생
*ptr = 10;
예방 방법
- 포인터를 초기화한 후 사용
- 메모리를 반환한 후 포인터를 반드시
NULL
로 설정
3. 이중 해제(Double Free)
메모리를 두 번 이상 반환하려고 하면 정의되지 않은 동작이 발생할 수 있습니다.
원인
- 같은 포인터를 두 번
free
호출
예시
int *ptr = (int *)malloc(sizeof(int));
free(ptr);
free(ptr); // 이중 해제 -> 정의되지 않은 동작
예방 방법
- 메모리를 반환한 후 포인터를
NULL
로 설정
4. 메모리 초과 사용(Buffer Overflow)
할당된 메모리 범위를 초과하여 접근하면 다른 메모리를 덮어쓰는 문제가 발생합니다.
원인
- 배열 인덱스 초과 접근
- 동적 메모리 크기 초과 접근
예시
int *arr = (int *)malloc(sizeof(int) * 5);
for (int i = 0; i <= 5; i++) { // 5번 인덱스 접근 -> 초과 접근
arr[i] = i;
}
예방 방법
- 메모리 접근 전에 유효 범위를 확인
- 동적 메모리 할당 시 필요한 크기보다 약간 더 할당
5. 잘못된 메모리 크기 할당
메모리 할당 시 필요한 크기를 잘못 계산하면 데이터가 손상되거나 런타임 오류가 발생할 수 있습니다.
예시
int *arr = (int *)malloc(sizeof(int)); // 배열 크기 대신 하나의 정수 크기만 할당
예방 방법
- 항상 올바른 크기를 계산하여 할당
sizeof
를 활용해 정확한 메모리 크기를 계산
종합적인 메모리 관리 팁
- 모든 동적 메모리는 반드시 사용 후 반환해야 합니다.
- 반환된 메모리를 참조하지 않도록 포인터를
NULL
로 초기화합니다. - 동적 메모리를 사용할 때는 항상 범위를 확인하여 접근해야 합니다.
- 메모리 오류를 조기에 발견하기 위해 디버깅 도구(예: Valgrind)를 사용합니다.
디버깅 도구 활용 예시
Linux에서 Valgrind를 사용하여 메모리 누수와 잘못된 메모리 사용을 점검할 수 있습니다.
valgrind --leak-check=full ./program
이 명령은 실행 중인 프로그램에서 발생하는 메모리 누수를 탐지하고 상세한 보고서를 제공합니다. 이를 통해 동적 메모리 관리의 오류를 효과적으로 해결할 수 있습니다.
실전 예제와 연습 문제
실전 예제: 동적 메모리와 링크드 리스트를 활용한 학생 정보 관리
아래 예제는 학생 정보를 저장, 삽입, 삭제, 출력하는 프로그램을 구현하여 동적 메모리와 링크드 리스트의 활용을 보여줍니다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct Student {
int id;
char name[50];
struct Student *next;
} Student;
// 새로운 학생 노드 생성
Student* createStudent(int id, const char *name) {
Student *newStudent = (Student *)malloc(sizeof(Student));
if (newStudent == NULL) {
printf("메모리 할당 실패\n");
exit(1);
}
newStudent->id = id;
strcpy(newStudent->name, name);
newStudent->next = NULL;
return newStudent;
}
// 리스트에 학생 추가
void addStudent(Student **head, int id, const char *name) {
Student *newStudent = createStudent(id, name);
if (*head == NULL) {
*head = newStudent;
return;
}
Student *temp = *head;
while (temp->next != NULL) {
temp = temp->next;
}
temp->next = newStudent;
}
// 학생 정보 출력
void printStudents(Student *head) {
Student *temp = head;
while (temp != NULL) {
printf("ID: %d, Name: %s\n", temp->id, temp->name);
temp = temp->next;
}
}
// 학생 삭제
void deleteStudent(Student **head, int id) {
if (*head == NULL) {
printf("리스트가 비어 있습니다.\n");
return;
}
Student *temp = *head, *prev = NULL;
if (temp != NULL && temp->id == id) {
*head = temp->next;
free(temp);
printf("ID %d 학생 삭제 완료\n", id);
return;
}
while (temp != NULL && temp->id != id) {
prev = temp;
temp = temp->next;
}
if (temp == NULL) {
printf("학생을 찾을 수 없습니다.\n");
return;
}
prev->next = temp->next;
free(temp);
printf("ID %d 학생 삭제 완료\n", id);
}
int main() {
Student *head = NULL;
addStudent(&head, 1, "Alice");
addStudent(&head, 2, "Bob");
addStudent(&head, 3, "Charlie");
printf("학생 리스트:\n");
printStudents(head);
printf("\nID 2 학생 삭제:\n");
deleteStudent(&head, 2);
printf("\n학생 리스트:\n");
printStudents(head);
// 메모리 해제
while (head != NULL) {
Student *temp = head;
head = head->next;
free(temp);
}
return 0;
}
연습 문제
- 문제 1: 역순으로 출력
- 링크드 리스트를 뒤에서부터 출력하는 함수
printReverse()
를 작성하세요.
- 문제 2: 중복 노드 제거
- 링크드 리스트에서 중복 데이터를 가진 노드를 제거하는 함수를 구현하세요.
- 문제 3: 중간 노드 찾기
- 링크드 리스트의 중간에 위치한 노드를 찾아 출력하는 함수를 작성하세요.
- 문제 4: 정렬된 삽입
- 정렬된 링크드 리스트에 새로운 데이터를 올바른 위치에 삽입하는 함수를 작성하세요.
- 문제 5: 원형 리스트 변환
- 기존 단일 링크드 리스트를 원형 링크드 리스트로 변환하는 함수를 구현하세요.
풀이 가이드
- 위 문제를 풀 때 반드시 동적 메모리 할당과 해제를 적절히 사용하세요.
- 코드 작성 후, 다양한 입력값으로 테스트하여 동작을 확인하세요.
- 디버깅 도구를 사용해 메모리 누수나 잘못된 접근을 점검하세요.
이 예제와 연습 문제를 통해 동적 메모리와 링크드 리스트의 핵심 기능을 실무에서 활용할 수 있는 능력을 배양할 수 있습니다.
요약
본 기사에서는 C 언어에서 동적 메모리 할당과 링크드 리스트의 개념, 구현 방법, 실전 예제를 다루었습니다. 이를 통해 동적 메모리 관리의 중요성을 이해하고, 노드의 삽입, 삭제, 그리고 복잡한 리스트 구현과 같은 실무 기술을 익힐 수 있습니다. 연습 문제를 통해 실력을 강화하며, 메모리 관리 오류를 방지하는 팁과 디버깅 도구 활용법도 습득할 수 있습니다. 효율적이고 안전한 코드 작성을 위한 기초를 다지세요.