Quản lý bộ nhớ và cấu trúc dữ liệu¶
Tổng quan về Memory Layout¶
Bộ nhớ trong chương trình được tổ chức thành các vùng khác nhau, mỗi vùng có mục đích và đặc điểm riêng:
+-------------------+ Địa chỉ cao (0xFFFFFFFF)
| Stack | ← LIFO, tự động quản lý
| ↓ | (biến cục bộ, tham số)
| |
| Free Memory |
| |
| ↑ |
| Heap | ← Cấp phát động
+-------------------+
| Static/Global | ← Biến toàn cục, static
+-------------------+
| Code Segment | ← Mã chương trình
+-------------------+ Địa chỉ thấp (0x00000000)
Vùng nhớ Stack¶
Stack là vùng nhớ được quản lý tự động, hoạt động theo nguyên tắc LIFO (Last In, First Out).
Đặc điểm của Stack¶
- Tự động quản lý: Hệ thống tự động cấp phát và giải phóng bộ nhớ
- Tốc độ cao: Rất nhanh, chỉ cần di chuyển con trỏ stack pointer
- Kích thước hạn chế: Thường từ 1-8MB (tùy hệ thống)
- An toàn: Không có memory leak
- Cấu trúc LIFO: Phần tử cuối cùng được thêm vào sẽ được lấy ra đầu tiên
Stack được sử dụng để lưu trữ:¶
- Biến cục bộ (local variables)
- Tham số hàm (function parameters)
- Địa chỉ trả về (return addresses)
- Thanh ghi được lưu trữ khi gọi hàm
Ví dụ hoạt động của Stack:¶
void function3() {
int z = 30; // z được push vào stack
printf("In function3: z = %d\n", z);
} // z được pop khỏi stack
void function2() {
int y = 20; // y được push vào stack
function3(); // địa chỉ trả về được push
printf("In function2: y = %d\n", y);
} // y được pop khỏi stack
void function1() {
int x = 10; // x được push vào stack
int arr[100]; // mảng 400 bytes được push
function2(); // địa chỉ trả về được push
printf("In function1: x = %d\n", x);
} // x và arr được pop khỏi stack
int main() {
function1();
return 0;
}
Minh họa Stack Operations:¶
Bước 1: Gọi function1()
Stack: [main_vars][return_addr][x=10][arr[100]]
Bước 2: Gọi function2() từ function1()
Stack: [main_vars][return_addr][x=10][arr[100]][return_addr][y=20]
Bước 3: Gọi function3() từ function2()
Stack: [main_vars][return_addr][x=10][arr[100]][return_addr][y=20][return_addr][z=30]
Bước 4: function3() kết thúc
Stack: [main_vars][return_addr][x=10][arr[100]][return_addr][y=20]
Bước 5: function2() kết thúc
Stack: [main_vars][return_addr][x=10][arr[100]]
Bước 6: function1() kết thúc
Stack: [main_vars]
Stack Overflow¶
Stack Overflow xảy ra khi stack đầy do:
// Đệ quy vô hạn
void recursiveFunction() {
int localVar = 42;
recursiveFunction(); // Không có điều kiện dừng!
}
// Mảng quá lớn
void hugeArray() {
int bigArray[10000000]; // 40MB - vượt quá stack size!
}
Vùng nhớ Heap¶
Heap là vùng nhớ dành cho việc cấp phát bộ nhớ động (dynamic memory allocation).
Đặc điểm của Heap¶
- Quản lý thủ công: Lập trình viên phải tự cấp phát và giải phóng
- Tốc độ chậm hơn: Phải tìm kiếm vùng trống phù hợp
- Kích thước lớn: Giới hạn bởi bộ nhớ hệ thống (RAM + Virtual Memory)
- Linh hoạt: Có thể cấp phát bất kỳ kích thước nào
- Rủi ro: Memory leak nếu không giải phóng đúng cách
Heap được sử dụng để:¶
- Cấp phát bộ nhớ động cho objects
- Mảng có kích thước không biết trước
- Cấu trúc dữ liệu lớn (trees, graphs)
- Objects có lifetime dài hơn scope của function
Ví dụ sử dụng Heap:¶
#include <stdlib.h>
#include <stdio.h>
void heapExample() {
// Cấp phát bộ nhớ động trên heap
int *ptr = malloc(sizeof(int) * 1000); // 4000 bytes
if (ptr == NULL) {
printf("Không thể cấp phát bộ nhớ!\n");
return;
}
// Sử dụng bộ nhớ
for (int i = 0; i < 1000; i++) {
ptr[i] = i * i;
}
// In một số giá trị
printf("ptr[100] = %d\n", ptr[100]);
// QUAN TRỌNG: Phải giải phóng bộ nhớ
free(ptr);
ptr = NULL; // Tránh dangling pointer
}
// Ví dụ với struct
typedef struct {
char name[50];
int age;
float salary;
} Employee;
Employee* createEmployee(const char* name, int age, float salary) {
Employee* emp = malloc(sizeof(Employee));
if (emp != NULL) {
strcpy(emp->name, name);
emp->age = age;
emp->salary = salary;
}
return emp; // Trả về pointer, object vẫn tồn tại trên heap
}
void useEmployee() {
Employee* emp = createEmployee("John Doe", 30, 50000.0);
if (emp != NULL) {
printf("Employee: %s, Age: %d, Salary: %.2f\n",
emp->name, emp->age, emp->salary);
free(emp); // Giải phóng bộ nhớ
}
}
Memory Leak và Dangling Pointer:¶
// MEMORY LEAK - Rò rỉ bộ nhớ
void memoryLeak() {
int *ptr = malloc(sizeof(int) * 100);
// Quên gọi free(ptr) - Memory leak!
// Bộ nhớ không được trả lại cho hệ thống
}
// DANGLING POINTER - Con trỏ lơ lửng
void danglingPointer() {
int *ptr = malloc(sizeof(int));
*ptr = 42;
free(ptr);
// ptr vẫn trỏ đến vùng nhớ đã giải phóng
printf("%d\n", *ptr); // NGUY HIỂM! Undefined behavior
ptr = NULL; // Nên gán NULL sau free
}
LIFO (Last In, First Out) - Stack¶
Stack là cấu trúc dữ liệu hoạt động theo nguyên tắc LIFO - phần tử cuối cùng được thêm vào sẽ được lấy ra đầu tiên.
Minh họa Stack Operations:¶
Stack rỗng: []
Push 10: [10] ← Top
Push 20: [20] ← Top
[10]
Push 30: [30] ← Top
[20]
[10]
Pop(): [20] ← Top (lấy ra 30)
[10]
Pop(): [10] ← Top (lấy ra 20)
Pop(): [] (lấy ra 10)
Implement Stack bằng Array:¶
#define MAX_SIZE 100
typedef struct {
int data[MAX_SIZE];
int top;
} Stack;
// Khởi tạo stack
void initStack(Stack* s) {
s->top = -1;
}
// Kiểm tra stack rỗng
int isEmpty(Stack* s) {
return s->top == -1;
}
// Kiểm tra stack đầy
int isFull(Stack* s) {
return s->top == MAX_SIZE - 1;
}
// Push element vào stack
int push(Stack* s, int value) {
if (isFull(s)) {
printf("Stack overflow!\n");
return 0;
}
s->data[++s->top] = value;
return 1;
}
// Pop element từ stack
int pop(Stack* s) {
if (isEmpty(s)) {
printf("Stack underflow!\n");
return -1;
}
return s->data[s->top--];
}
// Peek top element
int peek(Stack* s) {
if (isEmpty(s)) {
printf("Stack is empty!\n");
return -1;
}
return s->data[s->top];
}
Ứng dụng của Stack (LIFO):¶
- Call Stack - Quản lý lời gọi hàm
- Undo/Redo operations - Hoàn tác thao tác
- Expression evaluation - Tính toán biểu thức
- DFS (Depth-First Search) - Duyệt đồ thị theo chiều sâu
- Browser history - Lịch sử trình duyệt
- Parsing - Phân tích cú pháp
Ví dụ ứng dụng - Kiểm tra dấu ngoặc:¶
int isValidParentheses(char* str) {
Stack s;
initStack(&s);
for (int i = 0; str[i] != '\0'; i++) {
char ch = str[i];
// Push opening brackets
if (ch == '(' || ch == '[' || ch == '{') {
push(&s, ch);
}
// Check closing brackets
else if (ch == ')' || ch == ']' || ch == '}') {
if (isEmpty(&s)) return 0; // Unmatched closing
char top = pop(&s);
if ((ch == ')' && top != '(') ||
(ch == ']' && top != '[') ||
(ch == '}' && top != '{')) {
return 0; // Mismatched
}
}
}
return isEmpty(&s); // All brackets should be matched
}
// Test
printf("%d\n", isValidParentheses("()[]{}")); // 1 (valid)
printf("%d\n", isValidParentheses("([{}])")); // 1 (valid)
printf("%d\n", isValidParentheses("([)]")); // 0 (invalid)
FIFO (First In, First Out) - Queue¶
Queue là cấu trúc dữ liệu hoạt động theo nguyên tắc FIFO - phần tử đầu tiên được thêm vào sẽ được lấy ra đầu tiên.
Minh họa Queue Operations:¶
Queue rỗng: []
Enqueue 10: [10]
↑
Front/Rear
Enqueue 20: [10][20]
↑ ↑
Front Rear
Enqueue 30: [10][20][30]
↑ ↑
Front Rear
Dequeue: [20][30] (lấy ra 10)
↑ ↑
Front Rear
Dequeue: [30] (lấy ra 20)
↑
Front/Rear
Dequeue: [] (lấy ra 30)
Implement Queue bằng Array (Circular Queue):¶
#define MAX_SIZE 100
typedef struct {
int data[MAX_SIZE];
int front;
int rear;
int size;
} Queue;
// Khởi tạo queue
void initQueue(Queue* q) {
q->front = 0;
q->rear = -1;
q->size = 0;
}
// Kiểm tra queue rỗng
int isEmptyQueue(Queue* q) {
return q->size == 0;
}
// Kiểm tra queue đầy
int isFullQueue(Queue* q) {
return q->size == MAX_SIZE;
}
// Enqueue - thêm element vào cuối queue
int enqueue(Queue* q, int value) {
if (isFullQueue(q)) {
printf("Queue overflow!\n");
return 0;
}
q->rear = (q->rear + 1) % MAX_SIZE; // Circular
q->data[q->rear] = value;
q->size++;
return 1;
}
// Dequeue - lấy element từ đầu queue
int dequeue(Queue* q) {
if (isEmptyQueue(q)) {
printf("Queue underflow!\n");
return -1;
}
int value = q->data[q->front];
q->front = (q->front + 1) % MAX_SIZE; // Circular
q->size--;
return value;
}
// Peek front element
int front(Queue* q) {
if (isEmptyQueue(q)) {
printf("Queue is empty!\n");
return -1;
}
return q->data[q->front];
}
Ứng dụng của Queue (FIFO):¶
- Hàng đợi in ấn - Print queue
- Buffer trong streaming - Video/Audio buffering
- BFS (Breadth-First Search) - Duyệt đồ thị theo chiều rộng
- Task scheduling - Xếp hàng công việc
- Handling requests - Web server requests
- Simulation - Mô phỏng hàng đợi
Ví dụ ứng dụng - BFS Algorithm:¶
#include <stdbool.h>
// Graph representation (adjacency list)
typedef struct Node {
int vertex;
struct Node* next;
} Node;
typedef struct {
Node* head;
} AdjList;
typedef struct {
int V; // số đỉnh
AdjList* array;
} Graph;
void BFS(Graph* graph, int startVertex) {
bool visited[graph->V];
for (int i = 0; i < graph->V; i++)
visited[i] = false;
Queue q;
initQueue(&q);
// Bắt đầu từ startVertex
visited[startVertex] = true;
enqueue(&q, startVertex);
printf("BFS traversal: ");
while (!isEmptyQueue(&q)) {
int currentVertex = dequeue(&q);
printf("%d ", currentVertex);
// Duyệt tất cả đỉnh kề
Node* temp = graph->array[currentVertex].head;
while (temp) {
int adjVertex = temp->vertex;
if (!visited[adjVertex]) {
visited[adjVertex] = true;
enqueue(&q, adjVertex);
}
temp = temp->next;
}
}
printf("\n");
}
So sánh Stack vs Heap¶
| Aspect | Stack | Heap |
|---|---|---|
| Quản lý | Tự động | Thủ công |
| Tốc độ | Rất nhanh | Chậm hơn |
| Kích thước | Hạn chế (MB) | Lớn (GB) |
| Allocation | Compile time | Runtime |
| Deallocation | Tự động | Manual (free/delete) |
| Memory leak | Không | Có thể xảy ra |
| Fragmentation | Không | Có thể xảy ra |
| Thread safety | Thread-specific | Shared |
So sánh LIFO vs FIFO¶
| Aspect | LIFO (Stack) | FIFO (Queue) |
|---|---|---|
| Nguyên tắc | Last In, First Out | First In, First Out |
| Insertion | Top (push) | Rear (enqueue) |
| Deletion | Top (pop) | Front (dequeue) |
| Use cases | Undo/Redo, Call stack | Print queue, BFS |
| Memory pattern | Người cuối đến, ra trước | Người đầu đến, ra trước |
| Real-world analogy | Chồng đĩa | Hàng đợi ngân hàng |
Best Practices¶
Stack Management:¶
- Tránh mảng quá lớn trên stack
- Kiểm soát độ sâu đệ quy để tránh stack overflow
- Sử dụng heap cho data structures lớn
Heap Management:¶
- Luôn luôn free() sau malloc()
- Set pointer = NULL sau free()
- Kiểm tra return value của malloc()
- Sử dụng valgrind để detect memory leaks
Data Structure Choice:¶
- Stack (LIFO) cho: Function calls, expression parsing, undo operations
- Queue (FIFO) cho: Task scheduling, BFS, buffering, request handling
Nhớ rằng: Hiểu rõ memory management và data structures là nền tảng của lập trình hiệu quả và an toàn!