Tuy nhiên, có những trường hợp người lập trình muốn
Thêm vào các biến mới trong quá trình chương trình chạy và giải phóng chúng sau khi chúng hết nhiệm vụ
Sử dụng vùng nhớ độc lập với các vùng nhớ chứa các biến đã khai báo
Ví dụ, tìm lỗi sai trong đoạn chương trình sau:
int *a, *b, c;
a = &c; //Không có lỗi vì vùng nhớ chứa biến c //(địa chỉ của ô nhớ chứa giá trị biến //c) là đã xác chỉ của ô nhớ chứa giá trị biến //c) là đã xác định
*a = 0x1234; *b = *a;
Các lệnh cấp phát bộ nhớ
Tuy nhiên, có những trường hợp người lập trình muốn
Thêm vào các biến mới trong quá trình chương trình chạy và giải phóng chúng sau khi chúng hết nhiệm vụ
Sử dụng vùng nhớ độc lập với các vùng nhớ chứa các biến đã khai báo
Ví dụ, tìm lỗi sai trong đoạn chương trình sau:
int *a, *b, c;
a = &c; //Không có lỗi vì vùng nhớ chứa biến c //(địa chỉ của ô nhớ chứa giá trị biến //c) là đã (địa chỉ của ô nhớ chứa giá trị biến //c) là đã xác định
*a = 0x1234;
*b = *a; //Lỗi vì b chưa trỏ vào một vùng nhớ //xác định nào cả
Các lệnh cấp phát bộ nhớ
Khi mới khai báo biến con trỏ mà chưa chỉ rõ nó được trỏ tới địa chỉ nào, biến con trỏ sẽ mang giá trị là NULL
Khi đó, để dùng được con trỏ b, người lập trình cần phải thực hiện cấp phát động bộ nhớ cho nó, theo nghĩa, cần phải chỉ rõ, nó sẽ quản lý vùng nhớ nào, kích thước bao nhiêu, ...?
Để thực hiện cấp phát động, trong ANSI C (C chuẩn), ta dùng hàm malloc(), calloc() và realloc()
Các hàm malloc() và calloc() nằm trong thư viện stdlib.h nên khi dùng phải khai báo #include <stdlib.h>
Các lệnh cấp phát bộ nhớ
Cú pháp:
void *malloc(size_t size);
void *calloc(size_t nitems, size_t size);
void *realloc(void *ptr, size_t size);
malloc(): cấp phát vùng nhớ có kích thước size byte
calloc(): cấp phát vùng nhớ có kích thước nitems*size bytes
realloc(): cấp phát lại vùng nhớ có kích thước size byte. Sau khi gọi lệnh realloc(), con trỏ sẽ chỉ quản lý vùng nhớ có kích thước do hàm realloc() cấp, bất kể trước đó nó được cấp vùng nhớ bao nhiêu
Do các hàm malloc(), calloc() và realloc() có kiểu void nên ta phải ép nó về các kiểu tương ứng
Các lệnh cấp phát bộ nhớ
Ví dụ:
int *a;
float *b; ...
a = (int*) malloc(sizeof(int));
b = (float*) calloc(10, sizeof(float));... ...
Sau đoạn chương trình trên, con trỏ a sẽ trỏ tới một vùng nhớ có kích thước 4 bytes (sizeof(int))
Con trỏ b trỏ tới vùng nhớ có kích thước 40 Bytes gồm 10 block 4 Bytes.
Các lệnh cấp phát bộ nhớ
Ví dụ:
int *a;
float *b, *c;
char *s; ...
a = (int*) malloc(sizeof(int));s = (char*) malloc(500); s = (char*) malloc(500);
b = (float*) calloc(10, sizeof(float));c = (float*) malloc(15*sizeof(float)); c = (float*) malloc(15*sizeof(float)); ...
b = (float*) realloc(1000);
c = (float*) realloc(10*sizeof(float));... ...
Các lệnh cấp phát bộ nhớ
Trở lại ví dụ trước đó,
int *a, *b, c; a = &c;
*a = 0x1234;
*b = *a; //Lỗi vì b chưa trỏ vào một vùng nhớ //xác định nào cả
Khắc phục bằng cách cấp cho con trỏ b một vùng nhớ nào đó rồi mới gán *b = *a, khi đó, chương trình sẽ không báo lỗi nữa
int *a, *b, c; a = &c;
*a = 0x1234;
b = (int*) malloc(sizeof(int));*b = *a; *b = *a;
Các lệnh cấp phát bộ nhớ
Để giải phóng vùng nhớ được cấp phát bằng hàm malloc(), calloc() và realloc() ta dùng hàm free()
void free(void *s);
Ví dụ: để giải phóng vùng nhớ cấp cho 2 biến a, b trong ví dụ trước, ta dùng:
free(a); free(b);
Các lệnh cấp phát bộ nhớ
Hàm malloc(), calloc() và realloc() có cách dùng tương đối phức tạp và tương đối giống nhau
Trong C++/Visual C++, để thực hiện việc cấp phát bộ nhớ, ta dùng lệnh new và delete
Để tạo ra con trỏ a và b như ví dụ trước ta làm như sau:
int *a; float *b; ... a = new int; b = new float[10]; Các lệnh cấp phát bộ nhớ 36
Để thay đổi vùng nhớ đã cấp cho con trỏ, ta dùng lại lệnh new
theo sau bởi kích thước vùng nhớ mới:
new kiểu[kích thước vùng nhớ mới];
Ví dụ:
int *a = new int;//Con trỏ a trỏ đến vùng nhớ 4 Bytes
...
a = new int[10];//Con trỏ a trỏ đến vùng nhớ 40 Bytes
...
a = new int[5]; //Con trỏ a trỏ đến vùng nhớ 20 Bytes
Các lệnh cấp phát bộ nhớ
Để thu hồi vùng nhớ cấp cho con trỏ, ta làm như sau:
delete tên_con_trỏ;
nếu con trỏ gồm nhiều block, để giải phóng toàn bộ bộ nhớ đã cấp cho con trỏ, ta dùng lệnh sau: delete [] tên_con_trỏ; Ví dụ: int *a; float *b; ... a = new int; b = new float[10];
Để thu hồi vùng nhớ cấp cho a và b ta dùng lệnh:
delete a;
delete [] b;
• Để thuận tiện, các ví dụ về sau đối với bộ nhớ động sẽ chỉ làm việc với các lệnh new và
delete
Các lệnh cấp phát bộ nhớ
Ví dụ: tìm lỗi sai trong đoạn code sau:
int *a, *b = new int;
float *c, *d; ...
a = new int[10];
c = (float *) malloc(sizeof(int));d = calloc(1000); d = calloc(1000); ... delete c; delete a; free(b); delete [] d; Các lệnh cấp phát bộ nhớ 39
Mảng cũng là một con trỏ đặc biệt, gọi là con trỏ mảng
Khi ta khai báo một mảng, ví dụ:
int a[10];
thì bản thân a là con trỏ mảng
Vùng nhớ mà con trỏ a quản lý sẽ là số phần tử * sizeof(int),
(trong trường hợp này, a sẽ quản lý 40 bytes bộ nhớ)
Con trỏ a khi đó sẽ luôn luôn trỏ tới phần tử đầu tiên trong mảng (tức là *a luôn bằng a[0])
Mảng động
Mảng a[10] do đó gọi là mảng tĩnh, nghĩa là số phần tử của
mảng không thể thêm bớt trong quá trình chương trình chạy
Con trỏ mảng a gọi là con trỏ hằng, do địa chỉ mà nó trỏ tới
không thể thay đổi trong quá trình chạy (luôn luôn trỏ tới a[0])
Do đó, lệnh gán con trỏ a để trỏ tới địa chỉ khác hay tịnh tiến/lùi con trỏ a đều không hợp lệ
Mảng động
Tuy nhiên, trong nhiều ứng dụng, chẳng hạn:
khi không biết chắc chắn số phần tử của mảng khi số phần tử thường xuyên thay đổi
...
Khi đó, nảy sinh nhu cầu thêm bớt một/một số phần tử của mảng, thậm chí xóa hẳn mảng ra khỏi bộ nhớ trong thời gian chạy chương trình (Run Time)
Dùng cấu trúc mảng động Mảng động
Khai báo mảng động giống như khai báo một con trỏ bình thường:
kiểu * tên_con_trỏ;
Sau đó, để cấp phát động cho con trỏ mảng, ta dùng lệnh: tên_con_trỏ = new kiểu[số_phần_tử];
khi đó, tên_con_trỏ sẽ quản lý một vùng nhớ có kích thước bằng kiểu*số_phần_tử
Mảng động
Ví dụ, sau đoạn lệnh sau:
int *a;
a = new int[10];
con trỏ a sẽ trỏ đến một vùng nhớ có kích thước 4*10 = 40 Bytes, hay nói cách khác, con trỏ a sẽ trỏ tới một mảng 10 phần tử kiểu int
Các phần tử của mảng a sau đó được truy xuất như với mảng tĩnh thông thường, chẳng hạn:
Lệnh sau sẽ gán khởi tạo các phần tử của mảng bằng 0
for (i = 0; i<10; i++) a[i] = 0; Mảng động
Ví dụ, sau đoạn lệnh sau:
int *a;
a = new int[10];
Ngoài ra, ngay sau khi khởi tạo bằng lệnh new, con trỏ mảng
động a sẽ trỏ tới phần tử đầu tiên của mảng
Ta có thể thực hiện phép tịnh tiến/lùi trên con trỏ a. Lưu ý, khi đó, phần tử mà con trỏ a trỏ tới cũng sẽ tịnh tiến/lùi theo
Mảng động
Ví dụ:
int *a, i;
a = new int[10]; //a trỏ tới phần tử thứ 1
for (i=0; i<10; i++) a[i] = i;
a++; //a trỏ tới phần tử thứ 2 a += 5; //a trỏ tới phần tử thứ 7
*(a – 3) = 15; //phần tử thứ 4 gán bằng 15
for (i = 0; i<3; i++)
{*a = 2*i; a++;} //Kết thúc vòng lặp a trỏ
tới //phần tử thứ 10
a[-6] *= 2; //phần tử thứ 4 nhân gấp đôi ...
Mảng động
Ví dụ 9.4: Viết chương trình trên đọc file Data.in có cấu trúc gồm 2 dòng:
Dòng đầu chứa tổng số phần tử n Các dòng tiếp theo chứa n số nguyên
Số phần tử n là không biết trước và cũng không có giới hạn cụ thể Sau khi đọc, in ra màn hình các phần tử của mảng
Mảng động
• Tìm lỗi sai trong chương trình :
#include <stdio.h>
int *a, n;
FILE *f;
void Data(int *n) { int i;
f = fopen("Data.in", "rt");fscanf(f,"%d\n", n); fscanf(f,"%d\n", n);
a = new int[*n];
for (i = 0; i<*n; i++) {fscanf(f, "%d", a); a++;} fclose(f);
}
void main() { int i;
Data(&n);
for (i = 0; i<n; i++) printf(“%d “, a[i]); ...
}
Mảng động
• Chương trình được sửa lại
#include <stdio.h>
int *a, m;
FILE *f;
void Data(int *n) { int i;
f = fopen("Data.in", "rt");fscanf(f,"%d\n", n); fscanf(f,"%d\n", n);
a = new int[*n];
for (i = 0; i<*n; i++) {fscanf(f, "%d", a+i);}
fclose(f); }
void main() { int i;
Data(&m);
for (i = 0; i<m; i++) printf(“%d “, a[i]); ...
}
Mảng động
Khi lời gọi hàm được gọi, máy tính sẽ cấp phát bộ nhớ các tham số và các biến địa phương của hàm
Các tham số và biến của hàm đều được khai báo trong Stack
Nếu số lượng tham số và biến quá nhiều sẽ nhanh chóng làm tràn Stack sinh ra lỗi chương trình
Đặc biệt, nếu ta truyền tham số kiểu mảng thì ngoài biến mảng đã có ở toàn cục, máy tính sẽ cấp phát thêm một vùng nhớ tương đương nữa cho tham số mảng truyền vào chương trình con làm cạn kiệt Stack